sequant 2.11.0 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +13 -0
  4. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  5. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
  7. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  8. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  9. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  10. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  11. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  12. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  13. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  14. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  18. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  19. package/dist/src/commands/doctor.js +20 -18
  20. package/dist/src/commands/ready.js +4 -0
  21. package/dist/src/lib/ac-linter.js +26 -0
  22. package/dist/src/lib/ac-parser.d.ts +40 -0
  23. package/dist/src/lib/ac-parser.js +202 -16
  24. package/dist/src/lib/markdown-fence.d.ts +24 -0
  25. package/dist/src/lib/markdown-fence.js +51 -0
  26. package/dist/src/lib/mcp-config.d.ts +24 -0
  27. package/dist/src/lib/mcp-config.js +51 -0
  28. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  29. package/dist/src/lib/scope/analyzer.js +7 -1
  30. package/dist/src/lib/settings.d.ts +38 -1
  31. package/dist/src/lib/settings.js +14 -0
  32. package/dist/src/lib/system.d.ts +7 -3
  33. package/dist/src/lib/system.js +7 -3
  34. package/dist/src/lib/test-tautology-detector.js +50 -3
  35. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  36. package/dist/src/lib/workflow/batch-executor.js +77 -5
  37. package/dist/src/lib/workflow/config-resolver.js +1 -0
  38. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  39. package/dist/src/lib/workflow/drivers/claude-code.js +9 -3
  40. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  41. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  42. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  43. package/dist/src/lib/workflow/phase-executor.js +50 -4
  44. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  45. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  46. package/dist/src/lib/workflow/ready-gate.d.ts +25 -1
  47. package/dist/src/lib/workflow/ready-gate.js +81 -11
  48. package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
  49. package/dist/src/lib/workflow/run-log-schema.js +40 -0
  50. package/dist/src/lib/workflow/state-schema.d.ts +5 -1
  51. package/dist/src/lib/workflow/state-schema.js +8 -1
  52. package/dist/src/lib/workflow/types.d.ts +14 -0
  53. package/package.json +2 -2
  54. package/templates/hooks/pre-tool.sh +108 -17
  55. package/templates/skills/exec/SKILL.md +1 -1
  56. package/templates/skills/fullsolve/SKILL.md +62 -9
  57. package/templates/skills/loop/SKILL.md +71 -12
  58. package/templates/skills/merger/SKILL.md +32 -3
  59. package/templates/skills/qa/SKILL.md +247 -2
  60. package/templates/skills/spec/SKILL.md +9 -5
  61. package/templates/skills/test/SKILL.md +1 -1
@@ -59,7 +59,7 @@ When running as part of an orchestrated workflow (e.g., `sequant run` or `/fulls
59
59
  1. **Use provided worktree** - Work in `SEQUANT_WORKTREE` path directly
60
60
  2. **Use `SEQUANT_ISSUE`** - Skip issue number parsing from invocation
61
61
  3. **Reduce GitHub comment frequency** - Defer updates to orchestrator
62
- 4. **Trust issue context** - Orchestrator has already validated issue
62
+ 4. **Trust embedded context** - when the orchestrator injected a `<!-- SEQUANT_PROMPT_CONTEXT -->` block into this invocation, it is the authoritative QA-findings source; do not re-fetch from GitHub (see Step 1A below)
63
63
 
64
64
  **Behavior when standalone (SEQUANT_ORCHESTRATOR is NOT set):**
65
65
 
@@ -81,10 +81,20 @@ When running as part of an orchestrated workflow (e.g., `sequant run` or `/fulls
81
81
 
82
82
  #### Step 1A: Orchestrated Mode (SEQUANT_ORCHESTRATOR is set)
83
83
 
84
- When `SEQUANT_ORCHESTRATOR` is set, read QA findings from the GitHub issue comments instead of a log file:
84
+ <!-- BEGIN: step-1a-context-source (#960) -->
85
+
86
+ **Check your own invocation first — before fetching anything.** The orchestrator (`ready-gate.ts` or `batch-executor.ts`, via `buildLoopContext`) may already have embedded the QA findings directly into the prompt that invoked this `/loop` run, wrapped in a `<!-- SEQUANT_PROMPT_CONTEXT -->` / `<!-- /SEQUANT_PROMPT_CONTEXT -->` sentinel pair (`getPhasePrompt`, `src/lib/workflow/phase-executor.ts`). When that sentinel is present in your own invocation text:
87
+
88
+ - Treat the text between the markers as the authoritative QA findings (`qa_comment` below).
89
+ - **Do not** fetch `gh issue view` for QA comments — skip straight to "Parsing QA comment" below, using the embedded text in place of the fetched comment.
90
+ - **The embedded block's gap list IS your `recommendations`.** The block is not QA-comment-shaped — the parsing snippets below (verdict grep aside) won't extract anything from it. Read the list under `Gaps to address:` (ready-gate) or `QA Gaps:` (batch-executor) directly as the findings to fix; the `QA Verdict:` line is the verdict.
91
+ - That gap list is already `fixableGaps`-filtered by the orchestrator (`selectFixableGaps` in `batch-executor.ts`, or ready-gate's own filter), so the `document`/`pause_for_human` exclusion in "Excluded Finding Classes" below is redundant for it — it exists for the fetched-comment fallback. But this applies **only to the gap list**: any `Suggestions:` or `Last output:` sections in the block (batch-executor only) are raw, unfiltered context — use them for understanding, never as additional findings to fix.
92
+
93
+ **Only when the sentinel is absent** from your invocation — a standalone-style dispatch, or an orchestrator that doesn't inject `promptContext` — fall back to reading QA findings from the GitHub issue comments instead of a log file:
85
94
 
86
95
  ```bash
87
- # Check if we're in orchestrated mode
96
+ # Fallback: no embedded SEQUANT_PROMPT_CONTEXT sentinel in the invocation.
97
+ # Fetch QA findings from GitHub comments instead.
88
98
  if [[ -n "$SEQUANT_ORCHESTRATOR" ]]; then
89
99
  echo "Orchestrated mode detected (orchestrator: $SEQUANT_ORCHESTRATOR)"
90
100
 
@@ -96,6 +106,8 @@ if [[ -n "$SEQUANT_ORCHESTRATOR" ]]; then
96
106
  fi
97
107
  ```
98
108
 
109
+ <!-- END: step-1a-context-source (#960) -->
110
+
99
111
  **How to identify QA comments:**
100
112
 
101
113
  | Pattern | Meaning |
@@ -103,7 +115,7 @@ fi
103
115
  | `## QA Review for Issue #N` | QA phase comment header |
104
116
  | `### Verdict:` | Contains AC_NOT_MET, AC_MET_BUT_NOT_A_PLUS, etc. |
105
117
  | `### AC Coverage` | Table with MET/NOT_MET/PARTIALLY_MET statuses |
106
- | `### Required Fixes` or `### Recommendations` | Actionable items to fix |
118
+ | `<!-- SEQUANT_QA_GAPS: {...} -->` | Structured findings the primary source for `recommendations` (#937; see below) |
107
119
 
108
120
  **Parsing QA comment:**
109
121
 
@@ -116,14 +128,58 @@ verdict=$(echo "$qa_comment" | grep -oE "Verdict:\s*\w+" | head -1 | awk '{print
116
128
  # Extract NOT_MET AC items
117
129
  not_met_acs=$(echo "$qa_comment" | grep -E "NOT_MET|PARTIALLY_MET" || true)
118
130
 
119
- # Extract recommendations section
120
- recommendations=$(echo "$qa_comment" | sed -n '/### Required Fixes/,/###/p' | head -n -1)
131
+ # #937: prefer the structured SEQUANT_QA_GAPS marker over prose scraping.
132
+ # `### Required Fixes` was removed no QA output template ever emitted that
133
+ # heading, so the old `sed -n '/### Required Fixes/,/###/p'` extraction was
134
+ # dead code that always produced an empty `recommendations`. The marker's
135
+ # payload is an array of objects, so a single node -e does the stripping,
136
+ # matching, AND JSON.parse/filter in one script — instead of hand-rolled
137
+ # jq/awk — mirroring the "do real parsing in TS/JS, keep shell to presence
138
+ # checks" split from the #871 drift-guard lesson (the same taxonomy filter
139
+ # also lives in `selectFixableGaps`, `src/lib/workflow/phase-executor.ts` —
140
+ # this is the shell-side mirror for standalone/orchestrated-comment-scrape
141
+ # mode, not a duplicate contract).
142
+ #
143
+ # Stripping fenced/inline code before matching mirrors `stripMarkdownCode`
144
+ # (src/lib/workflow/phase-detection.ts) — a bare grep/sed would mistake a
145
+ # marker quoted inside a code fence (e.g. this skill's own "QA Log Example"
146
+ # below) for a real one. Latest-wins if more than one marker is present,
147
+ # matching `parseQaGapsMarker`'s semantics.
148
+ recommendations=$(printf '%s' "$qa_comment" | node -e '
149
+ let input = "";
150
+ process.stdin.on("data", (d) => (input += d));
151
+ process.stdin.on("end", () => {
152
+ const stripped = input
153
+ .replace(/`{3,}[\s\S]*?`{3,}|~{3,}[\s\S]*?~{3,}/g, "")
154
+ .replace(/`[^`\n]+`/g, "");
155
+ const matches = [...stripped.matchAll(/<!-- SEQUANT_QA_GAPS: (\{[\s\S]*?\}) -->/g)];
156
+ if (matches.length === 0) return;
157
+ try {
158
+ const payload = JSON.parse(matches[matches.length - 1][1]);
159
+ const findings = Array.isArray(payload.findings) ? payload.findings : [];
160
+ for (const f of findings) {
161
+ if (f.recommendedAction === "document" || f.recommendedAction === "pause_for_human") continue;
162
+ console.log(`- ${f.description}`);
163
+ }
164
+ } catch {
165
+ // Malformed marker JSON — leave recommendations empty, not a crash.
166
+ }
167
+ });
168
+ ' || true)
169
+
170
+ # No marker (QA output predates #937, or emitted an empty findings array
171
+ # with everything filtered) — fall back to the AC-table NOT_MET/PARTIALLY_MET
172
+ # rows already captured above; there is no prose "Required Fixes" section to
173
+ # scrape.
174
+ if [[ -z "$recommendations" ]]; then
175
+ recommendations="$not_met_acs"
176
+ fi
121
177
  ```
122
178
 
123
- **If no QA comment found in orchestrated mode:**
124
- 1. Log a clear error: `"Warning: No QA comment found in issue #N"`
179
+ **If neither an embedded `SEQUANT_PROMPT_CONTEXT` block nor a matching GitHub QA comment is found in orchestrated mode:**
180
+ 1. Log a clear error: `"Warning: No embedded context or QA comment found for issue #N"`
125
181
  2. Fall back to Step 1B (log file) as a recovery mechanism
126
- 3. If log file also doesn't exist, exit with error
182
+ 3. If log file also doesn't exist, exit with error — do not silently report "no actionable issues"; a silent miss here is the exact failure mode this section exists to prevent (#960)
127
183
 
128
184
  #### Step 1B: Standalone Mode (no SEQUANT_ORCHESTRATOR)
129
185
 
@@ -178,13 +234,14 @@ Some QA findings are real but **not fixable by a code change**. Feeding them to
178
234
  | Class | Marker in the QA comment | Why it is not actionable |
179
235
  |-------|--------------------------|--------------------------|
180
236
  | Infra-blocked CI | `<!-- qa:ci-infra-blocked -->` | Every check failed without a runner ever starting (e.g. an Actions spending-limit lockout). The cause is account/infrastructure state; no diff can turn the checks green. See `qa/SKILL.md` § "Infra-Blocked CI Detection". |
237
+ | `document`/`pause_for_human` findings | `<!-- SEQUANT_QA_GAPS: {...} -->`, per-finding `recommendedAction` | Quality/polish gaps deliberately deferred (`document`), or findings needing a human decision the loop can't make on its own (`pause_for_human`) — see #937. Filtered directly out of the `recommendations` extraction above (Step 1A), not by rebinding `qa_comment` — the marker's structure makes a per-finding filter more precise than a text-range delete. |
181
238
 
182
239
  ```bash
183
240
  # Drop the marked findings when QA flagged CI as infra-blocked, so its
184
241
  # NEEDS_VERIFICATION AC items are never mistaken for actionable findings.
185
242
  # Range is EXCLUSIVE of the next `### ` header — a `sed '/marker/,/^### /d'`
186
243
  # range would delete that header too and orphan the following section's
187
- # content (verified: it silently swallows `### Required Fixes`).
244
+ # content (verified against this file's own `### AC Coverage` header).
188
245
  qa_comment_raw="$qa_comment" # keep the original so the cause can be reported verbatim
189
246
 
190
247
  if echo "$qa_comment" | grep -q '<!-- qa:ci-infra-blocked -->'; then
@@ -513,13 +570,15 @@ For each iteration, output:
513
570
 
514
571
  ### Verdict: AC_NOT_MET
515
572
 
516
- ### Required Fixes
573
+ ### Next Steps
517
574
 
518
575
  1. Complete URL validation in ExternalUrlTab
519
576
  2. Add focal point persistence in updateArticleImage action
577
+
578
+ <!-- SEQUANT_QA_GAPS: {"findings":[{"category":"requirement_gap","evidence":"AC-3 row: PARTIALLY_MET — External URL validation incomplete","description":"Complete URL validation in ExternalUrlTab","recommendedAction":"fix_now","affectedAcs":["AC-3"]},{"category":"requirement_gap","evidence":"AC-4 row: NOT_MET — Focal point not persisted to database","description":"Add focal point persistence in updateArticleImage action","recommendedAction":"fix_now","affectedAcs":["AC-4"]}]} -->
520
579
  ```
521
580
 
522
- **Parsed Output:**
581
+ **Parsed Output** (marker-derived — this is the real emission shape, see §"How to build the marker" in `qa/SKILL.md`):
523
582
  - Last phase: `/qa`
524
583
  - Verdict: `AC_NOT_MET`
525
584
  - Issues to fix:
@@ -540,6 +540,32 @@ On a *failure* exit instead, the release is done at the halt site — the
540
540
  regression gate (Step 7) or "Error Handling" — never both, so exactly one release
541
541
  runs per invocation.
542
542
 
543
+ <!-- BEGIN: named-set-boundary (#961) -->
544
+ ## Named-Set Boundary (REQUIRED)
545
+
546
+ `/merger <issue-numbers>` is the user's consent to merge **exactly those
547
+ issues** — invoking it is the explicit authorization, the same way `sequant
548
+ merge`/`sequant merge --watch` are explicit and human-triggered (#958's
549
+ Non-Goals). That consent does not extend past the named set: never let
550
+ Dependency Detection or Stacked PR Detection below pull an**other** issue's PR
551
+ into `gh pr merge` just because it happens to unblock or precede a named one.
552
+
553
+ **Rule:** if merge ordering requires a PR whose issue number is **not** in the
554
+ invocation's arg list, halt before merging anything and report it — do not
555
+ widen the merge silently, and do not treat the stacked-PR "Continue anyway?
556
+ (y/N)" confirmation as license to include an unnamed predecessor. Offer the
557
+ widened command for the user to re-run:
558
+
559
+ ```text
560
+ ❌ Out-of-named-set dependency
561
+ #101 depends on #100, but #100 was not in your /merger invocation.
562
+ Halting before any merge.
563
+ To include it: /merger 100 101
564
+ ```
565
+
566
+ This rule governs both sections below.
567
+ <!-- END: named-set-boundary (#961) -->
568
+
543
569
  ## Dependency Detection
544
570
 
545
571
  Parse dependencies from issue body or comments:
@@ -557,7 +583,9 @@ Labels: depends-on/10
557
583
  gh issue view <issue> --json body,labels | jq '.body, .labels[].name'
558
584
  ```
559
585
 
560
- If dependencies found, enforce merge order.
586
+ If dependencies found, enforce merge order **among the named set**. If a
587
+ dependency's issue number falls outside the named set, apply the Named-Set
588
+ Boundary rule above: halt and report instead of merging it.
561
589
 
562
590
  ### Stacked PR Detection (#605)
563
591
 
@@ -578,8 +606,9 @@ gh pr view <PR_NUMBER> --json baseRefName,body | \
578
606
 
579
607
  1. Extract the stack manifest from each PR's body (`Part of stack: #100 → #101 (this) → #102`).
580
608
  2. Treat the order in the manifest as the merge order — earlier entries land first.
581
- 3. If the user requests an out-of-order merge (e.g. `/merger 102 100 101` for the stack above), **warn before proceeding** and recommend the manifest order.
582
- 4. GitHub auto-updates the dependent PR's base when its predecessor merges, so once the order is correct no manual rebasing is needed.
609
+ 3. If **every** predecessor is in the named set but requested out of order (e.g. `/merger 102 100 101` for the stack above), **warn before proceeding** and recommend the manifest order.
610
+ 4. If a predecessor is **not** in the named set at all (e.g. `/merger 102` alone for that stack), the Named-Set Boundary rule applies: halt and report — do not fall through to the "Continue anyway? (y/N)" prompt, since that prompt is for reordering named issues, not for merging an unnamed one.
611
+ 5. GitHub auto-updates the dependent PR's base when its predecessor merges, so once the order is correct no manual rebasing is needed.
583
612
 
584
613
  **Warning template:**
585
614
 
@@ -2402,6 +2402,181 @@ Two classes are explicitly **not** findings. Legitimate imperative *requirements
2402
2402
 
2403
2403
  ---
2404
2404
 
2405
+ ### 6h. Declared-Evidence Execution (REQUIRED for evidence-bearing ACs)
2406
+
2407
+ **Purpose:** For any AC with a declared `Evidence:` clause naming a runnable command, verify that exact command was executed against this diff (or a captured run of it verified) before marking the AC `MET`. Closes the #853 "marked MET by construction" path (#938) — a declared, checkable claim that goes unexecuted is a bug, not a checkbox.
2408
+
2409
+ **When to apply:** Any AC whose stored state includes a non-empty `evidence` field naming a backtick-quoted command (i.e., `verificationMethod` resolved to `unit_test` or `integration_test` via declaration, not inference — see `resolveVerificationMethod` in `ac-parser.ts`). Skip entirely when no AC declares a runnable command (cheap short-circuit).
2410
+
2411
+ **How to perform:**
2412
+
2413
+ ```bash
2414
+ # Retrieve declared evidence per AC from state.
2415
+ npx tsx -e '
2416
+ (async () => {
2417
+ const sm = await import("./src/lib/workflow/state-manager.ts");
2418
+ const mgr = new sm.StateManager(process.cwd());
2419
+ const ac = await mgr.getAcceptanceCriteria(<issue-number>);
2420
+ const declared = (ac?.items ?? []).filter(
2421
+ (i) => i.evidence && /`[^`]+`/.test(i.evidence),
2422
+ );
2423
+ console.log(JSON.stringify(declared.map((i) => ({ id: i.id, evidence: i.evidence })), null, 2));
2424
+ })();
2425
+ '
2426
+ ```
2427
+
2428
+ For each AC returned, **execute the exact backtick-quoted command** — or verify a captured run of it from earlier in this session's tool output — and record its exit code / pass-fail result before marking that AC `MET`. Do not mark a declared-evidence AC `MET` on reasoning alone; that is precisely the #853 gap this section closes.
2429
+
2430
+ **Status outcomes:**
2431
+
2432
+ | Status | Criteria |
2433
+ |--------|----------|
2434
+ | **Complete** | Every declared-evidence AC's command was executed (or a captured prior run verified) this QA pass |
2435
+ | **Incomplete** | One or more declared-evidence ACs have no executed/verified command |
2436
+ | **N/A** | No AC declares evidence with a runnable command |
2437
+
2438
+ **AC marking and verdict gating:**
2439
+
2440
+ - A declared-evidence AC whose command was NOT executed or verified this pass → AC status = `PENDING` (increments `pending_count`, same mechanism as the Manual Test AC Enforcement in step 3a) — it cannot be marked `MET` on reasoning alone.
2441
+ - `declared_evidence_status == "Incomplete"` also floors the verdict directly — see step 4's `Section 6h` branch.
2442
+
2443
+ **Output Format:**
2444
+
2445
+ ```markdown
2446
+ ### Declared-Evidence Execution
2447
+
2448
+ | AC | Evidence | Executed? | Result |
2449
+ |----|----------|-----------|--------|
2450
+ | AC-N | `npm test -- reset-expiry` | Yes | ✅ 4 passed |
2451
+ | AC-M | `npx sequant doctor --help` | No | ⚠️ Not run — AC marked PENDING |
2452
+
2453
+ **Status:** Complete / Incomplete / N/A
2454
+ ```
2455
+
2456
+ ---
2457
+
2458
+ ### 6i. Mutation Verification (REQUIRED for gate-test ACs)
2459
+
2460
+ **Purpose:** CLAUDE.md's testing rule — "Gate tests ship with a recorded mutation result: delete the thing it asserts, confirm exactly that test fails, restore, and record the result" — was honor-system prose until now: nothing parsed or checked the recorded result, so compliance was invisible (#830, and the same "prose only, and therefore unenforceable" defect class #834 fixed for `/qa`'s own §7 gates). This section promotes that record to a parseable `SEQUANT_MUTATION` PR-body marker and gates on it.
2461
+
2462
+ **When to apply:** Any AC whose declared `evidence` text matches the CLAUDE.md gate-test definition — a fixture-exists / section-present / flag-wired assertion (see `isGateTestEvidence` in `ac-parser.ts`). Skip entirely when no AC in the diff is a gate-test AC (cheap short-circuit — mark N/A).
2463
+
2464
+ > **Scope note (v1):** gate-test ACs only, not every AC with a declared runnable-command `Evidence:` clause. The population widens after a backtest measures the real authoring burden (#939 AC-5) — see that issue for the human-decision record if this scope is revisited.
2465
+
2466
+ **How to perform:**
2467
+
2468
+ ```bash
2469
+ # 1. Identify in-scope (gate-test) ACs for this issue.
2470
+ npx tsx -e '
2471
+ (async () => {
2472
+ const sm = await import("./src/lib/workflow/state-manager.ts");
2473
+ const acp = await import("./src/lib/ac-parser.ts");
2474
+ const mgr = new sm.StateManager(process.cwd());
2475
+ const ac = await mgr.getAcceptanceCriteria(<issue-number>);
2476
+ const gateTests = (ac?.items ?? []).filter(
2477
+ (i) => i.evidence && acp.isGateTestEvidence(i.evidence),
2478
+ );
2479
+ console.log(JSON.stringify(gateTests.map((i) => ({ id: i.id, evidence: i.evidence })), null, 2));
2480
+ })();
2481
+ '
2482
+
2483
+ # 2. Parse SEQUANT_MUTATION markers from the PR body — this single call
2484
+ # both parses AND rejects: given the diff's test-file paths, each
2485
+ # returned marker carries a "classification" ("valid" or
2486
+ # "test_not_in_diff"), so a marker naming a test absent from the diff
2487
+ # is surfaced, not silently dropped.
2488
+ npx tsx -e '
2489
+ (async () => {
2490
+ const mm = await import("./src/lib/workflow/mutation-marker.ts");
2491
+ const prBody = process.argv[1];
2492
+ const diffTestFiles = process.argv.slice(2);
2493
+ const markers = mm.parseMutationMarkers(prBody, diffTestFiles);
2494
+ const byAc = new Map(markers.map((m) => [m.ac, m]));
2495
+ for (const [ac, marker] of byAc) {
2496
+ console.log(ac, marker.classification, marker.failedTest);
2497
+ }
2498
+ })();
2499
+ ' -- "$PR_BODY" $(git diff origin/main...HEAD --diff-filter=AM --name-only | grep -E '\.(test|spec)\.')
2500
+ ```
2501
+
2502
+ **Safety rules (carry into any manual mutation performed during this check, per #883 and the commit-before-mutating discipline):**
2503
+ - **Commit before mutating.** `git checkout <file>` to revert a mutation wipes any uncommitted edits in that file, not just the mutation.
2504
+ - **Never mutate a variable a `finally` block passes to `rmSync`.** A mutation that touches cleanup-path state can delete a worktree (#883's motivating incident).
2505
+
2506
+ **Per-AC status outcomes:**
2507
+
2508
+ | Status | Criteria |
2509
+ |--------|----------|
2510
+ | **Verified** | The in-scope AC has a `SEQUANT_MUTATION` marker whose `failedTest` names a test file present in the diff |
2511
+ | **Missing** | The in-scope AC has no `SEQUANT_MUTATION` marker |
2512
+ | **Failed** | The in-scope AC has a marker naming a test file NOT present in the diff — a fabricated marker is worse than a missing one |
2513
+
2514
+ **Aggregate `mutation_verification_status`** (the single §7 step-2 token — worst case across in-scope ACs wins, mirroring §6e's per-AC-table-to-single-status rollup): `Failed` if any in-scope AC is `Failed`; else `Missing` if any in-scope AC is `Missing`; else `Verified` if every in-scope AC is `Verified`; `Not-Applicable` when no AC in the diff is a gate-test AC.
2515
+
2516
+ **AC marking and verdict gating:**
2517
+
2518
+ - Aggregate `Missing` → the mutation-verification gate caps the verdict at `AC_MET_BUT_NOT_A_PLUS` regardless of individual ACs' own MET status — see step 4's `mutation_verification_status == "Missing"` branch.
2519
+ - Aggregate `Failed` → floor the verdict at `AC_NOT_MET` — see step 4's `mutation_verification_status == "Failed"` branch. This is a hard floor, not a soft cap: a marker naming a test absent from the diff is affirmatively false evidence, not merely absent evidence.
2520
+
2521
+ **Output Format:**
2522
+
2523
+ ```markdown
2524
+ ### Mutation Verification
2525
+
2526
+ | AC | Gate Test? | Marker | Status |
2527
+ |----|-----------|--------|--------|
2528
+ | AC-N | Yes | `injection.test.ts > rejects payload` | Verified |
2529
+ | AC-M | Yes | — | Missing |
2530
+ | AC-P | Yes | `nonexistent.test.ts > some test` | Failed — test not in diff |
2531
+ | AC-Q | No | — | N/A |
2532
+
2533
+ **Status:** Verified / Missing / Failed / Not-Applicable
2534
+ ```
2535
+
2536
+ ---
2537
+
2538
+ ### 6j. Structured Gap Findings (REQUIRED)
2539
+
2540
+ **Purpose:** `parseQaSummary`'s gap channel (`src/lib/workflow/phase-executor.ts`) used to scrape only the `**Issues:**` bullet list under Code Review — one specific spot in this template. It missed AC-table `NOT_MET`/`PARTIALLY_MET` rows, §6d Adversarial Re-Read findings, and §5/Risk Assessment gaps entirely, so a QA pass that failed on a §6d "Severe Gap" could hand `/loop` an empty findings list (#937). This section closes that gap: every finding this review surfaces — anywhere in the output, not just Code Review — gets classified into the taxonomy below and emitted as one machine-readable marker.
2541
+
2542
+ **Taxonomy** (finite — six categories, pick the closest fit):
2543
+
2544
+ | Category | Use for |
2545
+ |----------|---------|
2546
+ | `requirement_gap` | An AC (or a table row) is `NOT_MET`/`PARTIALLY_MET` |
2547
+ | `dependency_gap` | Missing/incompatible dependency, unregistered CLI flag, wiring gap (§2h, §3g-class) |
2548
+ | `test_gap` | Missing/tautological/insufficient test coverage (§2b, §2d, §6i Missing/Failed) |
2549
+ | `repository_gap` | Code quality, duplication, anti-pattern, dead code (§2, §2e) |
2550
+ | `risk_gap` | §5 Risk Assessment item, §6d Adversarial Re-Read finding, §6f Trust-Boundary finding |
2551
+ | `execution_gap` | Build/lint/CI failure, script/CLI execution failure (§2a, §Phase 1 CI Status) |
2552
+
2553
+ **Per-finding fields:**
2554
+
2555
+ | Field | Required? | Content |
2556
+ |-------|-----------|---------|
2557
+ | `category` | Yes | One of the six above |
2558
+ | `evidence` | Yes | A concrete observation — file:line, failing check name, or table row — never speculation |
2559
+ | `description` | Yes | One-sentence statement of the gap (same text you'd put in a prose bullet) |
2560
+ | `recommendedAction` | Yes | `fix_now` (blocks `READY_FOR_MERGE`, actionable by a code change), `document` (real but non-blocking — quality/polish, or explicitly deferred), or `pause_for_human` (needs a decision `/loop` cannot make, e.g. `SCOPE_SPLIT_RECOMMENDED`-class ambiguity) |
2561
+ | `affectedAcs` | If applicable | e.g. `["AC-3"]` |
2562
+ | `nonGoal` | If applicable | `true` when the finding overlaps one of the issue's Non-Goals — report-only under `ac` policy, same semantics as `ReadyGapItem.nonGoal` |
2563
+
2564
+ **Fallback rule (no dropped findings):** if a real finding does not fit any of the six categories, **do not force it into the marker** — leave it as prose only (in Code Review's `**Issues:**`, Risk Assessment, or wherever it naturally belongs). The marker narrows the *structured* channel; every consumer that reads it also unions it with the prose scrape, so an unclassifiable finding is never silently lost — it just doesn't get machine-readable category/action metadata.
2565
+
2566
+ **How to build the marker:** after finishing every other section, walk back through this review's own output — the AC Coverage table's non-MET rows, §5 Risk Assessment, §6d Adversarial Re-Read (Standard only), §6f Trust-Boundary, and Code Review's `**Issues:**` — and classify each real finding. Append exactly one marker as the LAST line of the comment (after `### Next Steps`), single-line flat-ish JSON (no line breaks inside the `{...}`, and no literal `-->` inside any string value — both would break the parser's `[\s\S]*?-->` match):
2567
+
2568
+ ```markdown
2569
+ <!-- SEQUANT_QA_GAPS: {"findings":[{"category":"requirement_gap","evidence":"AC-3 table row: NOT_MET — no rate limit found in src/retry.ts","description":"AC-3 (rate limiting) is not implemented","recommendedAction":"fix_now","affectedAcs":["AC-3"]}]} -->
2570
+ ```
2571
+
2572
+ When there are no findings to report (clean pass), still emit the marker with an empty array — this positively confirms the structured channel ran, rather than leaving `/loop` to distinguish "clean" from "QA output predates this marker":
2573
+
2574
+ ```markdown
2575
+ <!-- SEQUANT_QA_GAPS: {"findings":[]} -->
2576
+ ```
2577
+
2578
+ ---
2579
+
2405
2580
 
2406
2581
  ### 7. A+ Status Verdict
2407
2582
 
@@ -2433,6 +2608,8 @@ Provide an overall verdict:
2433
2608
  - adversarial_reread_status = status from Section 6d (Clean/Gaps Found/Severe Gap) — REQUIRED for Standard QA, omitted for Simple Fix
2434
2609
  - behavior_rule_survival_status = status from Section 6e (Clean/Survivors Found/N/A) — REQUIRED when any AC triggers the behavior-rule heuristic, omitted otherwise
2435
2610
  - trust_boundary_status = status from Section 6f (Clean/Injection Acted On) — REQUIRED in **both** Standard QA and Simple Fix mode (unlike 6d, it is never omitted: an injected command is a small diff by definition)
2611
+ - declared_evidence_status = status from Section 6h (Complete/Incomplete/N/A) — REQUIRED when any AC declares evidence naming a runnable command, `N/A` otherwise
2612
+ - mutation_verification_status = status from Section 6i (Verified/Missing/Failed/Not-Applicable) — REQUIRED when any AC is a gate-test AC per `isGateTestEvidence`, `Not-Applicable` otherwise
2436
2613
  - cli_registration_status = status from Section 2h (Passed/Failed/N/A) — REQUIRED when option interfaces are modified, `N/A` otherwise; omitted in Simple Fix mode along with the rest of §2h
2437
2614
  - script_verification_status = status from Section 11 (Verified/Overridden/Not Verified/Not Required) — REQUIRED when `scripts/` or `templates/scripts/` files are modified, `Not Required` otherwise
2438
2615
  - changelog_required = true IFF Section 10a's `CHANGELOG.md` exists AND Section 10a's `user_facing` count is >0 (single source of truth — see §10a for the conventional-commit detection regex, which accepts unscoped, scoped, and breaking variants of `feat`/`fix`/`perf`/`refactor`/`docs`); false otherwise
@@ -2467,12 +2644,18 @@ Provide an overall verdict:
2467
2644
  → AC_NOT_MET (the diff acted on an agent-directed instruction embedded in untrusted external text — see Section 6f and _shared/references/trust-model.md; name the instruction verbatim with its path:line)
2468
2645
  - ELSE IF cli_registration_status == "Failed":
2469
2646
  → AC_NOT_MET (an option-interface field has runtime `mergedOptions.X` usage but no `.option()` registration in `bin/cli.ts`, so users cannot reach the feature from the command line — invisible to TypeScript, build, and unit tests; see Section 2h and #305. Name each unregistered field.)
2647
+ - ELSE IF mutation_verification_status == "Failed":
2648
+ → AC_NOT_MET (a `SEQUANT_MUTATION` marker names a test file absent from this diff — a fabricated mutation-verification record; see Section 6i and #939. A fabricated marker is worse than a missing one.)
2470
2649
  - ELSE IF adversarial_reread_status == "Severe Gap":
2471
2650
  → AC_NOT_MET (verbatim motivating-example fixture not run / evidence claim is bug reproduction not validation / AC marked MET without runtime or corpus check the AC text required)
2472
2651
  - ELSE IF skill_verification == "Failed":
2473
2652
  → AC_MET_BUT_NOT_A_PLUS (skill commands have issues - cannot be READY_FOR_MERGE)
2474
2653
  - ELSE IF execution_evidence == "Incomplete":
2475
2654
  → AC_MET_BUT_NOT_A_PLUS (scripts not verified - cannot be READY_FOR_MERGE)
2655
+ - ELSE IF declared_evidence_status == "Incomplete":
2656
+ → AC_MET_BUT_NOT_A_PLUS (a declared `Evidence:` command was not executed/verified for one or more ACs - see Section 6h; the #853 "marked MET by construction" path)
2657
+ - ELSE IF mutation_verification_status == "Missing":
2658
+ → AC_MET_BUT_NOT_A_PLUS (a gate-test AC has no recorded `SEQUANT_MUTATION` marker - see Section 6i; the honor-system-prose gap #939 closes)
2476
2659
  - ELSE IF script_verification_status == "Not Verified":
2477
2660
  → AC_MET_BUT_NOT_A_PLUS (`scripts/` changed with no `/verify` evidence and no approved §11a override — code review and unit tests miss integration failures; see Section 11)
2478
2661
  - ELSE IF changelog_required AND changelog_missing:
@@ -2713,7 +2896,10 @@ Produce a Markdown snippet for the PR/issue:
2713
2896
  ### 9. Update GitHub Issue
2714
2897
 
2715
2898
  **If orchestrated (SEQUANT_ORCHESTRATOR is set):**
2716
- - Skip posting GitHub comment (orchestrator handles aggregated summary)
2899
+ - Skip posting this skill's own GitHub comment under `sequant run`,
2900
+ `batch-executor.ts` posts a compact verdict comment (AC coverage + a
2901
+ `SEQUANT_QA_VERDICT` marker) itself right after this phase completes with a
2902
+ parseable verdict (#964); don't post a second, redundant one here
2717
2903
  - Include verdict and AC coverage in output for orchestrator to capture
2718
2904
  - Let orchestrator update labels based on final workflow status
2719
2905
 
@@ -2932,7 +3118,7 @@ When the size gate determined `SMALL_DIFF=true`, use the **simplified output tem
2932
3118
  - Skill Change Review
2933
3119
  - Adversarial Re-Read
2934
3120
 
2935
- **Not omitted:** the Trust-Boundary Check (§6f), the Behavior-Rule Survival Check (§6e), and the CHANGELOG Quality Gate (§10a) are all required in simple fix mode too — each is cheap, and each guards a defect class that a small diff is a *likely* carrier of rather than an unlikely one. Every `(REQUIRED` section must appear in either the required list below or the omitted list above; `scripts/lint-skill-gates.ts` (I3) fails the build on silence, because silence is how #819 F2 shipped a security check that Simple Fix mode switched off.
3121
+ **Not omitted:** the Trust-Boundary Check (§6f), the Behavior-Rule Survival Check (§6e), the Declared-Evidence Execution check (§6h), the Mutation Verification check (§6i), and the CHANGELOG Quality Gate (§10a) are all required in simple fix mode too — each is cheap (a short-circuit to N/A when no AC qualifies), and each guards a defect class that a small diff is a *likely* carrier of rather than an unlikely one — a gate test (fixture/section/flag assertion) is very often itself a small, localized diff. Every `(REQUIRED` section must appear in either the required list below or the omitted list above; `scripts/lint-skill-gates.ts` (I3) fails the build on silence, because silence is how #819 F2 shipped a security check that Simple Fix mode switched off.
2936
3122
 
2937
3123
  **Required sections for simple fix mode:**
2938
3124
 
@@ -2944,12 +3130,15 @@ When the size gate determined `SMALL_DIFF=true`, use the **simplified output tem
2944
3130
  - [ ] **Anti-Pattern Detection** - Code patterns check (lightweight)
2945
3131
  - [ ] **Trust-Boundary Check** - Required in simple fix mode too (see Section 6f); "Finding:" and "Status:" lines populated
2946
3132
  - [ ] **Behavior-Rule Survival Check** - Required in simple fix mode too (see Section 6e): a #533-class stale-rule survival is very plausibly a sub-threshold diff. Cheap short-circuit — mark "N/A" when no AC triggers the behavior-rule heuristic
3133
+ - [ ] **Declared-Evidence Execution** - Required in simple fix mode too (see Section 6h): a declared-evidence AC marked MET without running its command is exactly the #853 gap, regardless of diff size. Cheap short-circuit — mark "N/A" when no AC declares evidence naming a runnable command
3134
+ - [ ] **Mutation Verification** - Required in simple fix mode too (see Section 6i): a gate-test AC merged without a recorded mutation result is exactly the #830 gap, regardless of diff size. Cheap short-circuit — mark "N/A" when no AC is a gate-test AC
2947
3135
  - [ ] **CHANGELOG Verification** - Required in simple fix mode too (see Section 10a): a one-line user-facing fix still needs an `[Unreleased]` entry (or marked N/A)
2948
3136
  - [ ] **Risk Assessment** - Likely failure mode and coverage gaps stated
2949
3137
  - [ ] **Verdict** - One of: READY_FOR_MERGE, AC_MET_BUT_NOT_A_PLUS, NEEDS_VERIFICATION, AC_NOT_MET
2950
3138
  - [ ] **Documentation Check** - README/docs updated if feature adds new functionality
2951
3139
  - [ ] **Next Steps** - Clear, actionable recommendations
2952
3140
  - [ ] Adversarial re-read of core logic — list anything the structured pipeline didn't surface
3141
+ - [ ] **Structured Gap Findings** - `SEQUANT_QA_GAPS` marker present as the last line, findings classified per Section 6j (or `{"findings":[]}` on a clean pass)
2953
3142
 
2954
3143
  ### Standard QA (Implementation Exists, `SMALL_DIFF=false`)
2955
3144
 
@@ -2973,6 +3162,8 @@ When the size gate determined `SMALL_DIFF=true`, use the **simplified output tem
2973
3162
  - [ ] **Detection Pattern Verification** - Included if skill markdown adds new `grep`/`awk`/`jq`/`sed`/regex (or marked N/A)
2974
3163
  - [ ] **CLI Registration Verification** - Included if option interfaces modified (or marked N/A — see Section 2h); `Failed` floors the verdict at `AC_NOT_MET` via §7
2975
3164
  - [ ] **Behavior-Rule Survival Check** - Included if any AC triggers the behavior-rule heuristic (or marked N/A — see Section 6e); `Survivors Found` floors the verdict at `AC_NOT_MET` via §7
3165
+ - [ ] **Declared-Evidence Execution** - Included if any AC declares evidence naming a runnable command (or marked N/A — see Section 6h); `Incomplete` floors the verdict at `AC_MET_BUT_NOT_A_PLUS` via §7, and an unexecuted AC is marked PENDING rather than MET
3166
+ - [ ] **Mutation Verification** - Included if any AC is a gate-test AC per `isGateTestEvidence` (or marked Not-Applicable — see Section 6i); `Missing` floors the verdict at `AC_MET_BUT_NOT_A_PLUS` via §7, `Failed` floors it at `AC_NOT_MET`
2976
3167
  - [ ] **Skill Change Review** - Skill-specific verification prompts included if skills changed
2977
3168
  - [ ] **Smoke Test** - Included if workflow-affecting changes (skills, scripts, CLI), or marked "Not Required"
2978
3169
  - [ ] **Manual Test AC Enforcement** - Included if spec plan has Manual Test ACs (or marked N/A if no manual-test ACs detected)
@@ -2981,6 +3172,7 @@ When the size gate determined `SMALL_DIFF=true`, use the **simplified output tem
2981
3172
  - [ ] **Adversarial Re-Read** - Required structured section: all 5 sub-prompts answered with concrete content; "Findings:" and "Status:" lines populated; bare "No gaps" without specific reasoning fails verification (see Section 6d)
2982
3173
  - [ ] **Documentation Check** - README/docs updated if feature adds new functionality
2983
3174
  - [ ] **Next Steps** - Clear, actionable recommendations
3175
+ - [ ] **Structured Gap Findings** - `SEQUANT_QA_GAPS` marker present as the last line, findings classified per Section 6j (or `{"findings":[]}` on a clean pass)
2984
3176
 
2985
3177
  ### Early Exit (No Implementation)
2986
3178
 
@@ -3083,6 +3275,26 @@ When the size gate triggers simple fix mode, use this shorter template:
3083
3275
 
3084
3276
  ---
3085
3277
 
3278
+ ### Declared-Evidence Execution
3279
+
3280
+ | AC | Evidence | Executed? | Result |
3281
+ |----|----------|-----------|--------|
3282
+ | AC-N | `<command>` or — | Yes/No/— | [pass-fail result, or "Not run — AC marked PENDING"] |
3283
+
3284
+ **Status:** Complete / Incomplete / N/A
3285
+
3286
+ ---
3287
+
3288
+ ### Mutation Verification
3289
+
3290
+ | AC | Gate Test? | Marker | Status |
3291
+ |----|-----------|--------|--------|
3292
+ | AC-N | Yes/No | `<file.test.ts > test name>` or — | Verified / Missing / Failed / N/A |
3293
+
3294
+ **Status:** Verified / Missing / Failed / Not-Applicable
3295
+
3296
+ ---
3297
+
3086
3298
  ### CHANGELOG Verification
3087
3299
 
3088
3300
  **Result:** [CHANGELOG requirements met / Missing entry for user-facing changes / N/A (non-user-facing changes only)]
@@ -3110,6 +3322,10 @@ When the size gate triggers simple fix mode, use this shorter template:
3110
3322
  ### Next Steps
3111
3323
 
3112
3324
  1. [Action item]
3325
+
3326
+ ### Structured Gap Findings
3327
+
3328
+ <!-- SEQUANT_QA_GAPS: {"findings":[...]} -->
3113
3329
  ```
3114
3330
 
3115
3331
  ---
@@ -3453,6 +3669,31 @@ You MUST include these sections:
3453
3669
 
3454
3670
  ---
3455
3671
 
3672
+ ### Declared-Evidence Execution
3673
+
3674
+ | AC | Evidence | Executed? | Result |
3675
+ |----|----------|-----------|--------|
3676
+ | AC-N | `npm test -- reset-expiry` | Yes | ✅ 4 passed |
3677
+ | AC-M | `npx sequant doctor --help` | No | ⚠️ Not run — AC marked PENDING |
3678
+ | AC-P | — | — | N/A (no declared evidence) |
3679
+
3680
+ **Status:** Complete / Incomplete / N/A
3681
+
3682
+ ---
3683
+
3684
+ ### Mutation Verification
3685
+
3686
+ | AC | Gate Test? | Marker | Status |
3687
+ |----|-----------|--------|--------|
3688
+ | AC-N | Yes | `injection.test.ts > rejects payload` | Verified |
3689
+ | AC-M | Yes | — | Missing |
3690
+ | AC-P | Yes | `nonexistent.test.ts > some test` | Failed — test not in diff |
3691
+ | AC-Q | No | — | N/A |
3692
+
3693
+ **Status:** Verified / Missing / Failed / Not-Applicable
3694
+
3695
+ ---
3696
+
3456
3697
  ### CHANGELOG Verification
3457
3698
 
3458
3699
  | Check | Status |
@@ -3489,4 +3730,8 @@ You MUST include these sections:
3489
3730
 
3490
3731
  1. [Action item 1]
3491
3732
  2. [Action item 2]
3733
+
3734
+ ### Structured Gap Findings
3735
+
3736
+ <!-- SEQUANT_QA_GAPS: {"findings":[...]} -->
3492
3737
  ```
@@ -374,15 +374,19 @@ actually posted via `gh issue comment`, not just shown in this response.
374
374
  | Docs-only (`docs` label) | No | Skip testgen — no unit tests needed |
375
375
  | All ACs have "Manual Test" or "Browser Test" | No | Skip testgen — no code stubs to generate |
376
376
 
377
- **Detection logic:**
378
- 1. Count ACs with "Unit Test" → If >0, recommend testgen
379
- 2. Count ACs with "Integration Test" If >0, recommend testgen
380
- 3. Check labels: `bug`/`fix` only → Skip testgen. `docs` → Skip testgen.
377
+ **Detection logic — declared evidence counts before inferred (#938):**
378
+
379
+ `extractAcceptanceCriteria` resolves each AC's `verificationMethod` from a declared `Evidence:` clause when the AC line has one (`AC.evidence` is set), falling back to keyword inference only when it doesn't. Ground the recommendation in the stronger signal first:
380
+
381
+ 1. Count ACs with **declared** evidence (`AC.evidence` set) resolving to "Unit Test" or "Integration Test" → these are the reasoning's primary citation; if >0, recommend testgen and name them.
382
+ 2. Count remaining ACs — no declared evidence, method came from **inference** — that are "Unit Test" → if >0, recommend testgen.
383
+ 3. Count remaining inferred "Integration Test" ACs → if >0, recommend testgen.
384
+ 4. Check labels: `bug`/`fix` only → Skip testgen. `docs` → Skip testgen.
381
385
 
382
386
  **Example when testgen recommended:**
383
387
  ```markdown
384
388
  **Phases:** spec → testgen → exec → qa
385
- **Reasoning:** ACs include Unit Test verification methods; testgen will create stubs before implementation
389
+ **Reasoning:** AC-1 declares evidence (`npm test -- reset-expiry`, unit_test); testgen will create stubs before implementation
386
390
  ```
387
391
 
388
392
  ### Browser Testing Label Suggestion
@@ -582,7 +582,7 @@ Create structured test results:
582
582
  ### 3.2 GitHub Comment
583
583
 
584
584
  **If orchestrated (SEQUANT_ORCHESTRATOR is set):**
585
- - Skip posting GitHub comment (orchestrator handles summary)
585
+ - Skip posting this skill's own GitHub comment no per-phase comment is posted under `sequant run`; test results surface through the run summary and the PR body (#964)
586
586
  - Include test summary in output for orchestrator to capture
587
587
  - Let orchestrator aggregate results across phases
588
588