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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/dist/bin/cli.js +47 -2
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +206 -4
- package/dist/src/commands/ready.d.ts +6 -0
- package/dist/src/commands/ready.js +15 -1
- package/dist/src/commands/run-display.js +1 -0
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/cli-flags.d.ts +23 -0
- package/dist/src/lib/cli-flags.js +43 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
- package/dist/src/lib/cli-ui/run-renderer.js +7 -1
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +6 -3
- package/dist/src/lib/locks/index.js +4 -2
- package/dist/src/lib/locks/lock-manager.d.ts +81 -1
- package/dist/src/lib/locks/lock-manager.js +230 -5
- package/dist/src/lib/locks/types.d.ts +72 -0
- package/dist/src/lib/locks/types.js +28 -0
- package/dist/src/lib/settings.d.ts +73 -0
- package/dist/src/lib/settings.js +45 -0
- package/dist/src/lib/test-tautology-detector.d.ts +4 -3
- package/dist/src/lib/test-tautology-detector.js +101 -41
- package/dist/src/lib/workflow/batch-executor.js +78 -19
- package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
- package/dist/src/lib/workflow/config-resolver.js +89 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/log-writer.d.ts +10 -1
- package/dist/src/lib/workflow/log-writer.js +20 -0
- package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.js +10 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
- package/dist/src/lib/workflow/ready-gate.js +24 -3
- package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
- package/dist/src/lib/workflow/run-log-schema.js +31 -1
- package/dist/src/lib/workflow/run-orchestrator.js +27 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/types.d.ts +64 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +9 -1
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/package.json +3 -2
- package/templates/hooks/pre-tool.sh +228 -0
- package/templates/scripts/cleanup-worktree.sh +36 -15
- package/templates/scripts/new-feature.sh +25 -19
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/assess/SKILL.md +5 -1
- package/templates/skills/exec/SKILL.md +61 -7
- package/templates/skills/fullsolve/SKILL.md +127 -21
- package/templates/skills/loop/SKILL.md +56 -11
- package/templates/skills/merger/SKILL.md +98 -10
- package/templates/skills/qa/SKILL.md +59 -6
- package/templates/skills/release/SKILL.md +79 -0
- package/templates/skills/spec/SKILL.md +31 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- 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
|
-
|
|
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
|
-
|
|
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;
|