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
|
@@ -10,13 +10,15 @@
|
|
|
10
10
|
import chalk from "chalk";
|
|
11
11
|
import { spawnSync } from "child_process";
|
|
12
12
|
import { createPhaseLogFromTiming } from "./log-writer.js";
|
|
13
|
+
import { withEscalatedEffort } from "./effort-escalation.js";
|
|
13
14
|
import { classifyError, errorTypeToCategory, } from "./error-classifier.js";
|
|
14
15
|
import { getGitDiffStats, getCommitHash, resolveDiffBase, } from "./git-diff-utils.js";
|
|
15
16
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
16
|
-
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, isWindowExhaustedRateLimit, } from "./phase-executor.js";
|
|
17
|
+
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, hasExecChanges, isWindowExhaustedRateLimit, selectFixableGaps, } from "./phase-executor.js";
|
|
17
18
|
import { BillingError, RateLimitError, resetsAtToMs } from "../errors.js";
|
|
18
19
|
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
19
|
-
import {
|
|
20
|
+
import { determinePhasesForIssue, DOCS_LABELS } from "./phase-mapper.js";
|
|
21
|
+
import { resolveSpecRecommendation } from "./spec-recommendation.js";
|
|
20
22
|
import { activateRelay, deactivateRelay, } from "../relay/activation.js";
|
|
21
23
|
import { getSettings } from "../settings.js";
|
|
22
24
|
import { GitHubProvider } from "./platforms/github.js";
|
|
@@ -133,8 +135,11 @@ export function buildLoopContext(failedResult) {
|
|
|
133
135
|
if (failedResult.verdict) {
|
|
134
136
|
parts.push(`QA Verdict: ${failedResult.verdict}`);
|
|
135
137
|
}
|
|
136
|
-
|
|
137
|
-
|
|
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")}`);
|
|
138
143
|
}
|
|
139
144
|
if (failedResult.summary?.suggestions?.length) {
|
|
140
145
|
parts.push(`Suggestions:\n${failedResult.summary.suggestions.map((s) => `- ${s}`).join("\n")}`);
|
|
@@ -562,6 +567,50 @@ async function recordWindowHaltState(stateManager, issueNumber, phase, result) {
|
|
|
562
567
|
// State tracking errors shouldn't stop execution
|
|
563
568
|
}
|
|
564
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
|
+
}
|
|
565
614
|
/**
|
|
566
615
|
* Run the post-QA ready gate (#817) for a single issue at the run path's
|
|
567
616
|
* post-success / pre-PR seam.
|
|
@@ -592,6 +641,8 @@ async function runReadyGateForIssue(args) {
|
|
|
592
641
|
const getSettingsFn = args.getSettingsFn ?? getSettings;
|
|
593
642
|
const fetchBody = args.fetchBody ??
|
|
594
643
|
((n) => new GitHubProvider().fetchIssueBodySync(String(n)));
|
|
644
|
+
const postComment = args.postComment ??
|
|
645
|
+
((n, body) => new GitHubProvider().postComment(String(n), body));
|
|
595
646
|
try {
|
|
596
647
|
const settings = await getSettingsFn();
|
|
597
648
|
const policy = settings.ready.policy;
|
|
@@ -618,6 +669,8 @@ async function runReadyGateForIssue(args) {
|
|
|
618
669
|
verbose: config.verbose,
|
|
619
670
|
runPhase,
|
|
620
671
|
onProgress,
|
|
672
|
+
// #937 AC-4: persist the final gap report as an issue comment.
|
|
673
|
+
postReport: (body) => postComment(issueNumber, body),
|
|
621
674
|
});
|
|
622
675
|
log(result.ready
|
|
623
676
|
? chalk.green(` ✓ Ready gate: ${result.reason} — awaiting human merge (never merged)`)
|
|
@@ -634,7 +687,7 @@ async function runReadyGateForIssue(args) {
|
|
|
634
687
|
}
|
|
635
688
|
export async function runIssueWithLogging(ctx) {
|
|
636
689
|
// Destructure context for use throughout the function
|
|
637
|
-
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;
|
|
638
691
|
const worktreePath = worktree?.path;
|
|
639
692
|
const branch = worktree?.branch;
|
|
640
693
|
const chainMode = chain?.enabled;
|
|
@@ -876,22 +929,35 @@ export async function runIssueWithLogging(ctx) {
|
|
|
876
929
|
failureCategory: deriveFailureCategory(phaseResults),
|
|
877
930
|
};
|
|
878
931
|
}
|
|
879
|
-
//
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
932
|
+
// Resolve the spec→run phase recommendation through the ordered chain
|
|
933
|
+
// comment-marker → comment-prose → chat-text → label-fallback (#921).
|
|
934
|
+
// Chat-text parsing alone is nondeterministic: the spec agent's plan
|
|
935
|
+
// comment is the durable artifact, but the old code only ever looked at
|
|
936
|
+
// ephemeral chat output, silently dropping recommended phases (e.g.
|
|
937
|
+
// testgen) whenever the agent posted the plan via a body file (#814).
|
|
938
|
+
const resolved = resolveSpecRecommendation({
|
|
939
|
+
chatOutput: specResult.output ?? "",
|
|
940
|
+
issueNumber,
|
|
941
|
+
labels,
|
|
942
|
+
});
|
|
943
|
+
// `resolveSpecRecommendation` already excludes "spec" regardless of
|
|
944
|
+
// which step in the chain produced the result.
|
|
945
|
+
phases = resolved.phases;
|
|
946
|
+
detectedQualityLoop = resolved.qualityLoop;
|
|
947
|
+
if (logWriter) {
|
|
948
|
+
logWriter.setSpecRecommendation({ source: resolved.source, phases, qualityLoop: detectedQualityLoop }, issueNumber);
|
|
949
|
+
}
|
|
950
|
+
if (resolved.source === "marker") {
|
|
951
|
+
log(chalk.gray(` Spec recommends (marker): ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
952
|
+
}
|
|
953
|
+
else if (resolved.source === "comment-prose") {
|
|
954
|
+
log(chalk.gray(` Spec recommends (comment): ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
955
|
+
}
|
|
956
|
+
else if (resolved.source === "chat") {
|
|
957
|
+
log(chalk.gray(` Spec recommends (chat): ${phases.join(" → ")}${detectedQualityLoop ? " (quality loop)" : ""}`));
|
|
888
958
|
}
|
|
889
959
|
else {
|
|
890
|
-
// Fall back to label-based detection
|
|
891
960
|
log(chalk.yellow(` Could not parse spec recommendation, using label-based detection`));
|
|
892
|
-
const detected = detectPhasesFromLabels(labels);
|
|
893
|
-
phases = detected.phases.filter((p) => p !== "spec");
|
|
894
|
-
detectedQualityLoop = detected.qualityLoop;
|
|
895
961
|
log(chalk.gray(` Fallback: ${phases.join(" → ")}`));
|
|
896
962
|
}
|
|
897
963
|
}
|
|
@@ -1010,8 +1076,22 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1010
1076
|
// State tracking errors shouldn't stop execution
|
|
1011
1077
|
}
|
|
1012
1078
|
}
|
|
1079
|
+
// #915: iteration > 1 is the outer quality-loop's retry signal — the
|
|
1080
|
+
// same condition that triggers the "Quality loop iteration" log line
|
|
1081
|
+
// above. This loop re-runs the WHOLE `phases` list on every iteration
|
|
1082
|
+
// (it does not resume from the specific phase that failed), so the
|
|
1083
|
+
// escalation is per RETRIED ITERATION, not per specific-phase-that-
|
|
1084
|
+
// previously-failed: every phase dispatched while iteration > 1
|
|
1085
|
+
// escalates, including one that already succeeded on iteration 1 (e.g.
|
|
1086
|
+
// exec re-running alongside a retried qa). `withEscalatedEffort` is a
|
|
1087
|
+
// no-op (returns the input config by reference) whenever escalation is
|
|
1088
|
+
// off or this is the first attempt.
|
|
1089
|
+
const { config: dispatchConfig, record: escalationRecord } = withEscalatedEffort(withActivityHook(issueConfig, issueNumber, phase, onProgress, makeWaitTransition(phase)), phase, iteration > 1);
|
|
1090
|
+
if (escalationRecord && config.verbose) {
|
|
1091
|
+
log(chalk.gray(` effort: ${escalationRecord.base} → ${escalationRecord.escalated} (loop retry)`));
|
|
1092
|
+
}
|
|
1013
1093
|
const phaseStartTime = new Date();
|
|
1014
|
-
const result = await executePhaseWithRetry(issueNumber, phase,
|
|
1094
|
+
const result = await executePhaseWithRetry(issueNumber, phase, dispatchConfig, resumeHandle, worktreePath, shutdownManager, phasePauseHandle, undefined, // executePhaseFn — use the default
|
|
1015
1095
|
undefined, // delayFn — use the default
|
|
1016
1096
|
autoWaitLedger);
|
|
1017
1097
|
const phaseEndTime = new Date();
|
|
@@ -1027,7 +1107,15 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1027
1107
|
}
|
|
1028
1108
|
}
|
|
1029
1109
|
}
|
|
1030
|
-
phaseResults.push(
|
|
1110
|
+
phaseResults.push(escalationRecord
|
|
1111
|
+
? {
|
|
1112
|
+
...result,
|
|
1113
|
+
escalatedEffort: {
|
|
1114
|
+
base: escalationRecord.base,
|
|
1115
|
+
escalated: escalationRecord.escalated,
|
|
1116
|
+
},
|
|
1117
|
+
}
|
|
1118
|
+
: result);
|
|
1031
1119
|
// Emit completion/failure progress event (AC-8)
|
|
1032
1120
|
const phaseDurationSec = Math.round((phaseEndTime.getTime() - phaseStartTime.getTime()) / 1000);
|
|
1033
1121
|
if (result.success) {
|
|
@@ -1066,6 +1154,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1066
1154
|
/* progress errors must not halt */
|
|
1067
1155
|
}
|
|
1068
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
|
+
}
|
|
1069
1174
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
1070
1175
|
if (logWriter) {
|
|
1071
1176
|
// Resolve the diff base once (#878): worktrees branch from
|
|
@@ -1185,10 +1290,14 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1185
1290
|
// Build enriched config for loop phase with QA context (#488).
|
|
1186
1291
|
// Pass verdict, failed ACs, and error directly so the /loop skill
|
|
1187
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);
|
|
1188
1297
|
const loopConfig = {
|
|
1189
1298
|
...issueConfig,
|
|
1190
1299
|
lastVerdict: result.verdict ?? undefined,
|
|
1191
|
-
failedAcs:
|
|
1300
|
+
failedAcs: fixableGaps.length ? fixableGaps.join("; ") : undefined,
|
|
1192
1301
|
promptContext: buildLoopContext(result),
|
|
1193
1302
|
};
|
|
1194
1303
|
const loopStartTime = new Date();
|
|
@@ -1356,7 +1465,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1356
1465
|
// warning and leave the issue at `success`. Recorded here and folded into the
|
|
1357
1466
|
// returned `success` below.
|
|
1358
1467
|
let prCreationError;
|
|
1359
|
-
const
|
|
1468
|
+
const wouldCreatePR = success && worktreePath && branch && !options.noPr;
|
|
1469
|
+
// #920: a phase-restricted run (e.g. `--phases spec`) provisions a worktree
|
|
1470
|
+
// and branch unconditionally of which phases ran, so a clean spec-only pass
|
|
1471
|
+
// satisfies every conjunct above with zero commits — `gh pr create` then
|
|
1472
|
+
// fails with "No commits between main and …" and the run reports failed
|
|
1473
|
+
// for work that landed exactly where it was supposed to (the plan comment).
|
|
1474
|
+
// Gate on the evidence (commits ahead of base) rather than the phase list:
|
|
1475
|
+
// `hasExecChanges` fails open (`unknown` counts as "has changes") so a git
|
|
1476
|
+
// error here falls through to today's attempt-PR behavior, and it resolves
|
|
1477
|
+
// the base the same #537-aware way `classifyExecChanges` already does for
|
|
1478
|
+
// the exec zero-diff guard.
|
|
1479
|
+
let prSkippedReason;
|
|
1480
|
+
if (wouldCreatePR && !hasExecChanges(worktreePath)) {
|
|
1481
|
+
prSkippedReason = `no commits ahead of ${baseBranch ?? "main"} (no implementing phase ran)`;
|
|
1482
|
+
log(chalk.gray(` ℹ️ PR skipped — ${prSkippedReason}`));
|
|
1483
|
+
}
|
|
1484
|
+
const shouldCreatePR = wouldCreatePR && !prSkippedReason;
|
|
1360
1485
|
if (shouldCreatePR) {
|
|
1361
1486
|
// #605: under --stacked, target predecessor branch (only for non-first,
|
|
1362
1487
|
// non-last issues). Last PR keeps `main` so partial progress can land.
|
|
@@ -1424,10 +1549,16 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1424
1549
|
prNumber,
|
|
1425
1550
|
prUrl,
|
|
1426
1551
|
prCreationError,
|
|
1552
|
+
prSkippedReason,
|
|
1427
1553
|
checkpointFailed,
|
|
1428
1554
|
failureCategory: overallSuccess
|
|
1429
1555
|
? undefined
|
|
1430
|
-
:
|
|
1556
|
+
: // #920: a PR-creation failure has no failed phase for
|
|
1557
|
+
// `deriveFailureCategory` to classify — fall back to the dedicated
|
|
1558
|
+
// category so the metrics residual from #879 (empty failureCategory
|
|
1559
|
+
// on a PR-only failure) doesn't reopen for this failure path.
|
|
1560
|
+
(deriveFailureCategory(phaseResults) ??
|
|
1561
|
+
(prCreationError ? "pr_creation" : undefined)),
|
|
1431
1562
|
// #817: present only when `--ready-gate` ran the gate; the summary renders
|
|
1432
1563
|
// its terminal reason (AC-6).
|
|
1433
1564
|
readyGate: readyGateResult,
|
|
@@ -68,6 +68,31 @@ export declare function resolveRunOptions(cliOptions: RunOptions, settings: Sequ
|
|
|
68
68
|
* ```
|
|
69
69
|
*/
|
|
70
70
|
export declare function positiveOr(value: number | undefined, fallback: number): number;
|
|
71
|
+
/** A single phase's resolved model/effort override (#914). */
|
|
72
|
+
export interface PhasePolicy {
|
|
73
|
+
model?: string;
|
|
74
|
+
effort?: string;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Parse a `--models`/`--efforts` CLI spec into a phase → value map.
|
|
78
|
+
*
|
|
79
|
+
* Grammar: a bare value (`"sonnet"`) applies to every phase and resolves to
|
|
80
|
+
* `{"*": "sonnet"}`; a comma list of `phase=value` pairs (`"spec=fable,exec=sonnet"`)
|
|
81
|
+
* resolves per phase. Mixing the two forms, an empty phase/value, or an
|
|
82
|
+
* unrecognized phase name all fail fast — this is the CLI boundary, so a
|
|
83
|
+
* malformed spec must never silently resolve to "nothing configured".
|
|
84
|
+
*/
|
|
85
|
+
export declare function parsePhaseSpec(spec: string, phaseNames: string[]): Record<string, string>;
|
|
86
|
+
/**
|
|
87
|
+
* Resolve per-phase model/effort policies with CLI > settings > absent
|
|
88
|
+
* precedence.
|
|
89
|
+
*
|
|
90
|
+
* This is the single resolver both `buildExecutionConfig` (here) and
|
|
91
|
+
* `ready-gate.ts:buildPhaseConfig` call, so they cannot drift the way the
|
|
92
|
+
* two `phaseTimeout` producers did in #833 — see `positiveOr`'s doc comment
|
|
93
|
+
* for that history.
|
|
94
|
+
*/
|
|
95
|
+
export declare function resolvePhasePolicies(cliModels: string | undefined, cliEfforts: string | undefined, settingsPhases: Record<string, PhasePolicy> | undefined, phaseNames: string[]): Record<string, PhasePolicy>;
|
|
71
96
|
/**
|
|
72
97
|
* Build an ExecutionConfig from merged RunOptions and settings.
|
|
73
98
|
* Extracts the phase-timeout, MCP, retry, and mode resolution logic
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import { DEFAULT_CONFIG, DEFAULT_PHASES, } from "./types.js";
|
|
10
10
|
import { getEnvConfig } from "./batch-executor.js";
|
|
11
|
+
import { getPhaseNames } from "./phase-registry.js";
|
|
11
12
|
/**
|
|
12
13
|
* Coerce an env-var string to the type of the default value.
|
|
13
14
|
* Returns the string as-is if no default exists for type inference.
|
|
@@ -164,6 +165,86 @@ export function positiveOr(value, fallback) {
|
|
|
164
165
|
? value
|
|
165
166
|
: fallback;
|
|
166
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Parse a `--models`/`--efforts` CLI spec into a phase → value map.
|
|
170
|
+
*
|
|
171
|
+
* Grammar: a bare value (`"sonnet"`) applies to every phase and resolves to
|
|
172
|
+
* `{"*": "sonnet"}`; a comma list of `phase=value` pairs (`"spec=fable,exec=sonnet"`)
|
|
173
|
+
* resolves per phase. Mixing the two forms, an empty phase/value, or an
|
|
174
|
+
* unrecognized phase name all fail fast — this is the CLI boundary, so a
|
|
175
|
+
* malformed spec must never silently resolve to "nothing configured".
|
|
176
|
+
*/
|
|
177
|
+
export function parsePhaseSpec(spec, phaseNames) {
|
|
178
|
+
const trimmed = spec.trim();
|
|
179
|
+
if (!trimmed) {
|
|
180
|
+
throw new Error("Malformed spec: value is empty.");
|
|
181
|
+
}
|
|
182
|
+
if (!trimmed.includes("=")) {
|
|
183
|
+
return { "*": trimmed };
|
|
184
|
+
}
|
|
185
|
+
const result = {};
|
|
186
|
+
for (const segment of trimmed.split(",")) {
|
|
187
|
+
const eq = segment.indexOf("=");
|
|
188
|
+
if (eq === -1) {
|
|
189
|
+
throw new Error(`Malformed spec segment '${segment}' — expected 'phase=value' (cannot mix a bare value with phase=value pairs).`);
|
|
190
|
+
}
|
|
191
|
+
const phase = segment.slice(0, eq).trim();
|
|
192
|
+
const value = segment.slice(eq + 1).trim();
|
|
193
|
+
if (!phase || !value) {
|
|
194
|
+
throw new Error(`Malformed spec segment '${segment}' — both phase and value are required.`);
|
|
195
|
+
}
|
|
196
|
+
if (!phaseNames.includes(phase)) {
|
|
197
|
+
throw new Error(`Unknown phase '${phase}'. Available phases: ${phaseNames.join(", ")}.`);
|
|
198
|
+
}
|
|
199
|
+
result[phase] = value;
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
/** Apply a parsed phase-spec map onto a policy accumulator for one field. */
|
|
204
|
+
function applyPhaseSpec(target, parsed, field, phaseNames) {
|
|
205
|
+
const wildcard = parsed["*"];
|
|
206
|
+
if (wildcard !== undefined) {
|
|
207
|
+
for (const phase of phaseNames) {
|
|
208
|
+
target[phase] = { ...target[phase], [field]: wildcard };
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
for (const [phase, value] of Object.entries(parsed)) {
|
|
213
|
+
target[phase] = { ...target[phase], [field]: value };
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* Resolve per-phase model/effort policies with CLI > settings > absent
|
|
218
|
+
* precedence.
|
|
219
|
+
*
|
|
220
|
+
* This is the single resolver both `buildExecutionConfig` (here) and
|
|
221
|
+
* `ready-gate.ts:buildPhaseConfig` call, so they cannot drift the way the
|
|
222
|
+
* two `phaseTimeout` producers did in #833 — see `positiveOr`'s doc comment
|
|
223
|
+
* for that history.
|
|
224
|
+
*/
|
|
225
|
+
export function resolvePhasePolicies(cliModels, cliEfforts, settingsPhases, phaseNames) {
|
|
226
|
+
const result = {};
|
|
227
|
+
// Layer 1 (lowest): settings.run.phases. Skip any phase name settings
|
|
228
|
+
// validation already didn't recognize — that's surfaced as a settings
|
|
229
|
+
// warning at load time (AC-1), not a resolver-time failure.
|
|
230
|
+
if (settingsPhases) {
|
|
231
|
+
for (const [phase, policy] of Object.entries(settingsPhases)) {
|
|
232
|
+
if (!phaseNames.includes(phase))
|
|
233
|
+
continue;
|
|
234
|
+
result[phase] = { ...policy };
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Layer 2 (highest): CLI --models/--efforts. Malformed specs throw here —
|
|
238
|
+
// callers at the CLI boundary (cli-flags.ts) turn that into a fail-fast
|
|
239
|
+
// InvalidArgumentError.
|
|
240
|
+
if (cliModels) {
|
|
241
|
+
applyPhaseSpec(result, parsePhaseSpec(cliModels, phaseNames), "model", phaseNames);
|
|
242
|
+
}
|
|
243
|
+
if (cliEfforts) {
|
|
244
|
+
applyPhaseSpec(result, parsePhaseSpec(cliEfforts, phaseNames), "effort", phaseNames);
|
|
245
|
+
}
|
|
246
|
+
return result;
|
|
247
|
+
}
|
|
167
248
|
/**
|
|
168
249
|
* Build an ExecutionConfig from merged RunOptions and settings.
|
|
169
250
|
* Extracts the phase-timeout, MCP, retry, and mode resolution logic
|
|
@@ -204,6 +285,7 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
204
285
|
maxIterations: positiveOr(mergedOptions.maxIterations, positiveOr(settings.run.maxIterations, DEFAULT_CONFIG.maxIterations)),
|
|
205
286
|
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
206
287
|
mcp: mcpEnabled,
|
|
288
|
+
mcpAllowlist: settings.run.mcpAllowlist,
|
|
207
289
|
retry: retryEnabled,
|
|
208
290
|
// #804: default 0 (off) — the whole regression contract for auto-wait is
|
|
209
291
|
// that an unset flag leaves the #761/#799 halt path untouched.
|
|
@@ -219,5 +301,13 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
219
301
|
// load-bearing wire the #795 inert-flag class guards against — the flag is
|
|
220
302
|
// useless if it stops reaching the executor here.
|
|
221
303
|
readyGate: mergedOptions.readyGate ?? false,
|
|
304
|
+
// #914: CLI > settings > absent, via the shared resolver both
|
|
305
|
+
// ExecutionConfig producers call (see `resolvePhasePolicies`'s doc
|
|
306
|
+
// comment for the #833 drift this guards against).
|
|
307
|
+
phasePolicies: resolvePhasePolicies(mergedOptions.models, mergedOptions.efforts, settings.run.phases, getPhaseNames()),
|
|
308
|
+
// #915: CLI > settings > default `false` — mirrors the `readyGate`
|
|
309
|
+
// precedent above. Both `ExecutionConfig` producers (here and
|
|
310
|
+
// `ready-gate.ts:buildPhaseConfig`) resolve this the same way (#833).
|
|
311
|
+
effortEscalation: mergedOptions.escalateEffort ?? settings.run.effortEscalation ?? false,
|
|
222
312
|
};
|
|
223
313
|
}
|
|
@@ -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
|
*
|
|
@@ -49,6 +56,21 @@ export interface AgentExecutionConfig {
|
|
|
49
56
|
onStderr?: (text: string) => void;
|
|
50
57
|
/** Relevant files for the phase (used by file-oriented drivers like Aider) */
|
|
51
58
|
files?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Claude model to use for this phase (#914). Forwarded verbatim to the
|
|
61
|
+
* Agent SDK `query()` options by ClaudeCodeDriver; ignored by drivers
|
|
62
|
+
* without a model concept (Aider uses its own `AiderSettings.model`).
|
|
63
|
+
* Absent by default — the SDK falls back to the CLI default model.
|
|
64
|
+
*/
|
|
65
|
+
model?: string;
|
|
66
|
+
/**
|
|
67
|
+
* Reasoning effort for this phase (#914). Forwarded verbatim to the Agent
|
|
68
|
+
* SDK `query()` options by ClaudeCodeDriver; ignored by drivers without an
|
|
69
|
+
* effort concept. Absent by default — the SDK defaults to `high`. Matches
|
|
70
|
+
* the SDK's own closed `EffortLevel` enum, not a bare `string`, so a value
|
|
71
|
+
* that reaches this field type-checks against `query()`'s options.
|
|
72
|
+
*/
|
|
73
|
+
effort?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
52
74
|
}
|
|
53
75
|
/**
|
|
54
76
|
* Result returned by an agent after executing a phase.
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* orchestration layer should import the SDK directly.
|
|
6
6
|
*/
|
|
7
7
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
-
import {
|
|
8
|
+
import { getPhaseMcpServersConfig } from "../../mcp-config.js";
|
|
9
9
|
import { RateLimitError, BillingError, createRateLimitError, isWaitableWindow, isRateLimitFailureInfo, } from "../../errors.js";
|
|
10
10
|
import { RingBuffer } from "../ring-buffer.js";
|
|
11
11
|
export class ClaudeCodeDriver {
|
|
@@ -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: {
|
|
@@ -96,6 +102,11 @@ export class ClaudeCodeDriver {
|
|
|
96
102
|
...(resumeToken ? { resume: resumeToken } : {}),
|
|
97
103
|
env: config.env,
|
|
98
104
|
...(mcpServers ? { mcpServers } : {}),
|
|
105
|
+
// #914: per-phase model/effort override. Omitted entirely when
|
|
106
|
+
// unset (not `undefined`-valued) so the SDK's own default
|
|
107
|
+
// resolution is untouched — see the AC-3 key-presence test.
|
|
108
|
+
...(config.model ? { model: config.model } : {}),
|
|
109
|
+
...(config.effort ? { effort: config.effort } : {}),
|
|
99
110
|
stderr: (data) => {
|
|
100
111
|
capturedStderr += data;
|
|
101
112
|
// Split on newlines and push each line to the ring buffer
|
|
@@ -0,0 +1,73 @@
|
|
|
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
|
+
import type { ExecutionConfig, Phase } from "./types.js";
|
|
18
|
+
/**
|
|
19
|
+
* Base effort assumed for a phase with no configured `effort` override, when
|
|
20
|
+
* escalation needs a starting point to step up from (AC-5). `phase-executor.ts`
|
|
21
|
+
* omits the `effort` key entirely in that case (#914) so the Agent SDK's own
|
|
22
|
+
* default applies — #914 deliberately never encoded what that default is,
|
|
23
|
+
* since "omitted" is not the same claim as "equals X".
|
|
24
|
+
*
|
|
25
|
+
* Verified against `@anthropic-ai/claude-agent-sdk`'s own `query()` Options
|
|
26
|
+
* type (`sdk.d.ts`): `effort?: EffortLevel` is documented inline as
|
|
27
|
+
* `'high' — Deep reasoning (default)`. That is the SDK's default for the
|
|
28
|
+
* exact call sequant makes (raw `query()`, not the Claude Code CLI product —
|
|
29
|
+
* whose own `xhigh` default is a caller choice on top of this SDK, not the
|
|
30
|
+
* SDK's own default), so this constant is not a guess.
|
|
31
|
+
*/
|
|
32
|
+
export declare const DEFAULT_ESCALATION_BASE: (typeof EFFORT_LEVELS)[number];
|
|
33
|
+
/**
|
|
34
|
+
* Pure ladder step: one tier above `base` on `EFFORT_LEVELS`, capped at the
|
|
35
|
+
* top (`max`). Returns `base` unchanged whenever `enabled` is false or this
|
|
36
|
+
* isn't a retry — the disabled/first-attempt path must be indistinguishable
|
|
37
|
+
* from #914 with escalation never having existed (AC-2).
|
|
38
|
+
*
|
|
39
|
+
* Always escalates from the phase's CONFIGURED base, never from a previously
|
|
40
|
+
* escalated value — callers must not accumulate escalation across iterations
|
|
41
|
+
* (AC-6): base `high` on the 3rd loop iteration is `xhigh`, not `max`.
|
|
42
|
+
*/
|
|
43
|
+
export declare function resolveEscalatedEffort(base: string | undefined, isRetry: boolean, enabled: boolean): string | undefined;
|
|
44
|
+
/** One escalated execution, for observability (run metrics + verbose output). */
|
|
45
|
+
export interface EscalationRecord {
|
|
46
|
+
phase: Phase;
|
|
47
|
+
base: string;
|
|
48
|
+
escalated: string;
|
|
49
|
+
}
|
|
50
|
+
export interface EscalationOutcome {
|
|
51
|
+
/**
|
|
52
|
+
* The config to dispatch with. Identical by reference to the input `config`
|
|
53
|
+
* whenever nothing escalated — so a shared `ExecutionConfig` object is never
|
|
54
|
+
* mutated and an escalation never leaks into a phase execution it wasn't
|
|
55
|
+
* computed for (AC-7).
|
|
56
|
+
*/
|
|
57
|
+
config: ExecutionConfig;
|
|
58
|
+
/** Present only when this dispatch actually escalated. */
|
|
59
|
+
record?: EscalationRecord;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Apply escalation to ONE phase's execution, for THIS dispatch only.
|
|
63
|
+
*
|
|
64
|
+
* This is deliberately a per-execution decision made at the dispatch site,
|
|
65
|
+
* not a value baked into `ExecutionConfig` at build time: `buildExecutionConfig`
|
|
66
|
+
* / `buildPhaseConfig` run once per run/gate, not once per phase execution, so
|
|
67
|
+
* a static escalated value would leak across every phase in the chain and
|
|
68
|
+
* violate AC-7. The three retry-dispatch sites (batch-executor.ts's quality
|
|
69
|
+
* loop, ready-gate.ts's QA-pass loop `qa`/`loop` dispatch) call this function
|
|
70
|
+
* — and only this function — so they cannot drift on the cap/one-tier rules
|
|
71
|
+
* in AC-6 (see resolveEscalatedEffort's doc comment).
|
|
72
|
+
*/
|
|
73
|
+
export declare function withEscalatedEffort(config: ExecutionConfig, phase: Phase, isRetry: boolean): EscalationOutcome;
|
|
@@ -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
|
+
}
|