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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +19 -2
- package/dist/bin/cli.js +47 -2
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
- package/dist/src/commands/doctor.js +20 -18
- 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 +19 -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/ac-linter.js +26 -0
- package/dist/src/lib/ac-parser.d.ts +40 -0
- package/dist/src/lib/ac-parser.js +202 -16
- 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/markdown-fence.d.ts +24 -0
- package/dist/src/lib/markdown-fence.js +51 -0
- package/dist/src/lib/mcp-config.d.ts +24 -0
- package/dist/src/lib/mcp-config.js +51 -0
- package/dist/src/lib/scope/analyzer.d.ts +4 -0
- package/dist/src/lib/scope/analyzer.js +7 -1
- package/dist/src/lib/settings.d.ts +111 -1
- package/dist/src/lib/settings.js +59 -0
- package/dist/src/lib/system.d.ts +7 -3
- package/dist/src/lib/system.js +7 -3
- package/dist/src/lib/test-tautology-detector.d.ts +4 -3
- package/dist/src/lib/test-tautology-detector.js +147 -40
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +154 -23
- package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
- package/dist/src/lib/workflow/config-resolver.js +90 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +14 -3
- 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/mutation-marker.d.ts +86 -0
- package/dist/src/lib/workflow/mutation-marker.js +97 -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.d.ts +17 -0
- package/dist/src/lib/workflow/phase-executor.js +60 -4
- package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
- package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +53 -1
- package/dist/src/lib/workflow/ready-gate.js +105 -14
- package/dist/src/lib/workflow/run-log-schema.d.ts +175 -0
- package/dist/src/lib/workflow/run-log-schema.js +71 -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/state-schema.d.ts +5 -1
- package/dist/src/lib/workflow/state-schema.js +8 -1
- package/dist/src/lib/workflow/types.d.ts +78 -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 +4 -3
- package/templates/hooks/pre-tool.sh +331 -12
- 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 +62 -8
- package/templates/skills/fullsolve/SKILL.md +187 -28
- package/templates/skills/loop/SKILL.md +127 -23
- package/templates/skills/merger/SKILL.md +130 -13
- package/templates/skills/qa/SKILL.md +306 -8
- package/templates/skills/release/SKILL.md +79 -0
- package/templates/skills/spec/SKILL.md +40 -20
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -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
|
/**
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parseable mutation-verification markers for `/qa` §6i (#939).
|
|
3
|
+
*
|
|
4
|
+
* CLAUDE.md's testing rule ("Gate tests ship with a recorded mutation
|
|
5
|
+
* result") was honor-system prose — nothing parsed or checked the recorded
|
|
6
|
+
* result, so compliance was invisible (see #830, #834's "prose only, and
|
|
7
|
+
* therefore unenforceable" defect class). This module promotes that record
|
|
8
|
+
* to a parseable PR-body marker:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* <!-- SEQUANT_MUTATION: {"ac":"AC-3","mutation":"removed payload fixture
|
|
12
|
+
* block","failedTest":"injection.test.ts > rejects payload"} -->
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Same durable-marker idiom as `SEQUANT_SPEC` (spec-recommendation.ts) and
|
|
16
|
+
* `SEQUANT_PHASE` (phase-detection.ts): a flat-JSON HTML comment, matched
|
|
17
|
+
* with the `{[^}]+}` regex family. That regex stops at the FIRST `}`, so the
|
|
18
|
+
* payload must stay flat — a nested object would truncate mid-JSON and fail
|
|
19
|
+
* to parse. Unlike those two markers (one recommendation / one status per
|
|
20
|
+
* comment, latest-wins), a PR body carries one `SEQUANT_MUTATION` marker per
|
|
21
|
+
* AC, so all markers are collected, not just the latest.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
declare const MutationMarkerSchema: z.ZodObject<{
|
|
25
|
+
ac: z.ZodString;
|
|
26
|
+
mutation: z.ZodString;
|
|
27
|
+
failedTest: z.ZodString;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type MutationMarker = z.infer<typeof MutationMarkerSchema>;
|
|
30
|
+
export type MutationMarkerClassification = "valid" | "test_not_in_diff";
|
|
31
|
+
export interface ClassifiedMutationMarker extends MutationMarker {
|
|
32
|
+
classification: MutationMarkerClassification;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Parse every `SEQUANT_MUTATION` marker from a PR body.
|
|
36
|
+
*
|
|
37
|
+
* Markers inside fenced code blocks or inline code (e.g. a doc example
|
|
38
|
+
* showing the marker format) are ignored, matching `parsePhaseMarkers`.
|
|
39
|
+
* Malformed JSON or schema-invalid entries are skipped silently rather than
|
|
40
|
+
* thrown — one bad marker must not take down the rest of the PR body's
|
|
41
|
+
* markers.
|
|
42
|
+
*
|
|
43
|
+
* Called with just `prBody`, this only parses. Given a second argument —
|
|
44
|
+
* the diff's test-file paths — it also **rejects** markers naming a test
|
|
45
|
+
* absent from the diff, attaching a `classification` to each result rather
|
|
46
|
+
* than silently dropping the rejected ones (a fabricated marker is worse
|
|
47
|
+
* than a missing one; it must be visible, not discarded). The two-argument
|
|
48
|
+
* form is what makes this function itself the rejecting entry point AC-3
|
|
49
|
+
* names, while the one-argument form stays free of a git-diff dependency
|
|
50
|
+
* for parser-only tests.
|
|
51
|
+
*
|
|
52
|
+
* @param prBody - The full PR body text
|
|
53
|
+
* @param diffTestFiles - When provided, test file paths present in the PR's
|
|
54
|
+
* diff; each returned marker is classified against them
|
|
55
|
+
* @returns Every valid marker found, in document order — classified when
|
|
56
|
+
* `diffTestFiles` is provided
|
|
57
|
+
*/
|
|
58
|
+
export declare function parseMutationMarkers(prBody: string): MutationMarker[];
|
|
59
|
+
export declare function parseMutationMarkers(prBody: string, diffTestFiles: string[]): ClassifiedMutationMarker[];
|
|
60
|
+
/**
|
|
61
|
+
* Reduce a marker list to one-per-AC, the later marker winning.
|
|
62
|
+
*
|
|
63
|
+
* A re-run's marker (e.g. after amending the PR body) supersedes the
|
|
64
|
+
* earlier one for the same AC — the same "latest wins" idiom
|
|
65
|
+
* `resolveSpecRecommendation` uses for `SEQUANT_SPEC`.
|
|
66
|
+
*
|
|
67
|
+
* @param markers - Markers as returned by {@link parseMutationMarkers}
|
|
68
|
+
* @returns Map keyed by AC id, one marker per key
|
|
69
|
+
*/
|
|
70
|
+
export declare function latestMutationMarkerPerAc(markers: MutationMarker[]): Map<string, MutationMarker>;
|
|
71
|
+
/**
|
|
72
|
+
* Classify a marker's `failedTest` against the diff's actual test files.
|
|
73
|
+
*
|
|
74
|
+
* A fabricated marker (naming a test that doesn't exist in the diff) is
|
|
75
|
+
* worse than a missing one — it claims verification that never happened.
|
|
76
|
+
* `failedTest` follows the `<file> > <test name>` shape `/qa` §6i renders
|
|
77
|
+
* in its output table (e.g. `injection.test.ts > rejects payload`); only
|
|
78
|
+
* the file segment is checked, since the suite/test-name portion after
|
|
79
|
+
* ` > ` isn't independently verifiable without executing the test.
|
|
80
|
+
*
|
|
81
|
+
* @param marker - A single parsed marker
|
|
82
|
+
* @param diffTestFiles - Test file paths present in the PR's diff
|
|
83
|
+
* @returns `"valid"` when the named file is in the diff, `"test_not_in_diff"` otherwise
|
|
84
|
+
*/
|
|
85
|
+
export declare function classifyMutationMarker(marker: MutationMarker, diffTestFiles: string[]): MutationMarkerClassification;
|
|
86
|
+
export {};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parseable mutation-verification markers for `/qa` §6i (#939).
|
|
3
|
+
*
|
|
4
|
+
* CLAUDE.md's testing rule ("Gate tests ship with a recorded mutation
|
|
5
|
+
* result") was honor-system prose — nothing parsed or checked the recorded
|
|
6
|
+
* result, so compliance was invisible (see #830, #834's "prose only, and
|
|
7
|
+
* therefore unenforceable" defect class). This module promotes that record
|
|
8
|
+
* to a parseable PR-body marker:
|
|
9
|
+
*
|
|
10
|
+
* ```
|
|
11
|
+
* <!-- SEQUANT_MUTATION: {"ac":"AC-3","mutation":"removed payload fixture
|
|
12
|
+
* block","failedTest":"injection.test.ts > rejects payload"} -->
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* Same durable-marker idiom as `SEQUANT_SPEC` (spec-recommendation.ts) and
|
|
16
|
+
* `SEQUANT_PHASE` (phase-detection.ts): a flat-JSON HTML comment, matched
|
|
17
|
+
* with the `{[^}]+}` regex family. That regex stops at the FIRST `}`, so the
|
|
18
|
+
* payload must stay flat — a nested object would truncate mid-JSON and fail
|
|
19
|
+
* to parse. Unlike those two markers (one recommendation / one status per
|
|
20
|
+
* comment, latest-wins), a PR body carries one `SEQUANT_MUTATION` marker per
|
|
21
|
+
* AC, so all markers are collected, not just the latest.
|
|
22
|
+
*/
|
|
23
|
+
import { z } from "zod";
|
|
24
|
+
import { stripMarkdownCode } from "./phase-detection.js";
|
|
25
|
+
/** Regex to extract mutation-verification marker JSON from HTML comments. */
|
|
26
|
+
const MUTATION_MARKER_REGEX = /<!-- SEQUANT_MUTATION: (\{[^}]+\}) -->/g;
|
|
27
|
+
const MutationMarkerSchema = z.object({
|
|
28
|
+
ac: z.string().min(1),
|
|
29
|
+
mutation: z.string().min(1),
|
|
30
|
+
failedTest: z.string().min(1),
|
|
31
|
+
});
|
|
32
|
+
function parseMarkersRaw(prBody) {
|
|
33
|
+
const markers = [];
|
|
34
|
+
const stripped = stripMarkdownCode(prBody);
|
|
35
|
+
MUTATION_MARKER_REGEX.lastIndex = 0;
|
|
36
|
+
let match;
|
|
37
|
+
while ((match = MUTATION_MARKER_REGEX.exec(stripped)) !== null) {
|
|
38
|
+
let parsed;
|
|
39
|
+
try {
|
|
40
|
+
parsed = JSON.parse(match[1]);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
const result = MutationMarkerSchema.safeParse(parsed);
|
|
46
|
+
if (result.success) {
|
|
47
|
+
markers.push(result.data);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return markers;
|
|
51
|
+
}
|
|
52
|
+
export function parseMutationMarkers(prBody, diffTestFiles) {
|
|
53
|
+
const markers = parseMarkersRaw(prBody);
|
|
54
|
+
if (diffTestFiles === undefined) {
|
|
55
|
+
return markers;
|
|
56
|
+
}
|
|
57
|
+
return markers.map((marker) => ({
|
|
58
|
+
...marker,
|
|
59
|
+
classification: classifyMutationMarker(marker, diffTestFiles),
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Reduce a marker list to one-per-AC, the later marker winning.
|
|
64
|
+
*
|
|
65
|
+
* A re-run's marker (e.g. after amending the PR body) supersedes the
|
|
66
|
+
* earlier one for the same AC — the same "latest wins" idiom
|
|
67
|
+
* `resolveSpecRecommendation` uses for `SEQUANT_SPEC`.
|
|
68
|
+
*
|
|
69
|
+
* @param markers - Markers as returned by {@link parseMutationMarkers}
|
|
70
|
+
* @returns Map keyed by AC id, one marker per key
|
|
71
|
+
*/
|
|
72
|
+
export function latestMutationMarkerPerAc(markers) {
|
|
73
|
+
const byAc = new Map();
|
|
74
|
+
for (const marker of markers) {
|
|
75
|
+
byAc.set(marker.ac, marker);
|
|
76
|
+
}
|
|
77
|
+
return byAc;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Classify a marker's `failedTest` against the diff's actual test files.
|
|
81
|
+
*
|
|
82
|
+
* A fabricated marker (naming a test that doesn't exist in the diff) is
|
|
83
|
+
* worse than a missing one — it claims verification that never happened.
|
|
84
|
+
* `failedTest` follows the `<file> > <test name>` shape `/qa` §6i renders
|
|
85
|
+
* in its output table (e.g. `injection.test.ts > rejects payload`); only
|
|
86
|
+
* the file segment is checked, since the suite/test-name portion after
|
|
87
|
+
* ` > ` isn't independently verifiable without executing the test.
|
|
88
|
+
*
|
|
89
|
+
* @param marker - A single parsed marker
|
|
90
|
+
* @param diffTestFiles - Test file paths present in the PR's diff
|
|
91
|
+
* @returns `"valid"` when the named file is in the diff, `"test_not_in_diff"` otherwise
|
|
92
|
+
*/
|
|
93
|
+
export function classifyMutationMarker(marker, diffTestFiles) {
|
|
94
|
+
const namedFile = marker.failedTest.split(">")[0]?.trim() ?? "";
|
|
95
|
+
const found = diffTestFiles.some((file) => file === namedFile || file.endsWith(`/${namedFile}`));
|
|
96
|
+
return found ? "valid" : "test_not_in_diff";
|
|
97
|
+
}
|
|
@@ -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
|
|
@@ -225,6 +225,15 @@ export declare function endedWithoutVerdict(output: string | undefined): boolean
|
|
|
225
225
|
* @internal Exported for testing only
|
|
226
226
|
*/
|
|
227
227
|
export declare function parseQaSummary(output: string): QaSummary | null;
|
|
228
|
+
/**
|
|
229
|
+
* Select the gap descriptions from a QA summary appropriate to feed a fix
|
|
230
|
+
* loop (#937 AC-3): every gap, except one whose `SEQUANT_QA_GAPS` finding is
|
|
231
|
+
* explicitly marked `document` or `pause_for_human` — those are QA-real but
|
|
232
|
+
* not code-fixable (quality/polish, or a decision the loop can't make on its
|
|
233
|
+
* own). A gap with no matching finding (the legacy prose-scrape path) is
|
|
234
|
+
* always included, unchanged from pre-#937 behavior.
|
|
235
|
+
*/
|
|
236
|
+
export declare function selectFixableGaps(summary: QaSummary | null | undefined): string[];
|
|
228
237
|
/**
|
|
229
238
|
* Format duration in human-readable format
|
|
230
239
|
*/
|
|
@@ -341,6 +350,14 @@ export declare function mapAgentFailureToPhaseResult(phase: Phase, agentResult:
|
|
|
341
350
|
sessionId?: string;
|
|
342
351
|
resumeHandle?: ResumeHandle;
|
|
343
352
|
};
|
|
353
|
+
/**
|
|
354
|
+
* Marks the embedded `promptContext` block appended below so a phase's own
|
|
355
|
+
* skill (e.g. `/loop`) can detect it deterministically instead of pattern-
|
|
356
|
+
* matching prose that varies per producer (`ready-gate.ts` and
|
|
357
|
+
* `batch-executor.ts` build differently-worded context for the same slot).
|
|
358
|
+
* See #960.
|
|
359
|
+
*/
|
|
360
|
+
export declare const PROMPT_CONTEXT_SENTINEL = "SEQUANT_PROMPT_CONTEXT";
|
|
344
361
|
/**
|
|
345
362
|
* Get the prompt for a phase with the issue number substituted.
|
|
346
363
|
* Selects self-contained prompts for non-Claude agents.
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { execFileSync } from "child_process";
|
|
12
12
|
import { resolveDiffBase } from "./git-diff-utils.js";
|
|
13
|
+
import { parseQaGapsMarker } from "./qa-gaps-marker.js";
|
|
13
14
|
import { readAgentsMd } from "../agents-md.js";
|
|
14
15
|
import { getDriver } from "./drivers/index.js";
|
|
15
16
|
import { classifyError } from "./error-classifier.js";
|
|
@@ -499,9 +500,43 @@ export function parseQaSummary(output) {
|
|
|
499
500
|
}
|
|
500
501
|
if (acTotal === 0)
|
|
501
502
|
return null;
|
|
502
|
-
const
|
|
503
|
+
const proseGaps = parseListSection(output, /\*\*(?:Issues|Gaps)/);
|
|
503
504
|
const suggestions = parseListSection(output, /\*\*Suggestions/);
|
|
504
|
-
|
|
505
|
+
// #937: prefer the structured marker, but UNION with the prose scrape
|
|
506
|
+
// rather than replace it — a marker-carrying comment can still contain a
|
|
507
|
+
// gap the model couldn't fit into the six categories (the fallback rule),
|
|
508
|
+
// and that prose-only finding must not be silently dropped (AC-5).
|
|
509
|
+
const findings = parseQaGapsMarker(output) ?? undefined;
|
|
510
|
+
const seen = new Set();
|
|
511
|
+
const gaps = [];
|
|
512
|
+
for (const gap of [
|
|
513
|
+
...(findings?.map((f) => f.description) ?? []),
|
|
514
|
+
...proseGaps,
|
|
515
|
+
]) {
|
|
516
|
+
const key = gap.trim().toLowerCase();
|
|
517
|
+
if (seen.has(key))
|
|
518
|
+
continue;
|
|
519
|
+
seen.add(key);
|
|
520
|
+
gaps.push(gap);
|
|
521
|
+
}
|
|
522
|
+
return { acMet, acTotal, gaps, suggestions, ...(findings && { findings }) };
|
|
523
|
+
}
|
|
524
|
+
/**
|
|
525
|
+
* Select the gap descriptions from a QA summary appropriate to feed a fix
|
|
526
|
+
* loop (#937 AC-3): every gap, except one whose `SEQUANT_QA_GAPS` finding is
|
|
527
|
+
* explicitly marked `document` or `pause_for_human` — those are QA-real but
|
|
528
|
+
* not code-fixable (quality/polish, or a decision the loop can't make on its
|
|
529
|
+
* own). A gap with no matching finding (the legacy prose-scrape path) is
|
|
530
|
+
* always included, unchanged from pre-#937 behavior.
|
|
531
|
+
*/
|
|
532
|
+
export function selectFixableGaps(summary) {
|
|
533
|
+
if (!summary)
|
|
534
|
+
return [];
|
|
535
|
+
const excluded = new Set((summary.findings ?? [])
|
|
536
|
+
.filter((f) => f.recommendedAction === "document" ||
|
|
537
|
+
f.recommendedAction === "pause_for_human")
|
|
538
|
+
.map((f) => f.description.trim().toLowerCase()));
|
|
539
|
+
return summary.gaps.filter((g) => !excluded.has(g.trim().toLowerCase()));
|
|
505
540
|
}
|
|
506
541
|
/**
|
|
507
542
|
* Parse a markdown bullet list section, filtering out "None" variants.
|
|
@@ -850,6 +885,14 @@ export function mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds
|
|
|
850
885
|
exitCode: agentResult.exitCode,
|
|
851
886
|
};
|
|
852
887
|
}
|
|
888
|
+
/**
|
|
889
|
+
* Marks the embedded `promptContext` block appended below so a phase's own
|
|
890
|
+
* skill (e.g. `/loop`) can detect it deterministically instead of pattern-
|
|
891
|
+
* matching prose that varies per producer (`ready-gate.ts` and
|
|
892
|
+
* `batch-executor.ts` build differently-worded context for the same slot).
|
|
893
|
+
* See #960.
|
|
894
|
+
*/
|
|
895
|
+
export const PROMPT_CONTEXT_SENTINEL = "SEQUANT_PROMPT_CONTEXT";
|
|
853
896
|
/**
|
|
854
897
|
* Get the prompt for a phase with the issue number substituted.
|
|
855
898
|
* Selects self-contained prompts for non-Claude agents.
|
|
@@ -867,9 +910,11 @@ export async function getPhasePrompt(phase, issueNumber, agent, promptContext) {
|
|
|
867
910
|
: undefined;
|
|
868
911
|
const template = driverPrompt ?? definition.promptTemplate;
|
|
869
912
|
let basePrompt = template.replace(/\{issue\}/g, String(issueNumber));
|
|
870
|
-
// Append phase-specific context (e.g., QA findings for loop phase)
|
|
913
|
+
// Append phase-specific context (e.g., QA findings for loop phase),
|
|
914
|
+
// wrapped in a sentinel so the phase's own skill (e.g. /loop) can detect
|
|
915
|
+
// embedded orchestrator context without re-fetching it from GitHub (#960).
|
|
871
916
|
if (promptContext) {
|
|
872
|
-
basePrompt += `\n\n---\n\n${promptContext}
|
|
917
|
+
basePrompt += `\n\n---\n\n<!-- ${PROMPT_CONTEXT_SENTINEL} -->\n${promptContext}\n<!-- /${PROMPT_CONTEXT_SENTINEL} -->`;
|
|
873
918
|
}
|
|
874
919
|
// Include AGENTS.md content in the prompt context for non-Claude agent compatibility.
|
|
875
920
|
// Claude reads CLAUDE.md natively, but other agents (Aider, Codex, Gemini CLI)
|
|
@@ -1032,6 +1077,12 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
|
|
|
1032
1077
|
const eligibleHandle = resumeHandle && driver.canResume(resumeHandle, cwd)
|
|
1033
1078
|
? resumeHandle
|
|
1034
1079
|
: undefined;
|
|
1080
|
+
// #914: resolved per-phase model/effort, if this phase has one. Both
|
|
1081
|
+
// ExecutionConfig producers (buildExecutionConfig, ready-gate.ts's
|
|
1082
|
+
// buildPhaseConfig) populate `phasePolicies` the same way, so this is the
|
|
1083
|
+
// single site that turns it into driver-facing fields — see the doc
|
|
1084
|
+
// comment on ExecutionConfig.phasePolicies.
|
|
1085
|
+
const phasePolicy = config.phasePolicies?.[phase];
|
|
1035
1086
|
// Build AgentExecutionConfig for the driver
|
|
1036
1087
|
const agentConfig = {
|
|
1037
1088
|
cwd,
|
|
@@ -1040,9 +1091,14 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
|
|
|
1040
1091
|
phaseTimeout: config.phaseTimeout,
|
|
1041
1092
|
verbose: config.verbose,
|
|
1042
1093
|
mcp: config.mcp,
|
|
1094
|
+
mcpAllowlist: config.mcpAllowlist,
|
|
1043
1095
|
resumeHandle: eligibleHandle,
|
|
1044
1096
|
sessionId: eligibleHandle?.token,
|
|
1045
1097
|
files,
|
|
1098
|
+
...(phasePolicy?.model ? { model: phasePolicy.model } : {}),
|
|
1099
|
+
...(phasePolicy?.effort
|
|
1100
|
+
? { effort: phasePolicy.effort }
|
|
1101
|
+
: {}),
|
|
1046
1102
|
onOutput: config.verbose || reportActivity
|
|
1047
1103
|
? (text) => {
|
|
1048
1104
|
if (config.verbose) {
|