Skip to content

feat: Milestone 3 - The Triage Agent (In Progress)#3

Closed
flyingrobots wants to merge 25 commits intofeat/milestone-2-heartbeatfrom
feat/milestone-3-triage
Closed

feat: Milestone 3 - The Triage Agent (In Progress)#3
flyingrobots wants to merge 25 commits intofeat/milestone-2-heartbeatfrom
feat/milestone-3-triage

Conversation

@flyingrobots
Copy link
Owner

@flyingrobots flyingrobots commented Feb 16, 2026

Initial implementation of the Triage Agent:

  • TriageService for backlog auditing
  • origin_context linking to human intent
  • Task entity and Persistence expansion
  • Stacked on Milestone 2

Summary by CodeRabbit

  • New Features

    • Automated PR review and summarization via CodeRabbit
    • Comprehensive patch validation with detailed error reporting
    • Orchestration state machine for workflow tracking and audit trails
    • Task rebalancing with configurable hour limits per campaign
    • Cryptographic signing and verification for patch integrity
    • Intent linking for backlog triage and context enrichment
  • Documentation

    • Added comprehensive specifications for transactions, orchestration, and patch operations
    • Introduced audit event schema documentation
  • Infrastructure

    • Enhanced testing framework with mutation and fuzz testing
    • Updated CI/CD workflows and Docker configuration
    • Added cryptographic key management

@chatgpt-codex-connector
Copy link

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.
To continue using code reviews, you can upgrade your account or add credits to your account and enable them for code reviews in your settings.

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • ✅ Review completed - (🔄 Check again to review again)
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/milestone-3-triage

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Resolved 36/37 review issues across 14 commits; v1.0.0-alpha.2.

Guild terminology rename (Task→Quest), constructor validation, DI fixes, graceful daemon shutdown, safe prop extraction, tightened types, expanded test coverage (18 tests).
…codes

Implement 31-test matrix covering all 13 patch invariants via single-fault
mutation of a golden fixture. Fix 5 schema blockers for AJV strict mode,
add machine-assertable InvariantCode enum, and refactor validator to return
structured InvariantError objects.
Vitest shims __dirname but raw ESM execution would throw
ReferenceError. Use fileURLToPath(import.meta.url) pattern.
Also inline the test key constant to remove file dependency.
Move Ed25519 test seed to inline constant in helpers and
create-fixture.ts. Add test/private.key to .gitignore.
Also add .catch handler for unhandled promise rejection and
use node: import prefix for consistency.
- Add SCHEMA member to InvariantCode enum; use it in formatAjvErrors
- Comment explaining INV_007-010 gap (reserved for future DAG invariants)
- Cache compiled AJV validator per schemaPath (avoid re-compilation)
- Add keyringPath parameter to validatePatchOpsDocument API
- Remove hardcoded dist/ path from isMain detection
- Fix signPatch any type to Record<string, unknown>
- Remove redundant Uint8Array.from wrapper in signPatchFixture
- Type InvariantError.code as InvariantCode | string
- Add required: ["opType"] to all 9 operation if-clauses to prevent
  vacuous true evaluation when opType is absent
- Clarify cardinality allOf as documentation-only (INV_001 enforced)
- Use structuredClone in clonePatch helper (Node 22+)
- Merge duplicate ### Added sections in CHANGELOG alpha.1
- Regenerate golden fixture for updated schema hash
@flyingrobots flyingrobots force-pushed the feat/milestone-3-triage branch from 426bb8c to 3664b59 Compare February 16, 2026 11:18
@flyingrobots
Copy link
Owner Author

Superseded by PR #4 which is now rebased on main with all feedback addressed.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
xyph-actuator.ts (1)

16-31: ⚠️ Potential issue | 🟡 Minor

Inconsistent agent ID format.

Line 17 uses 'agent.prime' (dot separator) while line 76 uses 'agent:prime' (colon separator). This inconsistency could cause claim verification failures if the IDs don't match during comparison.

🔧 Proposed fix
 async function getGraph(): Promise<WarpGraph> {
-  const writerId = process.env['XYPH_AGENT_ID'] || 'agent.prime';
+  const writerId = process.env['XYPH_AGENT_ID'] || 'agent:prime';
   // Every agent identifies as a unique writer in the XYPH roadmap
src/domain/services/IngestService.ts (1)

1-29: ⚠️ Potential issue | 🟡 Minor

Update service comments to reflect Digital Guild terminology (Quests, Campaigns, Scrolls).

The ingestMarkdown method parses raw sources into Task entities that represent Quests. Per CLAUDE.md and project coding guidelines, service documentation and variable names should use Digital Guild terminology. Update the JSDoc comments to reference "Quest entities" instead of "Task entities", and clarify the semantic mapping (Task.type='task' represents a Quest).

Note: CONSTITUTION.md focuses on DAG integrity and does not mandate terminology; the Digital Guild vocabulary requirement comes from CLAUDE.md and the stated coding guidelines.

🤖 Fix all issues with AI agents
In @.coderabbit.yaml:
- Around line 5-17: The config uses unsupported keys: replace
reviews.target_branches with reviews.auto_review.base_branches (move the list
under the auto_review block), rename reviews.summary to
reviews.high_level_summary, and move reviews.chat.enabled up to a top-level
chat.enabled key; update the YAML to use reviews.auto_review.base_branches,
reviews.high_level_summary, and top-level chat.enabled so the settings match the
schema.

In `@src/domain/services/OrchestrationFSM.ts`:
- Around line 25-28: computeDigest currently calls JSON.stringify(data,
sortedKeys) which uses the replacer array and drops nested object keys; replace
this with a recursive canonicalization so nested keys are preserved — e.g., call
the existing canonicalize(data) (or implement a recursive key-sorting function)
to produce a deterministic JSON string and then hash that; update computeDigest
to use canonicalize(data) (or equivalent) before
crypto.createHash(...).update(...).digest(...) to avoid silent loss of nested
properties.
- Around line 34-49: transitionToNormalize must enforce that inputArtifact.state
=== 'INGEST' and compute a true outputDigest by hashing the complete output
artifact (everything except outputDigest) rather than using the placeholder;
update transitionToNormalize to validate the precondition (throw or return a
failed TransitionResult if inputArtifact.state !== 'INGEST'), build the
outputArtifact with schemaVersion, runId, state = 'NORMALIZE', createdAt,
inputDigest = inputArtifact.outputDigest, then call computeDigest on that output
object with outputDigest omitted (e.g., computeDigest({ schemaVersion, runId,
state, createdAt, inputDigest })) and set outputArtifact.outputDigest to that
real hash before returning; reference symbols: transitionToNormalize,
OrchestrationArtifact, inputArtifact.outputDigest, computeDigest, FSMContext,
OrchestrationState.

In `@src/validation/validatePatchOps.ts`:
- Around line 31-95: The code uses Task/Milestone/Patch/Operation domain names
(interfaces Operation, RollbackOperation, PatchOps and ORDERED_ENTITY_TYPES with
"TASK","MILESTONE","GRAPH_EDGE"); change these to Digital Guild terminology by
renaming types and enum values (e.g., Operation -> ScrollOperation or Operation
-> SealOperation per team convention, RollbackOperation -> ScrollRollback,
PatchOps -> CampaignPatch/CampaignOps) and replace the entityType literals
"TASK","MILESTONE","GRAPH_EDGE" with the required "Quest","Campaign","Seal" (or
create a clear mapping layer that translates between old schema values and new
code names to preserve payload/schema compatibility). Update
ORDERED_ENTITY_TYPES to use the Guild terms and update all references to the
interface names and entityType string checks throughout the file so invariants
and schemaVersion remain unchanged while code expresses
Quests/Campaigns/Scrolls/Seals.

In `@test/matrix/helpers/resignPatch.ts`:
- Around line 1-10: Remove the hardcoded TEST_PRIVATE_KEY constant in
resignPatch and instead load the key at runtime (e.g., from an environment
variable or a gitignored file) and fail fast if it's not present; update the
resignPatch function to read the key (and optionally TEST_KEY_ID) from
process.env or a file, validate the value is non-empty before calling
signPatch(patch, privateKey, keyId), and throw a clear error if the key is
missing so secret scanners and accidental commits are avoided.

In `@trust/keyring.json`:
- Around line 3-13: The default trust/keyring.json currently contains a test
signer (keyId "did:key:z6MkhTestSigner01") which lets anyone with that private
key create accepted patches; remove that test key from trust/keyring.json and
create a separate test-only keyring (e.g., test/keyring.test.json) containing
the test keys, update any test/config references to point to the new test
keyring, and ensure validatePatchOpsDocument continues to default to
trust/keyring.json for production use; search for references to the keyId
"did:key:z6MkhTestSigner01" and to validatePatchOpsDocument to update test
harnesses and CI to use the test keyring.
🟡 Minor comments (22)
test/unit/TriageService.test.ts-1-46 (1)

1-46: ⚠️ Potential issue | 🟡 Minor

Align test terminology with Digital Guild conventions used elsewhere in tests.

The test uses type: 'task' and references "backlog" rather than Digital Guild terminology (Quest, Campaign, Scroll, Seal) seen in other test files like RebalanceService.test.ts. Per coding guidelines, this file must use Digital Guild terminology. Update test descriptions, task types, and IDs to match the pattern used in other tests (e.g., type: 'campaign' or type: 'scroll').

scripts/setup-test-keys.ts-9-18 (1)

9-18: ⚠️ Potential issue | 🟡 Minor

Running this script multiple times will create duplicate keyring entries.

The script appends a new key entry without checking if testKeyId already exists. This could lead to duplicate entries in trust/keyring.json if the script is run multiple times.

🛡️ Proposed fix to make the script idempotent
   const keyring = JSON.parse(fs.readFileSync(keyringPath, 'utf8'));
   
   const testKeyId = 'did:key:z6MkhTestSigner01';
+  
+  // Remove existing entry if present (idempotent)
+  keyring.keys = keyring.keys.filter((k: { keyId: string }) => k.keyId !== testKeyId);
+  
   keyring.keys.push({
     keyId: testKeyId,
     alg: 'ed25519',
     publicKeyHex: keys.publicKeyHex
   });
scripts/setup-test-keys.ts-24-24 (1)

24-24: ⚠️ Potential issue | 🟡 Minor

Add error handling for the async setup function.

Unlike scripts/create-fixture.ts which has .catch() handling, this script's unhandled rejection could exit silently without a proper error message or exit code.

🛡️ Proposed fix
-setup();
+setup().catch((err) => {
+  console.error('Failed to setup test keys:', err);
+  process.exit(1);
+});
scripts/verify-patch-ops.mjs-5-17 (1)

5-17: ⚠️ Potential issue | 🟡 Minor

Use Digital Guild terminology in CLI output.

User-facing log strings use “patch” wording; update to Quest/Campaign/Scroll/Seal terminology for consistency.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

test/matrix/helpers/buildTwoOpPatch.ts-1-4 (1)

1-4: ⚠️ Potential issue | 🟡 Minor

Align helper naming/value labels with Digital Guild lexicon.

Function and fixture strings use “Milestone” terminology; update to Quest/Campaign/Scroll/Seal terminology if these are user-facing fixtures.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

test/integration/WarpRoadmapAdapter.test.ts-9-37 (1)

9-37: ⚠️ Potential issue | 🟡 Minor

Use Digital Guild terminology in test labels and data.

Test descriptions and sample data introduce generic “task” wording; adjust to Quest/Campaign/Scroll/Seal terminology where these labels are user-facing.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

test/matrix/schema-boundary.test.ts-7-9 (1)

7-9: ⚠️ Potential issue | 🟡 Minor

Align schema-boundary test labels with Digital Guild terminology.

The test descriptions use generic “schema/signature/patch” wording; update to Quest/Campaign/Scroll/Seal terminology where user-facing.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

test/matrix/helpers/resignPatch.ts-3-4 (1)

3-4: ⚠️ Potential issue | 🟡 Minor

Use Digital Guild terminology in helper comments/naming.

The helper comment uses generic crypto wording; adjust to the required lexicon (e.g., Seal for signatures) where user-facing.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

src/validation/InvariantError.ts-1-5 (1)

1-5: ⚠️ Potential issue | 🟡 Minor

Align public error naming with Digital Guild terminology.

This exported interface introduces a new public type but doesn't reflect the required lexicon. Consider renaming or adding a Digital Guild alias (e.g., SealError) so downstream code/docs comply.

✏️ Suggested alias to align terminology
 export interface InvariantError {
   code: InvariantCode | string;
   message: string;
 }
+
+// Digital Guild terminology alias for public API consistency.
+export type SealError = InvariantError;
As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.
scripts/verify-patch-ops.mjs-28-33 (1)

28-33: ⚠️ Potential issue | 🟡 Minor

Render validation errors with code/message instead of [object Object].

Interpolating error objects hides details. Format structured errors for readable CLI output.

🛠️ Suggested formatting for structured errors
-      for (const e of result.errors) {
-        console.error(`   - ${e}`);
-      }
+      for (const e of result.errors) {
+        if (typeof e === "string") {
+          console.error(`   - ${e}`);
+        } else {
+          console.error(`   - [${e.code}] ${e.message}`);
+        }
+      }
test/integration/WarpRoadmapAdapter.test.ts-58-72 (1)

58-72: ⚠️ Potential issue | 🟡 Minor

Make the second test independent of the first.

It currently relies on data created in the previous test (Line 70), which is order-dependent. Create the baseline task in this test or in shared setup.

✅ One way to make the test self-contained
   it('should return multiple tasks', async () => {
     const adapter = new WarpRoadmapAdapter(repoPath, graphName, writerId);
     
+    await adapter.upsertTask(new Task({
+      id: 'task:INT-001',
+      title: 'Integration Task',
+      status: 'BACKLOG',
+      hours: 4,
+      type: 'task',
+      originContext: 'intent-123'
+    }));
+
     await adapter.upsertTask(new Task({
       id: 'task:INT-002',
       title: 'Task 2',
       status: 'BACKLOG',
       hours: 2,
       type: 'task'
     }));
 
     const tasks = await adapter.getTasks();
-    // includes task:INT-001 from previous test because same repo/graph
     expect(tasks.length).toBeGreaterThanOrEqual(2);
     expect(tasks.some(t => t.id === 'task:INT-002')).toBe(true);
   });
test/matrix/helpers/buildTwoOpPatch.ts-61-113 (1)

61-113: ⚠️ Potential issue | 🟡 Minor

Replace test fixture terminology with Digital Guild vocabulary.

buildTwoOpPatch() uses generic "MILESTONE" and "Alpha/Beta Milestone" terminology. Per coding guidelines, test fixtures matching **/*.ts must use Digital Guild terminology (Quests, Campaigns, Scrolls, Seals). Update entity types and identifiers accordingly.

Note: The rollbackOperations ordering (OP-0004, OP-0003) is correct—rollback operations are intentionally in reverse order to match the INV_002_REVERSE_MAP invariant, which validates the pairing of reverse-indexed operations to forward-indexed rollback entries.

docs/canonical/PATCH_OPS_SCHEMA.json-420-520 (1)

420-520: ⚠️ Potential issue | 🟡 Minor

Schema terminology doesn’t use Digital Guild vocabulary.

Definitions like taskEntity, milestoneEntity, and related enums should be aligned to Quests/Campaigns/Scrolls/Seals (or add a formal mapping section) to match project terminology requirements.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

test/matrix/invariants.test.ts-11-35 (1)

11-35: ⚠️ Potential issue | 🟡 Minor

Use Digital Guild terminology in test descriptions.

The suite headings still use generic “Invariant” wording; update describe/it labels to the Quests/Campaigns/Scrolls/Seals vocabulary.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

test/matrix/signature.test.ts-14-30 (1)

14-30: ⚠️ Potential issue | 🟡 Minor

Use Digital Guild terminology in test descriptions.

The describe/it labels still use generic “Invariant” language; please rename them to the project’s Quests/Campaigns/Scrolls/Seals vocabulary for consistency.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

src/domain/services/OrchestrationFSM.ts-17-21 (1)

17-21: ⚠️ Potential issue | 🟡 Minor

Update naming/comments to Digital Guild terminology.

Class/docs here still use generic orchestration language; please align naming/commentary to Quests/Campaigns/Scrolls/Seals vocabulary.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

src/validation/signPatchFixture.ts-13-31 (1)

13-31: ⚠️ Potential issue | 🟡 Minor

Rename public API/fields to Digital Guild terminology.

The exported signPatch API and signature field names do not use Quests/Campaigns/Scrolls/Seals vocabulary required for new code. Consider renaming or providing a domain‑terminology wrapper.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

src/validation/InvariantCode.ts-1-18 (1)

1-18: ⚠️ Potential issue | 🟡 Minor

Align enum naming with Digital Guild terminology.

InvariantCode and its members use generic naming; please align to the required Quests/Campaigns/Scrolls/Seals vocabulary for new public identifiers.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

docs/canonical/ORCHESTRATION_SPEC.md-314-330 (1)

314-330: ⚠️ Potential issue | 🟡 Minor

Audit record example doesn’t match the code contract.

The JSON example shows actor as a string and includes violations/warnings, while AuditRecord in src/domain/entities/Orchestration.ts defines actor as an object and has no violations/warnings fields. Please align the spec example or the type contract to avoid confusion.

src/domain/entities/Orchestration.ts-6-45 (1)

6-45: ⚠️ Potential issue | 🟡 Minor

Align orchestration domain type names with Digital Guild terminology.

OrchestrationState, AuditRecord, and OrchestrationArtifact don’t use Quests/Campaigns/Scrolls/Seals vocabulary required for new public types.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

docs/canonical/ORCHESTRATION_SPEC.md-1-16 (1)

1-16: ⚠️ Potential issue | 🟡 Minor

Update documentation terminology to Digital Guild vocabulary.

The spec uses generic planning/orchestration phrasing; align the narrative to Quests/Campaigns/Scrolls/Seals terminology to meet project conventions.

As per coding guidelines, Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation.

docs/canonical/PATCH_OPS_SCHEMA.json-121-134 (1)

121-134: ⚠️ Potential issue | 🟡 Minor

Sync keyId pattern in canonical docs with actual source schema.

The canonical docs file has "pattern": "^KEY-[A-Z0-9]{6,24}$" but the actual source schema at schemas/PATCH_OPS_SCHEMA.v1.json permits both KEY-* and did:key:z6* formats: "^(KEY-[A-Z0-9]{6,24}|did:key:z6[A-Za-z0-9]+)$". All fixtures and tests use the did:key:z6* format. Update the canonical docs to reflect the correct, broader pattern.

🧹 Nitpick comments (8)
docs/canonical/APPLY_TRANSACTION_SPEC.md (1)

90-99: Consider using Digital Guild terminology.

The operation types use TASK and MILESTONE (e.g., ADD_TASK, MOVE_TASK_MILESTONE). As per coding guidelines, the project uses "Digital Guild" terminology where Tasks are called "Quests" and Milestones are called "Campaigns."

Consider whether these op types should be renamed (e.g., ADD_QUEST, MOVE_QUEST_CAMPAIGN) for consistency with the project's terminology, or document explicitly that the internal operation types use different naming than the user-facing concepts.

test/matrix/helpers/buildLinkDependencyOp.ts (2)

7-10: Hardcoded opIds may cause conflicts in multi-operation patches.

The opId values are hardcoded to "OP-0010" and "OP-0011". If this helper is called multiple times within the same test patch, duplicate opIds will result. Consider parameterizing the opId or accepting a base index.

♻️ Proposed fix to accept opId parameter
 export function buildLinkDependencyOp(
   from: string,
-  to: string
+  to: string,
+  opIdBase = 10
 ): { op: Record<string, unknown>; rollback: Record<string, unknown> } {
   const edgeId = `EDGE-${from.replace("TASK-", "")}${to.replace("TASK-", "")}`;
+  const opId = `OP-${String(opIdBase).padStart(4, '0')}`;
+  const rollbackOpId = `OP-${String(opIdBase + 1).padStart(4, '0')}`;

   const op: Record<string, unknown> = {
-    opId: "OP-0010",
+    opId,
     opType: "LINK_DEPENDENCY",
     // ...
   };

   const rollback: Record<string, unknown> = {
-    opId: "OP-0011",
-    revertsOpId: "OP-0010",
+    opId: rollbackOpId,
+    revertsOpId: opId,
     // ...
   };

Also applies to: 29-31


5-5: Edge ID generation assumes specific input format.

The edgeId construction assumes inputs always have the "TASK-" prefix. If a different format is passed, the replace won't match and the full string will be used, which may produce unexpected edge IDs.

test/unit/RebalanceService.test.ts (2)

8-8: Remove unnecessary async keyword.

The test functions are marked async but validateCampaign is synchronous and no await is used. This is misleading.

♻️ Proposed fix
-  it('should detect when a campaign exceeds 160 hours', async () => {
+  it('should detect when a campaign exceeds 160 hours', () => {
-  it('should pass when a campaign is within limits', async () => {
+  it('should pass when a campaign is within limits', () => {

Also applies to: 22-22


5-31: Consider adding boundary test cases.

The tests cover over-limit and under-limit scenarios but miss important boundaries:

  • Exactly 160 hours (boundary condition)
  • Empty tasks array (0 hours)
💚 Proposed additional test cases
it('should pass when a campaign is exactly at the 160 hour limit', () => {
  const tasks = [
    new Task({ id: 'task:1', title: 'Task A', status: 'BACKLOG', hours: 80, type: 'task' }),
    new Task({ id: 'task:2', title: 'Task B', status: 'BACKLOG', hours: 80, type: 'task' }),
  ];
  
  const result = service.validateCampaign('campaign:EXACT', tasks);
  
  expect(result.valid).toBe(true);
  expect(result.totalHours).toBe(160);
});

it('should pass with an empty task list', () => {
  const result = service.validateCampaign('campaign:EMPTY', []);
  
  expect(result.valid).toBe(true);
  expect(result.totalHours).toBe(0);
});
src/domain/services/TriageService.ts (2)

21-21: Consider replacing console.log with a structured logger.

Direct console.log calls make it harder to control log levels, filter output in production, or integrate with observability tooling. Consider injecting a logger or using a logging abstraction.

Also applies to: 45-47


20-26: Consider validating contextHash format.

The contextHash parameter is documented as a "BLAKE3 hash" but no validation ensures it follows the expected pattern (e.g., blake3:[a-f0-9]{64}). Invalid hashes could be persisted silently.

🛡️ Proposed validation
 public async linkIntent(taskId: string, contextHash: string): Promise<void> {
+  if (!/^blake3:[a-f0-9]{64}$/.test(contextHash)) {
+    throw new Error(`Invalid contextHash format: ${contextHash}`);
+  }
   console.log(chalk.cyan(`[Triage] Linking ${taskId} to intent ${contextHash}`));
Dockerfile (1)

11-14: Consider using a generic test identity.

Hardcoding a personal email in the Docker image works but could be replaced with a generic test identity (e.g., test@example.com / CI Test User) to avoid coupling the image to a specific individual.

♻️ Suggested change
 # Configure git for integration tests
-RUN git config --global user.email "james@flyingrobots.dev" && \
-    git config --global user.name "James Ross" && \
+RUN git config --global user.email "test@example.com" && \
+    git config --global user.name "CI Test User" && \
     git config --global init.defaultBranch main

Comment on lines +5 to +17
reviews:
# Enable reviews on PRs targeting any branch (useful for stacked PRs)
target_branches:
- "*"
# Auto-review all PRs
auto_review:
enabled: true
# High-level summary of the PR
summary:
enabled: true
# Chat with CodeRabbit
chat:
enabled: true
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Config keys don’t match the provided schema.
reviews.target_branches, reviews.summary, and reviews.chat aren’t defined in the schema, so these settings will be ignored. Use reviews.auto_review.base_branches, reviews.high_level_summary, and top-level chat instead.

✅ Suggested fix
 reviews:
-  target_branches:
-    - "*"
   auto_review:
     enabled: true
+    base_branches:
+      - ".*"
-  summary:
-    enabled: true
+  high_level_summary: true
-  chat:
-    enabled: true
+chat:
+  enabled: true
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
reviews:
# Enable reviews on PRs targeting any branch (useful for stacked PRs)
target_branches:
- "*"
# Auto-review all PRs
auto_review:
enabled: true
# High-level summary of the PR
summary:
enabled: true
# Chat with CodeRabbit
chat:
enabled: true
reviews:
# Auto-review all PRs
auto_review:
enabled: true
base_branches:
- ".*"
# High-level summary of the PR
high_level_summary: true
chat:
enabled: true
🤖 Prompt for AI Agents
In @.coderabbit.yaml around lines 5 - 17, The config uses unsupported keys:
replace reviews.target_branches with reviews.auto_review.base_branches (move the
list under the auto_review block), rename reviews.summary to
reviews.high_level_summary, and move reviews.chat.enabled up to a top-level
chat.enabled key; update the YAML to use reviews.auto_review.base_branches,
reviews.high_level_summary, and top-level chat.enabled so the settings match the
schema.

Comment on lines +25 to +28
public static computeDigest(data: Record<string, unknown>): string {
const sortedKeys = Object.keys(data).sort();
const json = JSON.stringify(data, sortedKeys);
return `sha256:${crypto.createHash('sha256').update(json).digest('hex')}`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

computeDigest drops nested keys due to JSON.stringify replacer.

JSON.stringify(data, sortedKeys) whitelists top‑level keys for all nested objects, silently omitting nested properties and causing digest collisions. Use a canonicalizer that sorts keys recursively without dropping them.

🛠️ Proposed fix (reuse canonicalize)
-import crypto from 'node:crypto';
+import crypto from 'node:crypto';
+import { canonicalize } from '../../validation/crypto.js';
@@
   public static computeDigest(data: Record<string, unknown>): string {
-    const sortedKeys = Object.keys(data).sort();
-    const json = JSON.stringify(data, sortedKeys);
+    const json = canonicalize(data as any);
     return `sha256:${crypto.createHash('sha256').update(json).digest('hex')}`;
   }
🤖 Prompt for AI Agents
In `@src/domain/services/OrchestrationFSM.ts` around lines 25 - 28, computeDigest
currently calls JSON.stringify(data, sortedKeys) which uses the replacer array
and drops nested object keys; replace this with a recursive canonicalization so
nested keys are preserved — e.g., call the existing canonicalize(data) (or
implement a recursive key-sorting function) to produce a deterministic JSON
string and then hash that; update computeDigest to use canonicalize(data) (or
equivalent) before crypto.createHash(...).update(...).digest(...) to avoid
silent loss of nested properties.

Comment on lines 34 to 49
public static transitionToNormalize(
inputArtifact: OrchestrationArtifact,
context: FSMContext,
decisionSummary: string,
durationMs: number
): TransitionResult {
const nextState: OrchestrationState = 'NORMALIZE';

const outputArtifact: OrchestrationArtifact = {
schemaVersion: 'v1.0',
runId: context.runId,
state: nextState,
createdAt: new Date().toISOString(),
inputDigest: inputArtifact.outputDigest,
outputDigest: this.computeDigest({ state: nextState, runId: context.runId }) // Placeholder
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate input state and compute a real outputDigest (not placeholder).

The transition should enforce the INGEST precondition and hash the full output artifact (excluding outputDigest) instead of a placeholder to keep the audit chain trustworthy.

🛠️ Proposed fix
   public static transitionToNormalize(
     inputArtifact: OrchestrationArtifact,
     context: FSMContext,
     decisionSummary: string,
     durationMs: number
   ): TransitionResult {
+    if (inputArtifact.state !== 'INGEST') {
+      throw new Error(`Invalid transition from ${inputArtifact.state} to NORMALIZE`);
+    }
     const nextState: OrchestrationState = 'NORMALIZE';
     
-    const outputArtifact: OrchestrationArtifact = {
-      schemaVersion: 'v1.0',
-      runId: context.runId,
-      state: nextState,
-      createdAt: new Date().toISOString(),
-      inputDigest: inputArtifact.outputDigest,
-      outputDigest: this.computeDigest({ state: nextState, runId: context.runId }) // Placeholder
-    };
+    const baseArtifact: Omit<OrchestrationArtifact, 'outputDigest'> = {
+      schemaVersion: 'v1.0',
+      runId: context.runId,
+      state: nextState,
+      createdAt: new Date().toISOString(),
+      inputDigest: inputArtifact.outputDigest
+    };
+    const outputArtifact: OrchestrationArtifact = {
+      ...baseArtifact,
+      outputDigest: this.computeDigest(baseArtifact)
+    };
🤖 Prompt for AI Agents
In `@src/domain/services/OrchestrationFSM.ts` around lines 34 - 49,
transitionToNormalize must enforce that inputArtifact.state === 'INGEST' and
compute a true outputDigest by hashing the complete output artifact (everything
except outputDigest) rather than using the placeholder; update
transitionToNormalize to validate the precondition (throw or return a failed
TransitionResult if inputArtifact.state !== 'INGEST'), build the outputArtifact
with schemaVersion, runId, state = 'NORMALIZE', createdAt, inputDigest =
inputArtifact.outputDigest, then call computeDigest on that output object with
outputDigest omitted (e.g., computeDigest({ schemaVersion, runId, state,
createdAt, inputDigest })) and set outputArtifact.outputDigest to that real hash
before returning; reference symbols: transitionToNormalize,
OrchestrationArtifact, inputArtifact.outputDigest, computeDigest, FSMContext,
OrchestrationState.

Comment on lines +31 to +95
interface Operation {
opId: string;
opType: string;
phase: number;
entityType: string;
entityId: string;
path: string;
value?: Json;
edge?: { fromTaskId: string; toTaskId: string };
precondition: {
exists: boolean;
expectedHash: string;
expectedStatus?: string;
};
invertibility: {
inverseOpType: string;
inversePath: string;
inverseValue: Json;
inversePreconditionHash: string;
};
rationale?: string;
}

interface RollbackOperation extends Operation {
revertsOpId: string;
}

interface PatchOps {
schemaVersion: "v1.0";
patchId: string;
runId: string;
baseSnapshotDigest: string;
policyPackRef: string;
configRef: string;
operations: Operation[];
rollbackOperations: RollbackOperation[];
approvals: {
required: boolean;
satisfied: boolean;
approverIds: string[];
approvalRef?: string;
};
metadata: {
createdAt: string;
author: { type: "human" | "agent" | "service"; id: string };
rationale: string;
idempotencyKey?: string;
};
signature: {
alg: "ed25519";
keyId: string;
payloadDigest: string;
sig: string;
};
}

/**
* ============================================================
* Constants
* ============================================================
*/

const ORDERED_ENTITY_TYPES = ["GRAPH_EDGE", "MILESTONE", "TASK"] as const;

/**
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Align domain naming with Digital Guild terminology.

This module introduces Task/Milestone/Patch/Operation identifiers and enums; the guideline requires Quests/Campaigns/Scrolls/Seals in code and documentation. Please rename or map these identifiers (and keep schema/invariants consistent).

As per coding guidelines, "Use 'Digital Guild' terminology (Quests, Campaigns, Scrolls, Seals) in code and documentation".

🤖 Prompt for AI Agents
In `@src/validation/validatePatchOps.ts` around lines 31 - 95, The code uses
Task/Milestone/Patch/Operation domain names (interfaces Operation,
RollbackOperation, PatchOps and ORDERED_ENTITY_TYPES with
"TASK","MILESTONE","GRAPH_EDGE"); change these to Digital Guild terminology by
renaming types and enum values (e.g., Operation -> ScrollOperation or Operation
-> SealOperation per team convention, RollbackOperation -> ScrollRollback,
PatchOps -> CampaignPatch/CampaignOps) and replace the entityType literals
"TASK","MILESTONE","GRAPH_EDGE" with the required "Quest","Campaign","Seal" (or
create a clear mapping layer that translates between old schema values and new
code names to preserve payload/schema compatibility). Update
ORDERED_ENTITY_TYPES to use the Guild terms and update all references to the
interface names and entityType string checks throughout the file so invariants
and schemaVersion remain unchanged while code expresses
Quests/Campaigns/Scrolls/Seals.

Comment on lines 1 to 10
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove the hardcoded test private key from source.

Even if test-only, committing a private key will trip secret scanners and risks unintended reuse. Load it from a gitignored file or env var and fail fast if missing.

🔐 Proposed fix: load the test key from env or a gitignored file
-import { signPatch } from "../../../src/validation/signPatchFixture.js";
+import { signPatch } from "../../../src/validation/signPatchFixture.js";
+import fs from "node:fs";
+import path from "node:path";
@@
-const TEST_PRIVATE_KEY = "5a10d58f976775c62f0c8443ef14b5b831561e26648a06f2791ad88e236375c7";
+const TEST_PRIVATE_KEY =
+  process.env.XYPH_TEST_PRIVATE_KEY ??
+  (fs.existsSync(path.resolve(process.cwd(), "test/private.key"))
+    ? fs.readFileSync(path.resolve(process.cwd(), "test/private.key"), "utf8").trim()
+    : "");
+
+if (!TEST_PRIVATE_KEY) {
+  throw new Error("Missing test private key. Run scripts/setup-test-keys.ts or set XYPH_TEST_PRIVATE_KEY.");
+}
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 4-4: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 5-5: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)


[high] 7-7: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In `@test/matrix/helpers/resignPatch.ts` around lines 1 - 10, Remove the hardcoded
TEST_PRIVATE_KEY constant in resignPatch and instead load the key at runtime
(e.g., from an environment variable or a gitignored file) and fail fast if it's
not present; update the resignPatch function to read the key (and optionally
TEST_KEY_ID) from process.env or a file, validate the value is non-empty before
calling signPatch(patch, privateKey, keyId), and throw a clear error if the key
is missing so secret scanners and accidental commits are avoided.

Comment on lines +3 to +13
"keys": [
{
"keyId": "did:key:z6MkhExampleSigner01",
"alg": "ed25519",
"publicKeyHex": "9f4c8b2b4c8b9fd9f06c01d6c9f4d88f2b8f7b8f6fd2a5c6e11a6f6ef9a5e123"
},
{
"keyId": "did:key:z6MkhTestSigner01",
"alg": "ed25519",
"publicKeyHex": "2577162074909c27c9c14979f2755816f649bd9ca42f140a37061bef25185375"
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep test/example keys out of the default trust keyring.

validatePatchOpsDocument defaults to trust/keyring.json, so including a known test signer allows anyone with the private key to produce accepted patches. Split prod vs test keyrings and point tests/config to the test keyring.

🧹 Proposed change (move test key to a test-only keyring)
     {
       "keyId": "did:key:z6MkhExampleSigner01",
       "alg": "ed25519",
       "publicKeyHex": "9f4c8b2b4c8b9fd9f06c01d6c9f4d88f2b8f7b8f6fd2a5c6e11a6f6ef9a5e123"
-    },
-    {
-      "keyId": "did:key:z6MkhTestSigner01",
-      "alg": "ed25519",
-      "publicKeyHex": "2577162074909c27c9c14979f2755816f649bd9ca42f140a37061bef25185375"
     }
🧰 Tools
🪛 Gitleaks (8.30.0)

[high] 10-10: Detected a Generic API Key, potentially exposing access to various services and sensitive operations.

(generic-api-key)

🤖 Prompt for AI Agents
In `@trust/keyring.json` around lines 3 - 13, The default trust/keyring.json
currently contains a test signer (keyId "did:key:z6MkhTestSigner01") which lets
anyone with that private key create accepted patches; remove that test key from
trust/keyring.json and create a separate test-only keyring (e.g.,
test/keyring.test.json) containing the test keys, update any test/config
references to point to the new test keyring, and ensure validatePatchOpsDocument
continues to default to trust/keyring.json for production use; search for
references to the keyId "did:key:z6MkhTestSigner01" and to
validatePatchOpsDocument to update test harnesses and CI to use the test
keyring.

@flyingrobots
Copy link
Owner Author

@coderabbitai review please

@coderabbitai
Copy link

coderabbitai bot commented Feb 16, 2026

Tip

For best results, initiate chat on the files or code changes.

@flyingrobots, I notice you mentioned earlier that this PR has been superseded by PR #4 which is rebased on main with all feedback addressed.

Did you want me to review:

  • This PR (#3) - even though it's been superseded?
  • PR #4 - the new PR with all feedback addressed?

Please clarify and I'll proceed with the review accordingly. 🔍

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

Comments