sequant 2.10.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -2
- package/dist/bin/cli.js +47 -2
- package/dist/src/commands/locks.d.ts +20 -1
- package/dist/src/commands/locks.js +206 -4
- package/dist/src/commands/ready.d.ts +6 -0
- package/dist/src/commands/ready.js +15 -1
- package/dist/src/commands/run-display.js +1 -0
- package/dist/src/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- package/dist/src/lib/cli-flags.d.ts +23 -0
- package/dist/src/lib/cli-flags.js +43 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +2 -0
- package/dist/src/lib/cli-ui/run-renderer.js +7 -1
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +6 -3
- package/dist/src/lib/locks/index.js +4 -2
- package/dist/src/lib/locks/lock-manager.d.ts +81 -1
- package/dist/src/lib/locks/lock-manager.js +230 -5
- package/dist/src/lib/locks/types.d.ts +72 -0
- package/dist/src/lib/locks/types.js +28 -0
- package/dist/src/lib/settings.d.ts +73 -0
- package/dist/src/lib/settings.js +45 -0
- package/dist/src/lib/test-tautology-detector.d.ts +4 -3
- package/dist/src/lib/test-tautology-detector.js +101 -41
- package/dist/src/lib/workflow/batch-executor.js +78 -19
- package/dist/src/lib/workflow/config-resolver.d.ts +25 -0
- package/dist/src/lib/workflow/config-resolver.js +89 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +15 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +5 -0
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- package/dist/src/lib/workflow/log-writer.d.ts +10 -1
- package/dist/src/lib/workflow/log-writer.js +20 -0
- package/dist/src/lib/workflow/metrics-schema.d.ts +49 -6
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.js +10 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +28 -0
- package/dist/src/lib/workflow/ready-gate.js +24 -3
- package/dist/src/lib/workflow/run-log-schema.d.ts +55 -0
- package/dist/src/lib/workflow/run-log-schema.js +31 -1
- package/dist/src/lib/workflow/run-orchestrator.js +27 -0
- package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -0
- package/dist/src/lib/workflow/types.d.ts +64 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +9 -1
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/package.json +3 -2
- package/templates/hooks/pre-tool.sh +228 -0
- package/templates/scripts/cleanup-worktree.sh +36 -15
- package/templates/scripts/new-feature.sh +25 -19
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/assess/SKILL.md +5 -1
- package/templates/skills/exec/SKILL.md +61 -7
- package/templates/skills/fullsolve/SKILL.md +127 -21
- package/templates/skills/loop/SKILL.md +56 -11
- package/templates/skills/merger/SKILL.md +98 -10
- package/templates/skills/qa/SKILL.md +59 -6
- package/templates/skills/release/SKILL.md +79 -0
- package/templates/skills/spec/SKILL.md +31 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -42,6 +42,33 @@ export const QaVerdictSchema = z.enum([
|
|
|
42
42
|
"AC_NOT_MET",
|
|
43
43
|
"NEEDS_VERIFICATION",
|
|
44
44
|
]);
|
|
45
|
+
/**
|
|
46
|
+
* Source that produced the resolved spec→run phase recommendation (#921).
|
|
47
|
+
*
|
|
48
|
+
* Ordered by resolution priority: a durable structured marker in the spec's
|
|
49
|
+
* GitHub comment beats the same comment's prose section, which beats the
|
|
50
|
+
* spec agent's ephemeral chat text, which beats label-based guessing.
|
|
51
|
+
*/
|
|
52
|
+
export const SpecRecommendationSourceSchema = z.enum([
|
|
53
|
+
"marker",
|
|
54
|
+
"comment-prose",
|
|
55
|
+
"chat",
|
|
56
|
+
"label-fallback",
|
|
57
|
+
]);
|
|
58
|
+
/**
|
|
59
|
+
* Resolved spec→run phase recommendation, recorded on the issue log so
|
|
60
|
+
* fallback frequency is auditable (#921 AC-4). Additive/optional — absent on
|
|
61
|
+
* runs that never reached spec resolution (e.g. spec failed) or predate this
|
|
62
|
+
* field, keeping the persisted-log schema stable at `version: 1`.
|
|
63
|
+
*/
|
|
64
|
+
export const SpecRecommendationSchema = z.object({
|
|
65
|
+
/** Which step in the resolution chain produced this result */
|
|
66
|
+
source: SpecRecommendationSourceSchema,
|
|
67
|
+
/** Resolved phases, spec excluded (spec already ran) */
|
|
68
|
+
phases: z.array(PhaseSchema),
|
|
69
|
+
/** Whether the quality loop should be enabled */
|
|
70
|
+
qualityLoop: z.boolean(),
|
|
71
|
+
});
|
|
45
72
|
/**
|
|
46
73
|
* File diff statistics for a single file (AC-3)
|
|
47
74
|
*/
|
|
@@ -82,7 +109,7 @@ export const ErrorContextSchema = z.object({
|
|
|
82
109
|
/**
|
|
83
110
|
* Classified error category (legacy, kept for backwards compatibility).
|
|
84
111
|
* Keep in sync with `ERROR_CATEGORIES` in `error-classifier.ts` —
|
|
85
|
-
* `rate_limit` / `billing` added by #761 AC-6.
|
|
112
|
+
* `rate_limit` / `billing` added by #761 AC-6, `pr_creation` by #920.
|
|
86
113
|
*/
|
|
87
114
|
category: z.enum([
|
|
88
115
|
"context_overflow",
|
|
@@ -92,6 +119,7 @@ export const ErrorContextSchema = z.object({
|
|
|
92
119
|
"timeout",
|
|
93
120
|
"rate_limit",
|
|
94
121
|
"billing",
|
|
122
|
+
"pr_creation",
|
|
95
123
|
"unknown",
|
|
96
124
|
]),
|
|
97
125
|
/** Typed error class name (AC-8), e.g. "ApiError", "BuildError" */
|
|
@@ -198,6 +226,8 @@ export const IssueLogSchema = z.object({
|
|
|
198
226
|
prNumber: z.number().int().positive().optional(),
|
|
199
227
|
/** PR URL if created after successful QA */
|
|
200
228
|
prUrl: z.string().optional(),
|
|
229
|
+
/** How the spec→run phase recommendation was resolved (#921 AC-4) */
|
|
230
|
+
specRecommendation: SpecRecommendationSchema.optional(),
|
|
201
231
|
});
|
|
202
232
|
/**
|
|
203
233
|
* Run configuration
|
|
@@ -1207,6 +1207,29 @@ export class RunOrchestrator {
|
|
|
1207
1207
|
// issue is the halting one; in parallel runs it is the earliest-listed
|
|
1208
1208
|
// failure. Undefined on success — the field is omitted from the record.
|
|
1209
1209
|
const failureCategory = results.find((r) => !r.success && r.failureCategory)?.failureCategory;
|
|
1210
|
+
// #915: one entry per escalated phase execution, from both retry sites —
|
|
1211
|
+
// the outer quality loop (`phaseResults[].escalatedEffort`) and the
|
|
1212
|
+
// `--ready-gate` QA-pass loop (`readyGate.effortEscalations`).
|
|
1213
|
+
const effortEscalations = [
|
|
1214
|
+
...results.flatMap((r) => r.phaseResults
|
|
1215
|
+
.filter((p) => p.escalatedEffort)
|
|
1216
|
+
.map((p) => ({
|
|
1217
|
+
phase: p.phase,
|
|
1218
|
+
base: p.escalatedEffort.base,
|
|
1219
|
+
escalated: p.escalatedEffort.escalated,
|
|
1220
|
+
}))),
|
|
1221
|
+
...results.flatMap((r) => r.readyGate?.effortEscalations ?? []),
|
|
1222
|
+
];
|
|
1223
|
+
// #915: the outer quality loop already logs its own escalations live at
|
|
1224
|
+
// the batch-executor dispatch site — only the `--ready-gate` QA-pass loop
|
|
1225
|
+
// has no equivalent live print, so surface those here.
|
|
1226
|
+
if (config.verbose) {
|
|
1227
|
+
for (const r of results) {
|
|
1228
|
+
for (const e of r.readyGate?.effortEscalations ?? []) {
|
|
1229
|
+
console.log(chalk.gray(` effort: ${e.base} → ${e.escalated} (ready-gate retry, #${r.issueNumber})`));
|
|
1230
|
+
}
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1210
1233
|
await metricsWriter.recordRun({
|
|
1211
1234
|
issues: issueNumbers,
|
|
1212
1235
|
phases: Array.from(allPhases),
|
|
@@ -1215,6 +1238,10 @@ export class RunOrchestrator {
|
|
|
1215
1238
|
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
1216
1239
|
flags: cliFlags,
|
|
1217
1240
|
failureCategory,
|
|
1241
|
+
// #914: resolved per-phase model/effort, when any phase had one.
|
|
1242
|
+
phasePolicies: config.phasePolicies,
|
|
1243
|
+
// #915: escalated tiers, when any phase execution escalated.
|
|
1244
|
+
effortEscalations,
|
|
1218
1245
|
metrics: {
|
|
1219
1246
|
tokensUsed: tokenUsage.tokensUsed,
|
|
1220
1247
|
filesChanged: totalFilesChanged,
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured resolution of the spec→run phase recommendation (#921).
|
|
3
|
+
*
|
|
4
|
+
* `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
|
|
5
|
+
* *ephemeral chat text* for a `## Recommended Workflow` section. When the
|
|
6
|
+
* spec agent posts its plan via a body file instead of restating it in
|
|
7
|
+
* chat, that regex has nothing to match and the run silently falls back to
|
|
8
|
+
* label-based phase detection — dropping any recommended phase the label
|
|
9
|
+
* fallback can never produce (e.g. `testgen`). See #814.
|
|
10
|
+
*
|
|
11
|
+
* This module resolves the recommendation through an ordered chain, each
|
|
12
|
+
* step falling through loudly to the next on failure:
|
|
13
|
+
*
|
|
14
|
+
* 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
|
|
15
|
+
* comment marker (this issue's fix)
|
|
16
|
+
* 2. `comment-prose` — the same regex as `chat`, applied to the spec
|
|
17
|
+
* plan's GitHub comment body instead of chat text
|
|
18
|
+
* 3. `chat` — `parseRecommendedWorkflow` over the agent's
|
|
19
|
+
* captured chat output (existing behavior)
|
|
20
|
+
* 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
|
|
21
|
+
*
|
|
22
|
+
* The durable comment marker is the system's existing idiom — see
|
|
23
|
+
* `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
|
|
24
|
+
* `assess-comment-parser.ts`.
|
|
25
|
+
*/
|
|
26
|
+
import type { Phase } from "./types.js";
|
|
27
|
+
import { GitHubProvider } from "./platforms/github.js";
|
|
28
|
+
import type { SpecRecommendationSource } from "./run-log-schema.js";
|
|
29
|
+
export interface ResolvedSpecRecommendation {
|
|
30
|
+
phases: Phase[];
|
|
31
|
+
qualityLoop: boolean;
|
|
32
|
+
source: SpecRecommendationSource;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Extract and validate the latest `SEQUANT_SPEC` marker across a set of
|
|
36
|
+
* comment bodies (oldest-to-newest order, matching `gh`'s natural order).
|
|
37
|
+
*
|
|
38
|
+
* Returns `null` when no marker is present at all. Returns `null` and logs a
|
|
39
|
+
* visible warning when the latest marker is malformed JSON, fails schema
|
|
40
|
+
* validation, or names a phase the registry doesn't recognize — per AC-2,
|
|
41
|
+
* an invalid marker falls through to the next resolution step rather than
|
|
42
|
+
* silently dropping the recommendation or silently ignoring the bad phase.
|
|
43
|
+
*
|
|
44
|
+
* Only the *latest* marker is considered (last comment, last match within
|
|
45
|
+
* it) — a re-spec's marker supersedes an earlier one, matching how a human
|
|
46
|
+
* reader would interpret the thread.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseSpecMarker(commentBodies: string[]): {
|
|
49
|
+
phases: Phase[];
|
|
50
|
+
qualityLoop: boolean;
|
|
51
|
+
} | null;
|
|
52
|
+
export interface ResolveSpecRecommendationInput {
|
|
53
|
+
/** The spec agent's captured chat output (existing `chat` fallback). */
|
|
54
|
+
chatOutput: string;
|
|
55
|
+
/** GitHub issue number, used to fetch its comments for the marker/prose steps. */
|
|
56
|
+
issueNumber: number;
|
|
57
|
+
/** Issue labels, used for the final label-based fallback. */
|
|
58
|
+
labels: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Injectable for tests — defaults to a real `GitHubProvider`. Comment
|
|
61
|
+
* fetch failures are already handled by `fetchIssueCommentBodiesSync`
|
|
62
|
+
* (returns `[]`), so the chain degrades to `chat`/`label-fallback`
|
|
63
|
+
* gracefully when GitHub is unreachable.
|
|
64
|
+
*/
|
|
65
|
+
githubProvider?: Pick<GitHubProvider, "fetchIssueCommentBodiesSync">;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the spec→run phase recommendation through the ordered chain:
|
|
69
|
+
* comment-marker → comment-prose → chat-text → label-fallback.
|
|
70
|
+
*/
|
|
71
|
+
export declare function resolveSpecRecommendation(input: ResolveSpecRecommendationInput): ResolvedSpecRecommendation;
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structured resolution of the spec→run phase recommendation (#921).
|
|
3
|
+
*
|
|
4
|
+
* `parseRecommendedWorkflow` (phase-mapper.ts) regexes the spec agent's
|
|
5
|
+
* *ephemeral chat text* for a `## Recommended Workflow` section. When the
|
|
6
|
+
* spec agent posts its plan via a body file instead of restating it in
|
|
7
|
+
* chat, that regex has nothing to match and the run silently falls back to
|
|
8
|
+
* label-based phase detection — dropping any recommended phase the label
|
|
9
|
+
* fallback can never produce (e.g. `testgen`). See #814.
|
|
10
|
+
*
|
|
11
|
+
* This module resolves the recommendation through an ordered chain, each
|
|
12
|
+
* step falling through loudly to the next on failure:
|
|
13
|
+
*
|
|
14
|
+
* 1. `marker` — the durable `<!-- SEQUANT_SPEC: {json} --> `
|
|
15
|
+
* comment marker (this issue's fix)
|
|
16
|
+
* 2. `comment-prose` — the same regex as `chat`, applied to the spec
|
|
17
|
+
* plan's GitHub comment body instead of chat text
|
|
18
|
+
* 3. `chat` — `parseRecommendedWorkflow` over the agent's
|
|
19
|
+
* captured chat output (existing behavior)
|
|
20
|
+
* 4. `label-fallback` — `detectPhasesFromLabels` (existing behavior)
|
|
21
|
+
*
|
|
22
|
+
* The durable comment marker is the system's existing idiom — see
|
|
23
|
+
* `SEQUANT_PHASE` in `phase-detection.ts` and the `/assess` HTML markers in
|
|
24
|
+
* `assess-comment-parser.ts`.
|
|
25
|
+
*/
|
|
26
|
+
import chalk from "chalk";
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { phaseRegistry } from "./phase-registry.js";
|
|
29
|
+
import { GitHubProvider } from "./platforms/github.js";
|
|
30
|
+
import { stripMarkdownCode } from "./phase-detection.js";
|
|
31
|
+
import { parseRecommendedWorkflow, detectPhasesFromLabels, } from "./phase-mapper.js";
|
|
32
|
+
/** Regex to extract the SEQUANT_SPEC marker JSON from an HTML comment. */
|
|
33
|
+
const SPEC_MARKER_REGEX = /<!-- SEQUANT_SPEC: (\{[^}]+\}) -->/g;
|
|
34
|
+
/**
|
|
35
|
+
* Structural shape of the marker JSON, before phase names are checked
|
|
36
|
+
* against the phase registry. `qualityLoop` defaults to `false` when
|
|
37
|
+
* omitted, matching `parseRecommendedWorkflow`'s prose-parsing default.
|
|
38
|
+
*/
|
|
39
|
+
const SpecMarkerJsonSchema = z.object({
|
|
40
|
+
phases: z.array(z.string()).min(1),
|
|
41
|
+
qualityLoop: z.boolean().optional().default(false),
|
|
42
|
+
});
|
|
43
|
+
/**
|
|
44
|
+
* Extract and validate the latest `SEQUANT_SPEC` marker across a set of
|
|
45
|
+
* comment bodies (oldest-to-newest order, matching `gh`'s natural order).
|
|
46
|
+
*
|
|
47
|
+
* Returns `null` when no marker is present at all. Returns `null` and logs a
|
|
48
|
+
* visible warning when the latest marker is malformed JSON, fails schema
|
|
49
|
+
* validation, or names a phase the registry doesn't recognize — per AC-2,
|
|
50
|
+
* an invalid marker falls through to the next resolution step rather than
|
|
51
|
+
* silently dropping the recommendation or silently ignoring the bad phase.
|
|
52
|
+
*
|
|
53
|
+
* Only the *latest* marker is considered (last comment, last match within
|
|
54
|
+
* it) — a re-spec's marker supersedes an earlier one, matching how a human
|
|
55
|
+
* reader would interpret the thread.
|
|
56
|
+
*/
|
|
57
|
+
export function parseSpecMarker(commentBodies) {
|
|
58
|
+
let latestRaw = null;
|
|
59
|
+
for (const body of commentBodies) {
|
|
60
|
+
const stripped = stripMarkdownCode(body);
|
|
61
|
+
SPEC_MARKER_REGEX.lastIndex = 0;
|
|
62
|
+
let match;
|
|
63
|
+
while ((match = SPEC_MARKER_REGEX.exec(stripped)) !== null) {
|
|
64
|
+
latestRaw = match[1];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (latestRaw === null) {
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
let parsedJson;
|
|
71
|
+
try {
|
|
72
|
+
parsedJson = JSON.parse(latestRaw);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker is not valid JSON — falling through to comment prose`));
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
const result = SpecMarkerJsonSchema.safeParse(parsedJson);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker failed schema validation — falling through to comment prose`));
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
const unknownPhases = result.data.phases.filter((name) => !phaseRegistry.has(name));
|
|
84
|
+
if (unknownPhases.length > 0) {
|
|
85
|
+
console.log(chalk.yellow(` ⚠ SEQUANT_SPEC marker names unknown phase(s): ${unknownPhases.join(", ")} — falling through to comment prose`));
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
return { phases: result.data.phases, qualityLoop: result.data.qualityLoop };
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Find the most recently posted comment containing a `## Recommended
|
|
92
|
+
* Workflow` section and parse it with the existing prose regex.
|
|
93
|
+
*
|
|
94
|
+
* Scans newest-to-oldest so a later spec re-run's comment takes precedence
|
|
95
|
+
* over an earlier one — same intuition as the marker step.
|
|
96
|
+
*/
|
|
97
|
+
function parseCommentProse(commentBodies) {
|
|
98
|
+
for (let i = commentBodies.length - 1; i >= 0; i--) {
|
|
99
|
+
const parsed = parseRecommendedWorkflow(commentBodies[i]);
|
|
100
|
+
if (parsed) {
|
|
101
|
+
return parsed;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Resolve the spec→run phase recommendation through the ordered chain:
|
|
108
|
+
* comment-marker → comment-prose → chat-text → label-fallback.
|
|
109
|
+
*/
|
|
110
|
+
export function resolveSpecRecommendation(input) {
|
|
111
|
+
const github = input.githubProvider ?? new GitHubProvider();
|
|
112
|
+
const commentBodies = github.fetchIssueCommentBodiesSync(String(input.issueNumber));
|
|
113
|
+
let phases;
|
|
114
|
+
let qualityLoop;
|
|
115
|
+
let source;
|
|
116
|
+
const marker = parseSpecMarker(commentBodies);
|
|
117
|
+
const prose = marker ? null : parseCommentProse(commentBodies);
|
|
118
|
+
const chat = marker || prose || !input.chatOutput
|
|
119
|
+
? null
|
|
120
|
+
: parseRecommendedWorkflow(input.chatOutput);
|
|
121
|
+
if (marker) {
|
|
122
|
+
({ phases, qualityLoop } = marker);
|
|
123
|
+
source = "marker";
|
|
124
|
+
}
|
|
125
|
+
else if (prose) {
|
|
126
|
+
({ phases, qualityLoop } = prose);
|
|
127
|
+
source = "comment-prose";
|
|
128
|
+
}
|
|
129
|
+
else if (chat) {
|
|
130
|
+
({ phases, qualityLoop } = chat);
|
|
131
|
+
source = "chat";
|
|
132
|
+
}
|
|
133
|
+
else {
|
|
134
|
+
const fallback = detectPhasesFromLabels(input.labels);
|
|
135
|
+
phases = fallback.phases;
|
|
136
|
+
qualityLoop = fallback.qualityLoop;
|
|
137
|
+
source = "label-fallback";
|
|
138
|
+
}
|
|
139
|
+
// Spec already ran by the time this resolves — never re-include it,
|
|
140
|
+
// regardless of which step in the chain produced the result.
|
|
141
|
+
return { phases: phases.filter((p) => p !== "spec"), qualityLoop, source };
|
|
142
|
+
}
|
|
@@ -182,6 +182,28 @@ export interface ExecutionConfig {
|
|
|
182
182
|
* see {@link AutoWaitLedger}.
|
|
183
183
|
*/
|
|
184
184
|
autoWaitMinutes?: number;
|
|
185
|
+
/**
|
|
186
|
+
* Resolved per-phase `model`/`effort` overrides (#914), keyed by phase
|
|
187
|
+
* name. Merged from `settings.run.phases` and the CLI's `--models`/
|
|
188
|
+
* `--efforts` flags via `resolvePhasePolicies` (CLI > settings > absent) —
|
|
189
|
+
* see `config-resolver.ts`. Absent/empty by default: `phase-executor.ts`
|
|
190
|
+
* only sets `AgentExecutionConfig.model`/`.effort` when a phase has an
|
|
191
|
+
* entry here, so an unconfigured run reaches the SDK unchanged.
|
|
192
|
+
*/
|
|
193
|
+
phasePolicies?: Record<string, {
|
|
194
|
+
model?: string;
|
|
195
|
+
effort?: string;
|
|
196
|
+
}>;
|
|
197
|
+
/**
|
|
198
|
+
* Evidence-based effort escalation on quality-loop retries (#915). CLI >
|
|
199
|
+
* settings > absent (`false`), resolved by `buildExecutionConfig`
|
|
200
|
+
* (`config-resolver.ts`) and `ready-gate.ts`'s `buildPhaseConfig` — the same
|
|
201
|
+
* two producers `phasePolicies` uses, so this cannot drift from that one
|
|
202
|
+
* (#833 class). Consumed only at dispatch time by
|
|
203
|
+
* `effort-escalation.ts`'s `withEscalatedEffort`, never baked statically
|
|
204
|
+
* into `phasePolicies` here — escalation is per-execution, not per-run.
|
|
205
|
+
*/
|
|
206
|
+
effortEscalation?: boolean;
|
|
185
207
|
}
|
|
186
208
|
/**
|
|
187
209
|
* A single liveness notice emitted during an auto-wait (#804 AC-7).
|
|
@@ -246,6 +268,16 @@ export interface PhaseResult {
|
|
|
246
268
|
stdoutTail?: string[];
|
|
247
269
|
/** Process exit code from the agent driver (#447) */
|
|
248
270
|
exitCode?: number;
|
|
271
|
+
/**
|
|
272
|
+
* Set when this execution's effort was escalated one tier above its
|
|
273
|
+
* resolved base (#915) — a quality-loop retry with `effortEscalation`
|
|
274
|
+
* enabled. Additive/optional, same shape as `capped?`/`structuredError?`;
|
|
275
|
+
* absent on every non-escalated execution.
|
|
276
|
+
*/
|
|
277
|
+
escalatedEffort?: {
|
|
278
|
+
base: string;
|
|
279
|
+
escalated: string;
|
|
280
|
+
};
|
|
249
281
|
}
|
|
250
282
|
/**
|
|
251
283
|
* Result of executing all phases for an issue
|
|
@@ -269,6 +301,15 @@ export interface IssueResult {
|
|
|
269
301
|
* with this string as the reason.
|
|
270
302
|
*/
|
|
271
303
|
prCreationError?: string;
|
|
304
|
+
/**
|
|
305
|
+
* Set when PR creation was skipped because the branch carried zero commits
|
|
306
|
+
* ahead of its base (#920) — a phase-restricted run (e.g. `--phases spec`)
|
|
307
|
+
* that implemented nothing has no deliverable to open a PR for, and
|
|
308
|
+
* attempting one would only fail with GitHub's "No commits between main and
|
|
309
|
+
* …" error. Distinct from {@link IssueResult.prCreationError}: this is not
|
|
310
|
+
* a failure — `success` stays whatever the phases produced.
|
|
311
|
+
*/
|
|
312
|
+
prSkippedReason?: string;
|
|
272
313
|
/**
|
|
273
314
|
* Set when the issue was skipped because another sequant session holds
|
|
274
315
|
* the per-issue lock (#625). Surfaced in the summary as
|
|
@@ -486,6 +527,29 @@ export interface RunOptions {
|
|
|
486
527
|
* and bounds wholesale — no new settings accompany this flag.
|
|
487
528
|
*/
|
|
488
529
|
readyGate?: boolean;
|
|
530
|
+
/**
|
|
531
|
+
* Per-phase model override (#914). Either a bare value applied to every
|
|
532
|
+
* phase (`--models sonnet`) or a comma list of `phase=model` pairs
|
|
533
|
+
* (`--models spec=fable,exec=sonnet`). Parsed and merged with
|
|
534
|
+
* `settings.run.phases` by `resolvePhasePolicies` (CLI > settings >
|
|
535
|
+
* absent) into `ExecutionConfig.phasePolicies`. Malformed specs fail fast
|
|
536
|
+
* at the Commander option boundary via `cli-flags.ts`.
|
|
537
|
+
*/
|
|
538
|
+
models?: string;
|
|
539
|
+
/**
|
|
540
|
+
* Per-phase reasoning-effort override (#914). Same grammar as {@link
|
|
541
|
+
* models} (bare value or comma list of `phase=effort` pairs); each value
|
|
542
|
+
* validates against the SDK's closed `low|medium|high|xhigh|max` enum.
|
|
543
|
+
*/
|
|
544
|
+
efforts?: string;
|
|
545
|
+
/**
|
|
546
|
+
* Evidence-based effort escalation on quality-loop retries (#915). Set via
|
|
547
|
+
* `--escalate-effort`. When true, a retried phase execution resolves one
|
|
548
|
+
* effort tier above its configured/inherited base for that execution only
|
|
549
|
+
* — see `effort-escalation.ts`. Default `false`: escalation raises token
|
|
550
|
+
* spend, so an unset flag leaves every run byte-identical to pre-#915.
|
|
551
|
+
*/
|
|
552
|
+
escalateEffort?: boolean;
|
|
489
553
|
}
|
|
490
554
|
/**
|
|
491
555
|
* CLI arguments for run command
|
|
@@ -104,8 +104,15 @@ export declare function checkWorktreeFreshness(worktreePath: string, verbose: bo
|
|
|
104
104
|
export declare function removeStaleWorktree(existingPath: string, branch: string, verbose: boolean): boolean;
|
|
105
105
|
/**
|
|
106
106
|
* List all active worktrees with their branches
|
|
107
|
+
*
|
|
108
|
+
* `git worktree list` only ever reports worktrees belonging to the repository
|
|
109
|
+
* containing `cwd`, which is what makes this a repo-scoped lookup: a sibling
|
|
110
|
+
* project's worktree can never appear here, even though `../worktrees/` is a
|
|
111
|
+
* single directory shared by every repo under the same parent (#899).
|
|
112
|
+
*
|
|
113
|
+
* @param cwd - Directory to run git in. Defaults to the current process cwd.
|
|
107
114
|
*/
|
|
108
|
-
export declare function listWorktrees(): Array<{
|
|
115
|
+
export declare function listWorktrees(cwd?: string): Array<{
|
|
109
116
|
path: string;
|
|
110
117
|
branch: string;
|
|
111
118
|
issue: number | null;
|
|
@@ -205,10 +205,18 @@ export function removeStaleWorktree(existingPath, branch, verbose) {
|
|
|
205
205
|
}
|
|
206
206
|
/**
|
|
207
207
|
* List all active worktrees with their branches
|
|
208
|
+
*
|
|
209
|
+
* `git worktree list` only ever reports worktrees belonging to the repository
|
|
210
|
+
* containing `cwd`, which is what makes this a repo-scoped lookup: a sibling
|
|
211
|
+
* project's worktree can never appear here, even though `../worktrees/` is a
|
|
212
|
+
* single directory shared by every repo under the same parent (#899).
|
|
213
|
+
*
|
|
214
|
+
* @param cwd - Directory to run git in. Defaults to the current process cwd.
|
|
208
215
|
*/
|
|
209
|
-
export function listWorktrees() {
|
|
216
|
+
export function listWorktrees(cwd) {
|
|
210
217
|
const result = spawnSync("git", ["worktree", "list", "--porcelain"], {
|
|
211
218
|
stdio: "pipe",
|
|
219
|
+
...(cwd ? { cwd } : {}),
|
|
212
220
|
});
|
|
213
221
|
if (result.status !== 0)
|
|
214
222
|
return [];
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-scoped worktree resolution and verification (#899).
|
|
3
|
+
*
|
|
4
|
+
* Skill-driven `/fullsolve` used to hand `/exec` an unexpanded glob
|
|
5
|
+
* (`../worktrees/feature/<issue>-` followed by a star and a slash) and `/exec`
|
|
6
|
+
* was instructed to trust it without checking. Two things went wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. When nothing had created the worktree, the `cd` failed and the agent
|
|
9
|
+
* silently kept working in the main checkout.
|
|
10
|
+
* 2. `../worktrees/` is one flat namespace shared by every repo under the same
|
|
11
|
+
* parent directory, and issue numbers are per-repo — so where the glob *did*
|
|
12
|
+
* match, it could match a sibling project's worktree.
|
|
13
|
+
*
|
|
14
|
+
* Both are fixed the same way: never resolve worktrees through the filesystem.
|
|
15
|
+
* `git worktree list` reports only the worktrees of the repository containing
|
|
16
|
+
* `cwd`, so scoping is structural rather than a filter that can be forgotten,
|
|
17
|
+
* and it reports the *branch*, which is the real identity — a worktree
|
|
18
|
+
* directory slug can drift from its branch after a rename.
|
|
19
|
+
*/
|
|
20
|
+
/** Named failure modes for `resolveIssueWorktree`. */
|
|
21
|
+
export type ResolveErrorCode = "WORKTREE_NOT_FOUND" | "WORKTREE_AMBIGUOUS";
|
|
22
|
+
/** Named failure modes for `verifyWorktreePath`. */
|
|
23
|
+
export type VerifyErrorCode = "SEQUANT_WORKTREE_NOT_FOUND" | "SEQUANT_WORKTREE_FOREIGN" | "SEQUANT_WORKTREE_ISSUE_MISMATCH";
|
|
24
|
+
/** A worktree of the current repository, as reported by git. */
|
|
25
|
+
export interface ResolvedWorktree {
|
|
26
|
+
/** Absolute path, exactly as git reports it. */
|
|
27
|
+
path: string;
|
|
28
|
+
/** Branch checked out there, e.g. `feature/899-some-slug`. */
|
|
29
|
+
branch: string;
|
|
30
|
+
}
|
|
31
|
+
export type ResolveResult = ({
|
|
32
|
+
ok: true;
|
|
33
|
+
} & ResolvedWorktree) | {
|
|
34
|
+
ok: false;
|
|
35
|
+
error: ResolveErrorCode;
|
|
36
|
+
message: string;
|
|
37
|
+
/** Populated for WORKTREE_AMBIGUOUS so the caller can report candidates. */
|
|
38
|
+
candidates: ResolvedWorktree[];
|
|
39
|
+
};
|
|
40
|
+
export type VerifyResult = ({
|
|
41
|
+
ok: true;
|
|
42
|
+
} & ResolvedWorktree) | {
|
|
43
|
+
ok: false;
|
|
44
|
+
error: VerifyErrorCode;
|
|
45
|
+
message: string;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Find the worktree of the *current* repository that holds issue `issue`.
|
|
49
|
+
*
|
|
50
|
+
* Selection keys on the branch git reports, never on the directory name, so a
|
|
51
|
+
* worktree whose slug has drifted from its branch still resolves — and a
|
|
52
|
+
* directory that merely looks like a match (a sibling repo's worktree, or a
|
|
53
|
+
* stray directory) never does.
|
|
54
|
+
*
|
|
55
|
+
* @param issue - Issue number to look up.
|
|
56
|
+
* @param cwd - Directory inside the repository to search. Defaults to process cwd.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveIssueWorktree(issue: number, cwd?: string): ResolveResult;
|
|
59
|
+
/**
|
|
60
|
+
* Verify that a caller-supplied worktree path is safe to work in.
|
|
61
|
+
*
|
|
62
|
+
* This is the guard `/exec` runs before trusting `SEQUANT_WORKTREE`. It fails
|
|
63
|
+
* closed on every uncertain case — a bad path must halt the run, never degrade
|
|
64
|
+
* into "keep going in whatever directory we happen to be in".
|
|
65
|
+
*
|
|
66
|
+
* @param worktreePath - The path to check (typically `$SEQUANT_WORKTREE`).
|
|
67
|
+
* @param options.issue - When set, the branch must belong to this issue.
|
|
68
|
+
* @param options.cwd - Directory inside the repository to check against.
|
|
69
|
+
*/
|
|
70
|
+
export declare function verifyWorktreePath(worktreePath: string, options?: {
|
|
71
|
+
issue?: number;
|
|
72
|
+
cwd?: string;
|
|
73
|
+
}): VerifyResult;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Repo-scoped worktree resolution and verification (#899).
|
|
3
|
+
*
|
|
4
|
+
* Skill-driven `/fullsolve` used to hand `/exec` an unexpanded glob
|
|
5
|
+
* (`../worktrees/feature/<issue>-` followed by a star and a slash) and `/exec`
|
|
6
|
+
* was instructed to trust it without checking. Two things went wrong:
|
|
7
|
+
*
|
|
8
|
+
* 1. When nothing had created the worktree, the `cd` failed and the agent
|
|
9
|
+
* silently kept working in the main checkout.
|
|
10
|
+
* 2. `../worktrees/` is one flat namespace shared by every repo under the same
|
|
11
|
+
* parent directory, and issue numbers are per-repo — so where the glob *did*
|
|
12
|
+
* match, it could match a sibling project's worktree.
|
|
13
|
+
*
|
|
14
|
+
* Both are fixed the same way: never resolve worktrees through the filesystem.
|
|
15
|
+
* `git worktree list` reports only the worktrees of the repository containing
|
|
16
|
+
* `cwd`, so scoping is structural rather than a filter that can be forgotten,
|
|
17
|
+
* and it reports the *branch*, which is the real identity — a worktree
|
|
18
|
+
* directory slug can drift from its branch after a rename.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, realpathSync, statSync } from "fs";
|
|
21
|
+
import path from "path";
|
|
22
|
+
import { listWorktrees } from "./worktree-manager.js";
|
|
23
|
+
/**
|
|
24
|
+
* Find the worktree of the *current* repository that holds issue `issue`.
|
|
25
|
+
*
|
|
26
|
+
* Selection keys on the branch git reports, never on the directory name, so a
|
|
27
|
+
* worktree whose slug has drifted from its branch still resolves — and a
|
|
28
|
+
* directory that merely looks like a match (a sibling repo's worktree, or a
|
|
29
|
+
* stray directory) never does.
|
|
30
|
+
*
|
|
31
|
+
* @param issue - Issue number to look up.
|
|
32
|
+
* @param cwd - Directory inside the repository to search. Defaults to process cwd.
|
|
33
|
+
*/
|
|
34
|
+
export function resolveIssueWorktree(issue, cwd) {
|
|
35
|
+
const matches = listWorktrees(cwd)
|
|
36
|
+
.filter((w) => w.issue === issue)
|
|
37
|
+
.map(({ path: p, branch }) => ({ path: p, branch }));
|
|
38
|
+
if (matches.length === 0) {
|
|
39
|
+
return {
|
|
40
|
+
ok: false,
|
|
41
|
+
error: "WORKTREE_NOT_FOUND",
|
|
42
|
+
message: `WORKTREE_NOT_FOUND: no worktree of this repository has a branch for issue #${issue}. ` +
|
|
43
|
+
`Create one with \`./scripts/new-feature.sh ${issue}\`.`,
|
|
44
|
+
candidates: [],
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
if (matches.length > 1) {
|
|
48
|
+
const list = matches.map((m) => `${m.branch} -> ${m.path}`).join("; ");
|
|
49
|
+
return {
|
|
50
|
+
ok: false,
|
|
51
|
+
error: "WORKTREE_AMBIGUOUS",
|
|
52
|
+
message: `WORKTREE_AMBIGUOUS: ${matches.length} worktrees claim issue #${issue} (${list}). ` +
|
|
53
|
+
`Remove the stale one, or set SEQUANT_WORKTREE explicitly.`,
|
|
54
|
+
candidates: matches,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: true, ...matches[0] };
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Canonicalize a path for comparison, tolerating paths that do not exist.
|
|
61
|
+
*
|
|
62
|
+
* `realpathSync` throws on a missing path, so fall back to resolving the
|
|
63
|
+
* deepest existing ancestor and re-appending the remainder — enough to make
|
|
64
|
+
* `/tmp/...` and `/private/tmp/...` compare equal on macOS.
|
|
65
|
+
*/
|
|
66
|
+
function canonicalize(target) {
|
|
67
|
+
const absolute = path.resolve(target);
|
|
68
|
+
try {
|
|
69
|
+
return realpathSync(absolute);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
const parent = path.dirname(absolute);
|
|
73
|
+
if (parent === absolute)
|
|
74
|
+
return absolute;
|
|
75
|
+
return path.join(canonicalize(parent), path.basename(absolute));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Verify that a caller-supplied worktree path is safe to work in.
|
|
80
|
+
*
|
|
81
|
+
* This is the guard `/exec` runs before trusting `SEQUANT_WORKTREE`. It fails
|
|
82
|
+
* closed on every uncertain case — a bad path must halt the run, never degrade
|
|
83
|
+
* into "keep going in whatever directory we happen to be in".
|
|
84
|
+
*
|
|
85
|
+
* @param worktreePath - The path to check (typically `$SEQUANT_WORKTREE`).
|
|
86
|
+
* @param options.issue - When set, the branch must belong to this issue.
|
|
87
|
+
* @param options.cwd - Directory inside the repository to check against.
|
|
88
|
+
*/
|
|
89
|
+
export function verifyWorktreePath(worktreePath, options = {}) {
|
|
90
|
+
const raw = worktreePath.trim();
|
|
91
|
+
if (raw.length === 0 || raw.includes("*")) {
|
|
92
|
+
return {
|
|
93
|
+
ok: false,
|
|
94
|
+
error: "SEQUANT_WORKTREE_NOT_FOUND",
|
|
95
|
+
message: `SEQUANT_WORKTREE_NOT_FOUND: ${raw.length === 0 ? "empty path" : `unexpanded glob "${raw}"`}. Expected a resolved absolute directory.`,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
const absolute = path.resolve(raw);
|
|
99
|
+
if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
|
|
100
|
+
return {
|
|
101
|
+
ok: false,
|
|
102
|
+
error: "SEQUANT_WORKTREE_NOT_FOUND",
|
|
103
|
+
message: `SEQUANT_WORKTREE_NOT_FOUND: "${raw}" is not an existing directory.`,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
const canonical = canonicalize(absolute);
|
|
107
|
+
const known = listWorktrees(options.cwd);
|
|
108
|
+
const match = known.find((w) => canonicalize(w.path) === canonical);
|
|
109
|
+
if (!match) {
|
|
110
|
+
return {
|
|
111
|
+
ok: false,
|
|
112
|
+
error: "SEQUANT_WORKTREE_FOREIGN",
|
|
113
|
+
message: `SEQUANT_WORKTREE_FOREIGN: "${raw}" is not a worktree of this repository. ` +
|
|
114
|
+
`It belongs to another project or is stale; \`../worktrees/\` is shared across repos.`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
if (options.issue !== undefined && match.issue !== options.issue) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: "SEQUANT_WORKTREE_ISSUE_MISMATCH",
|
|
121
|
+
message: `SEQUANT_WORKTREE_ISSUE_MISMATCH: "${raw}" has branch "${match.branch}", ` +
|
|
122
|
+
`which is not issue #${options.issue}.`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
return { ok: true, path: match.path, branch: match.branch };
|
|
126
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.11.0",
|
|
4
4
|
"description": "AI coding agent orchestrator — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -24,10 +24,11 @@
|
|
|
24
24
|
"build": "rm -rf dist/src dist/bin dist/dashboard && tsc && chmod +x dist/bin/cli.js",
|
|
25
25
|
"dev": "tsx bin/cli.ts",
|
|
26
26
|
"test": "vitest run",
|
|
27
|
-
"lint": "eslint src/ bin/ --max-warnings 0",
|
|
27
|
+
"lint": "eslint src/ bin/ scripts/ --max-warnings 0",
|
|
28
28
|
"sync:skills": "npx tsx scripts/check-skill-sync.ts --fix",
|
|
29
29
|
"sync:hooks": "bash scripts/sync-hooks.sh",
|
|
30
30
|
"validate:skills": "for skill in templates/skills/*/; do case \"$skill\" in *_shared*|*/references/*) continue;; esac; npx skills-ref validate \"$skill\"; done",
|
|
31
|
+
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
31
32
|
"lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
|
|
32
33
|
"lint:skill-gates": "npx tsx scripts/lint-skill-gates.ts",
|
|
33
34
|
"lint:skill-sync": "npx tsx scripts/check-skill-sync.ts",
|