sequant 2.10.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 (108) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +19 -2
  4. package/dist/bin/cli.js +47 -2
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
  8. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  9. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  10. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  11. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  12. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  13. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  14. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  15. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
  17. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  18. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  19. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  20. package/dist/src/commands/doctor.js +20 -18
  21. package/dist/src/commands/locks.d.ts +20 -1
  22. package/dist/src/commands/locks.js +206 -4
  23. package/dist/src/commands/ready.d.ts +6 -0
  24. package/dist/src/commands/ready.js +19 -1
  25. package/dist/src/commands/run-display.js +1 -0
  26. package/dist/src/commands/worktree.d.ts +31 -0
  27. package/dist/src/commands/worktree.js +95 -0
  28. package/dist/src/lib/ac-linter.js +26 -0
  29. package/dist/src/lib/ac-parser.d.ts +40 -0
  30. package/dist/src/lib/ac-parser.js +202 -16
  31. package/dist/src/lib/cli-flags.d.ts +23 -0
  32. package/dist/src/lib/cli-flags.js +43 -0
  33. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  34. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  35. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  36. package/dist/src/lib/locks/checkout-lock.js +389 -0
  37. package/dist/src/lib/locks/index.d.ts +6 -3
  38. package/dist/src/lib/locks/index.js +4 -2
  39. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  40. package/dist/src/lib/locks/lock-manager.js +230 -5
  41. package/dist/src/lib/locks/types.d.ts +72 -0
  42. package/dist/src/lib/locks/types.js +28 -0
  43. package/dist/src/lib/markdown-fence.d.ts +24 -0
  44. package/dist/src/lib/markdown-fence.js +51 -0
  45. package/dist/src/lib/mcp-config.d.ts +24 -0
  46. package/dist/src/lib/mcp-config.js +51 -0
  47. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  48. package/dist/src/lib/scope/analyzer.js +7 -1
  49. package/dist/src/lib/settings.d.ts +111 -1
  50. package/dist/src/lib/settings.js +59 -0
  51. package/dist/src/lib/system.d.ts +7 -3
  52. package/dist/src/lib/system.js +7 -3
  53. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  54. package/dist/src/lib/test-tautology-detector.js +147 -40
  55. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  56. package/dist/src/lib/workflow/batch-executor.js +154 -23
  57. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  58. package/dist/src/lib/workflow/config-resolver.js +90 -0
  59. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  60. package/dist/src/lib/workflow/drivers/claude-code.js +14 -3
  61. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  62. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  63. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  64. package/dist/src/lib/workflow/error-classifier.js +4 -0
  65. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  66. package/dist/src/lib/workflow/log-writer.js +20 -0
  67. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  68. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  69. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  70. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  71. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  72. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  73. package/dist/src/lib/workflow/phase-detection.js +5 -1
  74. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  75. package/dist/src/lib/workflow/phase-executor.js +60 -4
  76. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  77. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  78. package/dist/src/lib/workflow/ready-gate.d.ts +53 -1
  79. package/dist/src/lib/workflow/ready-gate.js +105 -14
  80. package/dist/src/lib/workflow/run-log-schema.d.ts +175 -0
  81. package/dist/src/lib/workflow/run-log-schema.js +71 -1
  82. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  83. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  84. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  85. package/dist/src/lib/workflow/state-schema.d.ts +5 -1
  86. package/dist/src/lib/workflow/state-schema.js +8 -1
  87. package/dist/src/lib/workflow/types.d.ts +78 -0
  88. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  89. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  90. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  91. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  92. package/package.json +4 -3
  93. package/templates/hooks/pre-tool.sh +331 -12
  94. package/templates/scripts/cleanup-worktree.sh +36 -15
  95. package/templates/scripts/new-feature.sh +25 -19
  96. package/templates/skills/_shared/references/subagent-types.md +7 -18
  97. package/templates/skills/assess/SKILL.md +5 -1
  98. package/templates/skills/exec/SKILL.md +62 -8
  99. package/templates/skills/fullsolve/SKILL.md +187 -28
  100. package/templates/skills/loop/SKILL.md +127 -23
  101. package/templates/skills/merger/SKILL.md +130 -13
  102. package/templates/skills/qa/SKILL.md +306 -8
  103. package/templates/skills/release/SKILL.md +79 -0
  104. package/templates/skills/spec/SKILL.md +40 -20
  105. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  106. package/templates/skills/test/SKILL.md +1 -1
  107. package/templates/skills/testgen/SKILL.md +23 -6
  108. package/templates/agents/sequant-explorer.md +0 -24
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Parseable structured gap-finding markers for `/qa` (#937).
3
+ *
4
+ * `parseQaSummary`'s prose scrape (`parseListSection` against a
5
+ * `**Issues:**`/`**Gaps**` header) is lossy — it misses AC-table NOT_MET
6
+ * rows, §6d Adversarial Re-Read findings, and §5 Risk Assessment. QA output
7
+ * templates now end with a structured marker:
8
+ *
9
+ * ```
10
+ * <!-- SEQUANT_QA_GAPS: {"findings":[{"category":"test_gap","evidence":"...",
11
+ * "description":"...","recommendedAction":"fix_now"}]} -->
12
+ * ```
13
+ *
14
+ * Same durable-marker idiom as `SEQUANT_SPEC` (spec-recommendation.ts),
15
+ * `SEQUANT_PHASE` (phase-detection.ts), and `SEQUANT_MUTATION`
16
+ * (mutation-marker.ts) — but unlike those three, this marker's payload is
17
+ * an array of objects, not a flat record, so it cannot reuse their
18
+ * `{[^}]+}` regex (that stops at the FIRST `}`, truncating mid-JSON on any
19
+ * nested object). Instead this matches lazily up to the closing `-->`,
20
+ * which the emitting template guarantees appears on the same line as the
21
+ * marker (single-line JSON, no `-->` inside string values).
22
+ */
23
+ import { type GapFinding } from "./run-log-schema.js";
24
+ /**
25
+ * Parse every `SEQUANT_QA_GAPS` marker from a QA comment/output and return
26
+ * the findings from the last valid one (latest-wins, matching
27
+ * `resolveSpecRecommendation`'s idiom for `SEQUANT_SPEC`).
28
+ *
29
+ * Markers inside fenced code blocks or inline code (e.g. a doc example
30
+ * showing the marker format) are ignored. Malformed JSON or a
31
+ * schema-invalid payload is skipped rather than thrown — an unparseable
32
+ * marker degrades to "no marker found", not a crash.
33
+ *
34
+ * @param output - QA comment body or agent output text
35
+ * @returns The findings array from the last valid marker, or `null` if no
36
+ * valid marker was found
37
+ */
38
+ export declare function parseQaGapsMarker(output: string): GapFinding[] | null;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Parseable structured gap-finding markers for `/qa` (#937).
3
+ *
4
+ * `parseQaSummary`'s prose scrape (`parseListSection` against a
5
+ * `**Issues:**`/`**Gaps**` header) is lossy — it misses AC-table NOT_MET
6
+ * rows, §6d Adversarial Re-Read findings, and §5 Risk Assessment. QA output
7
+ * templates now end with a structured marker:
8
+ *
9
+ * ```
10
+ * <!-- SEQUANT_QA_GAPS: {"findings":[{"category":"test_gap","evidence":"...",
11
+ * "description":"...","recommendedAction":"fix_now"}]} -->
12
+ * ```
13
+ *
14
+ * Same durable-marker idiom as `SEQUANT_SPEC` (spec-recommendation.ts),
15
+ * `SEQUANT_PHASE` (phase-detection.ts), and `SEQUANT_MUTATION`
16
+ * (mutation-marker.ts) — but unlike those three, this marker's payload is
17
+ * an array of objects, not a flat record, so it cannot reuse their
18
+ * `{[^}]+}` regex (that stops at the FIRST `}`, truncating mid-JSON on any
19
+ * nested object). Instead this matches lazily up to the closing `-->`,
20
+ * which the emitting template guarantees appears on the same line as the
21
+ * marker (single-line JSON, no `-->` inside string values).
22
+ */
23
+ import { z } from "zod";
24
+ import { GapFindingSchema } from "./run-log-schema.js";
25
+ import { stripMarkdownCode } from "./phase-detection.js";
26
+ /** Regex to extract the SEQUANT_QA_GAPS marker JSON from an HTML comment. */
27
+ const QA_GAPS_MARKER_REGEX = /<!-- SEQUANT_QA_GAPS: (\{[\s\S]*?\}) -->/g;
28
+ const QaGapsMarkerPayloadSchema = z.object({
29
+ findings: z.array(GapFindingSchema),
30
+ });
31
+ /**
32
+ * Parse every `SEQUANT_QA_GAPS` marker from a QA comment/output and return
33
+ * the findings from the last valid one (latest-wins, matching
34
+ * `resolveSpecRecommendation`'s idiom for `SEQUANT_SPEC`).
35
+ *
36
+ * Markers inside fenced code blocks or inline code (e.g. a doc example
37
+ * showing the marker format) are ignored. Malformed JSON or a
38
+ * schema-invalid payload is skipped rather than thrown — an unparseable
39
+ * marker degrades to "no marker found", not a crash.
40
+ *
41
+ * @param output - QA comment body or agent output text
42
+ * @returns The findings array from the last valid marker, or `null` if no
43
+ * valid marker was found
44
+ */
45
+ export function parseQaGapsMarker(output) {
46
+ if (!output)
47
+ return null;
48
+ const stripped = stripMarkdownCode(output);
49
+ QA_GAPS_MARKER_REGEX.lastIndex = 0;
50
+ let latest = null;
51
+ let match;
52
+ while ((match = QA_GAPS_MARKER_REGEX.exec(stripped)) !== null) {
53
+ let parsed;
54
+ try {
55
+ parsed = JSON.parse(match[1]);
56
+ }
57
+ catch {
58
+ continue;
59
+ }
60
+ const result = QaGapsMarkerPayloadSchema.safeParse(parsed);
61
+ if (result.success) {
62
+ latest = result.data.findings;
63
+ }
64
+ }
65
+ return latest;
66
+ }
@@ -22,7 +22,8 @@
22
22
  * lives in `src/commands/ready.ts`.
23
23
  */
24
24
  import type { ExecutionConfig, PhaseResult, ProgressCallback } from "./types.js";
25
- import type { QaVerdict } from "./run-log-schema.js";
25
+ import { type EscalationRecord } from "./effort-escalation.js";
26
+ import type { QaVerdict, GapCategory, GapAction } from "./run-log-schema.js";
26
27
  import type { ReadyPolicy } from "../settings.js";
27
28
  import type { IssueStatus } from "./state-schema.js";
28
29
  import { type LoopProgressSnapshot } from "./qa-stagnation.js";
@@ -68,6 +69,15 @@ export interface ReadyGapItem {
68
69
  * mode these are explicitly report-only (never fed to the fix loop).
69
70
  */
70
71
  nonGoal: boolean;
72
+ /**
73
+ * Structured taxonomy fields from the `SEQUANT_QA_GAPS` marker (#937),
74
+ * present only when this gap's description matched a marker finding.
75
+ * A gap surfaced only via the legacy prose scrape carries none of these.
76
+ */
77
+ category?: GapCategory;
78
+ evidence?: string;
79
+ recommendedAction?: GapAction;
80
+ affectedAcs?: string[];
71
81
  }
72
82
  /** Structured outcome of a ready-gate run. */
73
83
  export interface ReadyResult {
@@ -90,6 +100,12 @@ export interface ReadyResult {
90
100
  tokensUsed: number;
91
101
  /** Human-readable markdown gap report (AC-4). */
92
102
  report: string;
103
+ /**
104
+ * Effort escalations applied during this gate's QA-pass loop (#915),
105
+ * base+escalated tier per escalated `qa`/`loop` dispatch. Empty when
106
+ * `effortEscalation` is off or no dispatch escalated.
107
+ */
108
+ effortEscalations: EscalationRecord[];
93
109
  }
94
110
  /**
95
111
  * Thin phase-runner abstraction so the engine can be unit-tested without the
@@ -110,6 +126,13 @@ export interface RunReadyGateOptions {
110
126
  phaseTimeout: number;
111
127
  /** Whether MCP servers are enabled for phase execution. */
112
128
  mcp: boolean;
129
+ /**
130
+ * Claude Desktop MCP server names explicitly opted in (#936). Callers
131
+ * (`commands/ready.ts`) resolve this from `settings.run.mcpAllowlist` —
132
+ * see the doc comment on `ExecutionConfig.mcpAllowlist` for why this
133
+ * producer cannot drift from `buildExecutionConfig`'s own assignment.
134
+ */
135
+ mcpAllowlist?: string[];
113
136
  verbose?: boolean;
114
137
  /** Injectable phase runner — defaults to the real executePhaseWithRetry wrapper. */
115
138
  runPhase: ReadyPhaseRunner;
@@ -127,6 +150,35 @@ export interface RunReadyGateOptions {
127
150
  classifyChangesFn?: (cwd: string) => ExecChangeState;
128
151
  /** Injectable loop-progress snapshot — defaults to {@link snapshotLoopProgress}. */
129
152
  snapshotFn?: (cwd: string) => LoopProgressSnapshot;
153
+ /**
154
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
155
+ * name. Callers (e.g. `commands/ready.ts`) resolve this via
156
+ * `resolvePhasePolicies` — the same shared resolver `buildExecutionConfig`
157
+ * uses — so this producer cannot drift from that one (#833 class).
158
+ * `buildPhaseConfig` spreads it onto every `ExecutionConfig` it builds;
159
+ * `phase-executor.ts` applies the entry for the phase actually running.
160
+ */
161
+ phasePolicies?: Record<string, {
162
+ model?: string;
163
+ effort?: string;
164
+ }>;
165
+ /**
166
+ * Evidence-based effort escalation on quality-loop retries (#915). Callers
167
+ * (e.g. `commands/ready.ts`) resolve this CLI > settings > `false`, the same
168
+ * precedence `buildExecutionConfig` uses for the `run` path (#833 class).
169
+ * `buildPhaseConfig` spreads it onto every `ExecutionConfig` it builds;
170
+ * `withEscalatedEffort` (`effort-escalation.ts`) reads it at each QA-pass
171
+ * dispatch to decide whether that specific `qa`/`loop` call escalates.
172
+ */
173
+ effortEscalation?: boolean;
174
+ /**
175
+ * Persist the final gap report as an issue comment when the gate reaches
176
+ * a terminal state with a QA verdict (#937 AC-4) — callers wire this to
177
+ * `GitHubProvider.postComment`. Best-effort: a failure here is caught and
178
+ * swallowed, never failing the gate itself — `result.report` (returned to
179
+ * the caller either way) is the primary channel.
180
+ */
181
+ postReport?: (body: string) => Promise<void>;
130
182
  }
131
183
  /**
132
184
  * Pure exit predicate. Given a policy and a QA verdict, has the loop reached
@@ -21,6 +21,7 @@
21
21
  * (out of scope for #683) can reuse `runReadyGate` directly. The command shell
22
22
  * lives in `src/commands/ready.ts`.
23
23
  */
24
+ import { withEscalatedEffort, } from "./effort-escalation.js";
24
25
  import { snapshotLoopProgress, compareLoopProgress, } from "./qa-stagnation.js";
25
26
  import { classifyExecChanges } from "./phase-executor.js";
26
27
  import { readTokenUsageFiles, aggregateTokenUsage, TOKEN_USAGE_DIR, } from "./token-utils.js";
@@ -137,11 +138,28 @@ export function parseNonGoals(issueBody) {
137
138
  }
138
139
  return items;
139
140
  }
140
- function classifyGaps(gaps, nonGoals) {
141
- return gaps.map((g) => ({
142
- description: g,
143
- nonGoal: gapTouchesNonGoals(g, nonGoals),
144
- }));
141
+ /**
142
+ * Classify each gap description for the report, enriching with structured
143
+ * taxonomy fields (#937) when the gap matches a `SEQUANT_QA_GAPS` marker
144
+ * finding. `findings` are matched to `gaps` by description (trimmed,
145
+ * case-folded) since `parseQaSummary` already unions marker findings into
146
+ * `gaps` in document order — every marker finding's description is present.
147
+ */
148
+ function classifyGaps(gaps, nonGoals, findings) {
149
+ const byDescription = new Map((findings ?? []).map((f) => [f.description.trim().toLowerCase(), f]));
150
+ return gaps.map((g) => {
151
+ const finding = byDescription.get(g.trim().toLowerCase());
152
+ return {
153
+ description: g,
154
+ nonGoal: finding?.nonGoal ?? gapTouchesNonGoals(g, nonGoals),
155
+ ...(finding && {
156
+ category: finding.category,
157
+ evidence: finding.evidence,
158
+ recommendedAction: finding.recommendedAction,
159
+ ...(finding.affectedAcs && { affectedAcs: finding.affectedAcs }),
160
+ }),
161
+ };
162
+ });
145
163
  }
146
164
  function defaultReadTokensUsed(worktreePath) {
147
165
  const dir = path.join(worktreePath, TOKEN_USAGE_DIR);
@@ -166,7 +184,13 @@ function buildPhaseConfig(opts, extra) {
166
184
  noSmartTests: false,
167
185
  dryRun: false,
168
186
  mcp: opts.mcp,
187
+ mcpAllowlist: opts.mcpAllowlist,
169
188
  retry: true,
189
+ // #914: producer 2 (see the doc comment on RunReadyGateOptions.phasePolicies
190
+ // for why this can't drift from buildExecutionConfig's own assignment).
191
+ phasePolicies: opts.phasePolicies,
192
+ // #915: producer 2 (see RunReadyGateOptions.effortEscalation).
193
+ effortEscalation: opts.effortEscalation,
170
194
  ...extra,
171
195
  };
172
196
  }
@@ -223,14 +247,44 @@ export function formatReadyReport(result) {
223
247
  }
224
248
  else {
225
249
  for (const item of result.remaining) {
226
- const tag = item.nonGoal ? " _(Non-Goal — report-only)_" : "";
227
- lines.push(`- ${item.description}${tag}`);
250
+ const tags = [
251
+ item.category && item.recommendedAction
252
+ ? `\`${item.category} · ${item.recommendedAction}\``
253
+ : undefined,
254
+ item.nonGoal ? "_(Non-Goal — report-only)_" : undefined,
255
+ ].filter(Boolean);
256
+ const suffix = tags.length > 0 ? ` ${tags.join(" ")}` : "";
257
+ lines.push(`- ${item.description}${suffix}`);
228
258
  }
229
259
  }
230
260
  lines.push("");
231
261
  lines.push("> The human merge gate is intentional: `sequant ready` never merges. Review the gaps above, then merge manually when satisfied.");
262
+ lines.push("");
263
+ lines.push(formatReadyGapsMarker(result.remaining));
232
264
  return lines.join("\n");
233
265
  }
266
+ /**
267
+ * Render `remaining` as a `SEQUANT_QA_GAPS` marker so the persisted ready
268
+ * report (#937 AC-4) carries the same machine-readable channel `/qa` itself
269
+ * emits — only items that came from a structured finding (have a
270
+ * `category`) round-trip; legacy prose-only gaps are already in the prose
271
+ * list above and are not re-encoded here.
272
+ */
273
+ function formatReadyGapsMarker(remaining) {
274
+ const findings = remaining
275
+ .filter((g) => g.category !== undefined &&
276
+ g.evidence !== undefined &&
277
+ g.recommendedAction !== undefined)
278
+ .map((g) => ({
279
+ category: g.category,
280
+ evidence: g.evidence,
281
+ description: g.description,
282
+ recommendedAction: g.recommendedAction,
283
+ ...(g.affectedAcs && { affectedAcs: g.affectedAcs }),
284
+ ...(g.nonGoal && { nonGoal: g.nonGoal }),
285
+ }));
286
+ return `<!-- SEQUANT_QA_GAPS: ${JSON.stringify({ findings })} -->`;
287
+ }
234
288
  /**
235
289
  * Drive the policy-bounded `qa → loop → qa` ready gate.
236
290
  */
@@ -268,7 +322,10 @@ export async function runReadyGate(opts) {
268
322
  const autoFixed = [];
269
323
  let remaining = [];
270
324
  let tokensUsed = 0;
271
- const finish = (reason) => {
325
+ // #915: escalated (base, escalated) tiers, one entry per QA-pass dispatch
326
+ // that actually escalated. Populated at the two dispatch sites below.
327
+ const effortEscalations = [];
328
+ const finish = async (reason) => {
272
329
  const ready = reason === "AC_MET" || reason === "READY_FOR_MERGE";
273
330
  const issueStatus = ready
274
331
  ? "waiting_for_human_merge"
@@ -285,8 +342,25 @@ export async function runReadyGate(opts) {
285
342
  remaining,
286
343
  tokensUsed,
287
344
  report: "",
345
+ effortEscalations,
288
346
  };
289
347
  result.report = formatReadyReport(result);
348
+ // #937 AC-4: persist the gap report as an issue comment once the gate
349
+ // reaches a terminal state carrying a QA verdict. `finalVerdict === null`
350
+ // covers NO_IMPLEMENTATION/UNCOMMITTED_ONLY/NO_VERDICT and the guard
351
+ // TOKEN_BUDGET check before the first QA pass — none of those have a gap
352
+ // report worth persisting. Best-effort: a post failure must never fail
353
+ // the gate — `result.report` (returned either way) is the primary
354
+ // channel, same rationale as `sequant ready`'s existing state-persistence
355
+ // try/catch.
356
+ if (result.finalVerdict !== null && opts.postReport) {
357
+ try {
358
+ await opts.postReport(result.report);
359
+ }
360
+ catch {
361
+ // Non-fatal — see comment above.
362
+ }
363
+ }
290
364
  return result;
291
365
  };
292
366
  const budgetExceeded = () => typeof tokenBudget === "number" &&
@@ -299,7 +373,12 @@ export async function runReadyGate(opts) {
299
373
  return finish("TOKEN_BUDGET");
300
374
  }
301
375
  iterations++;
302
- const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
376
+ // #915: iterations > 1 means this QA pass is a retry of a prior
377
+ // unsatisfied verdict — the ready-gate's retry signal.
378
+ const qaEscalation = withEscalatedEffort(buildPhaseConfig(opts, { fullQa: true }), "qa", iterations > 1);
379
+ if (qaEscalation.record)
380
+ effortEscalations.push(qaEscalation.record);
381
+ const qaResult = await runPhaseTracked("qa", qaEscalation.config, iterations);
303
382
  tokensUsed = readTokensUsed(worktreePath);
304
383
  const verdict = qaResult.verdict ?? null;
305
384
  // #534 guard: a null verdict is never "ready". #853: report it as
@@ -323,7 +402,7 @@ export async function runReadyGate(opts) {
323
402
  }
324
403
  finalVerdict = verdict;
325
404
  const gaps = qaResult.summary?.gaps ?? [];
326
- remaining = classifyGaps(gaps, nonGoals);
405
+ remaining = classifyGaps(gaps, nonGoals, qaResult.summary?.findings);
327
406
  // Policy threshold reached → stop at the human merge gate.
328
407
  if (isAtThreshold(policy, verdict)) {
329
408
  return finish(verdict === "READY_FOR_MERGE" ? "READY_FOR_MERGE" : "AC_MET");
@@ -338,16 +417,28 @@ export async function runReadyGate(opts) {
338
417
  // Run one fix loop. In `ac` mode we only reach here on AC_NOT_MET, so the
339
418
  // gaps are AC gaps — feeding them via failedAcs keeps the loop scoped to
340
419
  // the AC boundary (quality gaps are never fixed under `ac`). Non-Goal-
341
- // touching findings are excluded from what we ask the loop to fix.
420
+ // touching findings are excluded from what we ask the loop to fix, as are
421
+ // `SEQUANT_QA_GAPS` findings explicitly marked `document` or
422
+ // `pause_for_human` (#937 AC-3) — a gap with no `recommendedAction` (the
423
+ // legacy prose-only path) is still treated as fixable, unchanged from
424
+ // pre-#937 behavior.
342
425
  const fixableGaps = remaining
343
- .filter((g) => !g.nonGoal)
426
+ .filter((g) => !g.nonGoal &&
427
+ g.recommendedAction !== "document" &&
428
+ g.recommendedAction !== "pause_for_human")
344
429
  .map((g) => g.description);
345
430
  const before = snapshotFn(worktreePath);
346
- const loopResult = await runPhaseTracked("loop", buildPhaseConfig(opts, {
431
+ // #915: iterations > 1 means this fix pass follows a QA pass that was
432
+ // itself a retry — the same ready-gate retry signal as the qa dispatch
433
+ // above.
434
+ const loopEscalation = withEscalatedEffort(buildPhaseConfig(opts, {
347
435
  lastVerdict: verdict,
348
436
  failedAcs: fixableGaps.join("; ") || undefined,
349
437
  promptContext: buildLoopContext(policy, verdict, fixableGaps),
350
- }), iterations);
438
+ }), "loop", iterations > 1);
439
+ if (loopEscalation.record)
440
+ effortEscalations.push(loopEscalation.record);
441
+ const loopResult = await runPhaseTracked("loop", loopEscalation.config, iterations);
351
442
  tokensUsed = readTokensUsed(worktreePath);
352
443
  if (!loopResult.success) {
353
444
  return finish("LOOP_FAILED");
@@ -48,6 +48,37 @@ export declare const QaVerdictSchema: z.ZodEnum<{
48
48
  NEEDS_VERIFICATION: "NEEDS_VERIFICATION";
49
49
  }>;
50
50
  export type QaVerdict = z.infer<typeof QaVerdictSchema>;
51
+ /**
52
+ * Source that produced the resolved spec→run phase recommendation (#921).
53
+ *
54
+ * Ordered by resolution priority: a durable structured marker in the spec's
55
+ * GitHub comment beats the same comment's prose section, which beats the
56
+ * spec agent's ephemeral chat text, which beats label-based guessing.
57
+ */
58
+ export declare const SpecRecommendationSourceSchema: z.ZodEnum<{
59
+ marker: "marker";
60
+ "comment-prose": "comment-prose";
61
+ chat: "chat";
62
+ "label-fallback": "label-fallback";
63
+ }>;
64
+ export type SpecRecommendationSource = z.infer<typeof SpecRecommendationSourceSchema>;
65
+ /**
66
+ * Resolved spec→run phase recommendation, recorded on the issue log so
67
+ * fallback frequency is auditable (#921 AC-4). Additive/optional — absent on
68
+ * runs that never reached spec resolution (e.g. spec failed) or predate this
69
+ * field, keeping the persisted-log schema stable at `version: 1`.
70
+ */
71
+ export declare const SpecRecommendationSchema: z.ZodObject<{
72
+ source: z.ZodEnum<{
73
+ marker: "marker";
74
+ "comment-prose": "comment-prose";
75
+ chat: "chat";
76
+ "label-fallback": "label-fallback";
77
+ }>;
78
+ phases: z.ZodArray<z.ZodString>;
79
+ qualityLoop: z.ZodBoolean;
80
+ }, z.core.$strip>;
81
+ export type SpecRecommendation = z.infer<typeof SpecRecommendationSchema>;
51
82
  /**
52
83
  * File diff statistics for a single file (AC-3)
53
84
  */
@@ -91,12 +122,57 @@ export declare const ErrorContextSchema: z.ZodObject<{
91
122
  build_error: "build_error";
92
123
  rate_limit: "rate_limit";
93
124
  billing: "billing";
125
+ pr_creation: "pr_creation";
94
126
  }>;
95
127
  errorType: z.ZodOptional<z.ZodString>;
96
128
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
97
129
  isRetryable: z.ZodOptional<z.ZodBoolean>;
98
130
  }, z.core.$strip>;
99
131
  export type ErrorContext = z.infer<typeof ErrorContextSchema>;
132
+ /**
133
+ * Finite taxonomy for a single gap finding surfaced by `/qa` (#937).
134
+ *
135
+ * Replaces the lossy prose-scrape gap channel (`parseListSection` against
136
+ * `**Issues:**`/`**Gaps**` headers, which misses AC-table NOT_MET rows, §6d
137
+ * Adversarial Re-Read findings, and §5 Risk Assessment). `evidence` is
138
+ * required so a finding can't be speculative — see #608's 0%-action-rate
139
+ * result for open-ended "what might we be missing" findings.
140
+ */
141
+ export declare const GapCategorySchema: z.ZodEnum<{
142
+ requirement_gap: "requirement_gap";
143
+ dependency_gap: "dependency_gap";
144
+ test_gap: "test_gap";
145
+ repository_gap: "repository_gap";
146
+ risk_gap: "risk_gap";
147
+ execution_gap: "execution_gap";
148
+ }>;
149
+ export type GapCategory = z.infer<typeof GapCategorySchema>;
150
+ export declare const GapActionSchema: z.ZodEnum<{
151
+ fix_now: "fix_now";
152
+ document: "document";
153
+ pause_for_human: "pause_for_human";
154
+ }>;
155
+ export type GapAction = z.infer<typeof GapActionSchema>;
156
+ export declare const GapFindingSchema: z.ZodObject<{
157
+ category: z.ZodEnum<{
158
+ requirement_gap: "requirement_gap";
159
+ dependency_gap: "dependency_gap";
160
+ test_gap: "test_gap";
161
+ repository_gap: "repository_gap";
162
+ risk_gap: "risk_gap";
163
+ execution_gap: "execution_gap";
164
+ }>;
165
+ evidence: z.ZodString;
166
+ description: z.ZodString;
167
+ recommendedAction: z.ZodEnum<{
168
+ fix_now: "fix_now";
169
+ document: "document";
170
+ pause_for_human: "pause_for_human";
171
+ }>;
172
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
173
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
174
+ }, z.core.$strip>;
175
+ export type GapFinding = z.infer<typeof GapFindingSchema>;
100
176
  /**
101
177
  * Condensed QA verdict summary for structured log output (#434).
102
178
  *
@@ -109,6 +185,25 @@ export declare const QaSummarySchema: z.ZodObject<{
109
185
  acTotal: z.ZodNumber;
110
186
  gaps: z.ZodArray<z.ZodString>;
111
187
  suggestions: z.ZodArray<z.ZodString>;
188
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
189
+ category: z.ZodEnum<{
190
+ requirement_gap: "requirement_gap";
191
+ dependency_gap: "dependency_gap";
192
+ test_gap: "test_gap";
193
+ repository_gap: "repository_gap";
194
+ risk_gap: "risk_gap";
195
+ execution_gap: "execution_gap";
196
+ }>;
197
+ evidence: z.ZodString;
198
+ description: z.ZodString;
199
+ recommendedAction: z.ZodEnum<{
200
+ fix_now: "fix_now";
201
+ document: "document";
202
+ pause_for_human: "pause_for_human";
203
+ }>;
204
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
205
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
206
+ }, z.core.$strip>>>;
112
207
  }, z.core.$strip>;
113
208
  export type QaSummary = z.infer<typeof QaSummarySchema>;
114
209
  /**
@@ -143,6 +238,25 @@ export declare const PhaseLogSchema: z.ZodObject<{
143
238
  acTotal: z.ZodNumber;
144
239
  gaps: z.ZodArray<z.ZodString>;
145
240
  suggestions: z.ZodArray<z.ZodString>;
241
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
242
+ category: z.ZodEnum<{
243
+ requirement_gap: "requirement_gap";
244
+ dependency_gap: "dependency_gap";
245
+ test_gap: "test_gap";
246
+ repository_gap: "repository_gap";
247
+ risk_gap: "risk_gap";
248
+ execution_gap: "execution_gap";
249
+ }>;
250
+ evidence: z.ZodString;
251
+ description: z.ZodString;
252
+ recommendedAction: z.ZodEnum<{
253
+ fix_now: "fix_now";
254
+ document: "document";
255
+ pause_for_human: "pause_for_human";
256
+ }>;
257
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
258
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
259
+ }, z.core.$strip>>>;
146
260
  }, z.core.$strip>>;
147
261
  commitHash: z.ZodOptional<z.ZodString>;
148
262
  fileDiffStats: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -174,6 +288,7 @@ export declare const PhaseLogSchema: z.ZodObject<{
174
288
  build_error: "build_error";
175
289
  rate_limit: "rate_limit";
176
290
  billing: "billing";
291
+ pr_creation: "pr_creation";
177
292
  }>;
178
293
  errorType: z.ZodOptional<z.ZodString>;
179
294
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -222,6 +337,25 @@ export declare const IssueLogSchema: z.ZodObject<{
222
337
  acTotal: z.ZodNumber;
223
338
  gaps: z.ZodArray<z.ZodString>;
224
339
  suggestions: z.ZodArray<z.ZodString>;
340
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
341
+ category: z.ZodEnum<{
342
+ requirement_gap: "requirement_gap";
343
+ dependency_gap: "dependency_gap";
344
+ test_gap: "test_gap";
345
+ repository_gap: "repository_gap";
346
+ risk_gap: "risk_gap";
347
+ execution_gap: "execution_gap";
348
+ }>;
349
+ evidence: z.ZodString;
350
+ description: z.ZodString;
351
+ recommendedAction: z.ZodEnum<{
352
+ fix_now: "fix_now";
353
+ document: "document";
354
+ pause_for_human: "pause_for_human";
355
+ }>;
356
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
357
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
358
+ }, z.core.$strip>>>;
225
359
  }, z.core.$strip>>;
226
360
  commitHash: z.ZodOptional<z.ZodString>;
227
361
  fileDiffStats: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -253,6 +387,7 @@ export declare const IssueLogSchema: z.ZodObject<{
253
387
  build_error: "build_error";
254
388
  rate_limit: "rate_limit";
255
389
  billing: "billing";
390
+ pr_creation: "pr_creation";
256
391
  }>;
257
392
  errorType: z.ZodOptional<z.ZodString>;
258
393
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -264,6 +399,16 @@ export declare const IssueLogSchema: z.ZodObject<{
264
399
  abortReason: z.ZodOptional<z.ZodString>;
265
400
  prNumber: z.ZodOptional<z.ZodNumber>;
266
401
  prUrl: z.ZodOptional<z.ZodString>;
402
+ specRecommendation: z.ZodOptional<z.ZodObject<{
403
+ source: z.ZodEnum<{
404
+ marker: "marker";
405
+ "comment-prose": "comment-prose";
406
+ chat: "chat";
407
+ "label-fallback": "label-fallback";
408
+ }>;
409
+ phases: z.ZodArray<z.ZodString>;
410
+ qualityLoop: z.ZodBoolean;
411
+ }, z.core.$strip>>;
267
412
  }, z.core.$strip>;
268
413
  export type IssueLog = z.infer<typeof IssueLogSchema>;
269
414
  /**
@@ -346,6 +491,25 @@ export declare const RunLogSchema: z.ZodObject<{
346
491
  acTotal: z.ZodNumber;
347
492
  gaps: z.ZodArray<z.ZodString>;
348
493
  suggestions: z.ZodArray<z.ZodString>;
494
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
495
+ category: z.ZodEnum<{
496
+ requirement_gap: "requirement_gap";
497
+ dependency_gap: "dependency_gap";
498
+ test_gap: "test_gap";
499
+ repository_gap: "repository_gap";
500
+ risk_gap: "risk_gap";
501
+ execution_gap: "execution_gap";
502
+ }>;
503
+ evidence: z.ZodString;
504
+ description: z.ZodString;
505
+ recommendedAction: z.ZodEnum<{
506
+ fix_now: "fix_now";
507
+ document: "document";
508
+ pause_for_human: "pause_for_human";
509
+ }>;
510
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
511
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
512
+ }, z.core.$strip>>>;
349
513
  }, z.core.$strip>>;
350
514
  commitHash: z.ZodOptional<z.ZodString>;
351
515
  fileDiffStats: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -377,6 +541,7 @@ export declare const RunLogSchema: z.ZodObject<{
377
541
  build_error: "build_error";
378
542
  rate_limit: "rate_limit";
379
543
  billing: "billing";
544
+ pr_creation: "pr_creation";
380
545
  }>;
381
546
  errorType: z.ZodOptional<z.ZodString>;
382
547
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -388,6 +553,16 @@ export declare const RunLogSchema: z.ZodObject<{
388
553
  abortReason: z.ZodOptional<z.ZodString>;
389
554
  prNumber: z.ZodOptional<z.ZodNumber>;
390
555
  prUrl: z.ZodOptional<z.ZodString>;
556
+ specRecommendation: z.ZodOptional<z.ZodObject<{
557
+ source: z.ZodEnum<{
558
+ marker: "marker";
559
+ "comment-prose": "comment-prose";
560
+ chat: "chat";
561
+ "label-fallback": "label-fallback";
562
+ }>;
563
+ phases: z.ZodArray<z.ZodString>;
564
+ qualityLoop: z.ZodBoolean;
565
+ }, z.core.$strip>>;
391
566
  }, z.core.$strip>>;
392
567
  summary: z.ZodObject<{
393
568
  totalIssues: z.ZodNumber;