diff --git a/.claude/agents/plan-execution-contract-author.md b/.claude/agents/plan-execution-contract-author.md index 95595615..536c22f5 100644 --- a/.claude/agents/plan-execution-contract-author.md +++ b/.claude/agents/plan-execution-contract-author.md @@ -6,9 +6,17 @@ model: inherit tools: ["Read", "Grep", "Glob", "Edit", "Write"] --- -You are the contract-author subagent for the `/plan-execution` orchestrator. Your axis is producing ONLY the contract artifact (interface, type definitions, Zod schema, SQL migration, or other declarative shape) for a task whose DAG `role` is `contract-author`. +You are the contract-author subagent for the `/plan-execution` orchestrator. Your job is to **write contract files** via the `Write` / `Edit` tools — interface, type definitions, Zod schema, SQL migration, or other declarative shape — for a task whose DAG `role` is `contract-author`. You are an executor; your output is the contract file on disk plus a `RESULT:` tag. -You are dispatched in isolation. You see only the input the orchestrator gave you and the corpus on disk. You have no access to the orchestrator's conversation, no awareness of sibling subagents' findings, and no ability to re-dispatch. Your one job is to produce ONLY the contract artifact for the assigned task and return your work plus a `RESULT:` tag as your final message. +You are dispatched in isolation. You see only the input the orchestrator gave you and the corpus on disk. You have no access to the orchestrator's conversation, no awareness of sibling subagents' findings, and no ability to re-dispatch. + +### Narration-mode warning (preemptive) + +A sibling subagent in this skill (`plan-execution-housekeeper`) was found in 2026-05-11 to consistently emit text-only `Tool: Write\n{...}` narration instead of invoking the Write tool API — producing `RESULT: DONE` with `totalToolUseCount: 0`. You share its tool set (`Read`, `Grep`, `Glob`, `Edit`, `Write`) and a similar abstract-task framing. To avoid the same trap: + +> **Your first concrete tool invocation MUST be `Read` on one of the `target_paths` (or, if creating from scratch, `Read` on a neighboring file in the same package).** Do not output `Tool: Read\n{...}` as text — invoke the tool API. If your transcript shows `Tool: Write` as plain text content without a corresponding tool_use event, you are in narration mode and your work has not landed. + +The orchestrator runs `pnpm --filter exec tsc --noEmit` plus `pnpm --filter test` against your `target_paths` after you return; if the files weren't actually written, typecheck fails and the orchestrator round-trips your dispatch. ## Inputs diff --git a/.claude/agents/plan-execution-housekeeper.md b/.claude/agents/plan-execution-housekeeper.md index 3d36da44..5cd6264c 100644 --- a/.claude/agents/plan-execution-housekeeper.md +++ b/.claude/agents/plan-execution-housekeeper.md @@ -1,14 +1,24 @@ --- name: plan-execution-housekeeper color: blue -description: Internal subagent for the /plan-execution orchestrator only. Do not invoke directly — the orchestrator dispatches this subagent in Phase E after running post-merge-housekeeper.mjs to perform semantic state hygiene (ready-set re-derivation, line-cite sweep, set-quantifier reverification, NS-XX auto-create, schema-violation reporting, completion-prose composition) on the merged PR's cross-plan-dependencies.md §6 + downstream-doc context. The orchestrator passes the manifest path + script exit code via the prompt parameter; this subagent edits affected files and returns an extended manifest plus a RESULT: tag. +description: Internal subagent for the /plan-execution orchestrator only. Do not invoke directly — the orchestrator dispatches this subagent in Phase E after running post-merge-housekeeper.mjs to edit the merged PR's cross-plan-dependencies.md §6 entry plus any downstream-doc surface the manifest names. The orchestrator passes the manifest path + script exit code via the prompt parameter; this subagent uses the Edit tool to apply each pending semantic edit, rewrites the manifest via Write, and returns a RESULT: tag. model: inherit tools: ["Read", "Grep", "Glob", "Edit", "Write"] --- -You are the housekeeper subagent for the `/plan-execution` orchestrator. Your axis is semantic state hygiene across the doc corpus after a plan-execution PR squash-merges. +You are the housekeeper subagent for the `/plan-execution` orchestrator. Your job is to **edit files**, then **write the manifest**, then return `RESULT:`. You are an executor, not an analyst — your output is the diff against `docs/architecture/cross-plan-dependencies.md` and the rewritten manifest, not a report describing what should happen. -You are dispatched in isolation. You see only the input the orchestrator gave you (manifest path + script exit code) and the corpus on disk. You have no `Bash`, no `git`, no ability to re-run the script. Your one job is to perform the semantic edits the script can't, validate them, and return a `RESULT:` tag. +You are dispatched in isolation. You see only the input the orchestrator gave you (manifest path + script exit code) and the corpus on disk. You have no `Bash`, no `git`, no ability to re-run the script. + +## Failure-mode warning — read before doing anything else + +This subagent has a documented narration-mode failure (post-mortem 2026-05-11, applied across PR #36 / #42 / #45 / #51 dispatches — all `totalToolUseCount: 0`): the model emits text of the form `Tool: Edit\n{'file_path': '...'}` as content instead of invoking the Edit tool API, then returns `RESULT: DONE` as if the work landed. The orchestrator's Phase E step 5 validator detects this signature (`narration_mode_detected` gap, exit code 1) and routes through the auto-deviation fallback — but the cost is a wasted dispatch. + +Do not be in that failure mode. The rule that prevents it: + +> **Your very first action MUST be a `Read` tool invocation against the manifest path.** Do not output any narrative, plan, or analysis before that Read. If you find yourself about to write text describing what you will do, STOP and invoke `Read` instead. + +If your transcript begins with prose like "I'll start by reading the manifest…" followed by a `Tool: Read` line as text content, you have already failed. The orchestrator's validator will catch it, surface a `narration_mode_detected` gap, and the orchestrator will apply your edits directly. Skip the failure: start with the Read invocation, period. ## Inputs @@ -18,26 +28,52 @@ The orchestrator passes you (via the `prompt` parameter): - Script exit code: 0 / 1 / 2 / 3 / 4 / 5 / ≥6 per spec §5.1 - PR number, plan, phase, optional task-id -If any input is missing or unparseable, return `RESULT: NEEDS_CONTEXT` with a description of the gap. +If any input is missing or unparseable, return `RESULT: NEEDS_CONTEXT` with a description of the gap. Otherwise proceed directly to the first action. + +## First action (mandatory) + +Invoke `Read` on the manifest path. No text before this call. The manifest tells you: + +- `affected_files: string[]` — the files you may edit (and only these) +- `mechanical_edits.status_flip.to_line` — usually contains a `` placeholder you must replace via Edit on the relevant file +- `semantic_work_pending: string[]` — the named work items you must address (one `semantic_edits[item]` entry OR one `concerns[].addressing: item` entry per name) +- `_script_stage` — read-only snapshot; copy through verbatim when you write the manifest back + +## Required tool sequence (in order) + +After the first Read, your invocations should look approximately like this: + +1. **`Read`** the manifest. (Done — this was the first action.) +2. **`Read`** each file in `affected_files` to ground your edits in actual file content. +3. **`Read`** any other file the manifest cites (e.g. the plan body when the manifest references `Plan-NNN:LLL-MMM` line ranges) so the line-cite sweep is grounded in real text, not assumed text. +4. **`Edit`** each file in `affected_files` to apply the semantic edits. The `old_string` MUST be a verbatim copy of text you just Read — do NOT paraphrase or reconstruct. If your `old_string` does not match the file, the Edit fails and the validator catches it. +5. **`Write`** the rewritten manifest (overwrite the manifest path) with populated `semantic_edits`, `concerns`, `result`, and `subagent_completed_at`. Preserve `_script_stage` verbatim. +6. **Return** `RESULT: ` plus the file list and a suggested commit message. + +If steps 4 or 5 are missing from your transcript, you are in narration mode. Stop and restart from step 1 with actual tool invocations. ## Mindset -Your axis is semantic state hygiene across the doc corpus. Mechanical edits are already done; your job is the work that needs to _understand_ the new state. +Your axis is semantic state hygiene across the doc corpus — but the work is concrete: read files, edit files, write the manifest. Mechanical edits the script already applied are visible in the manifest's `mechanical_edits` block; your job is the work the script flagged as `semantic_work_pending`. -For each `semantic_work_pending` item in the manifest, either: +For each `semantic_work_pending` item, either: -- perform the work and add a corresponding `semantic_edits` entry, OR -- explain why it's deferred via a `concerns` entry. +- perform the work (read context → edit file via `Edit` tool → record what you did in `semantic_edits[item]`), OR +- explain why it's deferred via a `concerns` entry whose `addressing` field equals the exact item key. Never silently skip a pending item. +The output that proves you did the work is the file diff. The `semantic_edits` summary is a record of the diff, not a substitute for it. + ## Hard rules +- **No narration of tool calls.** If you find yourself about to output text of the form `Tool: Edit\n{...}` or `Tool call: Read(...)` or any other "I will invoke X with these args" prose, STOP and invoke the actual tool API instead. The orchestrator's validator detects narration-mode output (`totalToolUseCount: 0` + script-stage manifest shape) and treats it as a failure regardless of the `RESULT:` tag you return. +- **First action is `Read` on the manifest.** Any text content before the first Read invocation is a narration-mode signal. - **No git, no Bash.** Mechanically enforced via `tools:` omission. You read + edit files only. - **Do NOT re-run the script.** It has already run; the manifest is its output. - **Edit only files declared in the manifest's `affected_files` list.** Extending the list is permitted when the line-cite sweep finds new affected files; the orchestrator validates the extension is justified (via a `concerns` entry of `kind: affected_files_extension`). - **Every `semantic_work_pending` item gets either a `semantic_edits` entry OR a `concerns` entry explaining deferral.** No silent skipping. -- **Replace any `` placeholders the script left in `Status:` lines** with composed one-line resolution prose matching the NS-12 precedent shape (see `references/post-merge-housekeeper-contract.md` § Status format). +- **Replace any `` placeholders the script left in `Status:` lines** with composed one-line resolution prose matching the NS-12 precedent shape (see `references/post-merge-housekeeper-contract.md` § Status format). The replacement is applied via the `Edit` tool against the file the placeholder lives in — recording the new prose in `semantic_edits[compose_status_completion_prose]` alone is not sufficient; the file must change. - **Schema violations from script exit 5 are surfaced in `concerns` with the violation's own `kind` verbatim (the script emits `"schema_violation"` for `PRs:` block / missing-required-field shapes and singleton kinds like `"auto_create_title_seed_underivable"` for AUTO-CREATE seed failures), plus matching `field` and `ns_id` when the violation carries them, plus a structured remediation hint, then return `RESULT: BLOCKED`.** Never silently fix. The orchestrator validator pairs each violation to its concern via `kind` (`+ field + ns_id` when present); a single generic concern cannot absorb multiple distinct-kind violations. This is the canonical "subagent cannot proceed" exit-state per `references/failure-modes.md` § BLOCKED — the housekeeper's contract is enforce-the-schema-or-halt, identical in shape to a reviewer's ACTIONABLE finding. - **PRs that touch NS-referenced files but whose body does not annotate any NS-XX** are surfaced as `concerns` with `kind: unannotated_ns_referenced_files` and the entry returns `RESULT: DONE_WITH_CONCERNS`. Do NOT silently no-op. The Reviewer/user decides whether to backfill the NS annotation in PR description or accept the omission. - **`manifest._script_stage` is READ-ONLY.** This is the script-embedded snapshot of the four arrays the validator enforces preservation/iteration on (`affected_files`, `schema_violations`, `verification_failures`, `semantic_work_pending`). When you rewrite the manifest, copy `_script_stage` through verbatim. The orchestrator plumbs its own stage-1 conversation-memory copy of these arrays as the validator's authoritative baseline; the manifest-embedded `_script_stage` is a redundant integrity signal — removing the key, replacing it with a non-object, or swapping any of its four fields for non-array values is itself a bypass attempt and surfaces in the validator as a structural-tampering gap forcing a round-trip. See `references/post-merge-housekeeper-contract.md` § `_script_stage` snapshot + orchestrator plumbing for the contract. diff --git a/.claude/skills/plan-execution/SKILL.md b/.claude/skills/plan-execution/SKILL.md index 97d541ad..d1489d74 100644 --- a/.claude/skills/plan-execution/SKILL.md +++ b/.claude/skills/plan-execution/SKILL.md @@ -473,16 +473,70 @@ The phase has 8 steps in this exact order — DO NOT reorder; step 6 (shipment-m - exit code matches `script_exit_code` - `mechanical_edits.status_flip.to_line` contains `` literal placeholder string (subagent fills this) - `affected_files` is a superset (or exact match) of files actually edited by the script — declared list must cover every actual edit so any out-of-scope write is detected before subagent dispatch (per `references/post-merge-housekeeper-contract.md` §Validation invariants line 93) + - **Snapshot for step-5 baseline (REQUIRED — must run BEFORE step 4 dispatch).** Copy the validated stage-1 manifest to a sidecar `.agents/tmp/housekeeper-stage1-PR.json` NOW, while the manifest is still in script-stage shape: + + ```bash + cp .agents/tmp/housekeeper-manifest-PR.json \ + .agents/tmp/housekeeper-stage1-PR.json + ``` + + Step 5's validator reads this sidecar as the untamperable baseline for preservation checks (#7/#9/#10/#11). **Step 5 cannot self-heal a missed snapshot** — by the time step 5 runs, the subagent has mutated the manifest in place; re-copying it then would alias the baseline to already-tampered state and silently disable the preservation checks (Codex PR #53 P1). Skipping the snapshot also forces step 5 into a round-trip loop: the validator surfaces a check #12 baseline-trust gap → exit 2 → no advance. If you discover at step 5 that this sidecar is missing, **halt Phase E and re-run from step 3** — do NOT manufacture the sidecar from the post-dispatch manifest. 4. **Decide routing on `script_exit_code`, then dispatch xor halt** — call `decideHousekeeperRouting({ scriptExitCode })` from `lib/housekeeper-orchestrator-helpers.mjs`. That helper is the single source of truth for the dispatch/halt mapping; edit it (and its unit tests in `scripts/__tests__/post-merge-housekeeper-orchestrator-helpers.test.mjs`) when the contract's exit-code semantics change. The helper returns either `{ action: "dispatch", exitClass: "subagent-handled" }` (exits 0 / 2 / 3 / 5 — happy path, subagent-handled BLOCKED, no-checklist, schema-violation surfacing) or `{ action: "halt", exitClass, reason, surfacePromptTemplate }` (exits 1 / 4 — orchestrator misdispatch; exit ≥6 — script crash; defensive fallback for unrecognized codes). - `action === "halt"` → relay `surfacePromptTemplate` verbatim to the user, halt Phase E, do NOT dispatch the subagent. The manifest reflects a script-stage / orchestrator-stage failure that needs operator action; routing it through the subagent would force the LLM to interpret a malformed/absent manifest and emit a `RESULT:` tag based on hallucinated state. - `action === "dispatch"` → invoke the `plan-execution-housekeeper` subagent with the manifest path. The subagent reads the manifest, composes completion-prose for each `` placeholder using merged-commit context, then re-derives set-quantifier claims by reading ONLY `docs/architecture/cross-plan-dependencies.md` §6 prose (per Plan §Decisions-Locked D-2 — NOT the design spec §6, which is `## 6. Data flow`). Writes back via Edit tool. Returns one of the four canonical exit-states (DONE / DONE_WITH_CONCERNS / NEEDS_CONTEXT / BLOCKED) — no new exit-state per Plan Invariant I-2. -5. **Validate the subagent-stage manifest**: - - `` literal is GONE from every line the script touched - - subagent's edits are confined to `affected_files` (out-of-scope edits → round-trip re-dispatch per `references/failure-modes.md` rule 20: orchestrator offers (a) revert the sprawl OR (b) extend `affected_files` AND add a `concerns` entry of `kind: affected_files_extension` to justify; DONE_WITH_CONCERNS only fires if the subagent picks (b) with weak justification — not on first detection) - - schema_violations from script stage are reconciled (each one either fixed or surfaced in `concerns`) - - `manifest._script_stage` (script-embedded snapshot of `affected_files` / `schema_violations` / `verification_failures` / `semantic_work_pending`) is preserved verbatim — but **the orchestrator MUST plumb the stage-1 snapshot read in step 3 forward as the four `scriptXXX` params on `validateManifestSubagentStage`**. The orchestrator's own conversation-memory record of the stage-1 manifest is the untamperable baseline (frozen in context before subagent dispatch; the subagent runs in a separated context and cannot rewrite what the orchestrator already saw). `manifest._script_stage` on the subagent-emitted manifest is subagent-controlled — the contract treats it as READ-ONLY but a tampered subagent could still clear it; the validator's structural sub-checks (check #12 — removed key, non-object value, non-array fields) catch outright shape tampering as a secondary signal but cannot detect the narrower case where `_script_stage.{field}` is cleared to `[]` while the top-level emit field is also cleared. Omitting any `scriptXXX` param surfaces a `manifest._script_stage is subagent-emitted and may be tampered — orchestrator MUST plumb …` gap so Phase E re-routes through the explicit-plumbing path before a tampered snapshot bypasses preservation checks #7/#9/#10/#11. See `references/post-merge-housekeeper-contract.md` § `_script_stage` immutability. +5. **Validate the subagent-stage manifest** — this step is MECHANICAL, not prose. The `--stage1` sidecar MUST already exist from step 3 (created BEFORE subagent dispatch). Do NOT `cp` the manifest here as a fallback: by step 5 it has been mutated by the subagent, and aliasing the stage-1 baseline to that mutated state silently disables preservation checks #7/#9/#10/#11. If the sidecar is missing, halt and re-run from step 3. + + ```bash + node --experimental-strip-types \ + .claude/skills/plan-execution/scripts/validate-subagent-manifest.mjs \ + .agents/tmp/housekeeper-manifest-PR.json \ + --stage1 .agents/tmp/housekeeper-stage1-PR.json + ``` + + Exit-code routing (single source of truth for what each code means): + - **0 — valid.** Advance to step 6. No round-trip needed. + - **1 — `narration_mode_detected`.** The subagent emitted text-only `Tool: Edit\n{...}` narration without invoking tools; the on-disk manifest matches the script-stage shape verbatim (observed across PR #36/#42/#45/#51 dispatches, all with `totalToolUseCount: 0`). Do NOT re-dispatch — the same prompt reproduces the failure because the root cause is the agent definition's analyst-style framing. Route through **§ Subagent narration auto-deviation fallback** below. + - **2 — generic validation gaps.** Round-trip the subagent with the verbatim gap list as the brief (out-of-scope edits, schema_violations not reconciled, preservation failures, etc.). After two consecutive rounds of generic gaps without narration mode, escalate to **§ Subagent narration auto-deviation fallback** (same playbook — the orchestrator's epistemic position is the same). + - **3 — invocation error** (missing manifest, malformed JSON). Halt Phase E and surface to user; the stage-1 → stage-2 transition broke. + + What the validator checks (authoritative list in `lib/housekeeper-orchestrator-helpers.mjs` § `validateManifestSubagentStage`; SKILL.md does NOT restate the rules — read the source for additions): + - `` literal is GONE from every line the script touched (check #3) + - subagent's edits are confined to `affected_files` (check #7: superset; check on each path that exists + is a regular file). Out-of-scope edits → round-trip per `references/failure-modes.md` rule 20: orchestrator offers (a) revert the sprawl OR (b) extend `affected_files` AND add a `concerns` entry of `kind: affected_files_extension` to justify; DONE_WITH_CONCERNS only fires if the subagent picks (b) with weak justification — not on first detection + - schema_violations from script stage are reconciled (check #5: surfaced 1:1 in `concerns` matched by kind+field+ns_id; check #9: preserved — subagent MUST NOT clear script-stage entries) + - verification_failures preserved (check #10) and force `result === BLOCKED` when non-empty (check #8) + - **narration_mode_detected** (check #13 — added 2026-05-11 after PR #51 post-mortem): manifest matches the script-stage shape verbatim (result non-canonical, semantic_edits empty, concerns empty, subagent_completed_at unset) while pending items remained → the subagent never wrote the manifest. Routes to the auto-deviation fallback (do NOT re-dispatch). + - `manifest._script_stage` (script-embedded snapshot of `affected_files` / `schema_violations` / `verification_failures` / `semantic_work_pending`) is preserved verbatim — but **the orchestrator MUST plumb the stage-1 snapshot via `--stage1` (or as the four `scriptXXX` params if calling the validator directly)**. The orchestrator's own conversation-memory record of the stage-1 manifest is the untamperable baseline (frozen in context before subagent dispatch; the subagent runs in a separated context and cannot rewrite what the orchestrator already saw). `manifest._script_stage` on the subagent-emitted manifest is subagent-controlled — the contract treats it as READ-ONLY but a tampered subagent could still clear it; the validator's structural sub-checks (check #12 — removed key, non-object value, non-array fields) catch outright shape tampering as a secondary signal but cannot detect the narrower case where `_script_stage.{field}` is cleared to `[]` while the top-level emit field is also cleared. Omitting `--stage1` surfaces a `manifest._script_stage is subagent-emitted and may be tampered — orchestrator MUST plumb …` gap so Phase E re-routes through the explicit-plumbing path before a tampered snapshot bypasses preservation checks #7/#9/#10/#11. See `references/post-merge-housekeeper-contract.md` § `_script_stage` immutability. + + ##### § Subagent narration auto-deviation fallback + + Triggered when exit 1 (narration_mode_detected) fires, OR when exit 2 fires on two consecutive rounds without progress. The contract violation ("you orchestrate; you don't implement", hard rule line 41) is waived inside this fallback path because the subagent is structurally unable to complete the work — re-dispatching wastes a turn and the deterministic fix is for the orchestrator to apply the semantic edits directly. + 1. **Apply the semantic edits directly.** Read each item in `_script_stage.semantic_work_pending`. For each one, perform the corresponding edit on the file(s) in `affected_files` using the orchestrator's own Edit tool. The composition rules are the same the subagent would have followed (NS-12 precedent shape for status prose, §6 ready-set re-derivation rules, etc.) — see `references/post-merge-housekeeper-contract.md` § canonical responsibilities for the per-item recipes. + + 2. **Rewrite the manifest.** Set `result` based on the halt-state arrays in `_script_stage`: + - If `_script_stage.schema_violations` OR `_script_stage.verification_failures` is non-empty → set `result: "BLOCKED"`. Validator check #8 enforces `result === "BLOCKED"` when either of those arrays carries entries; setting DONE_WITH_CONCERNS here would deterministically re-fail validation and trap the flow in retry loops (Codex PR #53 R4 P2). + - Otherwise → set `result: "DONE_WITH_CONCERNS"`. + + Populate `semantic_edits` with one entry per pending item (each carrying a short `summary` of what changed + the file + line), and add a `concerns` entry of the form: + + ```json + { + "kind": "orchestrator_applied_semantic_edits_due_to_subagent_narration", + "addressing": "subagent_dispatch_failure", + "summary": "The plan-execution-housekeeper subagent was dispatched time(s) and returned RESULT: DONE / DONE_WITH_CONCERNS with totalToolUseCount: 0 each time (narration mode — emitted Tool: Edit\\n{...} as text content rather than invoking the tool API). The orchestrator applied the semantic edits directly under the SKILL.md Phase E auto-deviation fallback. Tracking the agent-definition fix as a separate concern." + } + ``` + + When the result is BLOCKED, ALSO add a `concerns` entry per `_script_stage.schema_violations` / `_script_stage.verification_failures` entry per the standard reconciliation rules (kind+field+ns_id matched), so check #5 and check #10 are satisfied — auto-deviation does not waive halt-state surfacing, only the no-implement contract. + + Preserve `_script_stage` verbatim. Set `subagent_completed_at` to the dispatch's wall-clock end time. + + 3. **Re-run the validator.** Confirm exit 0 against the rewritten manifest. If gaps remain (e.g., a per-item summary still has a `` because the orchestrator's edit missed a placeholder), iterate on the orchestrator-applied edits, NOT on the subagent. The validator is the canonical signal that the manifest is internally consistent before commit. + + 4. **Advance to step 6** with the orchestrator-applied manifest. The housekeeping commit message can stay as the subagent suggested (or the orchestrator's preferred shape per §commit-msg conventions); the `concerns` entry above carries the auditable trail of why the deviation was necessary. + + This fallback is the codified version of the manual workaround applied in PR #51 housekeeping (post-mortem: TaskList #13). The deviation does NOT require user authorization — the validator's exit-1 signal is the gate; the user sees the deviation surfaced via the `concerns` entry in the housekeeping PR description and can elect to revert if the orchestrator's composition is wrong. 6. **Append the shipment-manifest entry** to the plan body's `## Progress Log` → `### Shipment Manifest` YAML block in `docs/plans/NNN-*.md`. This step explicitly MOVED from before-merge to after-housekeeping per spec §6.1 — the manifest entry records the squash-merge commit hash + audit-derived spec/invariant cites + any subagent concerns, so consumers (preflight Gate 3, future drift detectors) read "shipped + housekept" as one event. - Read the housekeeper manifest at `.agents/tmp/housekeeper-manifest-PR.json` and call `buildFinalManifestEntry({ housekeeperManifestPath, dagTask, notesOverride })` from `lib/housekeeper-orchestrator-helpers.mjs`. The helper extracts the script-emitted `proposed_manifest_entry` (script-knowable fields: phase, task, pr, sha, merged_at, files; audit-derived fields left empty) and merges in the DAG task's `verifies_invariant` and `spec_coverage`. Pass any subagent concerns or partial-ship caveats as `notesOverride` (free-form per-PR commentary). diff --git a/.claude/skills/plan-execution/lib/housekeeper-orchestrator-helpers.mjs b/.claude/skills/plan-execution/lib/housekeeper-orchestrator-helpers.mjs index c4b3ab69..d47e1a4d 100644 --- a/.claude/skills/plan-execution/lib/housekeeper-orchestrator-helpers.mjs +++ b/.claude/skills/plan-execution/lib/housekeeper-orchestrator-helpers.mjs @@ -371,6 +371,58 @@ export function validateManifestSubagentStage({ ); } + // Check #13 — narration-mode detection. + // Documented failure mode: a dispatched housekeeper subagent emits text-only + // "Tool: Edit\n{...}" narration without invoking the actual tool API (observed + // across PR #36/#42/#45/#51 dispatches, all with `totalToolUseCount: 0`). When + // this happens, the on-disk manifest stays in script-stage shape — the subagent + // never wrote it back. Each individual signal below would fire one of the + // existing gaps (check #1 on `result`, check #2 on per-item semantic_edits + // coverage); the COMBINATION is the unmistakable narration signature. + // + // Why a dedicated gap rather than relying on the individual ones: routing. + // A generic check-#1 + check-#2 fan-out invites the orchestrator to re-dispatch + // the subagent with a more prescriptive prompt — which fails the same way, + // because the bug is in the agent definition's analyst-style framing, not in + // the prompt. The narration gap routes the orchestrator straight to the + // SKILL.md Phase E § Subagent narration auto-deviation fallback path + // (orchestrator applies edits directly + records the deviation in concerns), + // skipping the wasted re-dispatch cycle. + const wroteResult = canonicalStates.has(manifest.result); + const semanticEdits = manifest.semantic_edits; + const semanticEditsIsEmpty = + semanticEdits == null || + (Array.isArray(semanticEdits) && semanticEdits.length === 0) || + (typeof semanticEdits === "object" && + !Array.isArray(semanticEdits) && + Object.keys(semanticEdits).length === 0); + const concernsIsEmpty = !Array.isArray(manifest.concerns) || manifest.concerns.length === 0; + const noCompletionStamp = + manifest.subagent_completed_at == null || manifest.subagent_completed_at === ""; + // Base pending-work detection on the TRUSTED stage-1 baseline, not the + // subagent-emitted `manifest.semantic_work_pending` top-level field. The + // narration scenario is precisely "subagent never wrote the manifest" — but + // a worse adversarial / partial-narration shape is one where the subagent + // narrated everything EXCEPT clearing the top-level `semantic_work_pending` + // array to `[]`. With `cleanedSemanticWorkPending` (subagent-controlled), + // that nukes `hasPendingWork → false` and silently demotes the narration + // gap to the generic round-trip path. `effScriptSemanticWorkPending` + // (orchestrator-plumbed > `_script_stage.semantic_work_pending` > null) is + // the immutable contract surface and the only safe source for this check. + const hasPendingWork = + Array.isArray(effScriptSemanticWorkPending) && effScriptSemanticWorkPending.length > 0; + if ( + !wroteResult && + semanticEditsIsEmpty && + concernsIsEmpty && + noCompletionStamp && + hasPendingWork + ) { + gaps.push( + `narration_mode_detected — manifest matches the script-stage shape verbatim (result is non-canonical, semantic_edits empty, concerns empty, subagent_completed_at unset) while ${effScriptSemanticWorkPending.length} pending item(s) remained per the trusted stage-1 baseline. The dispatched subagent never wrote the manifest; its response was text-only narration ("Tool: Edit\\n{...}") rather than actual tool invocations. Cross-check via the dispatch's totalToolUseCount field (expected > 0 for any genuine work). Re-dispatch typically reproduces the same failure (the bug is in the agent-definition framing, not the prompt). Route through SKILL.md Phase E § Subagent narration auto-deviation fallback — the orchestrator applies the semantic edits directly and records the deviation via concerns: [{kind: orchestrator_applied_semantic_edits_due_to_subagent_narration}].`, + ); + } + // Halt-state waiver: when subagent returns BLOCKED or NEEDS_CONTEXT, it stopped // before completing semantic work. The per-item semantic_work_pending pairing // would force false-gap round-trips. Other checks (placeholders, diff --git a/.claude/skills/plan-execution/scripts/__tests__/post-merge-housekeeper-orchestrator-helpers.test.mjs b/.claude/skills/plan-execution/scripts/__tests__/post-merge-housekeeper-orchestrator-helpers.test.mjs index 813a672c..261de04a 100644 --- a/.claude/skills/plan-execution/scripts/__tests__/post-merge-housekeeper-orchestrator-helpers.test.mjs +++ b/.claude/skills/plan-execution/scripts/__tests__/post-merge-housekeeper-orchestrator-helpers.test.mjs @@ -161,6 +161,302 @@ test("validateManifestSubagentStage: fail when pending item is unaddressed", () assert.match(result.gaps[0], /addressing: "ready_set_re_derivation"/); }); +// ---------- check #13: narration_mode_detected ---------- +// +// Documented failure mode from the PR #51 housekeeping post-mortem (TaskList #13): +// the plan-execution-housekeeper subagent has historically emitted text-only +// `Tool: Edit\n{...}` narration without invoking the tool API, leaving the +// on-disk manifest in script-stage shape. Observed across PR #36/#42/#45/#51 +// dispatches — all with `totalToolUseCount: 0`. Each individual symptom +// (result === null, semantic_edits empty, etc.) would fire one of the existing +// gaps; the COMBINATION is the unmistakable narration signature and routes the +// orchestrator directly to the SKILL.md Phase E auto-deviation fallback path +// instead of wasting a re-dispatch on the same prompt. + +test("validateManifestSubagentStage: narration mode — script-stage manifest with pending work fires narration_mode_detected", () => { + // Verbatim replay of the PR #45 housekeeper-manifest-PR45.json on-disk shape after the + // narration-mode dispatch returned: result null, semantic_edits {}, concerns [], + // subagent_completed_at null, 6 pending items unaddressed. + const manifest = { + pr_number: 45, + script_exit_code: 3, + affected_files: [], + semantic_work_pending: [ + "compose_status_completion_prose", + "ready_set_re_derivation", + "line_cite_sweep", + "set_quantifier_reverification", + "ns_auto_create_evaluation", + "unannotated_referenced_files_check", + ], + _script_stage: { + affected_files: [], + schema_violations: [], + verification_failures: [], + semantic_work_pending: [ + "compose_status_completion_prose", + "ready_set_re_derivation", + "line_cite_sweep", + "set_quantifier_reverification", + "ns_auto_create_evaluation", + "unannotated_referenced_files_check", + ], + }, + semantic_edits: {}, + concerns: [], + subagent_completed_at: null, + result: null, + }; + const r = validateManifestSubagentStage({ manifest, ...stageOneFromManifest(manifest) }); + assert.equal(r.valid, false); + const narration = r.gaps.find((g) => g.startsWith("narration_mode_detected")); + assert.ok(narration, "expected narration_mode_detected gap to fire"); + assert.match(narration, /6 pending item\(s\) remained per the trusted stage-1 baseline/); + assert.match(narration, /totalToolUseCount/); + assert.match(narration, /auto-deviation fallback/); + assert.match(narration, /orchestrator_applied_semantic_edits_due_to_subagent_narration/); +}); + +test("validateManifestSubagentStage: narration check does NOT fire when subagent completed work (semantic_edits populated)", () => { + const manifest = { + _script_stage: { + affected_files: [], + schema_violations: [], + verification_failures: [], + semantic_work_pending: ["compose_status_completion_prose"], + }, + semantic_work_pending: ["compose_status_completion_prose"], + semantic_edits: { compose_status_completion_prose: "Composed completion prose for NS-XX..." }, + concerns: [], + affected_files: [], + subagent_completed_at: "2026-05-11T17:50:00Z", + result: "DONE", + }; + const r = validateManifestSubagentStage({ manifest, ...stageOneFromManifest(manifest) }); + assert.equal(r.valid, true); + // Sanity: no narration gap among any optional warnings + if (!r.valid) { + const narration = r.gaps.find((g) => g.startsWith("narration_mode_detected")); + assert.equal(narration, undefined); + } +}); + +test("validateManifestSubagentStage: narration check does NOT fire on legitimate BLOCKED halt (subagent ran but halted on schema_violations)", () => { + // When the subagent legitimately halts with BLOCKED (e.g., on schema_violations + // it cannot fix), semantic_edits stays empty by design — the per-item pairing + // waiver fires (check #2 halt-state waiver). The narration check must respect + // the same waiver: a BLOCKED return is a canonical exit-state (wroteResult: true), + // so the narration signature (!wroteResult) does NOT match. + const manifest = { + _script_stage: { + affected_files: ["docs/architecture/cross-plan-dependencies.md"], + schema_violations: [{ kind: "schema_violation", field: "PRs:", ns_id: "NS-04" }], + verification_failures: [], + semantic_work_pending: ["compose_status_completion_prose"], + }, + schema_violations: [{ kind: "schema_violation", field: "PRs:", ns_id: "NS-04" }], + semantic_work_pending: ["compose_status_completion_prose"], + semantic_edits: {}, + concerns: [ + { + kind: "schema_violation", + field: "PRs:", + ns_id: "NS-04", + addressing: "schema_violation surfacing", + }, + ], + affected_files: ["docs/architecture/cross-plan-dependencies.md"], + subagent_completed_at: "2026-05-11T17:50:00Z", + result: "BLOCKED", + }; + // Use a tmpdir so the affected_files existence check doesn't fail and confuse the test. + const tmpRepo = mkdtempSync(join(tmpdir(), "validate-narration-blocked-")); + try { + mkdirSync(join(tmpRepo, "docs", "architecture"), { recursive: true }); + writeFileSync( + join(tmpRepo, "docs", "architecture", "cross-plan-dependencies.md"), + "(content without placeholder)\n", + ); + const r = validateManifestSubagentStage({ + manifest, + repoRoot: tmpRepo, + ...stageOneFromManifest(manifest), + }); + // BLOCKED is legitimate: schema_violations present + result === BLOCKED + per-entry concern. + // No narration gap should fire. + if (!r.valid) { + const narration = r.gaps.find((g) => g.startsWith("narration_mode_detected")); + assert.equal( + narration, + undefined, + `legitimate BLOCKED halt produced narration gap: ${narration}`, + ); + } + } finally { + rmSync(tmpRepo, { recursive: true, force: true }); + } +}); + +test("validateManifestSubagentStage: narration check does NOT fire when no pending work (vacuous case)", () => { + // If the script emitted no semantic_work_pending (rare — exit 0 happy path with + // all-mechanical-no-semantic edits), a subagent return that's a verbatim copy of + // the input plus `result: DONE` is correct, not narration. The narration check + // requires `hasPendingWork` so this case doesn't false-positive. + const manifest = { + _script_stage: { + affected_files: [], + schema_violations: [], + verification_failures: [], + semantic_work_pending: [], + }, + semantic_work_pending: [], + semantic_edits: {}, + concerns: [], + affected_files: [], + subagent_completed_at: null, + result: null, // even with non-canonical result, no pending work means no narration gap + }; + const r = validateManifestSubagentStage({ manifest, ...stageOneFromManifest(manifest) }); + // The result-canonicality gap (check #1) still fires, but the narration gap does not. + const narration = (r.valid ? [] : r.gaps).find((g) => g.startsWith("narration_mode_detected")); + assert.equal(narration, undefined); +}); + +test("validateManifestSubagentStage: narration check uses trusted stage-1 baseline, not subagent-emitted top-level semantic_work_pending (Codex PR #53 P2)", () => { + // Adversarial / partial-narration shape: subagent narrated EVERYTHING except + // clearing the top-level `manifest.semantic_work_pending` array to `[]`. + // `_script_stage.semantic_work_pending` is preserved (per contract; tampering + // there fires structural-tampering gaps instead). Before the fix, the + // narration check sourced `hasPendingWork` from the subagent-emitted top-level + // field — clearing it would demote the dedicated narration routing into the + // generic round-trip path, missing the auto-deviation fallback entirely. + const manifest = { + _script_stage: { + affected_files: [], + schema_violations: [], + verification_failures: [], + semantic_work_pending: ["compose_status_completion_prose", "ready_set_re_derivation"], + }, + semantic_work_pending: [], // subagent cleared this — adversarial + semantic_edits: {}, + concerns: [], + affected_files: [], + subagent_completed_at: null, + result: null, + }; + // Pass an explicit orchestrator-plumbed stage-1 baseline so the precedence + // chain (explicit > _script_stage > null) is exercised. Use a non-equal + // array to prove the explicit param wins over the embedded snapshot. + const r = validateManifestSubagentStage({ + manifest, + scriptSemanticWorkPending: ["compose_status_completion_prose", "ready_set_re_derivation"], + }); + assert.equal(r.valid, false); + const narration = r.gaps.find((g) => g.startsWith("narration_mode_detected")); + assert.ok(narration, "narration gap must fire even when subagent cleared top-level pending list"); + assert.match(narration, /2 pending item\(s\) remained per the trusted stage-1 baseline/); +}); + +test("validateManifestSubagentStage: narration check fires even when result is a non-canonical truthy string", () => { + // Sanity: a subagent that emitted gibberish `result: "ok"` but didn't actually + // do the work should also fire narration_mode_detected — `wroteResult` checks + // the canonical Set membership, not just truthiness. + const manifest = { + _script_stage: { + affected_files: [], + schema_violations: [], + verification_failures: [], + semantic_work_pending: ["compose_status_completion_prose"], + }, + semantic_work_pending: ["compose_status_completion_prose"], + semantic_edits: {}, + concerns: [], + affected_files: [], + subagent_completed_at: null, + result: "ok", // not in canonical set + }; + const r = validateManifestSubagentStage({ manifest, ...stageOneFromManifest(manifest) }); + assert.equal(r.valid, false); + const narration = r.gaps.find((g) => g.startsWith("narration_mode_detected")); + assert.ok(narration); +}); + +// CLI wrapper exit-code routing — narration alone returns 1, narration co-occurring +// with any other gap returns 2 (auto-deviation requires trusted `_script_stage`; +// any additional gap signals `_script_stage` itself may be untrusted). Codex PR #53 R4 P1. +test("validate-subagent-manifest CLI: exit 1 fires only when narration is the sole gap", () => { + const tmpRepo = mkdtempSync(join(tmpdir(), "validate-cli-exit1-")); + try { + // Manifest that triggers narration alone — fully-honest stage-1 plumbing, no other + // integrity gaps. Stage-1 sidecar is identical to the manifest's _script_stage. + const narrationOnlyManifest = { + _script_stage: { + affected_files: [], + schema_violations: [], + verification_failures: [], + semantic_work_pending: ["compose_status_completion_prose"], + }, + semantic_work_pending: ["compose_status_completion_prose"], + affected_files: [], + semantic_edits: {}, + concerns: [], + subagent_completed_at: null, + result: null, + }; + const manifestPath = join(tmpRepo, "manifest.json"); + const stage1Path = join(tmpRepo, "stage1.json"); + writeFileSync(manifestPath, JSON.stringify(narrationOnlyManifest)); + writeFileSync(stage1Path, JSON.stringify(narrationOnlyManifest._script_stage)); + + const cliResolved = join(import.meta.dirname, "..", "validate-subagent-manifest.mjs"); + + const runCli = (extraArgs) => { + try { + const stdout = execSync( + `node --experimental-strip-types ${cliResolved} ${manifestPath}${extraArgs}`, + { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }, + ); + return { status: 0, stdout, stderr: "" }; + } catch (e) { + return { + status: e.status, + stdout: e.stdout?.toString() ?? "", + stderr: e.stderr?.toString() ?? "", + }; + } + }; + + // Narration-only invocation → expect exit 1 (every gap is "definitional" for narration: + // result non-canonical, narration_mode_detected, per-item unaddressed). + const narrationOnly = runCli(` --stage1 ${stage1Path}`); + assert.equal( + narrationOnly.status, + 1, + `narration-only should exit 1; got ${narrationOnly.status}. stderr: ${narrationOnly.stderr}`, + ); + const parsed = JSON.parse(narrationOnly.stdout); + assert.equal(parsed.narration_detected, true); + + // Mixed-gap invocation — omit --stage1 so check #12 baseline-trust ALSO fires. + // narrationDetected is still true, but a non-definitional integrity gap is present, + // so exit should drop to 2. + const mixed = runCli(""); + const parsedMixed = JSON.parse(mixed.stdout); + assert.equal(parsedMixed.narration_detected, true, "narration still fires"); + assert.ok( + parsedMixed.gaps.some((g) => g.includes("subagent-emitted and may be tampered")), + `expected baseline-trust gap when --stage1 omitted; got ${JSON.stringify(parsedMixed.gaps)}`, + ); + assert.equal( + mixed.status, + 2, + `mixed-gap (narration + baseline-trust) must exit 2 so the orchestrator round-trips/halts rather than auto-deviating off untrusted _script_stage; got ${mixed.status}. stderr: ${mixed.stderr}`, + ); + } finally { + rmSync(tmpRepo, { recursive: true, force: true }); + } +}); + test("validateManifestSubagentStage: fails when affected_files entry is missing from disk", () => { // Codex P1 (PR #33 R6 / Finding 10): the placeholder-scan loop's `if (existsSync(full))` // gate had no else-branch, so a subagent run that DELETED a declared affected_files entry diff --git a/.claude/skills/plan-execution/scripts/validate-subagent-manifest.mjs b/.claude/skills/plan-execution/scripts/validate-subagent-manifest.mjs new file mode 100755 index 00000000..18a0de0d --- /dev/null +++ b/.claude/skills/plan-execution/scripts/validate-subagent-manifest.mjs @@ -0,0 +1,156 @@ +#!/usr/bin/env node +// validate-subagent-manifest.mjs — orchestrator wrapper for `validateManifestSubagentStage`. +// Authoritative validator contract: ../lib/housekeeper-orchestrator-helpers.mjs § validateManifestSubagentStage. +// +// Usage (Phase E step 5, after housekeeper subagent dispatch returns): +// +// node --experimental-strip-types \ +// .claude/skills/plan-execution/scripts/validate-subagent-manifest.mjs \ +// .agents/tmp/housekeeper-manifest-PR.json \ +// [--stage1 .agents/tmp/housekeeper-stage1-PR.json] +// +// Exit codes: +// 0 — manifest valid (subagent contract satisfied) +// 1 — narration_mode_detected; orchestrator should route to SKILL.md Phase E +// § Subagent narration auto-deviation fallback (do NOT re-dispatch — the +// same prompt reproduces the failure) +// 2 — other validation gaps; orchestrator should round-trip the subagent +// with the gap list +// 3 — invocation error (missing/unreadable manifest, malformed JSON) +// +// Stdout shape (single JSON object on a single line for the orchestrator's +// programmatic consumption — keep it cut-paste-into-jq friendly): +// +// { valid: bool, narration_detected: bool, gaps: string[] } +// +// Stderr is reserved for diagnostics + the human-readable gap list. The +// orchestrator can `cat the JSON | jq ...` from stdout while a tail of the +// stderr is surfaced to the user when an exit-2 round-trip is required. + +import { readFileSync, existsSync } from "node:fs"; +import process from "node:process"; + +import { validateManifestSubagentStage } from "../lib/housekeeper-orchestrator-helpers.mjs"; + +const REPO_ROOT = process.cwd(); + +// Use `process.exitCode = main()` rather than `process.exit(N)`. The script +// writes its machine-readable JSON to stdout before terminating; when stdout +// is a pipe (e.g., `... | jq`, CI log collectors), stdout writes are async +// and `process.exit()` does not wait for them to drain — Node can terminate +// mid-write, truncating the JSON payload and breaking orchestrator parsing +// even when validation succeeded. Setting `exitCode` and letting the event +// loop drain naturally avoids the race. (Codex PR #53 R3 P1.) +function main() { + const args = process.argv.slice(2); + if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + process.stderr.write( + "Usage: validate-subagent-manifest.mjs [--stage1 ]\n", + ); + return 3; + } + + const manifestPath = args[0]; + let stage1Path = null; + for (let i = 1; i < args.length; i++) { + if (args[i] === "--stage1") { + // Trailing `--stage1` with no path argument (or empty string) is a + // malformed invocation: silently ignoring it would route the orchestrator + // through the stage-1-absent fallback (validator uses subagent-emitted + // `manifest._script_stage` as the baseline), changing exit-code semantics + // from 3 (invocation error) to 1/2 (narration / round-trip). Fail fast. + const next = args[i + 1]; + if (next == null || next === "") { + process.stderr.write("error: --stage1 requires a non-empty path argument\n"); + return 3; + } + stage1Path = next; + i++; + } + } + + if (!existsSync(manifestPath)) { + process.stderr.write(`error: manifest not found at ${manifestPath}\n`); + return 3; + } + + let manifest; + try { + manifest = JSON.parse(readFileSync(manifestPath, "utf8")); + } catch (e) { + process.stderr.write(`error: manifest at ${manifestPath} is not valid JSON: ${e.message}\n`); + return 3; + } + + // Stage-1 snapshot plumbing — when the orchestrator wrote a sidecar + // `housekeeper-stage1-PR.json` capturing the script-stage manifest BEFORE + // subagent dispatch (Phase E step 3), pass it here so the validator's + // preservation checks (#7/#9/#10/#11) compare against the untamperable + // baseline. When omitted, the validator falls back to `manifest._script_stage` + // (subagent-controlled — surfaces a baseline-trust gap that routes through + // exit 2 / round-trip, so explicit plumbing is functionally required). + let stage1 = null; + if (stage1Path) { + if (!existsSync(stage1Path)) { + process.stderr.write(`error: stage-1 snapshot not found at ${stage1Path}\n`); + return 3; + } + try { + stage1 = JSON.parse(readFileSync(stage1Path, "utf8")); + } catch (e) { + process.stderr.write( + `error: stage-1 snapshot at ${stage1Path} is not valid JSON: ${e.message}\n`, + ); + return 3; + } + } + + const result = validateManifestSubagentStage({ + manifest, + repoRoot: REPO_ROOT, + scriptAffectedFiles: stage1?.affected_files ?? null, + scriptSchemaViolations: stage1?.schema_violations ?? null, + scriptVerificationFailures: stage1?.verification_failures ?? null, + scriptSemanticWorkPending: stage1?.semantic_work_pending ?? null, + }); + + const gaps = result.valid ? [] : result.gaps; + const narrationDetected = gaps.some((g) => g.startsWith("narration_mode_detected")); + + // Single-line JSON for the orchestrator's programmatic path. Pretty-printed + // gap list goes to stderr below so a human reading the output can scan it. + process.stdout.write( + JSON.stringify({ valid: result.valid, narration_detected: narrationDetected, gaps }) + "\n", + ); + + if (!result.valid) { + process.stderr.write(`\n${gaps.length} gap(s):\n`); + gaps.forEach((g, i) => process.stderr.write(` ${i + 1}. ${g}\n`)); + } + + // Exit-code routing — narration_mode_detected wins exit 1 only when every + // gap is "definitional" for narration (i.e., directly downstream of the + // subagent doing nothing). The narration scenario inherently produces three + // gap classes: + // - check #1: `result` non-canonical + // - check #2: per-item semantic_work_pending unaddressed (one per item) + // - check #13: narration_mode_detected itself + // Any OTHER gap (baseline-trust #12, preservation #7/#9/#10/#11, halt-state + // #8, schema-surfacing #5, placeholder leak, on-disk affected_files + // failures, etc.) signals state the auto-deviation fallback cannot trust — + // applying edits from `_script_stage` then would compound the bug. Mixed + // cases must round-trip / halt through exit 2. (Codex PR #53 R4 P1.) + const DEFINITIONAL_NARRATION_GAP_PATTERNS = [ + /^narration_mode_detected/, + /^`result` is /, + / listed in semantic_work_pending but absent from semantic_edits and concerns /, + ]; + const allGapsAreDefinitional = gaps.every((g) => + DEFINITIONAL_NARRATION_GAP_PATTERNS.some((p) => p.test(g)), + ); + if (narrationDetected && allGapsAreDefinitional) return 1; + if (!result.valid) return 2; + return 0; +} + +process.exitCode = main();