sequant 2.11.0 → 2.13.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 +47 -3
- package/dist/dashboard/server.js +4 -0
- 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/README.md +9 -0
- 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 +42 -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/ready.js +7 -1
- package/dist/src/commands/status.js +4 -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/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 +73 -14
- package/dist/src/lib/settings.js +45 -3
- package/dist/src/lib/system.d.ts +7 -3
- package/dist/src/lib/system.js +7 -3
- package/dist/src/lib/test-tautology-detector.js +50 -3
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +81 -6
- package/dist/src/lib/workflow/config-resolver.d.ts +30 -2
- package/dist/src/lib/workflow/config-resolver.js +59 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +36 -4
- package/dist/src/lib/workflow/metrics-schema.d.ts +10 -1
- package/dist/src/lib/workflow/metrics-schema.js +13 -1
- package/dist/src/lib/workflow/metrics-writer.d.ts +3 -1
- 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-executor.d.ts +17 -0
- package/dist/src/lib/workflow/phase-executor.js +60 -6
- 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 +25 -1
- package/dist/src/lib/workflow/ready-gate.js +81 -11
- package/dist/src/lib/workflow/reconcile.js +4 -2
- package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
- package/dist/src/lib/workflow/run-log-schema.js +40 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +18 -0
- package/dist/src/lib/workflow/run-orchestrator.js +38 -2
- package/dist/src/lib/workflow/state-cleanup.d.ts +4 -4
- package/dist/src/lib/workflow/state-cleanup.js +9 -5
- package/dist/src/lib/workflow/state-schema.d.ts +10 -1
- package/dist/src/lib/workflow/state-schema.js +13 -1
- package/dist/src/lib/workflow/types.d.ts +20 -0
- package/dist/src/mcp/tools/run.js +10 -1
- package/package.json +13 -12
- package/templates/hooks/pre-tool.sh +108 -17
- package/templates/memory/constitution.md +112 -45
- package/templates/skills/exec/SKILL.md +1 -1
- package/templates/skills/fullsolve/SKILL.md +62 -9
- package/templates/skills/loop/SKILL.md +71 -12
- package/templates/skills/merger/SKILL.md +32 -3
- package/templates/skills/qa/SKILL.md +247 -2
- package/templates/skills/spec/SKILL.md +11 -5
- package/templates/skills/test/SKILL.md +1 -1
|
@@ -14,7 +14,7 @@ import { withEscalatedEffort } from "./effort-escalation.js";
|
|
|
14
14
|
import { classifyError, errorTypeToCategory, } from "./error-classifier.js";
|
|
15
15
|
import { getGitDiffStats, getCommitHash, resolveDiffBase, } from "./git-diff-utils.js";
|
|
16
16
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
17
|
-
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, hasExecChanges, isWindowExhaustedRateLimit, } from "./phase-executor.js";
|
|
17
|
+
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, hasExecChanges, isWindowExhaustedRateLimit, selectFixableGaps, } from "./phase-executor.js";
|
|
18
18
|
import { BillingError, RateLimitError, resetsAtToMs } from "../errors.js";
|
|
19
19
|
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
20
20
|
import { determinePhasesForIssue, DOCS_LABELS } from "./phase-mapper.js";
|
|
@@ -135,8 +135,11 @@ export function buildLoopContext(failedResult) {
|
|
|
135
135
|
if (failedResult.verdict) {
|
|
136
136
|
parts.push(`QA Verdict: ${failedResult.verdict}`);
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// #937 AC-3: exclude findings marked `document`/`pause_for_human` — those
|
|
139
|
+
// are QA-real but not something a fix loop should chase.
|
|
140
|
+
const fixableGaps = selectFixableGaps(failedResult.summary);
|
|
141
|
+
if (fixableGaps.length) {
|
|
142
|
+
parts.push(`QA Gaps:\n${fixableGaps.map((gap) => `- ${gap}`).join("\n")}`);
|
|
140
143
|
}
|
|
141
144
|
if (failedResult.summary?.suggestions?.length) {
|
|
142
145
|
parts.push(`Suggestions:\n${failedResult.summary.suggestions.map((s) => `- ${s}`).join("\n")}`);
|
|
@@ -564,6 +567,50 @@ async function recordWindowHaltState(stateManager, issueNumber, phase, result) {
|
|
|
564
567
|
// State tracking errors shouldn't stop execution
|
|
565
568
|
}
|
|
566
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Build the comment body for a standard-qa-phase verdict post (#964).
|
|
572
|
+
* Includes AC coverage and any gaps/suggestions from the parsed `QaSummary`,
|
|
573
|
+
* plus a machine marker so a future dedup pass has an anchor.
|
|
574
|
+
* @internal Exported for testing.
|
|
575
|
+
*/
|
|
576
|
+
export function buildQaVerdictComment(verdict, summary, commitHash, iteration) {
|
|
577
|
+
const lines = [`## QA Verdict: ${verdict}`];
|
|
578
|
+
if (summary) {
|
|
579
|
+
lines.push("", `AC coverage: ${summary.acMet}/${summary.acTotal} met`);
|
|
580
|
+
if (summary.gaps.length > 0) {
|
|
581
|
+
lines.push("", "**Gaps:**", ...summary.gaps.map((g) => `- ${g}`));
|
|
582
|
+
}
|
|
583
|
+
if (summary.suggestions.length > 0) {
|
|
584
|
+
lines.push("", "**Suggestions:**", ...summary.suggestions.map((s) => `- ${s}`));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
lines.push("", `<!-- SEQUANT_QA_VERDICT: ${JSON.stringify({
|
|
588
|
+
verdict,
|
|
589
|
+
commit: commitHash ?? null,
|
|
590
|
+
iteration,
|
|
591
|
+
})} -->`);
|
|
592
|
+
return lines.join("\n");
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Post the qa-verdict comment for a standard (non-ready-gate) qa phase under
|
|
596
|
+
* orchestrated `sequant run` (#964). This is the channel `qa/SKILL.md` §9
|
|
597
|
+
* promises ("orchestrator handles aggregated summary") but batch-executor
|
|
598
|
+
* never backed — a re-run producing a fresh, different verdict left a stale,
|
|
599
|
+
* contradicted comment as the only externally-visible one.
|
|
600
|
+
*
|
|
601
|
+
* Best-effort: a post failure is caught and logged, never fails the run —
|
|
602
|
+
* mirrors {@link runReadyGateForIssue}'s `postReport` contract (#937 AC-4).
|
|
603
|
+
* @internal Exported for testing.
|
|
604
|
+
*/
|
|
605
|
+
export async function postQaVerdictComment(issueNumber, verdict, summary, commitHash, iteration, log, postComment = (n, body) => new GitHubProvider().postComment(String(n), body)) {
|
|
606
|
+
try {
|
|
607
|
+
const body = buildQaVerdictComment(verdict, summary, commitHash, iteration);
|
|
608
|
+
await postComment(issueNumber, body);
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
log(chalk.yellow(` ! Failed to post QA verdict comment: ${err}`));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
567
614
|
/**
|
|
568
615
|
* Run the post-QA ready gate (#817) for a single issue at the run path's
|
|
569
616
|
* post-success / pre-PR seam.
|
|
@@ -594,6 +641,8 @@ async function runReadyGateForIssue(args) {
|
|
|
594
641
|
const getSettingsFn = args.getSettingsFn ?? getSettings;
|
|
595
642
|
const fetchBody = args.fetchBody ??
|
|
596
643
|
((n) => new GitHubProvider().fetchIssueBodySync(String(n)));
|
|
644
|
+
const postComment = args.postComment ??
|
|
645
|
+
((n, body) => new GitHubProvider().postComment(String(n), body));
|
|
597
646
|
try {
|
|
598
647
|
const settings = await getSettingsFn();
|
|
599
648
|
const policy = settings.ready.policy;
|
|
@@ -620,6 +669,8 @@ async function runReadyGateForIssue(args) {
|
|
|
620
669
|
verbose: config.verbose,
|
|
621
670
|
runPhase,
|
|
622
671
|
onProgress,
|
|
672
|
+
// #937 AC-4: persist the final gap report as an issue comment.
|
|
673
|
+
postReport: (body) => postComment(issueNumber, body),
|
|
623
674
|
});
|
|
624
675
|
log(result.ready
|
|
625
676
|
? chalk.green(` ✓ Ready gate: ${result.reason} — awaiting human merge (never merged)`)
|
|
@@ -636,7 +687,7 @@ async function runReadyGateForIssue(args) {
|
|
|
636
687
|
}
|
|
637
688
|
export async function runIssueWithLogging(ctx) {
|
|
638
689
|
// Destructure context for use throughout the function
|
|
639
|
-
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
|
|
690
|
+
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, postComment: injectedPostComment, } = ctx;
|
|
640
691
|
const worktreePath = worktree?.path;
|
|
641
692
|
const branch = worktree?.branch;
|
|
642
693
|
const chainMode = chain?.enabled;
|
|
@@ -1103,6 +1154,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1103
1154
|
/* progress errors must not halt */
|
|
1104
1155
|
}
|
|
1105
1156
|
}
|
|
1157
|
+
// #964: post the verdict from a standard (non-ready-gate) qa phase.
|
|
1158
|
+
// qa/SKILL.md §9 promises "the orchestrator handles aggregated summary"
|
|
1159
|
+
// under SEQUANT_ORCHESTRATOR, but nothing backed that promise — a
|
|
1160
|
+
// re-run producing a fresh, different verdict left the stale prior
|
|
1161
|
+
// comment as the only externally-visible one. Gating on
|
|
1162
|
+
// `result.success && result.verdict` also excludes turn-capped and
|
|
1163
|
+
// unparseable-verdict phases (AC-4) without extra bookkeeping, since
|
|
1164
|
+
// both already flow through the `else` branch above.
|
|
1165
|
+
if (phase === "qa" && result.success && result.verdict) {
|
|
1166
|
+
const verdictDiffBase = worktreePath
|
|
1167
|
+
? resolveDiffBase(worktreePath, baseBranch ?? "main")
|
|
1168
|
+
: undefined;
|
|
1169
|
+
const verdictCommitHash = worktreePath && verdictDiffBase
|
|
1170
|
+
? getCommitHash(worktreePath, verdictDiffBase)
|
|
1171
|
+
: undefined;
|
|
1172
|
+
await postQaVerdictComment(issueNumber, result.verdict, result.summary, verdictCommitHash, iteration, log, injectedPostComment);
|
|
1173
|
+
}
|
|
1106
1174
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
1107
1175
|
if (logWriter) {
|
|
1108
1176
|
// Resolve the diff base once (#878): worktrees branch from
|
|
@@ -1222,10 +1290,14 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1222
1290
|
// Build enriched config for loop phase with QA context (#488).
|
|
1223
1291
|
// Pass verdict, failed ACs, and error directly so the /loop skill
|
|
1224
1292
|
// doesn't need to reconstruct context from GitHub comments.
|
|
1293
|
+
// #937 AC-3: exclude `document`/`pause_for_human`-tagged findings
|
|
1294
|
+
// from what the loop is told to fix (same filter as ready-gate's
|
|
1295
|
+
// fixableGaps).
|
|
1296
|
+
const fixableGaps = selectFixableGaps(result.summary);
|
|
1225
1297
|
const loopConfig = {
|
|
1226
1298
|
...issueConfig,
|
|
1227
1299
|
lastVerdict: result.verdict ?? undefined,
|
|
1228
|
-
failedAcs:
|
|
1300
|
+
failedAcs: fixableGaps.length ? fixableGaps.join("; ") : undefined,
|
|
1229
1301
|
promptContext: buildLoopContext(result),
|
|
1230
1302
|
};
|
|
1231
1303
|
const loopStartTime = new Date();
|
|
@@ -1335,10 +1407,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1335
1407
|
// Hoisted out of the `if (stateManager)` block below because the checkpoint
|
|
1336
1408
|
// warning also has to name this status, and naming the wrong one is exactly
|
|
1337
1409
|
// the #837 inaccuracy being fixed here.
|
|
1410
|
+
const qaVerdict = phaseResults.find((p) => p.phase === "qa")?.verdict;
|
|
1338
1411
|
const finalStatus = readyGateResult
|
|
1339
1412
|
? readyGateResult.issueStatus
|
|
1340
1413
|
: success
|
|
1341
|
-
? "
|
|
1414
|
+
? qaVerdict === "NEEDS_VERIFICATION"
|
|
1415
|
+
? "awaiting_verification"
|
|
1416
|
+
: "ready_for_merge"
|
|
1342
1417
|
: "in_progress";
|
|
1343
1418
|
if (stateManager) {
|
|
1344
1419
|
try {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { type ExecutionConfig, type RunOptions } from "./types.js";
|
|
10
|
-
import type { SequantSettings } from "../settings.js";
|
|
10
|
+
import type { SequantSettings, ModelRoles } from "../settings.js";
|
|
11
11
|
/**
|
|
12
12
|
* Layers for config resolution.
|
|
13
13
|
* Each field is optional — only defined values participate in merging.
|
|
@@ -72,6 +72,19 @@ export declare function positiveOr(value: number | undefined, fallback: number):
|
|
|
72
72
|
export interface PhasePolicy {
|
|
73
73
|
model?: string;
|
|
74
74
|
effort?: string;
|
|
75
|
+
/**
|
|
76
|
+
* The original `role:<name>` string before resolution (#975). Set only when
|
|
77
|
+
* a `role:` prefix was used; absent for raw model strings. Allows metrics to
|
|
78
|
+
* record both what the user configured and what was actually dispatched.
|
|
79
|
+
*/
|
|
80
|
+
requestedModel?: string;
|
|
81
|
+
/**
|
|
82
|
+
* Concrete model ID from `modelUsage` after phase execution (#975). Not
|
|
83
|
+
* present on the static config — populated by `enrichPhasePoliciesFromResults`
|
|
84
|
+
* just before the run is written to metrics. Absent for phases that did not
|
|
85
|
+
* execute or drivers that do not report `modelUsage`.
|
|
86
|
+
*/
|
|
87
|
+
resolvedModel?: string;
|
|
75
88
|
}
|
|
76
89
|
/**
|
|
77
90
|
* Parse a `--models`/`--efforts` CLI spec into a phase → value map.
|
|
@@ -83,6 +96,21 @@ export interface PhasePolicy {
|
|
|
83
96
|
* malformed spec must never silently resolve to "nothing configured".
|
|
84
97
|
*/
|
|
85
98
|
export declare function parsePhaseSpec(spec: string, phaseNames: string[]): Record<string, string>;
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a model-or-role reference to a concrete model string (#975).
|
|
101
|
+
*
|
|
102
|
+
* - No `role:` prefix → returns verbatim (backward compat, AC-3).
|
|
103
|
+
* - `role:<name>` → looks up `<name>` in `modelRoles`.
|
|
104
|
+
* - Missing entry → throws, naming the role and available keys (AC-2).
|
|
105
|
+
* - String value → desugars to `{ "claude-code": value }`, picks by `activeDriver`.
|
|
106
|
+
* - Object value → picks by `activeDriver`; missing driver key → throws.
|
|
107
|
+
* - A role that resolves to a string for a different driver throws (AC-2).
|
|
108
|
+
*
|
|
109
|
+
* @param roleOrString - The model string from config/CLI (may have `role:` prefix).
|
|
110
|
+
* @param modelRoles - The resolved `run.modelRoles` map.
|
|
111
|
+
* @param activeDriver - Driver registry name (default: `"claude-code"`).
|
|
112
|
+
*/
|
|
113
|
+
export declare function resolveRoleToModel(roleOrString: string, modelRoles?: ModelRoles, activeDriver?: string): string;
|
|
86
114
|
/**
|
|
87
115
|
* Resolve per-phase model/effort policies with CLI > settings > absent
|
|
88
116
|
* precedence.
|
|
@@ -92,7 +120,7 @@ export declare function parsePhaseSpec(spec: string, phaseNames: string[]): Reco
|
|
|
92
120
|
* two `phaseTimeout` producers did in #833 — see `positiveOr`'s doc comment
|
|
93
121
|
* for that history.
|
|
94
122
|
*/
|
|
95
|
-
export declare function resolvePhasePolicies(cliModels: string | undefined, cliEfforts: string | undefined, settingsPhases: Record<string, PhasePolicy> | undefined, phaseNames: string[]): Record<string, PhasePolicy>;
|
|
123
|
+
export declare function resolvePhasePolicies(cliModels: string | undefined, cliEfforts: string | undefined, settingsPhases: Record<string, PhasePolicy> | undefined, phaseNames: string[], modelRoles?: ModelRoles, activeDriver?: string): Record<string, PhasePolicy>;
|
|
96
124
|
/**
|
|
97
125
|
* Build an ExecutionConfig from merged RunOptions and settings.
|
|
98
126
|
* Extracts the phase-timeout, MCP, retry, and mode resolution logic
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* @module
|
|
8
8
|
*/
|
|
9
9
|
import { DEFAULT_CONFIG, DEFAULT_PHASES, } from "./types.js";
|
|
10
|
+
import { DEFAULT_MODEL_ROLES } from "../settings.js";
|
|
10
11
|
import { getEnvConfig } from "./batch-executor.js";
|
|
11
12
|
import { getPhaseNames } from "./phase-registry.js";
|
|
12
13
|
/**
|
|
@@ -213,6 +214,46 @@ function applyPhaseSpec(target, parsed, field, phaseNames) {
|
|
|
213
214
|
target[phase] = { ...target[phase], [field]: value };
|
|
214
215
|
}
|
|
215
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* Resolve a model-or-role reference to a concrete model string (#975).
|
|
219
|
+
*
|
|
220
|
+
* - No `role:` prefix → returns verbatim (backward compat, AC-3).
|
|
221
|
+
* - `role:<name>` → looks up `<name>` in `modelRoles`.
|
|
222
|
+
* - Missing entry → throws, naming the role and available keys (AC-2).
|
|
223
|
+
* - String value → desugars to `{ "claude-code": value }`, picks by `activeDriver`.
|
|
224
|
+
* - Object value → picks by `activeDriver`; missing driver key → throws.
|
|
225
|
+
* - A role that resolves to a string for a different driver throws (AC-2).
|
|
226
|
+
*
|
|
227
|
+
* @param roleOrString - The model string from config/CLI (may have `role:` prefix).
|
|
228
|
+
* @param modelRoles - The resolved `run.modelRoles` map.
|
|
229
|
+
* @param activeDriver - Driver registry name (default: `"claude-code"`).
|
|
230
|
+
*/
|
|
231
|
+
export function resolveRoleToModel(roleOrString, modelRoles = DEFAULT_MODEL_ROLES, activeDriver = "claude-code") {
|
|
232
|
+
if (!roleOrString.startsWith("role:")) {
|
|
233
|
+
return roleOrString;
|
|
234
|
+
}
|
|
235
|
+
const roleName = roleOrString.slice("role:".length);
|
|
236
|
+
if (!roleName) {
|
|
237
|
+
throw new Error(`Invalid role reference "role:" — role name is empty. Available roles: ${Object.keys(modelRoles).join(", ")}.`);
|
|
238
|
+
}
|
|
239
|
+
const entry = modelRoles[roleName];
|
|
240
|
+
if (entry === undefined) {
|
|
241
|
+
throw new Error(`Role "${roleName}" is not defined in run.modelRoles. Available roles: ${Object.keys(modelRoles).join(", ")}.`);
|
|
242
|
+
}
|
|
243
|
+
if (typeof entry === "string") {
|
|
244
|
+
// String shorthand desugars to { "claude-code": value } — only for claude-code.
|
|
245
|
+
if (activeDriver !== "claude-code") {
|
|
246
|
+
throw new Error(`Role "${roleName}" uses a string shorthand (claude-code only) but the active driver is "${activeDriver}". Use an object map to define per-driver models.`);
|
|
247
|
+
}
|
|
248
|
+
return entry;
|
|
249
|
+
}
|
|
250
|
+
// Object map: pick by driver.
|
|
251
|
+
const resolved = entry[activeDriver];
|
|
252
|
+
if (resolved === undefined) {
|
|
253
|
+
throw new Error(`Role "${roleName}" has no entry for driver "${activeDriver}". Available drivers in this role: ${Object.keys(entry).join(", ")}.`);
|
|
254
|
+
}
|
|
255
|
+
return resolved;
|
|
256
|
+
}
|
|
216
257
|
/**
|
|
217
258
|
* Resolve per-phase model/effort policies with CLI > settings > absent
|
|
218
259
|
* precedence.
|
|
@@ -222,7 +263,7 @@ function applyPhaseSpec(target, parsed, field, phaseNames) {
|
|
|
222
263
|
* two `phaseTimeout` producers did in #833 — see `positiveOr`'s doc comment
|
|
223
264
|
* for that history.
|
|
224
265
|
*/
|
|
225
|
-
export function resolvePhasePolicies(cliModels, cliEfforts, settingsPhases, phaseNames) {
|
|
266
|
+
export function resolvePhasePolicies(cliModels, cliEfforts, settingsPhases, phaseNames, modelRoles, activeDriver) {
|
|
226
267
|
const result = {};
|
|
227
268
|
// Layer 1 (lowest): settings.run.phases. Skip any phase name settings
|
|
228
269
|
// validation already didn't recognize — that's surfaced as a settings
|
|
@@ -243,6 +284,20 @@ export function resolvePhasePolicies(cliModels, cliEfforts, settingsPhases, phas
|
|
|
243
284
|
if (cliEfforts) {
|
|
244
285
|
applyPhaseSpec(result, parsePhaseSpec(cliEfforts, phaseNames), "effort", phaseNames);
|
|
245
286
|
}
|
|
287
|
+
// Role resolution (#975): resolve any `role:<name>` model references to
|
|
288
|
+
// concrete model strings. Raw strings (no `role:` prefix) pass through
|
|
289
|
+
// verbatim — this is AC-3 backward compat.
|
|
290
|
+
if (modelRoles) {
|
|
291
|
+
for (const [phase, policy] of Object.entries(result)) {
|
|
292
|
+
if (policy.model && policy.model.startsWith("role:")) {
|
|
293
|
+
result[phase] = {
|
|
294
|
+
...policy,
|
|
295
|
+
requestedModel: policy.model, // capture pre-resolution value (AC-4)
|
|
296
|
+
model: resolveRoleToModel(policy.model, modelRoles, activeDriver),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
246
301
|
return result;
|
|
247
302
|
}
|
|
248
303
|
/**
|
|
@@ -285,6 +340,7 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
285
340
|
maxIterations: positiveOr(mergedOptions.maxIterations, positiveOr(settings.run.maxIterations, DEFAULT_CONFIG.maxIterations)),
|
|
286
341
|
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
287
342
|
mcp: mcpEnabled,
|
|
343
|
+
mcpAllowlist: settings.run.mcpAllowlist,
|
|
288
344
|
retry: retryEnabled,
|
|
289
345
|
// #804: default 0 (off) — the whole regression contract for auto-wait is
|
|
290
346
|
// that an unset flag leaves the #761/#799 halt path untouched.
|
|
@@ -303,7 +359,8 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
303
359
|
// #914: CLI > settings > absent, via the shared resolver both
|
|
304
360
|
// ExecutionConfig producers call (see `resolvePhasePolicies`'s doc
|
|
305
361
|
// comment for the #833 drift this guards against).
|
|
306
|
-
|
|
362
|
+
// #975: pass modelRoles + active driver so `role:` prefixes resolve.
|
|
363
|
+
phasePolicies: resolvePhasePolicies(mergedOptions.models, mergedOptions.efforts, settings.run.phases, getPhaseNames(), settings.run.modelRoles, settings.run.agent ?? "claude-code"),
|
|
307
364
|
// #915: CLI > settings > default `false` — mirrors the `readyGate`
|
|
308
365
|
// precedent above. Both `ExecutionConfig` producers (here and
|
|
309
366
|
// `ready-gate.ts:buildPhaseConfig`) resolve this the same way (#833).
|
|
@@ -32,6 +32,13 @@ export interface AgentExecutionConfig {
|
|
|
32
32
|
phaseTimeout: number;
|
|
33
33
|
verbose: boolean;
|
|
34
34
|
mcp: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Claude Desktop MCP server names explicitly opted in to pass through to
|
|
37
|
+
* this phase, despite `mcp`'s default exclusion (#936). Forwarded
|
|
38
|
+
* verbatim to `getPhaseMcpServersConfig`'s `desktopAllowlist` by
|
|
39
|
+
* ClaudeCodeDriver; ignored by drivers without an MCP concept.
|
|
40
|
+
*/
|
|
41
|
+
mcpAllowlist?: string[];
|
|
35
42
|
/**
|
|
36
43
|
* Resume a previous session (driver-specific; ignored if unsupported).
|
|
37
44
|
*
|
|
@@ -102,6 +109,13 @@ export interface AgentPhaseResult {
|
|
|
102
109
|
stdoutTail?: string[];
|
|
103
110
|
/** Process exit code (undefined for SDK-based drivers) (#447) */
|
|
104
111
|
exitCode?: number;
|
|
112
|
+
/**
|
|
113
|
+
* Per-model usage totals from the SDK result (#975). Keys are concrete model
|
|
114
|
+
* IDs (e.g. `"claude-sonnet-5"`); first key is the resolved model string used
|
|
115
|
+
* for this phase. Only set by SDK-based drivers (claude-code); undefined for
|
|
116
|
+
* subprocess drivers (aider).
|
|
117
|
+
*/
|
|
118
|
+
modelUsage?: Record<string, unknown>;
|
|
105
119
|
}
|
|
106
120
|
/**
|
|
107
121
|
* Interface that all agent backends must implement.
|
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* orchestration layer should import the SDK directly.
|
|
6
6
|
*/
|
|
7
7
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
-
import {
|
|
9
|
-
import { RateLimitError, BillingError, createRateLimitError, isWaitableWindow, isRateLimitFailureInfo, } from "../../errors.js";
|
|
8
|
+
import { getPhaseMcpServersConfig } from "../../mcp-config.js";
|
|
9
|
+
import { ApiError, RateLimitError, BillingError, createRateLimitError, isWaitableWindow, isRateLimitFailureInfo, } from "../../errors.js";
|
|
10
10
|
import { RingBuffer } from "../ring-buffer.js";
|
|
11
11
|
export class ClaudeCodeDriver {
|
|
12
12
|
name = "claude-code";
|
|
@@ -80,8 +80,14 @@ export class ClaudeCodeDriver {
|
|
|
80
80
|
resumeToken = undefined;
|
|
81
81
|
}
|
|
82
82
|
try {
|
|
83
|
-
// Get MCP servers config if enabled
|
|
84
|
-
|
|
83
|
+
// Get MCP servers config if enabled — allowlisted, not passed through
|
|
84
|
+
// from Claude Desktop config (#936), except for servers explicitly
|
|
85
|
+
// named in config.mcpAllowlist (settings.run.mcpAllowlist).
|
|
86
|
+
const mcpServers = config.mcp
|
|
87
|
+
? getPhaseMcpServersConfig(config.cwd, {
|
|
88
|
+
desktopAllowlist: config.mcpAllowlist,
|
|
89
|
+
})
|
|
90
|
+
: undefined;
|
|
85
91
|
const queryInstance = query({
|
|
86
92
|
prompt,
|
|
87
93
|
options: {
|
|
@@ -167,6 +173,31 @@ export class ClaudeCodeDriver {
|
|
|
167
173
|
const structuredError = this.buildStructuredError(rateLimitInfo, assistantError, apiRetryError);
|
|
168
174
|
if (resultMessage) {
|
|
169
175
|
if (resultMessage.subtype === "success") {
|
|
176
|
+
// The SDK emits subtype "success" with is_error:true when the turn
|
|
177
|
+
// ends on an API error (e.g. unrecognized model). Treat it as a
|
|
178
|
+
// phase failure so the caller gets a real error, not a zero-work
|
|
179
|
+
// success. See #973 / SDK docs §SDKResultMessage.
|
|
180
|
+
if (resultMessage.is_error) {
|
|
181
|
+
const metadata = {};
|
|
182
|
+
if (resultMessage.api_error_status != null) {
|
|
183
|
+
metadata.statusCode = resultMessage.api_error_status;
|
|
184
|
+
metadata.api_error_status = resultMessage.api_error_status;
|
|
185
|
+
}
|
|
186
|
+
if (resultMessage.terminal_reason != null) {
|
|
187
|
+
metadata.terminal_reason = resultMessage.terminal_reason;
|
|
188
|
+
}
|
|
189
|
+
const apiError = new ApiError(resultMessage.result, metadata);
|
|
190
|
+
return {
|
|
191
|
+
success: false,
|
|
192
|
+
output: capturedOutput,
|
|
193
|
+
sessionId: resultSessionId,
|
|
194
|
+
resumeHandle,
|
|
195
|
+
error: resultMessage.result,
|
|
196
|
+
structuredError: apiError,
|
|
197
|
+
stderrTail: stderrBuffer.getLines(),
|
|
198
|
+
stdoutTail: stdoutBuffer.getLines(),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
170
201
|
return {
|
|
171
202
|
success: true,
|
|
172
203
|
output: capturedOutput,
|
|
@@ -174,6 +205,7 @@ export class ClaudeCodeDriver {
|
|
|
174
205
|
resumeHandle,
|
|
175
206
|
stderrTail: stderrBuffer.getLines(),
|
|
176
207
|
stdoutTail: stdoutBuffer.getLines(),
|
|
208
|
+
modelUsage: resultMessage.modelUsage,
|
|
177
209
|
};
|
|
178
210
|
}
|
|
179
211
|
// Turn-cap is a soft, recoverable outcome, not a hard failure: the
|
|
@@ -116,6 +116,8 @@ export declare const MetricRunSchema: z.ZodObject<{
|
|
|
116
116
|
phasePolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
117
117
|
model: z.ZodOptional<z.ZodString>;
|
|
118
118
|
effort: z.ZodOptional<z.ZodString>;
|
|
119
|
+
requestedModel: z.ZodOptional<z.ZodString>;
|
|
120
|
+
resolvedModel: z.ZodOptional<z.ZodString>;
|
|
119
121
|
}, z.core.$strip>>>;
|
|
120
122
|
effortEscalations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
121
123
|
phase: z.ZodString;
|
|
@@ -176,6 +178,8 @@ export declare const MetricsSchema: z.ZodObject<{
|
|
|
176
178
|
phasePolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
177
179
|
model: z.ZodOptional<z.ZodString>;
|
|
178
180
|
effort: z.ZodOptional<z.ZodString>;
|
|
181
|
+
requestedModel: z.ZodOptional<z.ZodString>;
|
|
182
|
+
resolvedModel: z.ZodOptional<z.ZodString>;
|
|
179
183
|
}, z.core.$strip>>>;
|
|
180
184
|
effortEscalations: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
181
185
|
phase: z.ZodString;
|
|
@@ -215,15 +219,20 @@ export declare function createMetricRun(options: {
|
|
|
215
219
|
flags?: string[];
|
|
216
220
|
failureCategory?: FailureCategory;
|
|
217
221
|
/**
|
|
218
|
-
* Resolved per-phase model/effort overrides (#914), keyed by phase name.
|
|
222
|
+
* Resolved per-phase model/effort overrides (#914/#975), keyed by phase name.
|
|
219
223
|
* Pass only the phases that actually had a configured override — a phase
|
|
220
224
|
* that inherited the CLI default should not appear here at all. See
|
|
221
225
|
* `resolvePhasePolicies` in `config-resolver.ts`, which already produces
|
|
222
226
|
* a map shaped this way.
|
|
227
|
+
*
|
|
228
|
+
* `requestedModel` and `resolvedModel` are #975 additions — pass when available
|
|
229
|
+
* to enable cross-time benchmark comparisons as the model roster evolves.
|
|
223
230
|
*/
|
|
224
231
|
phasePolicies?: Record<string, {
|
|
225
232
|
model?: string;
|
|
226
233
|
effort?: string;
|
|
234
|
+
requestedModel?: string;
|
|
235
|
+
resolvedModel?: string;
|
|
227
236
|
}>;
|
|
228
237
|
/**
|
|
229
238
|
* Effort escalations applied during this run (#915), one entry per
|
|
@@ -103,9 +103,21 @@ export const MetricRunSchema = z.object({
|
|
|
103
103
|
* undefined fields. Enum/alias strings only, consistent with this
|
|
104
104
|
* schema's no-file-paths/no-content privacy contract. Optional and
|
|
105
105
|
* additive — absent on records written before this field existed.
|
|
106
|
+
*
|
|
107
|
+
* `requestedModel` and `resolvedModel` are added in #975 to record the
|
|
108
|
+
* role string (pre-resolution) and the concrete model ID from `modelUsage`
|
|
109
|
+
* (post-execution) respectively — enabling cross-time benchmark comparisons
|
|
110
|
+
* as the model roster moves under aliases.
|
|
106
111
|
*/
|
|
107
112
|
phasePolicies: z
|
|
108
|
-
.record(z.string(), z.object({
|
|
113
|
+
.record(z.string(), z.object({
|
|
114
|
+
model: z.string().optional(),
|
|
115
|
+
effort: z.string().optional(),
|
|
116
|
+
/** The role string or raw model string as configured (pre-resolution, #975). */
|
|
117
|
+
requestedModel: z.string().optional(),
|
|
118
|
+
/** The concrete model ID from `modelUsage` after execution (#975). */
|
|
119
|
+
resolvedModel: z.string().optional(),
|
|
120
|
+
}))
|
|
109
121
|
.optional(),
|
|
110
122
|
/**
|
|
111
123
|
* Effort escalations applied during this run (#915), one entry per
|
|
@@ -70,10 +70,12 @@ 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`. */
|
|
73
|
+
/** Resolved per-phase model/effort overrides (#914/#975). See `createMetricRun`. */
|
|
74
74
|
phasePolicies?: Record<string, {
|
|
75
75
|
model?: string;
|
|
76
76
|
effort?: string;
|
|
77
|
+
requestedModel?: string;
|
|
78
|
+
resolvedModel?: string;
|
|
77
79
|
}>;
|
|
78
80
|
/** Effort escalations applied during this run (#915). See `createMetricRun`. */
|
|
79
81
|
effortEscalations?: Array<{
|
|
@@ -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 {};
|