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
@@ -42,6 +42,33 @@ export const QaVerdictSchema = z.enum([
42
42
  "AC_NOT_MET",
43
43
  "NEEDS_VERIFICATION",
44
44
  ]);
45
+ /**
46
+ * Source that produced the resolved spec→run phase recommendation (#921).
47
+ *
48
+ * Ordered by resolution priority: a durable structured marker in the spec's
49
+ * GitHub comment beats the same comment's prose section, which beats the
50
+ * spec agent's ephemeral chat text, which beats label-based guessing.
51
+ */
52
+ export const SpecRecommendationSourceSchema = z.enum([
53
+ "marker",
54
+ "comment-prose",
55
+ "chat",
56
+ "label-fallback",
57
+ ]);
58
+ /**
59
+ * Resolved spec→run phase recommendation, recorded on the issue log so
60
+ * fallback frequency is auditable (#921 AC-4). Additive/optional — absent on
61
+ * runs that never reached spec resolution (e.g. spec failed) or predate this
62
+ * field, keeping the persisted-log schema stable at `version: 1`.
63
+ */
64
+ export const SpecRecommendationSchema = z.object({
65
+ /** Which step in the resolution chain produced this result */
66
+ source: SpecRecommendationSourceSchema,
67
+ /** Resolved phases, spec excluded (spec already ran) */
68
+ phases: z.array(PhaseSchema),
69
+ /** Whether the quality loop should be enabled */
70
+ qualityLoop: z.boolean(),
71
+ });
45
72
  /**
46
73
  * File diff statistics for a single file (AC-3)
47
74
  */
@@ -82,7 +109,7 @@ export const ErrorContextSchema = z.object({
82
109
  /**
83
110
  * Classified error category (legacy, kept for backwards compatibility).
84
111
  * Keep in sync with `ERROR_CATEGORIES` in `error-classifier.ts` —
85
- * `rate_limit` / `billing` added by #761 AC-6.
112
+ * `rate_limit` / `billing` added by #761 AC-6, `pr_creation` by #920.
86
113
  */
87
114
  category: z.enum([
88
115
  "context_overflow",
@@ -92,6 +119,7 @@ export const ErrorContextSchema = z.object({
92
119
  "timeout",
93
120
  "rate_limit",
94
121
  "billing",
122
+ "pr_creation",
95
123
  "unknown",
96
124
  ]),
97
125
  /** Typed error class name (AC-8), e.g. "ApiError", "BuildError" */
@@ -101,6 +129,39 @@ export const ErrorContextSchema = z.object({
101
129
  /** Whether this error type is retryable (AC-9) */
102
130
  isRetryable: z.boolean().optional(),
103
131
  });
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 const GapCategorySchema = z.enum([
142
+ "requirement_gap",
143
+ "dependency_gap",
144
+ "test_gap",
145
+ "repository_gap",
146
+ "risk_gap",
147
+ "execution_gap",
148
+ ]);
149
+ export const GapActionSchema = z.enum([
150
+ "fix_now",
151
+ "document",
152
+ "pause_for_human",
153
+ ]);
154
+ export const GapFindingSchema = z.object({
155
+ category: GapCategorySchema,
156
+ /** Concrete observation grounding the finding — never speculation. */
157
+ evidence: z.string().min(1),
158
+ description: z.string().min(1),
159
+ recommendedAction: GapActionSchema,
160
+ /** ACs this finding relates to, e.g. ["AC-3"]. */
161
+ affectedAcs: z.array(z.string()).optional(),
162
+ /** True when the finding overlaps one of the issue's Non-Goals. */
163
+ nonGoal: z.boolean().optional(),
164
+ });
104
165
  /**
105
166
  * Condensed QA verdict summary for structured log output (#434).
106
167
  *
@@ -117,6 +178,13 @@ export const QaSummarySchema = z.object({
117
178
  gaps: z.array(z.string()),
118
179
  /** List of improvement suggestions from QA */
119
180
  suggestions: z.array(z.string()),
181
+ /**
182
+ * Structured gap findings parsed from the `SEQUANT_QA_GAPS` marker (#937).
183
+ * Present only when the marker was found and validated; `gaps` above
184
+ * always carries the union of marker + prose descriptions (dedupe'd) so
185
+ * marker-unaware consumers never regress.
186
+ */
187
+ findings: z.array(GapFindingSchema).optional(),
120
188
  });
121
189
  /**
122
190
  * Log entry for a single phase execution
@@ -198,6 +266,8 @@ export const IssueLogSchema = z.object({
198
266
  prNumber: z.number().int().positive().optional(),
199
267
  /** PR URL if created after successful QA */
200
268
  prUrl: z.string().optional(),
269
+ /** How the spec→run phase recommendation was resolved (#921 AC-4) */
270
+ specRecommendation: SpecRecommendationSchema.optional(),
201
271
  });
202
272
  /**
203
273
  * Run configuration
@@ -1207,6 +1207,29 @@ export class RunOrchestrator {
1207
1207
  // issue is the halting one; in parallel runs it is the earliest-listed
1208
1208
  // failure. Undefined on success — the field is omitted from the record.
1209
1209
  const failureCategory = results.find((r) => !r.success && r.failureCategory)?.failureCategory;
1210
+ // #915: one entry per escalated phase execution, from both retry sites —
1211
+ // the outer quality loop (`phaseResults[].escalatedEffort`) and the
1212
+ // `--ready-gate` QA-pass loop (`readyGate.effortEscalations`).
1213
+ const effortEscalations = [
1214
+ ...results.flatMap((r) => r.phaseResults
1215
+ .filter((p) => p.escalatedEffort)
1216
+ .map((p) => ({
1217
+ phase: p.phase,
1218
+ base: p.escalatedEffort.base,
1219
+ escalated: p.escalatedEffort.escalated,
1220
+ }))),
1221
+ ...results.flatMap((r) => r.readyGate?.effortEscalations ?? []),
1222
+ ];
1223
+ // #915: the outer quality loop already logs its own escalations live at
1224
+ // the batch-executor dispatch site — only the `--ready-gate` QA-pass loop
1225
+ // has no equivalent live print, so surface those here.
1226
+ if (config.verbose) {
1227
+ for (const r of results) {
1228
+ for (const e of r.readyGate?.effortEscalations ?? []) {
1229
+ console.log(chalk.gray(` effort: ${e.base} → ${e.escalated} (ready-gate retry, #${r.issueNumber})`));
1230
+ }
1231
+ }
1232
+ }
1210
1233
  await metricsWriter.recordRun({
1211
1234
  issues: issueNumbers,
1212
1235
  phases: Array.from(allPhases),
@@ -1215,6 +1238,10 @@ export class RunOrchestrator {
1215
1238
  model: process.env.ANTHROPIC_MODEL ?? "opus",
1216
1239
  flags: cliFlags,
1217
1240
  failureCategory,
1241
+ // #914: resolved per-phase model/effort, when any phase had one.
1242
+ phasePolicies: config.phasePolicies,
1243
+ // #915: escalated tiers, when any phase execution escalated.
1244
+ effortEscalations,
1218
1245
  metrics: {
1219
1246
  tokensUsed: tokenUsage.tokensUsed,
1220
1247
  filesChanged: totalFilesChanged,
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Structured resolution of the spec→run phase recommendation (#921).
3
+ *
4
+ * `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
5
+ * *ephemeral chat text* for a `## Recommended Workflow` section. When the
6
+ * spec agent posts its plan via a body file instead of restating it in
7
+ * chat, that regex has nothing to match and the run silently falls back to
8
+ * label-based phase detection — dropping any recommended phase the label
9
+ * fallback can never produce (e.g. `testgen`). See #814.
10
+ *
11
+ * This module resolves the recommendation through an ordered chain, each
12
+ * step falling through loudly to the next on failure:
13
+ *
14
+ * 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
15
+ * comment marker (this issue's fix)
16
+ * 2. `comment-prose` — the same regex as `chat`, applied to the spec
17
+ * plan's GitHub comment body instead of chat text
18
+ * 3. `chat` — `parseRecommendedWorkflow` over the agent's
19
+ * captured chat output (existing behavior)
20
+ * 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
21
+ *
22
+ * The durable comment marker is the system's existing idiom — see
23
+ * `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
24
+ * `assess-comment-parser.ts`.
25
+ */
26
+ import type { Phase } from "./types.js";
27
+ import { GitHubProvider } from "./platforms/github.js";
28
+ import type { SpecRecommendationSource } from "./run-log-schema.js";
29
+ export interface ResolvedSpecRecommendation {
30
+ phases: Phase[];
31
+ qualityLoop: boolean;
32
+ source: SpecRecommendationSource;
33
+ }
34
+ /**
35
+ * Extract and validate the latest `SEQUANT_SPEC` marker across a set of
36
+ * comment bodies (oldest-to-newest order, matching `gh`'s natural order).
37
+ *
38
+ * Returns `null` when no marker is present at all. Returns `null` and logs a
39
+ * visible warning when the latest marker is malformed JSON, fails schema
40
+ * validation, or names a phase the registry doesn't recognize — per AC-2,
41
+ * an invalid marker falls through to the next resolution step rather than
42
+ * silently dropping the recommendation or silently ignoring the bad phase.
43
+ *
44
+ * Only the *latest* marker is considered (last comment, last match within
45
+ * it) — a re-spec's marker supersedes an earlier one, matching how a human
46
+ * reader would interpret the thread.
47
+ */
48
+ export declare function parseSpecMarker(commentBodies: string[]): {
49
+ phases: Phase[];
50
+ qualityLoop: boolean;
51
+ } | null;
52
+ export interface ResolveSpecRecommendationInput {
53
+ /** The spec agent's captured chat output (existing `chat` fallback). */
54
+ chatOutput: string;
55
+ /** GitHub issue number, used to fetch its comments for the marker/prose steps. */
56
+ issueNumber: number;
57
+ /** Issue labels, used for the final label-based fallback. */
58
+ labels: string[];
59
+ /**
60
+ * Injectable for tests — defaults to a real `GitHubProvider`. Comment
61
+ * fetch failures are already handled by `fetchIssueCommentBodiesSync`
62
+ * (returns `[]`), so the chain degrades to `chat`/`label-fallback`
63
+ * gracefully when GitHub is unreachable.
64
+ */
65
+ githubProvider?: Pick<GitHubProvider, "fetchIssueCommentBodiesSync">;
66
+ }
67
+ /**
68
+ * Resolve the spec→run phase recommendation through the ordered chain:
69
+ * comment-marker → comment-prose → chat-text → label-fallback.
70
+ */
71
+ export declare function resolveSpecRecommendation(input: ResolveSpecRecommendationInput): ResolvedSpecRecommendation;
@@ -0,0 +1,142 @@
1
+ /**
2
+ * Structured resolution of the spec→run phase recommendation (#921).
3
+ *
4
+ * `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
5
+ * *ephemeral chat text* for a `## Recommended Workflow` section. When the
6
+ * spec agent posts its plan via a body file instead of restating it in
7
+ * chat, that regex has nothing to match and the run silently falls back to
8
+ * label-based phase detection — dropping any recommended phase the label
9
+ * fallback can never produce (e.g. `testgen`). See #814.
10
+ *
11
+ * This module resolves the recommendation through an ordered chain, each
12
+ * step falling through loudly to the next on failure:
13
+ *
14
+ * 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
15
+ * comment marker (this issue's fix)
16
+ * 2. `comment-prose` — the same regex as `chat`, applied to the spec
17
+ * plan's GitHub comment body instead of chat text
18
+ * 3. `chat` — `parseRecommendedWorkflow` over the agent's
19
+ * captured chat output (existing behavior)
20
+ * 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
21
+ *
22
+ * The durable comment marker is the system's existing idiom — see
23
+ * `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
24
+ * `assess-comment-parser.ts`.
25
+ */
26
+ import chalk from "chalk";
27
+ import { z } from "zod";
28
+ import { phaseRegistry } from "./phase-registry.js";
29
+ import { GitHubProvider } from "./platforms/github.js";
30
+ import { stripMarkdownCode } from "./phase-detection.js";
31
+ import { parseRecommendedWorkflow, detectPhasesFromLabels, } from "./phase-mapper.js";
32
+ /** Regex to extract the SEQUANT_SPEC marker JSON from an HTML comment. */
33
+ const SPEC_MARKER_REGEX = /<!-- SEQUANT_SPEC: (\{[^}]+\}) -->/g;
34
+ /**
35
+ * Structural shape of the marker JSON, before phase names are checked
36
+ * against the phase registry. `qualityLoop` defaults to `false` when
37
+ * omitted, matching `parseRecommendedWorkflow`'s prose-parsing default.
38
+ */
39
+ const SpecMarkerJsonSchema = z.object({
40
+ phases: z.array(z.string()).min(1),
41
+ qualityLoop: z.boolean().optional().default(false),
42
+ });
43
+ /**
44
+ * Extract and validate the latest `SEQUANT_SPEC` marker across a set of
45
+ * comment bodies (oldest-to-newest order, matching `gh`'s natural order).
46
+ *
47
+ * Returns `null` when no marker is present at all. Returns `null` and logs a
48
+ * visible warning when the latest marker is malformed JSON, fails schema
49
+ * validation, or names a phase the registry doesn't recognize — per AC-2,
50
+ * an invalid marker falls through to the next resolution step rather than
51
+ * silently dropping the recommendation or silently ignoring the bad phase.
52
+ *
53
+ * Only the *latest* marker is considered (last comment, last match within
54
+ * it) — a re-spec's marker supersedes an earlier one, matching how a human
55
+ * reader would interpret the thread.
56
+ */
57
+ export function parseSpecMarker(commentBodies) {
58
+ let latestRaw = null;
59
+ for (const body of commentBodies) {
60
+ const stripped = stripMarkdownCode(body);
61
+ SPEC_MARKER_REGEX.lastIndex = 0;
62
+ let match;
63
+ while ((match = SPEC_MARKER_REGEX.exec(stripped)) !== null) {
64
+ latestRaw = match[1];
65
+ }
66
+ }
67
+ if (latestRaw === null) {
68
+ return null;
69
+ }
70
+ let parsedJson;
71
+ try {
72
+ parsedJson = JSON.parse(latestRaw);
73
+ }
74
+ catch {
75
+ console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker is not valid JSON — falling through to comment prose`));
76
+ return null;
77
+ }
78
+ const result = SpecMarkerJsonSchema.safeParse(parsedJson);
79
+ if (!result.success) {
80
+ console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker failed schema validation — falling through to comment prose`));
81
+ return null;
82
+ }
83
+ const unknownPhases = result.data.phases.filter((name) => !phaseRegistry.has(name));
84
+ if (unknownPhases.length > 0) {
85
+ console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker names unknown phase(s): ${unknownPhases.join(", ")} — falling through to comment prose`));
86
+ return null;
87
+ }
88
+ return { phases: result.data.phases, qualityLoop: result.data.qualityLoop };
89
+ }
90
+ /**
91
+ * Find the most recently posted comment containing a `## Recommended
92
+ * Workflow` section and parse it with the existing prose regex.
93
+ *
94
+ * Scans newest-to-oldest so a later spec re-run's comment takes precedence
95
+ * over an earlier one — same intuition as the marker step.
96
+ */
97
+ function parseCommentProse(commentBodies) {
98
+ for (let i = commentBodies.length - 1; i >= 0; i--) {
99
+ const parsed = parseRecommendedWorkflow(commentBodies[i]);
100
+ if (parsed) {
101
+ return parsed;
102
+ }
103
+ }
104
+ return null;
105
+ }
106
+ /**
107
+ * Resolve the spec→run phase recommendation through the ordered chain:
108
+ * comment-marker → comment-prose → chat-text → label-fallback.
109
+ */
110
+ export function resolveSpecRecommendation(input) {
111
+ const github = input.githubProvider ?? new GitHubProvider();
112
+ const commentBodies = github.fetchIssueCommentBodiesSync(String(input.issueNumber));
113
+ let phases;
114
+ let qualityLoop;
115
+ let source;
116
+ const marker = parseSpecMarker(commentBodies);
117
+ const prose = marker ? null : parseCommentProse(commentBodies);
118
+ const chat = marker || prose || !input.chatOutput
119
+ ? null
120
+ : parseRecommendedWorkflow(input.chatOutput);
121
+ if (marker) {
122
+ ({ phases, qualityLoop } = marker);
123
+ source = "marker";
124
+ }
125
+ else if (prose) {
126
+ ({ phases, qualityLoop } = prose);
127
+ source = "comment-prose";
128
+ }
129
+ else if (chat) {
130
+ ({ phases, qualityLoop } = chat);
131
+ source = "chat";
132
+ }
133
+ else {
134
+ const fallback = detectPhasesFromLabels(input.labels);
135
+ phases = fallback.phases;
136
+ qualityLoop = fallback.qualityLoop;
137
+ source = "label-fallback";
138
+ }
139
+ // Spec already ran by the time this resolves — never re-include it,
140
+ // regardless of which step in the chain produced the result.
141
+ return { phases: phases.filter((p) => p !== "spec"), qualityLoop, source };
142
+ }
@@ -149,6 +149,7 @@ export declare const AcceptanceCriterionSchema: z.ZodObject<{
149
149
  }>;
150
150
  verifiedAt: z.ZodOptional<z.ZodString>;
151
151
  notes: z.ZodOptional<z.ZodString>;
152
+ evidence: z.ZodOptional<z.ZodString>;
152
153
  }, z.core.$strip>;
153
154
  export type AcceptanceCriterion = z.infer<typeof AcceptanceCriterionSchema>;
154
155
  /**
@@ -172,6 +173,7 @@ export declare const AcceptanceCriteriaSchema: z.ZodObject<{
172
173
  }>;
173
174
  verifiedAt: z.ZodOptional<z.ZodString>;
174
175
  notes: z.ZodOptional<z.ZodString>;
176
+ evidence: z.ZodOptional<z.ZodString>;
175
177
  }, z.core.$strip>>;
176
178
  extractedAt: z.ZodString;
177
179
  summary: z.ZodObject<{
@@ -281,6 +283,7 @@ export declare const IssueStateSchema: z.ZodObject<{
281
283
  }>;
282
284
  verifiedAt: z.ZodOptional<z.ZodString>;
283
285
  notes: z.ZodOptional<z.ZodString>;
286
+ evidence: z.ZodOptional<z.ZodString>;
284
287
  }, z.core.$strip>>;
285
288
  extractedAt: z.ZodString;
286
289
  summary: z.ZodObject<{
@@ -430,6 +433,7 @@ export declare const WorkflowStateSchema: z.ZodObject<{
430
433
  }>;
431
434
  verifiedAt: z.ZodOptional<z.ZodString>;
432
435
  notes: z.ZodOptional<z.ZodString>;
436
+ evidence: z.ZodOptional<z.ZodString>;
433
437
  }, z.core.$strip>>;
434
438
  extractedAt: z.ZodString;
435
439
  summary: z.ZodObject<{
@@ -538,7 +542,7 @@ export declare function createPhaseState(status?: PhaseStatus): PhaseState;
538
542
  /**
539
543
  * Create acceptance criterion
540
544
  */
541
- export declare function createAcceptanceCriterion(id: string, description: string, verificationMethod?: ACVerificationMethod): AcceptanceCriterion;
545
+ export declare function createAcceptanceCriterion(id: string, description: string, verificationMethod?: ACVerificationMethod, evidence?: string): AcceptanceCriterion;
542
546
  /**
543
547
  * Create acceptance criteria tracking structure
544
548
  */
@@ -149,6 +149,12 @@ export const AcceptanceCriterionSchema = z.object({
149
149
  verifiedAt: z.string().datetime().optional(),
150
150
  /** Additional notes about verification */
151
151
  notes: z.string().optional(),
152
+ /**
153
+ * Explicit `Evidence:` clause declared on the AC line (#938). When present,
154
+ * this is what verificationMethod was resolved from instead of keyword
155
+ * inference — see `resolveVerificationMethod` in ac-parser.ts.
156
+ */
157
+ evidence: z.string().optional(),
152
158
  });
153
159
  /**
154
160
  * Acceptance criteria tracking for an issue
@@ -369,12 +375,13 @@ export function createPhaseState(status = "pending") {
369
375
  /**
370
376
  * Create acceptance criterion
371
377
  */
372
- export function createAcceptanceCriterion(id, description, verificationMethod = "manual") {
378
+ export function createAcceptanceCriterion(id, description, verificationMethod = "manual", evidence) {
373
379
  return {
374
380
  id,
375
381
  description,
376
382
  verificationMethod,
377
383
  status: "pending",
384
+ ...(evidence !== undefined ? { evidence } : {}),
378
385
  };
379
386
  }
380
387
  /**
@@ -83,6 +83,15 @@ export interface ExecutionConfig {
83
83
  dryRun: boolean;
84
84
  /** Enable MCP servers in headless mode (true by default, false if --no-mcp flag used) */
85
85
  mcp: boolean;
86
+ /**
87
+ * Claude Desktop MCP server names explicitly opted in via
88
+ * `settings.run.mcpAllowlist` (#936). Both `ExecutionConfig` producers
89
+ * (`buildExecutionConfig`, `ready-gate.ts`'s `buildPhaseConfig`) read
90
+ * this from settings the same way, so it cannot drift between them the
91
+ * way #833's `phaseTimeout` once did. `phase-executor.ts` forwards it
92
+ * verbatim onto `AgentExecutionConfig.mcpAllowlist`.
93
+ */
94
+ mcpAllowlist?: string[];
86
95
  /**
87
96
  * Enable automatic retry with MCP fallback.
88
97
  * When true (default), failed phases are retried with MCP disabled.
@@ -182,6 +191,28 @@ export interface ExecutionConfig {
182
191
  * see {@link AutoWaitLedger}.
183
192
  */
184
193
  autoWaitMinutes?: number;
194
+ /**
195
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
196
+ * name. Merged from `settings.run.phases` and the CLI's `--models`/
197
+ * `--efforts` flags via `resolvePhasePolicies` (CLI > settings > absent) —
198
+ * see `config-resolver.ts`. Absent/empty by default: `phase-executor.ts`
199
+ * only sets `AgentExecutionConfig.model`/`.effort` when a phase has an
200
+ * entry here, so an unconfigured run reaches the SDK unchanged.
201
+ */
202
+ phasePolicies?: Record<string, {
203
+ model?: string;
204
+ effort?: string;
205
+ }>;
206
+ /**
207
+ * Evidence-based effort escalation on quality-loop retries (#915). CLI >
208
+ * settings > absent (`false`), resolved by `buildExecutionConfig`
209
+ * (`config-resolver.ts`) and `ready-gate.ts`'s `buildPhaseConfig` — the same
210
+ * two producers `phasePolicies` uses, so this cannot drift from that one
211
+ * (#833 class). Consumed only at dispatch time by
212
+ * `effort-escalation.ts`'s `withEscalatedEffort`, never baked statically
213
+ * into `phasePolicies` here — escalation is per-execution, not per-run.
214
+ */
215
+ effortEscalation?: boolean;
185
216
  }
186
217
  /**
187
218
  * A single liveness notice emitted during an auto-wait (#804 AC-7).
@@ -246,6 +277,16 @@ export interface PhaseResult {
246
277
  stdoutTail?: string[];
247
278
  /** Process exit code from the agent driver (#447) */
248
279
  exitCode?: number;
280
+ /**
281
+ * Set when this execution's effort was escalated one tier above its
282
+ * resolved base (#915) — a quality-loop retry with `effortEscalation`
283
+ * enabled. Additive/optional, same shape as `capped?`/`structuredError?`;
284
+ * absent on every non-escalated execution.
285
+ */
286
+ escalatedEffort?: {
287
+ base: string;
288
+ escalated: string;
289
+ };
249
290
  }
250
291
  /**
251
292
  * Result of executing all phases for an issue
@@ -269,6 +310,15 @@ export interface IssueResult {
269
310
  * with this string as the reason.
270
311
  */
271
312
  prCreationError?: string;
313
+ /**
314
+ * Set when PR creation was skipped because the branch carried zero commits
315
+ * ahead of its base (#920) — a phase-restricted run (e.g. `--phases spec`)
316
+ * that implemented nothing has no deliverable to open a PR for, and
317
+ * attempting one would only fail with GitHub's "No commits between main and
318
+ * …" error. Distinct from {@link IssueResult.prCreationError}: this is not
319
+ * a failure — `success` stays whatever the phases produced.
320
+ */
321
+ prSkippedReason?: string;
272
322
  /**
273
323
  * Set when the issue was skipped because another sequant session holds
274
324
  * the per-issue lock (#625). Surfaced in the summary as
@@ -486,6 +536,29 @@ export interface RunOptions {
486
536
  * and bounds wholesale — no new settings accompany this flag.
487
537
  */
488
538
  readyGate?: boolean;
539
+ /**
540
+ * Per-phase model override (#914). Either a bare value applied to every
541
+ * phase (`--models sonnet`) or a comma list of `phase=model` pairs
542
+ * (`--models spec=fable,exec=sonnet`). Parsed and merged with
543
+ * `settings.run.phases` by `resolvePhasePolicies` (CLI > settings >
544
+ * absent) into `ExecutionConfig.phasePolicies`. Malformed specs fail fast
545
+ * at the Commander option boundary via `cli-flags.ts`.
546
+ */
547
+ models?: string;
548
+ /**
549
+ * Per-phase reasoning-effort override (#914). Same grammar as {@link
550
+ * models} (bare value or comma list of `phase=effort` pairs); each value
551
+ * validates against the SDK's closed `low|medium|high|xhigh|max` enum.
552
+ */
553
+ efforts?: string;
554
+ /**
555
+ * Evidence-based effort escalation on quality-loop retries (#915). Set via
556
+ * `--escalate-effort`. When true, a retried phase execution resolves one
557
+ * effort tier above its configured/inherited base for that execution only
558
+ * — see `effort-escalation.ts`. Default `false`: escalation raises token
559
+ * spend, so an unset flag leaves every run byte-identical to pre-#915.
560
+ */
561
+ escalateEffort?: boolean;
489
562
  }
490
563
  /**
491
564
  * CLI arguments for run command
@@ -641,4 +714,9 @@ export interface IssueExecutionContext {
641
714
  * around verbose Claude streaming.
642
715
  */
643
716
  phasePauseHandle?: PhasePauseHandle;
717
+ /**
718
+ * @internal Test seam for the #964 qa-verdict comment post. Defaults to a
719
+ * real `GitHubProvider().postComment` when unset.
720
+ */
721
+ postComment?: (issueNumber: number, body: string) => Promise<void>;
644
722
  }
@@ -104,8 +104,15 @@ export declare function checkWorktreeFreshness(worktreePath: string, verbose: bo
104
104
  export declare function removeStaleWorktree(existingPath: string, branch: string, verbose: boolean): boolean;
105
105
  /**
106
106
  * List all active worktrees with their branches
107
+ *
108
+ * `git worktree list` only ever reports worktrees belonging to the repository
109
+ * containing `cwd`, which is what makes this a repo-scoped lookup: a sibling
110
+ * project's worktree can never appear here, even though `../worktrees/` is a
111
+ * single directory shared by every repo under the same parent (#899).
112
+ *
113
+ * @param cwd - Directory to run git in. Defaults to the current process cwd.
107
114
  */
108
- export declare function listWorktrees(): Array<{
115
+ export declare function listWorktrees(cwd?: string): Array<{
109
116
  path: string;
110
117
  branch: string;
111
118
  issue: number | null;
@@ -205,10 +205,18 @@ export function removeStaleWorktree(existingPath, branch, verbose) {
205
205
  }
206
206
  /**
207
207
  * List all active worktrees with their branches
208
+ *
209
+ * `git worktree list` only ever reports worktrees belonging to the repository
210
+ * containing `cwd`, which is what makes this a repo-scoped lookup: a sibling
211
+ * project's worktree can never appear here, even though `../worktrees/` is a
212
+ * single directory shared by every repo under the same parent (#899).
213
+ *
214
+ * @param cwd - Directory to run git in. Defaults to the current process cwd.
208
215
  */
209
- export function listWorktrees() {
216
+ export function listWorktrees(cwd) {
210
217
  const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
211
218
  stdio: "pipe",
219
+ ...(cwd ? { cwd } : {}),
212
220
  });
213
221
  if (result.status !== 0)
214
222
  return [];
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Repo-scoped worktree resolution and verification (#899).
3
+ *
4
+ * Skill-driven `/fullsolve` used to hand `/exec` an unexpanded glob
5
+ * (`../worktrees/feature/<issue>-` followed by a star and a slash) and `/exec`
6
+ * was instructed to trust it without checking. Two things went wrong:
7
+ *
8
+ * 1. When nothing had created the worktree, the `cd` failed and the agent
9
+ * silently kept working in the main checkout.
10
+ * 2. `../worktrees/` is one flat namespace shared by every repo under the same
11
+ * parent directory, and issue numbers are per-repo — so where the glob *did*
12
+ * match, it could match a sibling project's worktree.
13
+ *
14
+ * Both are fixed the same way: never resolve worktrees through the filesystem.
15
+ * `git worktree list` reports only the worktrees of the repository containing
16
+ * `cwd`, so scoping is structural rather than a filter that can be forgotten,
17
+ * and it reports the *branch*, which is the real identity — a worktree
18
+ * directory slug can drift from its branch after a rename.
19
+ */
20
+ /** Named failure modes for `resolveIssueWorktree`. */
21
+ export type ResolveErrorCode = "WORKTREE_NOT_FOUND" | "WORKTREE_AMBIGUOUS";
22
+ /** Named failure modes for `verifyWorktreePath`. */
23
+ export type VerifyErrorCode = "SEQUANT_WORKTREE_NOT_FOUND" | "SEQUANT_WORKTREE_FOREIGN" | "SEQUANT_WORKTREE_ISSUE_MISMATCH";
24
+ /** A worktree of the current repository, as reported by git. */
25
+ export interface ResolvedWorktree {
26
+ /** Absolute path, exactly as git reports it. */
27
+ path: string;
28
+ /** Branch checked out there, e.g. `feature/899-some-slug`. */
29
+ branch: string;
30
+ }
31
+ export type ResolveResult = ({
32
+ ok: true;
33
+ } & ResolvedWorktree) | {
34
+ ok: false;
35
+ error: ResolveErrorCode;
36
+ message: string;
37
+ /** Populated for WORKTREE_AMBIGUOUS so the caller can report candidates. */
38
+ candidates: ResolvedWorktree[];
39
+ };
40
+ export type VerifyResult = ({
41
+ ok: true;
42
+ } & ResolvedWorktree) | {
43
+ ok: false;
44
+ error: VerifyErrorCode;
45
+ message: string;
46
+ };
47
+ /**
48
+ * Find the worktree of the *current* repository that holds issue `issue`.
49
+ *
50
+ * Selection keys on the branch git reports, never on the directory name, so a
51
+ * worktree whose slug has drifted from its branch still resolves — and a
52
+ * directory that merely looks like a match (a sibling repo's worktree, or a
53
+ * stray directory) never does.
54
+ *
55
+ * @param issue - Issue number to look up.
56
+ * @param cwd - Directory inside the repository to search. Defaults to process cwd.
57
+ */
58
+ export declare function resolveIssueWorktree(issue: number, cwd?: string): ResolveResult;
59
+ /**
60
+ * Verify that a caller-supplied worktree path is safe to work in.
61
+ *
62
+ * This is the guard `/exec` runs before trusting `SEQUANT_WORKTREE`. It fails
63
+ * closed on every uncertain case — a bad path must halt the run, never degrade
64
+ * into "keep going in whatever directory we happen to be in".
65
+ *
66
+ * @param worktreePath - The path to check (typically `$SEQUANT_WORKTREE`).
67
+ * @param options.issue - When set, the branch must belong to this issue.
68
+ * @param options.cwd - Directory inside the repository to check against.
69
+ */
70
+ export declare function verifyWorktreePath(worktreePath: string, options?: {
71
+ issue?: number;
72
+ cwd?: string;
73
+ }): VerifyResult;