sequant 2.10.0 → 2.11.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 (71) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/README.md +6 -2
  3. package/dist/bin/cli.js +47 -2
  4. package/dist/src/commands/locks.d.ts +20 -1
  5. package/dist/src/commands/locks.js +206 -4
  6. package/dist/src/commands/ready.d.ts +6 -0
  7. package/dist/src/commands/ready.js +15 -1
  8. package/dist/src/commands/run-display.js +1 -0
  9. package/dist/src/commands/worktree.d.ts +31 -0
  10. package/dist/src/commands/worktree.js +95 -0
  11. package/dist/src/lib/cli-flags.d.ts +23 -0
  12. package/dist/src/lib/cli-flags.js +43 -0
  13. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
  14. package/dist/src/lib/cli-ui/run-renderer.js +7 -1
  15. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  16. package/dist/src/lib/locks/checkout-lock.js +389 -0
  17. package/dist/src/lib/locks/index.d.ts +6 -3
  18. package/dist/src/lib/locks/index.js +4 -2
  19. package/dist/src/lib/locks/lock-manager.d.ts +81 -1
  20. package/dist/src/lib/locks/lock-manager.js +230 -5
  21. package/dist/src/lib/locks/types.d.ts +72 -0
  22. package/dist/src/lib/locks/types.js +28 -0
  23. package/dist/src/lib/settings.d.ts +73 -0
  24. package/dist/src/lib/settings.js +45 -0
  25. package/dist/src/lib/test-tautology-detector.d.ts +4 -3
  26. package/dist/src/lib/test-tautology-detector.js +101 -41
  27. package/dist/src/lib/workflow/batch-executor.js +78 -19
  28. package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
  29. package/dist/src/lib/workflow/config-resolver.js +89 -0
  30. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
  31. package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
  32. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  33. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  34. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  35. package/dist/src/lib/workflow/error-classifier.js +4 -0
  36. package/dist/src/lib/workflow/log-writer.d.ts +10 -1
  37. package/dist/src/lib/workflow/log-writer.js +20 -0
  38. package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
  39. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  40. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  41. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  42. package/dist/src/lib/workflow/phase-detection.js +5 -1
  43. package/dist/src/lib/workflow/phase-executor.js +10 -0
  44. package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
  45. package/dist/src/lib/workflow/ready-gate.js +24 -3
  46. package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
  47. package/dist/src/lib/workflow/run-log-schema.js +31 -1
  48. package/dist/src/lib/workflow/run-orchestrator.js +27 -0
  49. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  50. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  51. package/dist/src/lib/workflow/types.d.ts +64 -0
  52. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  53. package/dist/src/lib/workflow/worktree-manager.js +9 -1
  54. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  55. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  56. package/package.json +3 -2
  57. package/templates/hooks/pre-tool.sh +228 -0
  58. package/templates/scripts/cleanup-worktree.sh +36 -15
  59. package/templates/scripts/new-feature.sh +25 -19
  60. package/templates/skills/_shared/references/subagent-types.md +7 -18
  61. package/templates/skills/assess/SKILL.md +5 -1
  62. package/templates/skills/exec/SKILL.md +61 -7
  63. package/templates/skills/fullsolve/SKILL.md +127 -21
  64. package/templates/skills/loop/SKILL.md +56 -11
  65. package/templates/skills/merger/SKILL.md +98 -10
  66. package/templates/skills/qa/SKILL.md +59 -6
  67. package/templates/skills/release/SKILL.md +79 -0
  68. package/templates/skills/spec/SKILL.md +31 -15
  69. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  70. package/templates/skills/testgen/SKILL.md +23 -6
  71. package/templates/agents/sequant-explorer.md +0 -24
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Evidence-based effort escalation on quality-loop retries (#915).
3
+ *
4
+ * Sequant already detects several "this attempt is a retry" moments: the
5
+ * outer quality-loop re-entering a phase (`batch-executor.ts`) and the
6
+ * `sequant ready` QA-pass loop re-running `qa`/`loop` (`ready-gate.ts`).
7
+ * Escalation raises the phase's reasoning effort one tier for exactly that
8
+ * retried execution when the workflow observed a prior attempt fail — never
9
+ * speculatively, and never more than one tier per retry (see AC-6).
10
+ *
11
+ * Deliberately its own module rather than living beside `resolvePhasePolicies`
12
+ * in `config-resolver.ts`: `config-resolver.ts` imports `getEnvConfig` from
13
+ * `batch-executor.ts`, and `batch-executor.ts` is one of this module's
14
+ * dispatch-time callers, so co-locating here avoids introducing that cycle.
15
+ */
16
+ import { EFFORT_LEVELS } from "../settings.js";
17
+ /**
18
+ * Base effort assumed for a phase with no configured `effort` override, when
19
+ * escalation needs a starting point to step up from (AC-5). `phase-executor.ts`
20
+ * omits the `effort` key entirely in that case (#914) so the Agent SDK's own
21
+ * default applies — #914 deliberately never encoded what that default is,
22
+ * since "omitted" is not the same claim as "equals X".
23
+ *
24
+ * Verified against `@anthropic-ai/claude-agent-sdk`'s own `query()` Options
25
+ * type (`sdk.d.ts`): `effort?: EffortLevel` is documented inline as
26
+ * `'high' — Deep reasoning (default)`. That is the SDK's default for the
27
+ * exact call sequant makes (raw `query()`, not the Claude Code CLI product —
28
+ * whose own `xhigh` default is a caller choice on top of this SDK, not the
29
+ * SDK's own default), so this constant is not a guess.
30
+ */
31
+ export const DEFAULT_ESCALATION_BASE = "high";
32
+ /**
33
+ * Pure ladder step: one tier above `base` on `EFFORT_LEVELS`, capped at the
34
+ * top (`max`). Returns `base` unchanged whenever `enabled` is false or this
35
+ * isn't a retry — the disabled/first-attempt path must be indistinguishable
36
+ * from #914 with escalation never having existed (AC-2).
37
+ *
38
+ * Always escalates from the phase's CONFIGURED base, never from a previously
39
+ * escalated value — callers must not accumulate escalation across iterations
40
+ * (AC-6): base `high` on the 3rd loop iteration is `xhigh`, not `max`.
41
+ */
42
+ export function resolveEscalatedEffort(base, isRetry, enabled) {
43
+ if (!enabled || !isRetry)
44
+ return base;
45
+ const effectiveBase = (base ??
46
+ DEFAULT_ESCALATION_BASE);
47
+ const baseIdx = EFFORT_LEVELS.indexOf(effectiveBase);
48
+ const resolvedIdx = baseIdx === -1 ? EFFORT_LEVELS.indexOf(DEFAULT_ESCALATION_BASE) : baseIdx;
49
+ const nextIdx = Math.min(resolvedIdx + 1, EFFORT_LEVELS.length - 1);
50
+ return EFFORT_LEVELS[nextIdx];
51
+ }
52
+ /**
53
+ * Apply escalation to ONE phase's execution, for THIS dispatch only.
54
+ *
55
+ * This is deliberately a per-execution decision made at the dispatch site,
56
+ * not a value baked into `ExecutionConfig` at build time: `buildExecutionConfig`
57
+ * / `buildPhaseConfig` run once per run/gate, not once per phase execution, so
58
+ * a static escalated value would leak across every phase in the chain and
59
+ * violate AC-7. The three retry-dispatch sites (batch-executor.ts's quality
60
+ * loop, ready-gate.ts's QA-pass loop `qa`/`loop` dispatch) call this function
61
+ * — and only this function — so they cannot drift on the cap/one-tier rules
62
+ * in AC-6 (see resolveEscalatedEffort's doc comment).
63
+ */
64
+ export function withEscalatedEffort(config, phase, isRetry) {
65
+ if (!config.effortEscalation || !isRetry)
66
+ return { config };
67
+ const currentPolicy = config.phasePolicies?.[phase];
68
+ const base = currentPolicy?.effort;
69
+ const escalated = resolveEscalatedEffort(base, isRetry, true);
70
+ if (!escalated || escalated === base)
71
+ return { config };
72
+ return {
73
+ config: {
74
+ ...config,
75
+ phasePolicies: {
76
+ ...config.phasePolicies,
77
+ [phase]: { ...currentPolicy, effort: escalated },
78
+ },
79
+ },
80
+ record: { phase, base: base ?? DEFAULT_ESCALATION_BASE, escalated },
81
+ };
82
+ }
@@ -11,9 +11,12 @@ import { SequantError } from "../errors.js";
11
11
  * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
12
  * errors — `classifyError` never produces them, since stderr text cannot
13
13
  * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
15
+ * site, not by `classifyError` — a failed `createPR` has no failing phase to
16
+ * classify, so it would otherwise leave `failureCategory` `undefined`.
14
17
  * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
18
  */
16
- export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "unknown"];
19
+ export declare const ERROR_CATEGORIES: readonly ["context_overflow", "api_error", "hook_failure", "build_error", "timeout", "rate_limit", "billing", "pr_creation", "unknown"];
17
20
  export type ErrorCategory = (typeof ERROR_CATEGORIES)[number];
18
21
  /**
19
22
  * Map from error type name to legacy category string.
@@ -11,6 +11,9 @@ import { ContextOverflowError, ApiError, HookFailureError, BuildError, TimeoutEr
11
11
  * `rate_limit` / `billing` (#761 AC-6) only arise from the driver's structured
12
12
  * errors — `classifyError` never produces them, since stderr text cannot
13
13
  * distinguish a window-exhausted limit from a transient 429 (`api_error`).
14
+ * `pr_creation` (#920) is assigned only at the `deriveFailureCategory` call
15
+ * site, not by `classifyError` — a failed `createPR` has no failing phase to
16
+ * classify, so it would otherwise leave `failureCategory` `undefined`.
14
17
  * Keep in sync with the inline category enum in `run-log-schema.ts`.
15
18
  */
16
19
  export const ERROR_CATEGORIES = [
@@ -21,6 +24,7 @@ export const ERROR_CATEGORIES = [
21
24
  "timeout",
22
25
  "rate_limit",
23
26
  "billing",
27
+ "pr_creation",
24
28
  "unknown",
25
29
  ];
26
30
  /**
@@ -13,7 +13,7 @@
13
13
  * await writer.finalize();
14
14
  * ```
15
15
  */
16
- import { type RunLog, type RunConfig, type PhaseLog, type Phase } from "./run-log-schema.js";
16
+ import { type RunLog, type RunConfig, type PhaseLog, type Phase, type SpecRecommendation } from "./run-log-schema.js";
17
17
  import { type RotationSettings } from "./log-rotation.js";
18
18
  export interface LogWriterOptions {
19
19
  /** Path to log directory (default: .sequant/logs in current directory) */
@@ -82,6 +82,15 @@ export declare class LogWriter {
82
82
  * Set PR info on the current issue (call before completeIssue)
83
83
  */
84
84
  setPRInfo(prNumber: number, prUrl: string, issueNumber?: number): void;
85
+ /**
86
+ * Record how the spec→run phase recommendation was resolved (#921 AC-4).
87
+ *
88
+ * Called right after `resolveSpecRecommendation` runs, before the next
89
+ * phase starts — mirrors {@link setPRInfo}'s post-hoc-setter shape because
90
+ * the spec `PhaseLog` is already written by the time resolution (which does
91
+ * its own comment-fetch I/O) completes.
92
+ */
93
+ setSpecRecommendation(recommendation: SpecRecommendation, issueNumber?: number): void;
85
94
  /**
86
95
  * Force the in-flight issue's status to `failure` (#879).
87
96
  *
@@ -134,6 +134,23 @@ export class LogWriter {
134
134
  issue.prNumber = prNumber;
135
135
  issue.prUrl = prUrl;
136
136
  }
137
+ /**
138
+ * Record how the spec→run phase recommendation was resolved (#921 AC-4).
139
+ *
140
+ * Called right after `resolveSpecRecommendation` runs, before the next
141
+ * phase starts — mirrors {@link setPRInfo}'s post-hoc-setter shape because
142
+ * the spec `PhaseLog` is already written by the time resolution (which does
143
+ * its own comment-fetch I/O) completes.
144
+ */
145
+ setSpecRecommendation(recommendation, issueNumber) {
146
+ const issue = issueNumber
147
+ ? (this.activeIssues.get(issueNumber) ?? this.currentIssue)
148
+ : this.currentIssue;
149
+ if (!issue) {
150
+ return;
151
+ }
152
+ issue.specRecommendation = recommendation;
153
+ }
137
154
  /**
138
155
  * Force the in-flight issue's status to `failure` (#879).
139
156
  *
@@ -209,6 +226,9 @@ export class LogWriter {
209
226
  ...(issue.prUrl != null && {
210
227
  prUrl: issue.prUrl,
211
228
  }),
229
+ ...(issue.specRecommendation != null && {
230
+ specRecommendation: issue.specRecommendation,
231
+ }),
212
232
  };
213
233
  this.runLog.issues.push(issueLog);
214
234
  // Clean up from activeIssues map
@@ -41,6 +41,7 @@ export declare const FailureCategorySchema: z.ZodEnum<{
41
41
  build_error: "build_error";
42
42
  rate_limit: "rate_limit";
43
43
  billing: "billing";
44
+ pr_creation: "pr_creation";
44
45
  }>;
45
46
  export type FailureCategory = z.infer<typeof FailureCategorySchema>;
46
47
  /**
@@ -48,12 +49,12 @@ export type FailureCategory = z.infer<typeof FailureCategorySchema>;
48
49
  */
49
50
  export declare const MetricPhaseSchema: z.ZodEnum<{
50
51
  exec: "exec";
51
- qa: "qa";
52
- loop: "loop";
53
52
  spec: "spec";
54
53
  "security-review": "security-review";
55
54
  testgen: "testgen";
56
55
  test: "test";
56
+ qa: "qa";
57
+ loop: "loop";
57
58
  }>;
58
59
  export type MetricPhase = z.infer<typeof MetricPhaseSchema>;
59
60
  /**
@@ -86,12 +87,12 @@ export declare const MetricRunSchema: z.ZodObject<{
86
87
  issues: z.ZodArray<z.ZodNumber>;
87
88
  phases: z.ZodArray<z.ZodEnum<{
88
89
  exec: "exec";
89
- qa: "qa";
90
- loop: "loop";
91
90
  spec: "spec";
92
91
  "security-review": "security-review";
93
92
  testgen: "testgen";
94
93
  test: "test";
94
+ qa: "qa";
95
+ loop: "loop";
95
96
  }>>;
96
97
  outcome: z.ZodEnum<{
97
98
  success: "success";
@@ -110,7 +111,17 @@ export declare const MetricRunSchema: z.ZodObject<{
110
111
  build_error: "build_error";
111
112
  rate_limit: "rate_limit";
112
113
  billing: "billing";
114
+ pr_creation: "pr_creation";
113
115
  }>>;
116
+ phasePolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
117
+ model: z.ZodOptional<z.ZodString>;
118
+ effort: z.ZodOptional<z.ZodString>;
119
+ }, z.core.$strip>>>;
120
+ effortEscalations: z.ZodOptional<z.ZodArray<z.ZodObject<{
121
+ phase: z.ZodString;
122
+ base: z.ZodString;
123
+ escalated: z.ZodString;
124
+ }, z.core.$strip>>>;
114
125
  metrics: z.ZodObject<{
115
126
  tokensUsed: z.ZodNumber;
116
127
  filesChanged: z.ZodNumber;
@@ -136,12 +147,12 @@ export declare const MetricsSchema: z.ZodObject<{
136
147
  issues: z.ZodArray<z.ZodNumber>;
137
148
  phases: z.ZodArray<z.ZodEnum<{
138
149
  exec: "exec";
139
- qa: "qa";
140
- loop: "loop";
141
150
  spec: "spec";
142
151
  "security-review": "security-review";
143
152
  testgen: "testgen";
144
153
  test: "test";
154
+ qa: "qa";
155
+ loop: "loop";
145
156
  }>>;
146
157
  outcome: z.ZodEnum<{
147
158
  success: "success";
@@ -160,7 +171,17 @@ export declare const MetricsSchema: z.ZodObject<{
160
171
  build_error: "build_error";
161
172
  rate_limit: "rate_limit";
162
173
  billing: "billing";
174
+ pr_creation: "pr_creation";
163
175
  }>>;
176
+ phasePolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
177
+ model: z.ZodOptional<z.ZodString>;
178
+ effort: z.ZodOptional<z.ZodString>;
179
+ }, z.core.$strip>>>;
180
+ effortEscalations: z.ZodOptional<z.ZodArray<z.ZodObject<{
181
+ phase: z.ZodString;
182
+ base: z.ZodString;
183
+ escalated: z.ZodString;
184
+ }, z.core.$strip>>>;
164
185
  metrics: z.ZodObject<{
165
186
  tokensUsed: z.ZodNumber;
166
187
  filesChanged: z.ZodNumber;
@@ -193,6 +214,28 @@ export declare function createMetricRun(options: {
193
214
  model?: string;
194
215
  flags?: string[];
195
216
  failureCategory?: FailureCategory;
217
+ /**
218
+ * Resolved per-phase model/effort overrides (#914), keyed by phase name.
219
+ * Pass only the phases that actually had a configured override — a phase
220
+ * that inherited the CLI default should not appear here at all. See
221
+ * `resolvePhasePolicies` in `config-resolver.ts`, which already produces
222
+ * a map shaped this way.
223
+ */
224
+ phasePolicies?: Record<string, {
225
+ model?: string;
226
+ effort?: string;
227
+ }>;
228
+ /**
229
+ * Effort escalations applied during this run (#915), one entry per
230
+ * escalated phase execution. Pass only executions that actually escalated
231
+ * — see `MetricRunSchema.effortEscalations`'s doc comment for why this is
232
+ * a sibling array rather than an extension of `phasePolicies`.
233
+ */
234
+ effortEscalations?: Array<{
235
+ phase: string;
236
+ base: string;
237
+ escalated: string;
238
+ }>;
196
239
  metrics?: Partial<RunMetrics>;
197
240
  }): MetricRun;
198
241
  /**
@@ -96,6 +96,33 @@ export const MetricRunSchema = z.object({
96
96
  * existed (additive — no `version` bump required).
97
97
  */
98
98
  failureCategory: FailureCategorySchema.optional(),
99
+ /**
100
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
101
+ * name. Only phases with a configured override get an entry — a phase
102
+ * that inherited the CLI default is omitted entirely, not recorded with
103
+ * undefined fields. Enum/alias strings only, consistent with this
104
+ * schema's no-file-paths/no-content privacy contract. Optional and
105
+ * additive — absent on records written before this field existed.
106
+ */
107
+ phasePolicies: z
108
+ .record(z.string(), z.object({ model: z.string().optional(), effort: z.string().optional() }))
109
+ .optional(),
110
+ /**
111
+ * Effort escalations applied during this run (#915), one entry per
112
+ * escalated phase execution — distinct from `phasePolicies`, which is a
113
+ * flat phase→policy map recorded once per run and can't express a value
114
+ * that changes per retry. Only populated when at least one execution
115
+ * escalated; omitted entirely (not an empty array) otherwise, matching
116
+ * `phasePolicies`'s omit-when-empty convention. Phase names and enum
117
+ * effort strings only, consistent with this schema's privacy contract.
118
+ */
119
+ effortEscalations: z
120
+ .array(z.object({
121
+ phase: z.string(),
122
+ base: z.string(),
123
+ escalated: z.string(),
124
+ }))
125
+ .optional(),
99
126
  /** Aggregate metrics */
100
127
  metrics: RunMetricsSchema,
101
128
  });
@@ -137,6 +164,12 @@ export function createMetricRun(options) {
137
164
  model: options.model ?? "unknown",
138
165
  flags: options.flags ?? [],
139
166
  failureCategory: options.failureCategory,
167
+ ...(options.phasePolicies && Object.keys(options.phasePolicies).length > 0
168
+ ? { phasePolicies: options.phasePolicies }
169
+ : {}),
170
+ ...(options.effortEscalations && options.effortEscalations.length > 0
171
+ ? { effortEscalations: options.effortEscalations }
172
+ : {}),
140
173
  metrics: {
141
174
  tokensUsed: options.metrics?.tokensUsed ?? 0,
142
175
  filesChanged: options.metrics?.filesChanged ?? 0,
@@ -70,6 +70,17 @@ export declare class MetricsWriter {
70
70
  model?: string;
71
71
  flags?: string[];
72
72
  failureCategory?: FailureCategory;
73
+ /** Resolved per-phase model/effort overrides (#914). See `createMetricRun`. */
74
+ phasePolicies?: Record<string, {
75
+ model?: string;
76
+ effort?: string;
77
+ }>;
78
+ /** Effort escalations applied during this run (#915). See `createMetricRun`. */
79
+ effortEscalations?: Array<{
80
+ phase: string;
81
+ base: string;
82
+ escalated: string;
83
+ }>;
73
84
  metrics?: Partial<RunMetrics>;
74
85
  }): Promise<MetricRun>;
75
86
  /**
@@ -11,6 +11,18 @@
11
11
  * ```
12
12
  */
13
13
  import { type Phase, type PhaseMarker } from "./state-schema.js";
14
+ /**
15
+ * Strip markdown code blocks and inline code from text.
16
+ * This prevents phase markers inside code examples from being parsed.
17
+ *
18
+ * Exported for reuse by `spec-recommendation.ts` (#921), which applies the
19
+ * same code-fence stripping to the `SEQUANT_SPEC` marker so documentation
20
+ * examples embedding that marker can't false-positive either.
21
+ *
22
+ * @param text - The text to strip code from
23
+ * @returns Text with code blocks and inline code removed
24
+ */
25
+ export declare function stripMarkdownCode(text: string): string;
14
26
  /**
15
27
  * Format a phase marker as an HTML comment string for embedding in GitHub comments.
16
28
  *
@@ -25,10 +25,14 @@ const INLINE_CODE_REGEX = /`[^`\n]+`/g;
25
25
  * Strip markdown code blocks and inline code from text.
26
26
  * This prevents phase markers inside code examples from being parsed.
27
27
  *
28
+ * Exported for reuse by `spec-recommendation.ts` (#921), which applies the
29
+ * same code-fence stripping to the `SEQUANT_SPEC` marker so documentation
30
+ * examples embedding that marker can't false-positive either.
31
+ *
28
32
  * @param text - The text to strip code from
29
33
  * @returns Text with code blocks and inline code removed
30
34
  */
31
- function stripMarkdownCode(text) {
35
+ export function stripMarkdownCode(text) {
32
36
  // First remove fenced code blocks (multi-line)
33
37
  let result = text.replace(FENCED_CODE_BLOCK_REGEX, "");
34
38
  // Then remove inline code
@@ -1032,6 +1032,12 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
1032
1032
  const eligibleHandle = resumeHandle && driver.canResume(resumeHandle, cwd)
1033
1033
  ? resumeHandle
1034
1034
  : undefined;
1035
+ // #914: resolved per-phase model/effort, if this phase has one. Both
1036
+ // ExecutionConfig producers (buildExecutionConfig, ready-gate.ts's
1037
+ // buildPhaseConfig) populate `phasePolicies` the same way, so this is the
1038
+ // single site that turns it into driver-facing fields — see the doc
1039
+ // comment on ExecutionConfig.phasePolicies.
1040
+ const phasePolicy = config.phasePolicies?.[phase];
1035
1041
  // Build AgentExecutionConfig for the driver
1036
1042
  const agentConfig = {
1037
1043
  cwd,
@@ -1043,6 +1049,10 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
1043
1049
  resumeHandle: eligibleHandle,
1044
1050
  sessionId: eligibleHandle?.token,
1045
1051
  files,
1052
+ ...(phasePolicy?.model ? { model: phasePolicy.model } : {}),
1053
+ ...(phasePolicy?.effort
1054
+ ? { effort: phasePolicy.effort }
1055
+ : {}),
1046
1056
  onOutput: config.verbose || reportActivity
1047
1057
  ? (text) => {
1048
1058
  if (config.verbose) {
@@ -22,6 +22,7 @@
22
22
  * lives in `src/commands/ready.ts`.
23
23
  */
24
24
  import type { ExecutionConfig, PhaseResult, ProgressCallback } from "./types.js";
25
+ import { type EscalationRecord } from "./effort-escalation.js";
25
26
  import type { QaVerdict } from "./run-log-schema.js";
26
27
  import type { ReadyPolicy } from "../settings.js";
27
28
  import type { IssueStatus } from "./state-schema.js";
@@ -90,6 +91,12 @@ export interface ReadyResult {
90
91
  tokensUsed: number;
91
92
  /** Human-readable markdown gap report (AC-4). */
92
93
  report: string;
94
+ /**
95
+ * Effort escalations applied during this gate's QA-pass loop (#915),
96
+ * base+escalated tier per escalated `qa`/`loop` dispatch. Empty when
97
+ * `effortEscalation` is off or no dispatch escalated.
98
+ */
99
+ effortEscalations: EscalationRecord[];
93
100
  }
94
101
  /**
95
102
  * Thin phase-runner abstraction so the engine can be unit-tested without the
@@ -127,6 +134,27 @@ export interface RunReadyGateOptions {
127
134
  classifyChangesFn?: (cwd: string) => ExecChangeState;
128
135
  /** Injectable loop-progress snapshot — defaults to {@link snapshotLoopProgress}. */
129
136
  snapshotFn?: (cwd: string) => LoopProgressSnapshot;
137
+ /**
138
+ * Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
139
+ * name. Callers (e.g. `commands/ready.ts`) resolve this via
140
+ * `resolvePhasePolicies` — the same shared resolver `buildExecutionConfig`
141
+ * uses — so this producer cannot drift from that one (#833 class).
142
+ * `buildPhaseConfig` spreads it onto every `ExecutionConfig` it builds;
143
+ * `phase-executor.ts` applies the entry for the phase actually running.
144
+ */
145
+ phasePolicies?: Record<string, {
146
+ model?: string;
147
+ effort?: string;
148
+ }>;
149
+ /**
150
+ * Evidence-based effort escalation on quality-loop retries (#915). Callers
151
+ * (e.g. `commands/ready.ts`) resolve this CLI > settings > `false`, the same
152
+ * precedence `buildExecutionConfig` uses for the `run` path (#833 class).
153
+ * `buildPhaseConfig` spreads it onto every `ExecutionConfig` it builds;
154
+ * `withEscalatedEffort` (`effort-escalation.ts`) reads it at each QA-pass
155
+ * dispatch to decide whether that specific `qa`/`loop` call escalates.
156
+ */
157
+ effortEscalation?: boolean;
130
158
  }
131
159
  /**
132
160
  * 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";
@@ -167,6 +168,11 @@ function buildPhaseConfig(opts, extra) {
167
168
  dryRun: false,
168
169
  mcp: opts.mcp,
169
170
  retry: true,
171
+ // #914: producer 2 (see the doc comment on RunReadyGateOptions.phasePolicies
172
+ // for why this can't drift from buildExecutionConfig's own assignment).
173
+ phasePolicies: opts.phasePolicies,
174
+ // #915: producer 2 (see RunReadyGateOptions.effortEscalation).
175
+ effortEscalation: opts.effortEscalation,
170
176
  ...extra,
171
177
  };
172
178
  }
@@ -268,6 +274,9 @@ export async function runReadyGate(opts) {
268
274
  const autoFixed = [];
269
275
  let remaining = [];
270
276
  let tokensUsed = 0;
277
+ // #915: escalated (base, escalated) tiers, one entry per QA-pass dispatch
278
+ // that actually escalated. Populated at the two dispatch sites below.
279
+ const effortEscalations = [];
271
280
  const finish = (reason) => {
272
281
  const ready = reason === "AC_MET" || reason === "READY_FOR_MERGE";
273
282
  const issueStatus = ready
@@ -285,6 +294,7 @@ export async function runReadyGate(opts) {
285
294
  remaining,
286
295
  tokensUsed,
287
296
  report: "",
297
+ effortEscalations,
288
298
  };
289
299
  result.report = formatReadyReport(result);
290
300
  return result;
@@ -299,7 +309,12 @@ export async function runReadyGate(opts) {
299
309
  return finish("TOKEN_BUDGET");
300
310
  }
301
311
  iterations++;
302
- const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
312
+ // #915: iterations > 1 means this QA pass is a retry of a prior
313
+ // unsatisfied verdict — the ready-gate's retry signal.
314
+ const qaEscalation = withEscalatedEffort(buildPhaseConfig(opts, { fullQa: true }), "qa", iterations > 1);
315
+ if (qaEscalation.record)
316
+ effortEscalations.push(qaEscalation.record);
317
+ const qaResult = await runPhaseTracked("qa", qaEscalation.config, iterations);
303
318
  tokensUsed = readTokensUsed(worktreePath);
304
319
  const verdict = qaResult.verdict ?? null;
305
320
  // #534 guard: a null verdict is never "ready". #853: report it as
@@ -343,11 +358,17 @@ export async function runReadyGate(opts) {
343
358
  .filter((g) => !g.nonGoal)
344
359
  .map((g) => g.description);
345
360
  const before = snapshotFn(worktreePath);
346
- const loopResult = await runPhaseTracked("loop", buildPhaseConfig(opts, {
361
+ // #915: iterations > 1 means this fix pass follows a QA pass that was
362
+ // itself a retry — the same ready-gate retry signal as the qa dispatch
363
+ // above.
364
+ const loopEscalation = withEscalatedEffort(buildPhaseConfig(opts, {
347
365
  lastVerdict: verdict,
348
366
  failedAcs: fixableGaps.join("; ") || undefined,
349
367
  promptContext: buildLoopContext(policy, verdict, fixableGaps),
350
- }), iterations);
368
+ }), "loop", iterations > 1);
369
+ if (loopEscalation.record)
370
+ effortEscalations.push(loopEscalation.record);
371
+ const loopResult = await runPhaseTracked("loop", loopEscalation.config, iterations);
351
372
  tokensUsed = readTokensUsed(worktreePath);
352
373
  if (!loopResult.success) {
353
374
  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,6 +122,7 @@ 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>>;
@@ -174,6 +206,7 @@ export declare const PhaseLogSchema: z.ZodObject<{
174
206
  build_error: "build_error";
175
207
  rate_limit: "rate_limit";
176
208
  billing: "billing";
209
+ pr_creation: "pr_creation";
177
210
  }>;
178
211
  errorType: z.ZodOptional<z.ZodString>;
179
212
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -253,6 +286,7 @@ export declare const IssueLogSchema: z.ZodObject<{
253
286
  build_error: "build_error";
254
287
  rate_limit: "rate_limit";
255
288
  billing: "billing";
289
+ pr_creation: "pr_creation";
256
290
  }>;
257
291
  errorType: z.ZodOptional<z.ZodString>;
258
292
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -264,6 +298,16 @@ export declare const IssueLogSchema: z.ZodObject<{
264
298
  abortReason: z.ZodOptional<z.ZodString>;
265
299
  prNumber: z.ZodOptional<z.ZodNumber>;
266
300
  prUrl: z.ZodOptional<z.ZodString>;
301
+ specRecommendation: z.ZodOptional<z.ZodObject<{
302
+ source: z.ZodEnum<{
303
+ marker: "marker";
304
+ "comment-prose": "comment-prose";
305
+ chat: "chat";
306
+ "label-fallback": "label-fallback";
307
+ }>;
308
+ phases: z.ZodArray<z.ZodString>;
309
+ qualityLoop: z.ZodBoolean;
310
+ }, z.core.$strip>>;
267
311
  }, z.core.$strip>;
268
312
  export type IssueLog = z.infer<typeof IssueLogSchema>;
269
313
  /**
@@ -377,6 +421,7 @@ export declare const RunLogSchema: z.ZodObject<{
377
421
  build_error: "build_error";
378
422
  rate_limit: "rate_limit";
379
423
  billing: "billing";
424
+ pr_creation: "pr_creation";
380
425
  }>;
381
426
  errorType: z.ZodOptional<z.ZodString>;
382
427
  errorMetadata: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
@@ -388,6 +433,16 @@ export declare const RunLogSchema: z.ZodObject<{
388
433
  abortReason: z.ZodOptional<z.ZodString>;
389
434
  prNumber: z.ZodOptional<z.ZodNumber>;
390
435
  prUrl: z.ZodOptional<z.ZodString>;
436
+ specRecommendation: z.ZodOptional<z.ZodObject<{
437
+ source: z.ZodEnum<{
438
+ marker: "marker";
439
+ "comment-prose": "comment-prose";
440
+ chat: "chat";
441
+ "label-fallback": "label-fallback";
442
+ }>;
443
+ phases: z.ZodArray<z.ZodString>;
444
+ qualityLoop: z.ZodBoolean;
445
+ }, z.core.$strip>>;
391
446
  }, z.core.$strip>>;
392
447
  summary: z.ZodObject<{
393
448
  totalIssues: z.ZodNumber;