sequant 2.12.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 +34 -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/skills/spec/SKILL.md +2 -0
- package/dist/src/commands/ready.js +3 -1
- package/dist/src/commands/status.js +4 -0
- package/dist/src/lib/settings.d.ts +35 -13
- package/dist/src/lib/settings.js +31 -3
- package/dist/src/lib/workflow/batch-executor.js +4 -1
- package/dist/src/lib/workflow/config-resolver.d.ts +30 -2
- package/dist/src/lib/workflow/config-resolver.js +58 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +27 -1
- 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/phase-executor.js +10 -2
- package/dist/src/lib/workflow/reconcile.js +4 -2
- 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 +5 -0
- package/dist/src/lib/workflow/state-schema.js +5 -0
- package/dist/src/lib/workflow/types.d.ts +6 -0
- package/dist/src/mcp/tools/run.js +10 -1
- package/package.json +12 -11
- package/templates/memory/constitution.md +112 -45
- package/templates/skills/spec/SKILL.md +2 -0
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "sequant",
|
|
10
10
|
"description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Includes 17 skills, workflow MCP tools, and pre/post-tool hooks.",
|
|
11
|
-
"version": "2.
|
|
11
|
+
"version": "2.13.0",
|
|
12
12
|
"author": {
|
|
13
13
|
"name": "sequant-io",
|
|
14
14
|
"email": "hello@sequant.io"
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
3
|
"description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees and quality gates, through spec → exec → qa phases.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.13.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sequant-io",
|
|
7
7
|
"email": "hello@sequant.io"
|
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# Sequant
|
|
2
2
|
|
|
3
|
-
**
|
|
3
|
+
**AI coding agents that prove their work — every acceptance criterion verified, and you hold the merge button.**
|
|
4
4
|
|
|
5
|
-
For
|
|
5
|
+
For developers who won't merge what they can't trust — solo or on a team. Solve GitHub issues with structured phases and quality gates, from issue to merge-ready PR.
|
|
6
6
|
|
|
7
7
|
**[sequant.io](https://sequant.io)** — docs, guides, and getting started.
|
|
8
8
|
|
|
@@ -12,10 +12,17 @@ For teams that can't ship un-reviewed AI code. Solve GitHub issues with structur
|
|
|
12
12
|
[](https://github.com/sequant-io/sequant/actions/workflows/ci.yml)
|
|
13
13
|
[](https://opensource.org/licenses/MIT)
|
|
14
14
|
|
|
15
|
-
AI coding agents write code well, but leave you to run the workflow around it — planning, isolation, review, and merge safety. Sequant wraps an agent in a structured **spec → exec → qa** pipeline with isolated git worktrees and quality gates, taking a GitHub issue from triage to a merge-ready PR without babysitting each step.
|
|
15
|
+
AI coding agents write code well, but leave you to run the workflow around it — planning, isolation, review, and merge safety. Sequant wraps an agent in a structured **spec → exec → qa** pipeline with isolated git worktrees and quality gates, taking a GitHub issue from triage to a merge-ready PR without babysitting each step. The PR arrives with evidence — each acceptance criterion checked against the code — and the merge is always yours.
|
|
16
16
|
|
|
17
17
|
See the [CHANGELOG](CHANGELOG.md) for release notes, or the [migration guide](CHANGELOG.md#migration-from-v1x) if upgrading from v1.x.
|
|
18
18
|
|
|
19
|
+
### What's new in 2.13
|
|
20
|
+
|
|
21
|
+
- **NEEDS_VERIFICATION verdicts stop blocking their own follow-up** — a QA verdict of NEEDS_VERIFICATION now maps to a dedicated `awaiting_verification` state instead of `ready_for_merge`, so after you execute the ACs the qa re-run just runs — no `--force`, no editing `state.json` by hand. The state shows up in `sequant status` and the dashboard with a re-run hint, and an issue whose PR you merge directly still sweeps to `merged`. MCP `sequant_run` also gains a real `force` parameter (previously silently ignored). The full verdict→state contract is documented in [qa-verdict-workflow-states.md](docs/features/qa-verdict-workflow-states.md) (#972).
|
|
22
|
+
- **A bad model name is now a loud failure, not a silent no-op** — when a phase's session ends on an API error (e.g. a typo'd or roster-stale model string), the driver now fails the phase with the API error text and structured `terminal_reason`/`api_error_status` context, instead of reporting a zero-work "success" that only surfaced downstream as an empty diff entering QA (#973).
|
|
23
|
+
- **Model roles: name the tier, not the model** — `run.modelRoles` maps semantic roles (defaults: `fast`, `strong`, `frontier`) to model strings, and phase policy or `--models` can reference them as `role:fast`. Raw model strings still pass through verbatim, a missing role fails loudly at config-resolution before any session spawns, and run metrics record both the requested value and the concrete model ID actually dispatched — so a roster change means editing one map, not every settings file (#975). `agents.model` now accepts any model string instead of a stale three-model enum.
|
|
24
|
+
- **A constitution with teeth** — the constitution template is now the enforceable agent contract: its Definition of Done table is *generated* from `/qa`'s real gate list and drift-gated in CI (`lint:constitution-dod`), an AC authoring standard is cross-linked from `/spec`'s lint output, and every Boundaries/Budgets rule names its actual enforcing mechanism — a gate test asserts every cited settings key and hook path resolves. Customized constitutions are preserved by `update`/`sync`; the new template reaches fresh `init` projects only (#943).
|
|
25
|
+
|
|
19
26
|
### What's new in 2.12
|
|
20
27
|
|
|
21
28
|
- **QA gates you can parse, not just read** — `/qa` now closes every review with a structured findings marker (six-category taxonomy, evidence, recommended action) that `/loop` and `sequant ready` consume directly, so a finding QA itself called non-blocking is never burned as a fix iteration (#937). Gate-test ACs must ship a machine-checkable `SEQUANT_MUTATION` record in the PR body, enforced by `/qa` (#939), and an AC can declare its own verification command via a trailing `Evidence:` clause that QA must actually execute (#938). A new advisory CI job annotates PRs with likely-vacuous tests (#940).
|
|
@@ -175,6 +182,17 @@ your-project/ # Main repo (stays on main branch)
|
|
|
175
182
|
- Each issue has its own dependencies and build
|
|
176
183
|
- Safe to discard failed experiments
|
|
177
184
|
|
|
185
|
+
### Agent Contract
|
|
186
|
+
|
|
187
|
+
Every project gets `.claude/memory/constitution.md` — the machine-enforceable agent contract that answers "what will the agent refuse to do, what must every PR satisfy, and where does it stop for me?":
|
|
188
|
+
|
|
189
|
+
- **Definition of Done** — generated from `/qa`'s §7 gate list; a CI check (`lint:constitution-dod`) fails when the table drifts from the live gates. These are project-wide — issue ACs don't restate them.
|
|
190
|
+
- **AC Authoring Standard** — the house format for writing ACs (single-line, `Evidence:`/`Risk:`/`Human decision` fields, Non-Goals); referenced by `/spec`'s lint warnings.
|
|
191
|
+
- **Boundaries** — every rule names its enforcer: the force-push hook, the worktree-only editing guard, the mutation-verification gate.
|
|
192
|
+
- **Budgets & Stop Conditions** — iteration caps, the human merge gate, hold states, and gap-prompt discipline.
|
|
193
|
+
|
|
194
|
+
The file is a `CUSTOMIZABLE_FILES` entry: plain `sequant update` and `sync` preserve your edits. Only `--force` replaces it.
|
|
195
|
+
|
|
178
196
|
### Quality Gates
|
|
179
197
|
|
|
180
198
|
Every `/qa` runs automated checks:
|
|
@@ -190,6 +208,19 @@ Every `/qa` runs automated checks:
|
|
|
190
208
|
|
|
191
209
|
When checks fail, `/loop` automatically fixes and re-runs (up to 3x).
|
|
192
210
|
|
|
211
|
+
### Thinking of building this on a graph framework?
|
|
212
|
+
|
|
213
|
+
You could assemble this workflow yourself on LangGraph, CrewAI, Mastra, or the Claude Agent SDK — they sell the primitives and leave the workflow as an exercise. Sequant is the finished, hardened version of what you'd end up building, in the frameworks' own vocabulary:
|
|
214
|
+
|
|
215
|
+
- **Isolated execution** — one git worktree per issue, resolved by the branch git reports rather than directory globs
|
|
216
|
+
- **Human-in-the-loop approval gate** — the pipeline stops at the PR and never merges; this is an invariant, not optional wiring
|
|
217
|
+
- **Guardrails** — QA verdicts with an enforced floor, mutation-verified gate tests, scope and security checks
|
|
218
|
+
- **Durable execution** — a rate-limit halt writes a resumable record; `sequant resume` picks up where it left off, skipping completed phases
|
|
219
|
+
- **State management** — per-issue and checkout-scoped locks, so concurrent sessions can't interleave git operations in the same tree
|
|
220
|
+
- **Deterministic control flow** — dependency-ordered batch scheduling over `blocked by #N` relationships
|
|
221
|
+
|
|
222
|
+
On a general framework, every one of these is something you wire up — and can wire wrong or skip. Here they hold for every run. The boring 80% (retries, resume, locking, exit codes your scripts can trust) has already been run in anger; the [CHANGELOG](CHANGELOG.md) is the receipts.
|
|
223
|
+
|
|
193
224
|
---
|
|
194
225
|
|
|
195
226
|
## Using Sequant
|
package/dist/dashboard/server.js
CHANGED
|
@@ -42,6 +42,8 @@ function getStatusClass(status) {
|
|
|
42
42
|
return "primary";
|
|
43
43
|
case "waiting_for_qa_gate":
|
|
44
44
|
return "warning";
|
|
45
|
+
case "awaiting_verification":
|
|
46
|
+
return "warning";
|
|
45
47
|
case "ready_for_merge":
|
|
46
48
|
return "success";
|
|
47
49
|
case "blocked":
|
|
@@ -347,6 +349,7 @@ function renderIssuesList(issues) {
|
|
|
347
349
|
in_progress: [],
|
|
348
350
|
waiting_for_qa_gate: [],
|
|
349
351
|
waiting_for_human_merge: [],
|
|
352
|
+
awaiting_verification: [],
|
|
350
353
|
ready_for_merge: [],
|
|
351
354
|
blocked: [],
|
|
352
355
|
not_started: [],
|
|
@@ -364,6 +367,7 @@ function renderIssuesList(issues) {
|
|
|
364
367
|
const statusOrder = [
|
|
365
368
|
"in_progress",
|
|
366
369
|
"waiting_for_qa_gate",
|
|
370
|
+
"awaiting_verification",
|
|
367
371
|
"ready_for_merge",
|
|
368
372
|
"blocked",
|
|
369
373
|
"not_started",
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
3
|
"description": "AI coding agent orchestrator for Claude Code — resolve GitHub issues end-to-end with isolated git worktrees and quality gates, through spec → exec → qa phases.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.13.0",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "sequant-io",
|
|
7
7
|
"email": "hello@sequant.io"
|
|
@@ -75,6 +75,15 @@ sequant init
|
|
|
75
75
|
- **Pre-tool guardrails** — blocks dangerous commands, enforces worktree safety
|
|
76
76
|
- **Post-tool tracking** — timing, quality metrics, smart test runner
|
|
77
77
|
|
|
78
|
+
### Agent Contract
|
|
79
|
+
|
|
80
|
+
Every project gets `.claude/memory/constitution.md` — the machine-enforceable agent contract that answers "what must every PR satisfy, and where does the agent stop?":
|
|
81
|
+
|
|
82
|
+
- **Definition of Done** — generated from `/qa`'s §7 gate list; a CI check fails on drift
|
|
83
|
+
- **AC Authoring Standard** — the house format for ACs; referenced by `/spec`'s lint warnings
|
|
84
|
+
- **Boundaries** — every rule names its enforcer (hook, gate, or settings key)
|
|
85
|
+
- **Budgets & Stop Conditions** — iteration caps, the human merge gate, and hold states
|
|
86
|
+
|
|
78
87
|
## Quick Start
|
|
79
88
|
|
|
80
89
|
```
|
|
@@ -107,6 +107,8 @@ Mark tier in HTML comment for downstream parsing: `<!-- SEQUANT_SPEC_TIER: [tier
|
|
|
107
107
|
| Open-ended | "etc.", "and more" | Scope undefined |
|
|
108
108
|
| Title/body tension | doc-noun title ("note", "comment", "snippet") + runtime-imperative body ("execute", "trigger", "capture", incl. inflections like `triggered`/`captured`, `run /<cmd>`); separators `.`/`\n`/`:`/`—` | Two different verification bars |
|
|
109
109
|
|
|
110
|
+
> For the house AC format rules (single-line constraint, `Evidence:`/`Risk:`/`Human decision` fields, Non-Goals section), see the constitution's §2 AC Authoring Standard (`.claude/memory/constitution.md`).
|
|
111
|
+
|
|
110
112
|
3. **Scope Assessment** (unless `--skip-scope-check`): Use `performScopeAssessment` from `./src/lib/scope/index.ts` with settings from `getSettings()`. Verdicts: SCOPE_OK (green), SCOPE_WARNING (yellow, auto-enables quality loop), SCOPE_SPLIT_RECOMMENDED (red). Store results in state.
|
|
111
113
|
|
|
112
114
|
### If guard fails (consumer projects):
|
|
@@ -107,7 +107,9 @@ export async function readyCommand(issueArg, options) {
|
|
|
107
107
|
const policy = resolvePolicy(options.policy, settings.ready.policy);
|
|
108
108
|
const { maxIterations, tokenBudget, phaseTimeout } = resolveReadyLimits(options, settings);
|
|
109
109
|
const mcp = options.mcp !== false;
|
|
110
|
-
|
|
110
|
+
// #975: pass modelRoles + active driver so `role:` prefixes resolve,
|
|
111
|
+
// matching the same call in buildExecutionConfig (AC-5 drift guard).
|
|
112
|
+
const phasePolicies = resolvePhasePolicies(options.models, options.efforts, settings.run.phases, getPhaseNames(), settings.run.modelRoles, settings.run.agent ?? "claude-code");
|
|
111
113
|
// #915: CLI > settings > default `false`, same precedence as the `run`
|
|
112
114
|
// path's `buildExecutionConfig` (config-resolver.ts).
|
|
113
115
|
const effortEscalation = options.escalateEffort ?? settings.run.effortEscalation ?? false;
|
|
@@ -69,6 +69,8 @@ function colorStatus(status, resolvedAt) {
|
|
|
69
69
|
return chalk.yellow(status);
|
|
70
70
|
case "waiting_for_human_merge":
|
|
71
71
|
return chalk.green(status);
|
|
72
|
+
case "awaiting_verification":
|
|
73
|
+
return chalk.magenta(status);
|
|
72
74
|
case "ready_for_merge":
|
|
73
75
|
return chalk.green(status);
|
|
74
76
|
case "merged":
|
|
@@ -171,6 +173,7 @@ function displayIssueSummary(issues) {
|
|
|
171
173
|
in_progress: [],
|
|
172
174
|
waiting_for_qa_gate: [],
|
|
173
175
|
waiting_for_human_merge: [],
|
|
176
|
+
awaiting_verification: [],
|
|
174
177
|
ready_for_merge: [],
|
|
175
178
|
blocked: [],
|
|
176
179
|
not_started: [],
|
|
@@ -184,6 +187,7 @@ function displayIssueSummary(issues) {
|
|
|
184
187
|
const statusOrder = [
|
|
185
188
|
"in_progress",
|
|
186
189
|
"waiting_for_qa_gate",
|
|
190
|
+
"awaiting_verification",
|
|
187
191
|
"waiting_for_human_merge",
|
|
188
192
|
"ready_for_merge",
|
|
189
193
|
"blocked",
|
|
@@ -40,14 +40,14 @@ export interface AgentSettings {
|
|
|
40
40
|
*/
|
|
41
41
|
parallel: boolean;
|
|
42
42
|
/**
|
|
43
|
-
* Default model for sub-agents.
|
|
44
|
-
* Options: "haiku" (cheapest), "sonnet" (balanced), "opus" (most capable)
|
|
43
|
+
* Default model for sub-agents (free string — any alias or dated ID accepted).
|
|
45
44
|
* Default: "haiku" — currently inert per anthropics/claude-code#43869.
|
|
45
|
+
* See `run.modelRoles` for semantic role indirection (#975).
|
|
46
46
|
* @deprecated currently inert; see anthropics/claude-code#43869. Subagents
|
|
47
47
|
* inherit the parent session's model regardless of this value. Kept so
|
|
48
48
|
* existing user settings.json files continue to parse without error.
|
|
49
49
|
*/
|
|
50
|
-
model:
|
|
50
|
+
model: string;
|
|
51
51
|
/**
|
|
52
52
|
* Isolate parallel agent groups in separate worktrees.
|
|
53
53
|
* When true, each agent in a parallel group gets its own sub-worktree,
|
|
@@ -64,6 +64,23 @@ export interface AgentSettings {
|
|
|
64
64
|
* two validation points cannot drift apart on which values are accepted.
|
|
65
65
|
*/
|
|
66
66
|
export declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
|
|
67
|
+
/**
|
|
68
|
+
* Map of semantic role names to model strings, used by `run.modelRoles` (#975).
|
|
69
|
+
*
|
|
70
|
+
* A value is either:
|
|
71
|
+
* - a plain string shorthand — desugars to `{ "claude-code": value }` (claude-code driver only)
|
|
72
|
+
* - an object keyed by driver registry name, for cross-driver mappings
|
|
73
|
+
*
|
|
74
|
+
* Role references use a `role:` prefix in phase policy / ladder entries;
|
|
75
|
+
* bare strings pass through verbatim (AC-3 backward compat).
|
|
76
|
+
*/
|
|
77
|
+
export declare const ModelRolesSchema: z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>;
|
|
78
|
+
export type ModelRoles = z.infer<typeof ModelRolesSchema>;
|
|
79
|
+
/**
|
|
80
|
+
* Shipped default role map (#975). Family aliases only — no dated model IDs.
|
|
81
|
+
* Claude-code shorthand form; no opencode/aider entries shipped by default.
|
|
82
|
+
*/
|
|
83
|
+
export declare const DEFAULT_MODEL_ROLES: ModelRoles;
|
|
67
84
|
/**
|
|
68
85
|
* A single phase's `model`/`effort` override for the claude-code driver
|
|
69
86
|
* (#914). See `RunSettings.phases`.
|
|
@@ -231,6 +248,17 @@ export interface RunSettings {
|
|
|
231
248
|
* never merges regardless of this setting).
|
|
232
249
|
*/
|
|
233
250
|
autoMerge: boolean;
|
|
251
|
+
/**
|
|
252
|
+
* Map of semantic role names to model strings (#975).
|
|
253
|
+
*
|
|
254
|
+
* Config expresses **roles**; this map resolves roles to concrete model
|
|
255
|
+
* strings. Phase policy, ladder, and other model-referencing surfaces use
|
|
256
|
+
* `role:<name>` to reference an entry; bare strings pass through verbatim.
|
|
257
|
+
*
|
|
258
|
+
* Default: `{ fast: "sonnet", strong: "opus", frontier: "fable" }`.
|
|
259
|
+
* `sequant setup` writes nothing here — absent key → defaults apply.
|
|
260
|
+
*/
|
|
261
|
+
modelRoles: ModelRoles;
|
|
234
262
|
}
|
|
235
263
|
/**
|
|
236
264
|
* Scope assessment threshold configuration
|
|
@@ -362,11 +390,7 @@ export declare const AiderSettingsSchema: z.ZodObject<{
|
|
|
362
390
|
/** Zod schema for AgentSettings */
|
|
363
391
|
export declare const AgentSettingsSchema: z.ZodObject<{
|
|
364
392
|
parallel: z.ZodDefault<z.ZodBoolean>;
|
|
365
|
-
model: z.ZodDefault<z.
|
|
366
|
-
haiku: "haiku";
|
|
367
|
-
sonnet: "sonnet";
|
|
368
|
-
opus: "opus";
|
|
369
|
-
}>>;
|
|
393
|
+
model: z.ZodDefault<z.ZodString>;
|
|
370
394
|
isolateParallel: z.ZodDefault<z.ZodBoolean>;
|
|
371
395
|
}, z.core.$strip>;
|
|
372
396
|
/**
|
|
@@ -432,6 +456,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
|
|
|
432
456
|
}, z.core.$strip>>>;
|
|
433
457
|
effortEscalation: z.ZodDefault<z.ZodBoolean>;
|
|
434
458
|
autoMerge: z.ZodDefault<z.ZodBoolean>;
|
|
459
|
+
modelRoles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
|
|
435
460
|
}, z.core.$strip>;
|
|
436
461
|
/** Zod schema for ScopeThreshold (base — fields required, no defaults) */
|
|
437
462
|
export declare const ScopeThresholdSchema: z.ZodObject<{
|
|
@@ -538,14 +563,11 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
538
563
|
}, z.core.$strip>>>;
|
|
539
564
|
effortEscalation: z.ZodDefault<z.ZodBoolean>;
|
|
540
565
|
autoMerge: z.ZodDefault<z.ZodBoolean>;
|
|
566
|
+
modelRoles: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodString, z.ZodRecord<z.ZodString, z.ZodString>]>>>;
|
|
541
567
|
}, z.core.$strip>>;
|
|
542
568
|
agents: z.ZodDefault<z.ZodObject<{
|
|
543
569
|
parallel: z.ZodDefault<z.ZodBoolean>;
|
|
544
|
-
model: z.ZodDefault<z.
|
|
545
|
-
haiku: "haiku";
|
|
546
|
-
sonnet: "sonnet";
|
|
547
|
-
opus: "opus";
|
|
548
|
-
}>>;
|
|
570
|
+
model: z.ZodDefault<z.ZodString>;
|
|
549
571
|
isolateParallel: z.ZodDefault<z.ZodBoolean>;
|
|
550
572
|
}, z.core.$strip>>;
|
|
551
573
|
scopeAssessment: z.ZodDefault<z.ZodObject<{
|
package/dist/src/lib/settings.js
CHANGED
|
@@ -25,6 +25,26 @@ export const SETTINGS_VERSION = "1.0";
|
|
|
25
25
|
* two validation points cannot drift apart on which values are accepted.
|
|
26
26
|
*/
|
|
27
27
|
export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
|
|
28
|
+
/**
|
|
29
|
+
* Map of semantic role names to model strings, used by `run.modelRoles` (#975).
|
|
30
|
+
*
|
|
31
|
+
* A value is either:
|
|
32
|
+
* - a plain string shorthand — desugars to `{ "claude-code": value }` (claude-code driver only)
|
|
33
|
+
* - an object keyed by driver registry name, for cross-driver mappings
|
|
34
|
+
*
|
|
35
|
+
* Role references use a `role:` prefix in phase policy / ladder entries;
|
|
36
|
+
* bare strings pass through verbatim (AC-3 backward compat).
|
|
37
|
+
*/
|
|
38
|
+
export const ModelRolesSchema = z.record(z.string(), z.union([z.string(), z.record(z.string(), z.string())]));
|
|
39
|
+
/**
|
|
40
|
+
* Shipped default role map (#975). Family aliases only — no dated model IDs.
|
|
41
|
+
* Claude-code shorthand form; no opencode/aider entries shipped by default.
|
|
42
|
+
*/
|
|
43
|
+
export const DEFAULT_MODEL_ROLES = {
|
|
44
|
+
fast: "sonnet",
|
|
45
|
+
strong: "opus",
|
|
46
|
+
frontier: "fable",
|
|
47
|
+
};
|
|
28
48
|
// ─── Zod Schemas (AC-1, AC-5) ────────────────────────────────────────────────
|
|
29
49
|
/** Zod schema for RotationSettings */
|
|
30
50
|
export const RotationSettingsSchema = z.object({
|
|
@@ -41,7 +61,7 @@ export const AiderSettingsSchema = z.object({
|
|
|
41
61
|
/** Zod schema for AgentSettings */
|
|
42
62
|
export const AgentSettingsSchema = z.object({
|
|
43
63
|
parallel: z.boolean().default(false),
|
|
44
|
-
model: z.
|
|
64
|
+
model: z.string().default("haiku"),
|
|
45
65
|
isolateParallel: z.boolean().default(false),
|
|
46
66
|
});
|
|
47
67
|
/**
|
|
@@ -111,6 +131,12 @@ export const RunSettingsSchema = z.object({
|
|
|
111
131
|
* #817–#819. Overridable per-invocation with `--auto-merge`.
|
|
112
132
|
*/
|
|
113
133
|
autoMerge: z.boolean().default(false),
|
|
134
|
+
/**
|
|
135
|
+
* Semantic role → model string map (#975). See `ModelRolesSchema` and
|
|
136
|
+
* `DEFAULT_MODEL_ROLES` for the shipped defaults. Absent from generated
|
|
137
|
+
* settings — absent key → defaults apply. Do not emit in `generateSettingsJsonc`.
|
|
138
|
+
*/
|
|
139
|
+
modelRoles: ModelRolesSchema.default(() => ({ ...DEFAULT_MODEL_ROLES })),
|
|
114
140
|
});
|
|
115
141
|
/** Zod schema for ScopeThreshold (base — fields required, no defaults) */
|
|
116
142
|
export const ScopeThresholdSchema = z.object({
|
|
@@ -233,6 +259,7 @@ const KNOWN_KEYS = {
|
|
|
233
259
|
"aider",
|
|
234
260
|
"relay",
|
|
235
261
|
"phases",
|
|
262
|
+
"modelRoles",
|
|
236
263
|
]),
|
|
237
264
|
// #914: keyed by real phase name so a typo (`run.phases.exce`) warns
|
|
238
265
|
// instead of silently resolving to nothing. Computed from the registry
|
|
@@ -460,6 +487,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
460
487
|
relay: true, // Enable interactive relay (#383) by default
|
|
461
488
|
effortEscalation: false, // #915: off by default — raises token spend
|
|
462
489
|
autoMerge: false, // #958: off by default — preserves the human merge gate
|
|
490
|
+
modelRoles: DEFAULT_MODEL_ROLES, // #975: shipped defaults; absent key → these
|
|
463
491
|
},
|
|
464
492
|
agents: DEFAULT_AGENT_SETTINGS,
|
|
465
493
|
scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
|
|
@@ -616,7 +644,7 @@ export function generateSettingsJsonc(settings) {
|
|
|
616
644
|
lines.push(` "agents": {`);
|
|
617
645
|
lines.push(` // Run agents in parallel (faster, higher token usage)`);
|
|
618
646
|
lines.push(` "parallel": ${JSON.stringify(settings.agents.parallel)},`);
|
|
619
|
-
lines.push(` // Default model for sub-agents (
|
|
647
|
+
lines.push(` // Default model for sub-agents (any alias/ID) — currently inert per anthropics/claude-code#43869`);
|
|
620
648
|
lines.push(` "model": ${JSON.stringify(settings.agents.model)},`);
|
|
621
649
|
lines.push(` // Isolate parallel agent groups in separate worktrees`);
|
|
622
650
|
lines.push(` "isolateParallel": ${JSON.stringify(settings.agents.isolateParallel)}`);
|
|
@@ -738,7 +766,7 @@ Generated by \`sequant init\`. See defaults below.
|
|
|
738
766
|
| Key | Type | Default | Description |
|
|
739
767
|
|-----|------|---------|-------------|
|
|
740
768
|
| \`parallel\` | boolean | \`false\` | Run agents in parallel (faster, higher token usage) |
|
|
741
|
-
| \`model\` |
|
|
769
|
+
| \`model\` | string | \`"haiku"\` | Default model (any alias or dated ID). **Currently inert** per [anthropics/claude-code#43869](https://github.com/anthropics/claude-code/issues/43869) — subagents inherit the parent session's model. See \`run.modelRoles\` for semantic roles (#975). |
|
|
742
770
|
| \`isolateParallel\` | boolean | \`false\` | Isolate parallel agents in separate worktrees |
|
|
743
771
|
|
|
744
772
|
## \`scopeAssessment\` — Scope Assessment Settings
|
|
@@ -1407,10 +1407,13 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1407
1407
|
// Hoisted out of the `if (stateManager)` block below because the checkpoint
|
|
1408
1408
|
// warning also has to name this status, and naming the wrong one is exactly
|
|
1409
1409
|
// the #837 inaccuracy being fixed here.
|
|
1410
|
+
const qaVerdict = phaseResults.find((p) => p.phase === "qa")?.verdict;
|
|
1410
1411
|
const finalStatus = readyGateResult
|
|
1411
1412
|
? readyGateResult.issueStatus
|
|
1412
1413
|
: success
|
|
1413
|
-
? "
|
|
1414
|
+
? qaVerdict === "NEEDS_VERIFICATION"
|
|
1415
|
+
? "awaiting_verification"
|
|
1416
|
+
: "ready_for_merge"
|
|
1414
1417
|
: "in_progress";
|
|
1415
1418
|
if (stateManager) {
|
|
1416
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
|
/**
|
|
@@ -304,7 +359,8 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
304
359
|
// #914: CLI > settings > absent, via the shared resolver both
|
|
305
360
|
// ExecutionConfig producers call (see `resolvePhasePolicies`'s doc
|
|
306
361
|
// comment for the #833 drift this guards against).
|
|
307
|
-
|
|
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"),
|
|
308
364
|
// #915: CLI > settings > default `false` — mirrors the `readyGate`
|
|
309
365
|
// precedent above. Both `ExecutionConfig` producers (here and
|
|
310
366
|
// `ready-gate.ts:buildPhaseConfig`) resolve this the same way (#833).
|
|
@@ -109,6 +109,13 @@ export interface AgentPhaseResult {
|
|
|
109
109
|
stdoutTail?: string[];
|
|
110
110
|
/** Process exit code (undefined for SDK-based drivers) (#447) */
|
|
111
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>;
|
|
112
119
|
}
|
|
113
120
|
/**
|
|
114
121
|
* Interface that all agent backends must implement.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
8
|
import { getPhaseMcpServersConfig } from "../../mcp-config.js";
|
|
9
|
-
import { RateLimitError, BillingError, createRateLimitError, isWaitableWindow, isRateLimitFailureInfo, } from "../../errors.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";
|
|
@@ -173,6 +173,31 @@ export class ClaudeCodeDriver {
|
|
|
173
173
|
const structuredError = this.buildStructuredError(rateLimitInfo, assistantError, apiRetryError);
|
|
174
174
|
if (resultMessage) {
|
|
175
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
|
+
}
|
|
176
201
|
return {
|
|
177
202
|
success: true,
|
|
178
203
|
output: capturedOutput,
|
|
@@ -180,6 +205,7 @@ export class ClaudeCodeDriver {
|
|
|
180
205
|
resumeHandle,
|
|
181
206
|
stderrTail: stderrBuffer.getLines(),
|
|
182
207
|
stdoutTail: stdoutBuffer.getLines(),
|
|
208
|
+
modelUsage: resultMessage.modelUsage,
|
|
183
209
|
};
|
|
184
210
|
}
|
|
185
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<{
|
|
@@ -1138,10 +1138,18 @@ async function executePhase(issueNumber, phase, config, resumeHandle, worktreePa
|
|
|
1138
1138
|
shutdownManager.removeAbortController(abortController);
|
|
1139
1139
|
}
|
|
1140
1140
|
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
1141
|
+
// #975: extract the concrete model ID from the driver's modelUsage map.
|
|
1142
|
+
// First key is the primary model dispatched; undefined for drivers that
|
|
1143
|
+
// don't populate modelUsage (aider, subprocess paths).
|
|
1144
|
+
const resolvedModel = agentResult.modelUsage
|
|
1145
|
+
? Object.keys(agentResult.modelUsage)[0]
|
|
1146
|
+
: undefined;
|
|
1141
1147
|
if (agentResult.success) {
|
|
1142
|
-
|
|
1148
|
+
const result = mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
|
|
1149
|
+
return resolvedModel ? { ...result, resolvedModel } : result;
|
|
1143
1150
|
}
|
|
1144
|
-
|
|
1151
|
+
const result = mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds);
|
|
1152
|
+
return resolvedModel ? { ...result, resolvedModel } : result;
|
|
1145
1153
|
}
|
|
1146
1154
|
/**
|
|
1147
1155
|
* Execute a phase with automatic retry for cold-start failures and MCP fallback.
|
|
@@ -88,13 +88,15 @@ export function getNextActionHint(issue) {
|
|
|
88
88
|
if (issue.currentPhase) {
|
|
89
89
|
const failedPhase = Object.entries(issue.phases).find(([, ps]) => ps.status === "failed");
|
|
90
90
|
if (failedPhase) {
|
|
91
|
-
return `sequant run ${issue.number} --
|
|
91
|
+
return `sequant run ${issue.number} --phases ${failedPhase[0]}`;
|
|
92
92
|
}
|
|
93
93
|
}
|
|
94
94
|
return `sequant run ${issue.number}`;
|
|
95
95
|
}
|
|
96
96
|
case "waiting_for_qa_gate":
|
|
97
|
-
return `sequant run ${issue.number} --
|
|
97
|
+
return `sequant run ${issue.number} --phases qa`;
|
|
98
|
+
case "awaiting_verification":
|
|
99
|
+
return `sequant run ${issue.number} --phases qa`;
|
|
98
100
|
case "waiting_for_human_merge":
|
|
99
101
|
// `sequant ready` certified the work; a human reviews + merges manually.
|
|
100
102
|
if (issue.pr?.number) {
|
|
@@ -16,6 +16,7 @@ import type { LockFile } from "../locks/index.js";
|
|
|
16
16
|
import { type ChainResumePlan } from "./chain-resume.js";
|
|
17
17
|
import { WorkflowEventEmitter } from "./event-emitter.js";
|
|
18
18
|
import type { SequantSettings } from "../settings.js";
|
|
19
|
+
import { type PhasePolicy } from "./config-resolver.js";
|
|
19
20
|
/**
|
|
20
21
|
* Build the stack-manifest line emitted into PR bodies under --stacked.
|
|
21
22
|
*
|
|
@@ -172,6 +173,23 @@ export interface RunResult {
|
|
|
172
173
|
*/
|
|
173
174
|
wallClockDurationSeconds: number;
|
|
174
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Merge per-phase `resolvedModel` from execution results into the static
|
|
178
|
+
* phasePolicies map before it is written to metrics (#975 AC-4).
|
|
179
|
+
*
|
|
180
|
+
* `config.phasePolicies` carries the model alias resolved at config time (e.g.
|
|
181
|
+
* `"sonnet"`). The concrete model ID only becomes known after execution, from
|
|
182
|
+
* the driver's `modelUsage` map. This function enriches each phase entry with
|
|
183
|
+
* that runtime-observed value so benchmark comparisons across roster changes
|
|
184
|
+
* (#916/#944) see the actual model dispatched, not just the alias.
|
|
185
|
+
*
|
|
186
|
+
* `requestedModel` (the pre-resolution role string, e.g. `"role:fast"`) flows
|
|
187
|
+
* through from `config.phasePolicies` unchanged — it is already set by
|
|
188
|
+
* `resolvePhasePolicies` for role references and absent for raw strings.
|
|
189
|
+
*
|
|
190
|
+
* @internal Exported for testing only.
|
|
191
|
+
*/
|
|
192
|
+
export declare function enrichPhasePoliciesFromResults(phasePolicies: Record<string, PhasePolicy> | undefined, results: IssueResult[]): Record<string, PhasePolicy> | undefined;
|
|
175
193
|
/**
|
|
176
194
|
* CLI-free workflow execution engine.
|
|
177
195
|
*
|
|
@@ -82,7 +82,7 @@ import { MetricsWriter } from "./metrics-writer.js";
|
|
|
82
82
|
import { WorkflowEventEmitter } from "./event-emitter.js";
|
|
83
83
|
import { determineOutcome } from "./metrics-schema.js";
|
|
84
84
|
import { getTokenUsageForRun } from "./token-utils.js";
|
|
85
|
-
import { resolveRunOptions, buildExecutionConfig } from "./config-resolver.js";
|
|
85
|
+
import { resolveRunOptions, buildExecutionConfig, } from "./config-resolver.js";
|
|
86
86
|
import { pipelineHasFailed } from "./status-derivation.js";
|
|
87
87
|
/**
|
|
88
88
|
* Build the stack-manifest line emitted into PR bodies under --stacked.
|
|
@@ -96,6 +96,40 @@ export function buildStackManifest(issueNumbers, currentIndex) {
|
|
|
96
96
|
const parts = issueNumbers.map((n, i) => i === currentIndex ? `#${n} (this)` : `#${n}`);
|
|
97
97
|
return `Part of stack: ${parts.join(" → ")}`;
|
|
98
98
|
}
|
|
99
|
+
// ── Metrics helpers ─────────────────────────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Merge per-phase `resolvedModel` from execution results into the static
|
|
102
|
+
* phasePolicies map before it is written to metrics (#975 AC-4).
|
|
103
|
+
*
|
|
104
|
+
* `config.phasePolicies` carries the model alias resolved at config time (e.g.
|
|
105
|
+
* `"sonnet"`). The concrete model ID only becomes known after execution, from
|
|
106
|
+
* the driver's `modelUsage` map. This function enriches each phase entry with
|
|
107
|
+
* that runtime-observed value so benchmark comparisons across roster changes
|
|
108
|
+
* (#916/#944) see the actual model dispatched, not just the alias.
|
|
109
|
+
*
|
|
110
|
+
* `requestedModel` (the pre-resolution role string, e.g. `"role:fast"`) flows
|
|
111
|
+
* through from `config.phasePolicies` unchanged — it is already set by
|
|
112
|
+
* `resolvePhasePolicies` for role references and absent for raw strings.
|
|
113
|
+
*
|
|
114
|
+
* @internal Exported for testing only.
|
|
115
|
+
*/
|
|
116
|
+
export function enrichPhasePoliciesFromResults(phasePolicies, results) {
|
|
117
|
+
const resolvedByPhase = new Map();
|
|
118
|
+
for (const result of results) {
|
|
119
|
+
for (const pr of result.phaseResults) {
|
|
120
|
+
if (pr.resolvedModel) {
|
|
121
|
+
resolvedByPhase.set(pr.phase, pr.resolvedModel);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (resolvedByPhase.size === 0)
|
|
126
|
+
return phasePolicies;
|
|
127
|
+
const enriched = { ...(phasePolicies ?? {}) };
|
|
128
|
+
for (const [phase, resolvedModel] of resolvedByPhase.entries()) {
|
|
129
|
+
enriched[phase] = { ...enriched[phase], resolvedModel };
|
|
130
|
+
}
|
|
131
|
+
return enriched;
|
|
132
|
+
}
|
|
99
133
|
// ── Orchestrator ────────────────────────────────────────────────────────────
|
|
100
134
|
/**
|
|
101
135
|
* CLI-free workflow execution engine.
|
|
@@ -1239,7 +1273,9 @@ export class RunOrchestrator {
|
|
|
1239
1273
|
flags: cliFlags,
|
|
1240
1274
|
failureCategory,
|
|
1241
1275
|
// #914: resolved per-phase model/effort, when any phase had one.
|
|
1242
|
-
|
|
1276
|
+
// #975: enriched with resolvedModel from execution (modelUsage) and
|
|
1277
|
+
// requestedModel already flows through from resolvePhasePolicies.
|
|
1278
|
+
phasePolicies: enrichPhasePoliciesFromResults(config.phasePolicies, results),
|
|
1243
1279
|
// #915: escalated tiers, when any phase execution escalated.
|
|
1244
1280
|
effortEscalations,
|
|
1245
1281
|
metrics: {
|
|
@@ -58,7 +58,7 @@ export interface ReconcileOptions {
|
|
|
58
58
|
export interface ReconcileResult {
|
|
59
59
|
/** Whether reconciliation was successful */
|
|
60
60
|
success: boolean;
|
|
61
|
-
/** Issues advanced to `merged` (from `ready_for_merge`, `in_progress`, or `
|
|
61
|
+
/** Issues advanced to `merged` (from `ready_for_merge`, `in_progress`, `waiting_for_qa_gate`, `waiting_for_human_merge`, or `awaiting_verification`) */
|
|
62
62
|
advanced: number[];
|
|
63
63
|
/** Issues checked but not yet merged (status unchanged) */
|
|
64
64
|
stillPending: number[];
|
|
@@ -68,9 +68,9 @@ export interface ReconcileResult {
|
|
|
68
68
|
/**
|
|
69
69
|
* Lightweight state reconciliation at run start
|
|
70
70
|
*
|
|
71
|
-
* Checks issues in `ready_for_merge`, `in_progress`,
|
|
72
|
-
*
|
|
73
|
-
* are in main.
|
|
71
|
+
* Checks issues in `ready_for_merge`, `in_progress`, `waiting_for_qa_gate`,
|
|
72
|
+
* `waiting_for_human_merge`, or `awaiting_verification` state and advances
|
|
73
|
+
* them to `merged` if their PRs are merged or their branches are in main.
|
|
74
74
|
*
|
|
75
75
|
* Including `in_progress` covers the case where a PR was merged outside
|
|
76
76
|
* this sequant session (separate process, `gh pr merge`, web UI) — without
|
|
@@ -188,9 +188,9 @@ export async function cleanupStaleEntries(options = {}) {
|
|
|
188
188
|
/**
|
|
189
189
|
* Lightweight state reconciliation at run start
|
|
190
190
|
*
|
|
191
|
-
* Checks issues in `ready_for_merge`, `in_progress`,
|
|
192
|
-
*
|
|
193
|
-
* are in main.
|
|
191
|
+
* Checks issues in `ready_for_merge`, `in_progress`, `waiting_for_qa_gate`,
|
|
192
|
+
* `waiting_for_human_merge`, or `awaiting_verification` state and advances
|
|
193
|
+
* them to `merged` if their PRs are merged or their branches are in main.
|
|
194
194
|
*
|
|
195
195
|
* Including `in_progress` covers the case where a PR was merged outside
|
|
196
196
|
* this sequant session (separate process, `gh pr merge`, web UI) — without
|
|
@@ -227,14 +227,18 @@ export async function reconcileStateAtStartup(options = {}) {
|
|
|
227
227
|
// waiting_for_human_merge covers #817's `--ready-gate` terminal: a gated
|
|
228
228
|
// issue never reaches ready_for_merge, so without it a gated issue whose PR
|
|
229
229
|
// a human then merged stayed here forever and never advanced to merged
|
|
230
|
-
// (#837).
|
|
230
|
+
// (#837). awaiting_verification covers the same shape for #972: the issue
|
|
231
|
+
// has an open PR pending human verification, and the human may verify and
|
|
232
|
+
// merge it directly instead of re-running qa first.
|
|
233
|
+
// Note this list is deliberately WIDER than `isCompletedIssueStatus`
|
|
231
234
|
// — it asks "might this have a merged PR?", not "is this done?", which is
|
|
232
235
|
// why in_progress belongs here but not there.
|
|
233
236
|
for (const [issueNumStr, issueState] of Object.entries(state.issues)) {
|
|
234
237
|
if (issueState.status !== "ready_for_merge" &&
|
|
235
238
|
issueState.status !== "in_progress" &&
|
|
236
239
|
issueState.status !== "waiting_for_qa_gate" &&
|
|
237
|
-
issueState.status !== "waiting_for_human_merge"
|
|
240
|
+
issueState.status !== "waiting_for_human_merge" &&
|
|
241
|
+
issueState.status !== "awaiting_verification") {
|
|
238
242
|
continue;
|
|
239
243
|
}
|
|
240
244
|
const issueNum = parseInt(issueNumStr, 10);
|
|
@@ -47,6 +47,7 @@ export declare const IssueStatusSchema: z.ZodEnum<{
|
|
|
47
47
|
not_started: "not_started";
|
|
48
48
|
waiting_for_qa_gate: "waiting_for_qa_gate";
|
|
49
49
|
waiting_for_human_merge: "waiting_for_human_merge";
|
|
50
|
+
awaiting_verification: "awaiting_verification";
|
|
50
51
|
ready_for_merge: "ready_for_merge";
|
|
51
52
|
blocked: "blocked";
|
|
52
53
|
abandoned: "abandoned";
|
|
@@ -72,6 +73,8 @@ export declare const PhaseMarkerSchema: z.ZodObject<{
|
|
|
72
73
|
pr: z.ZodOptional<z.ZodNumber>;
|
|
73
74
|
error: z.ZodOptional<z.ZodString>;
|
|
74
75
|
commitSHA: z.ZodOptional<z.ZodString>;
|
|
76
|
+
requestedModel: z.ZodOptional<z.ZodString>;
|
|
77
|
+
resolvedModel: z.ZodOptional<z.ZodString>;
|
|
75
78
|
}, z.core.$strip>;
|
|
76
79
|
export type PhaseMarker = z.infer<typeof PhaseMarkerSchema>;
|
|
77
80
|
/**
|
|
@@ -235,6 +238,7 @@ export declare const IssueStateSchema: z.ZodObject<{
|
|
|
235
238
|
not_started: "not_started";
|
|
236
239
|
waiting_for_qa_gate: "waiting_for_qa_gate";
|
|
237
240
|
waiting_for_human_merge: "waiting_for_human_merge";
|
|
241
|
+
awaiting_verification: "awaiting_verification";
|
|
238
242
|
ready_for_merge: "ready_for_merge";
|
|
239
243
|
blocked: "blocked";
|
|
240
244
|
abandoned: "abandoned";
|
|
@@ -385,6 +389,7 @@ export declare const WorkflowStateSchema: z.ZodObject<{
|
|
|
385
389
|
not_started: "not_started";
|
|
386
390
|
waiting_for_qa_gate: "waiting_for_qa_gate";
|
|
387
391
|
waiting_for_human_merge: "waiting_for_human_merge";
|
|
392
|
+
awaiting_verification: "awaiting_verification";
|
|
388
393
|
ready_for_merge: "ready_for_merge";
|
|
389
394
|
blocked: "blocked";
|
|
390
395
|
abandoned: "abandoned";
|
|
@@ -47,6 +47,7 @@ export const IssueStatusSchema = z.enum([
|
|
|
47
47
|
"in_progress", // Actively being worked on
|
|
48
48
|
"waiting_for_qa_gate", // QA completed, waiting for gate approval in chain mode
|
|
49
49
|
"waiting_for_human_merge", // `sequant ready` (#683) finished its A+ gate; awaiting human merge decision (never auto-merges)
|
|
50
|
+
"awaiting_verification", // QA returned NEEDS_VERIFICATION; human must execute ACs before QA re-run (#972)
|
|
50
51
|
"ready_for_merge", // All phases passed, PR ready for review
|
|
51
52
|
"merged", // PR merged, work complete
|
|
52
53
|
"blocked", // Waiting on external input or dependency
|
|
@@ -72,6 +73,10 @@ export const PhaseMarkerSchema = z.object({
|
|
|
72
73
|
error: z.string().optional(),
|
|
73
74
|
/** Git HEAD SHA at time of phase completion (used for incremental QA) */
|
|
74
75
|
commitSHA: z.string().optional(),
|
|
76
|
+
/** Role string or raw model string as configured for this phase (#975). */
|
|
77
|
+
requestedModel: z.string().optional(),
|
|
78
|
+
/** Concrete model ID from `modelUsage` after phase execution (#975). */
|
|
79
|
+
resolvedModel: z.string().optional(),
|
|
75
80
|
});
|
|
76
81
|
/**
|
|
77
82
|
* Individual phase state within an issue
|
|
@@ -287,6 +287,12 @@ export interface PhaseResult {
|
|
|
287
287
|
base: string;
|
|
288
288
|
escalated: string;
|
|
289
289
|
};
|
|
290
|
+
/**
|
|
291
|
+
* Concrete model ID from the SDK `modelUsage` map for this phase execution
|
|
292
|
+
* (#975). First key of `modelUsage` — records the actual model dispatched,
|
|
293
|
+
* distinct from the configured alias. Used to populate metrics `phasePolicies`.
|
|
294
|
+
*/
|
|
295
|
+
resolvedModel?: string;
|
|
290
296
|
}
|
|
291
297
|
/**
|
|
292
298
|
* Result of executing all phases for an issue
|
|
@@ -390,6 +390,12 @@ const runToolInputSchema = {
|
|
|
390
390
|
.boolean()
|
|
391
391
|
.optional()
|
|
392
392
|
.describe("Enable auto-retry on QA failure"),
|
|
393
|
+
force: z
|
|
394
|
+
.boolean()
|
|
395
|
+
.optional()
|
|
396
|
+
.describe("Re-run even if the issue is already in a completed state such as " +
|
|
397
|
+
"ready_for_merge (mirrors CLI --force). Not needed after a " +
|
|
398
|
+
"NEEDS_VERIFICATION verdict: awaiting_verification re-runs freely."),
|
|
393
399
|
agent: z
|
|
394
400
|
.string()
|
|
395
401
|
.optional()
|
|
@@ -410,7 +416,7 @@ export function registerRunTool(server) {
|
|
|
410
416
|
openWorldHint: true,
|
|
411
417
|
},
|
|
412
418
|
inputSchema: runToolInputSchema,
|
|
413
|
-
}, (async ({ issues, phases, qualityLoop, agent, }, extra) => {
|
|
419
|
+
}, (async ({ issues, phases, qualityLoop, force, agent, }, extra) => {
|
|
414
420
|
if (!issues || issues.length === 0) {
|
|
415
421
|
return {
|
|
416
422
|
content: [
|
|
@@ -435,6 +441,9 @@ export function registerRunTool(server) {
|
|
|
435
441
|
if (qualityLoop) {
|
|
436
442
|
args.push("--quality-loop");
|
|
437
443
|
}
|
|
444
|
+
if (force) {
|
|
445
|
+
args.push("--force");
|
|
446
|
+
}
|
|
438
447
|
if (agent) {
|
|
439
448
|
args.push("--agent", agent);
|
|
440
449
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
|
-
"version": "2.
|
|
4
|
-
"description": "AI coding agent orchestrator —
|
|
3
|
+
"version": "2.13.0",
|
|
4
|
+
"description": "AI coding agent orchestrator that proves its work — GitHub issues to merge-ready PRs with every acceptance criterion verified, isolated git worktrees, quality gates, an MCP server, and a human merge gate. Works with Claude Code or Aider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sequant": "dist/bin/cli.js"
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
"typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
|
|
32
32
|
"lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
|
|
33
33
|
"lint:skill-gates": "npx tsx scripts/lint-skill-gates.ts",
|
|
34
|
+
"lint:constitution-dod": "npx tsx scripts/check-constitution-dod.ts",
|
|
34
35
|
"lint:skill-sync": "npx tsx scripts/check-skill-sync.ts",
|
|
35
36
|
"prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
|
|
36
37
|
"validate:marketplace": "npx tsx scripts/prepare-marketplace.ts --validate-only",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
}
|
|
92
93
|
},
|
|
93
94
|
"dependencies": {
|
|
94
|
-
"@anthropic-ai/claude-agent-sdk": "^0.3.
|
|
95
|
+
"@anthropic-ai/claude-agent-sdk": "^0.3.242",
|
|
95
96
|
"@hono/node-server": "^2.0.0",
|
|
96
97
|
"boxen": "^8.0.1",
|
|
97
98
|
"chalk": "^6.0.0",
|
|
@@ -99,9 +100,9 @@
|
|
|
99
100
|
"cli-table3": "^0.6.5",
|
|
100
101
|
"commander": "^14.0.3",
|
|
101
102
|
"diff": "^9.0.0",
|
|
102
|
-
"hono": "^4.
|
|
103
|
+
"hono": "^4.13.4",
|
|
103
104
|
"ink": "^7.0.1",
|
|
104
|
-
"inquirer": "^14.0
|
|
105
|
+
"inquirer": "^14.1.0",
|
|
105
106
|
"log-update": "^7.0.1",
|
|
106
107
|
"open": "^11.0.0",
|
|
107
108
|
"ora": "^9.3.0",
|
|
@@ -114,16 +115,16 @@
|
|
|
114
115
|
"devDependencies": {
|
|
115
116
|
"@eslint/js": "^10.0.1",
|
|
116
117
|
"@types/inquirer": "^9.0.7",
|
|
117
|
-
"@types/node": "^26.0
|
|
118
|
+
"@types/node": "^26.3.0",
|
|
118
119
|
"@types/react": "^19.2.14",
|
|
119
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
120
|
-
"@typescript-eslint/parser": "^8.
|
|
121
|
-
"eslint": "^10.1
|
|
120
|
+
"@typescript-eslint/eslint-plugin": "^8.68.0",
|
|
121
|
+
"@typescript-eslint/parser": "^8.68.0",
|
|
122
|
+
"eslint": "^10.9.1",
|
|
122
123
|
"globals": "^17.0.0",
|
|
123
124
|
"ink-testing-library": "^4.0.0",
|
|
124
125
|
"tsx": "^4.19.2",
|
|
125
126
|
"typescript": "^6.0.2",
|
|
126
|
-
"typescript-eslint": "^8.
|
|
127
|
-
"vitest": "^4.1.
|
|
127
|
+
"typescript-eslint": "^8.68.0",
|
|
128
|
+
"vitest": "^4.1.11"
|
|
128
129
|
}
|
|
129
130
|
}
|
|
@@ -1,64 +1,131 @@
|
|
|
1
|
-
# {{PROJECT_NAME}}
|
|
1
|
+
# {{PROJECT_NAME}} Agent Contract
|
|
2
2
|
|
|
3
|
-
This document
|
|
3
|
+
This document is the enforceable contract surface for AI-assisted development in this project. Every section either machine-checked or referenced by a named skill at a named decision point. Values with no consumer are not present.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
---
|
|
6
6
|
|
|
7
|
-
1.
|
|
8
|
-
2. **Test Everything** - All features must have appropriate test coverage
|
|
9
|
-
3. **Document Decisions** - Important decisions should be captured in issue comments
|
|
10
|
-
4. **Incremental Progress** - Break large tasks into small, reviewable chunks
|
|
11
|
-
5. **Respect Existing Patterns** - Follow established project conventions
|
|
7
|
+
## 1. Definition of Done
|
|
12
8
|
|
|
13
|
-
|
|
9
|
+
Every PR must pass all gates below before merge. Verified by `/qa` §7 — the table is generated from that section and cannot drift from it (`lint:constitution-dod` fails CI on divergence).
|
|
14
10
|
|
|
15
|
-
|
|
16
|
-
- Understand requirements and acceptance criteria
|
|
17
|
-
- Identify potential risks and dependencies
|
|
18
|
-
- Draft implementation plan for approval
|
|
11
|
+
**These are project-wide gates. Do not restate them as issue-level ACs.**
|
|
19
12
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
<!-- BEGIN:DOD-GATES -->
|
|
14
|
+
| Gate | Trigger | Verdict impact |
|
|
15
|
+
|------|---------|----------------|
|
|
16
|
+
| All ACs MET | any `NOT_MET` or `PARTIALLY_MET` | `AC_NOT_MET` — blocks merge |
|
|
17
|
+
| Detection patterns (§6c) | `Failed` | `AC_NOT_MET` — blocks merge |
|
|
18
|
+
| Behavior-rule check (§6e) | `Survivors Found` | `AC_NOT_MET` — blocks merge |
|
|
19
|
+
| Trust boundary (§6f) | `Injection Acted On` | `AC_NOT_MET` — blocks merge |
|
|
20
|
+
| CLI registration (§2h) | `Failed` | `AC_NOT_MET` — blocks merge |
|
|
21
|
+
| Mutation verification (§6i) | `Failed` | `AC_NOT_MET` — blocks merge |
|
|
22
|
+
| Adversarial re-read (§6d) | `Severe Gap` | `AC_NOT_MET` — blocks merge |
|
|
23
|
+
| Skill verification (§6a) | `Failed` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
24
|
+
| Script execution evidence | `Incomplete` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
25
|
+
| Declared evidence (§6h) | `Incomplete` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
26
|
+
| Mutation verification (§6i) | `Missing` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
27
|
+
| Script verification (§11) | `Not Verified` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
28
|
+
| CHANGELOG entry (§10a) | both conditions true | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
29
|
+
| Quality plan (Phase 0b) | both conditions true | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
30
|
+
| Browser test | condition true | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
31
|
+
| Pending verifications | count `> 0` | `NEEDS_VERIFICATION` — holds for external verification |
|
|
32
|
+
| Quality plan (Phase 0b) | `Partial` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
33
|
+
| Smoke tests (§6b) | `Partial` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
34
|
+
| Detection patterns (§6c) | `Insufficient Samples` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
35
|
+
| Detection patterns (§6c) | `Skipped` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
36
|
+
| Adversarial re-read (§6d) | `Gaps Found` | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
37
|
+
| Improvement suggestions | list non-empty | `AC_MET_BUT_NOT_A_PLUS` — cannot be A+ |
|
|
38
|
+
<!-- END:DOD-GATES -->
|
|
24
39
|
|
|
25
|
-
|
|
26
|
-
- Review against acceptance criteria
|
|
27
|
-
- Run all quality checks
|
|
28
|
-
- Address feedback before merge
|
|
40
|
+
---
|
|
29
41
|
|
|
30
|
-
##
|
|
42
|
+
## 2. AC Authoring Standard
|
|
31
43
|
|
|
32
|
-
|
|
33
|
-
- Use descriptive variable and function names
|
|
34
|
-
- Follow language-specific conventions (camelCase, snake_case, etc.)
|
|
44
|
+
Referenced by `/spec`'s AC Quality Check step when flagging lint warnings.
|
|
35
45
|
|
|
36
|
-
###
|
|
37
|
-
- Handle errors gracefully
|
|
38
|
-
- Log meaningful error messages
|
|
39
|
-
- Don't swallow exceptions silently
|
|
46
|
+
### Format rules
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
- Write tests for new features
|
|
43
|
-
- Update tests when modifying existing code
|
|
44
|
-
- Test edge cases and error paths
|
|
48
|
+
**Write each AC on a single line.** The parser is line-anchored; an AC that wraps to a second line is silently truncated at the first newline, producing a partial description with no `Evidence:` or `Risk:` clause. This is the most common AC-hygiene defect in this repo.
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
**Required fields for testable ACs:**
|
|
47
51
|
|
|
48
|
-
|
|
|
49
|
-
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
| `/qa` | Quality review before merge |
|
|
54
|
-
| `/loop` | Fix iteration when tests fail |
|
|
55
|
-
| `/docs` | Generate feature documentation |
|
|
52
|
+
| Field | Purpose | Required when |
|
|
53
|
+
|-------|---------|---------------|
|
|
54
|
+
| `Evidence:` | Names the artifact that proves the AC was met | AC is verifiable by code review or test |
|
|
55
|
+
| `Risk:` | Names the failure mode if this AC is wrong | AC has a non-obvious failure mode |
|
|
56
|
+
| `Human decision` | Flags the AC as requiring human judgment | AC cannot be verified mechanically |
|
|
56
57
|
|
|
57
|
-
|
|
58
|
+
**Non-Goals section:** Every issue must have a `## Non-Goals` section. Scope without a boundary is unbounded scope. The Non-Goals section is where you declare what the issue explicitly does not do.
|
|
59
|
+
|
|
60
|
+
### Examples
|
|
61
|
+
|
|
62
|
+
**Bad** (three violations on one AC):
|
|
63
|
+
```
|
|
64
|
+
- [ ] **AC-3:** The feature should work correctly and handle errors
|
|
65
|
+
gracefully with good test coverage.
|
|
66
|
+
```
|
|
67
|
+
Problems: wraps to second line (parser truncates), "work correctly" is not measurable, no `Evidence:` clause.
|
|
68
|
+
|
|
69
|
+
**Good** (single line, measurable, evidence declared):
|
|
70
|
+
```
|
|
71
|
+
- [ ] **AC-3:** `/spec`'s AC Quality Check output references the constitution AC standard. Evidence: scoped gate test on the spec skill's reference string, mutation-verified.
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. Boundaries
|
|
77
|
+
|
|
78
|
+
Every rule below names its enforcing mechanism. Rules without a named enforcer are not in this section.
|
|
79
|
+
|
|
80
|
+
| Rule | Enforcing mechanism |
|
|
81
|
+
|------|---------------------|
|
|
82
|
+
| No force-push or amend on pushed branches | `templates/hooks/pre-tool.sh` (pre-tool hook, `HOOK_BLOCKED: Force push`) |
|
|
83
|
+
| No edits outside the issue worktree | `templates/hooks/pre-tool.sh` (worktree-only editing guard) |
|
|
84
|
+
| Gate tests must be mutation-verified | `/qa` §6i + `SEQUANT_MUTATION` marker in PR body (#939); `Missing` caps at `AC_MET_BUT_NOT_A_PLUS`, `Failed` floors at `AC_NOT_MET` |
|
|
85
|
+
| All §1 Definition of Done gates | `/qa` §7 verdict algorithm (see §1 above) |
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 4. Budgets & Stop Conditions
|
|
90
|
+
|
|
91
|
+
### Iteration and token caps
|
|
92
|
+
|
|
93
|
+
| Cap | Setting key | Default |
|
|
94
|
+
|-----|-------------|---------|
|
|
95
|
+
| Quality-loop max iterations | `run.maxIterations` | 3 |
|
|
96
|
+
| Auto-wait budget (rate-limit windows) | `run.autoWaitMinutes` | 0 (off) |
|
|
97
|
+
|
|
98
|
+
When a cap is reached, the run stops at the human merge gate rather than continuing indefinitely.
|
|
99
|
+
|
|
100
|
+
### Stop and hold states
|
|
101
|
+
|
|
102
|
+
The workflow stops or holds at these states — do not attempt to continue past them:
|
|
103
|
+
|
|
104
|
+
| State | Meaning | Action |
|
|
105
|
+
|-------|---------|--------|
|
|
106
|
+
| `waiting_for_human_merge` | All gates passed; PR is open | Human reviews and merges |
|
|
107
|
+
| `awaiting_verification` | At least one AC is `PENDING` external verification | Wait for the external signal; see [`docs/features/qa-verdict-workflow-states.md`](../../docs/features/qa-verdict-workflow-states.md) |
|
|
108
|
+
| `blocked` | A guard halted the run | Investigate the block; do not bypass |
|
|
109
|
+
|
|
110
|
+
### Gap-prompt discipline
|
|
111
|
+
|
|
112
|
+
**Diagnostic prompts are cheap and high-yield. Imperative prompts require triage first.**
|
|
113
|
+
|
|
114
|
+
- **Diagnostic** ("what are the gaps?"): run freely. Surfaces unknowns at low cost.
|
|
115
|
+
- **Imperative** ("fix all gaps"): requires triage first. An unfocused imperative prompt against a list of gaps produces shallow patches for every item rather than deep fixes for the real ones. The 2026-08 incident (#930) demonstrated this directly: an imperative "fix all gaps" run against a QA verdict produced 12 surface-level changes that passed re-QA but left the root-cause gap intact, requiring a third QA cycle.
|
|
116
|
+
|
|
117
|
+
When QA returns gaps: run a diagnostic first ("which gap is the most blocking?"), then issue a targeted imperative for that gap specifically.
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 5. Stack-Specific Notes
|
|
58
122
|
|
|
59
123
|
{{STACK_NOTES}}
|
|
60
124
|
|
|
61
|
-
|
|
125
|
+
---
|
|
62
126
|
|
|
63
|
-
|
|
127
|
+
## Project-Specific Notes
|
|
64
128
|
|
|
129
|
+
<!-- Add your project-specific guidelines below this line.
|
|
130
|
+
This section is preserved across `sequant update` and `sync` runs
|
|
131
|
+
(it is in CUSTOMIZABLE_FILES). Everything above is updated automatically. -->
|
|
@@ -107,6 +107,8 @@ Mark tier in HTML comment for downstream parsing: `<!-- SEQUANT_SPEC_TIER: [tier
|
|
|
107
107
|
| Open-ended | "etc.", "and more" | Scope undefined |
|
|
108
108
|
| Title/body tension | doc-noun title ("note", "comment", "snippet") + runtime-imperative body ("execute", "trigger", "capture", incl. inflections like `triggered`/`captured`, `run /<cmd>`); separators `.`/`\n`/`:`/`—` | Two different verification bars |
|
|
109
109
|
|
|
110
|
+
> For the house AC format rules (single-line constraint, `Evidence:`/`Risk:`/`Human decision` fields, Non-Goals section), see the constitution's §2 AC Authoring Standard (`.claude/memory/constitution.md`).
|
|
111
|
+
|
|
110
112
|
3. **Scope Assessment** (unless `--skip-scope-check`): Use `performScopeAssessment` from `./src/lib/scope/index.ts` with settings from `getSettings()`. Verdicts: SCOPE_OK (green), SCOPE_WARNING (yellow, auto-enables quality loop), SCOPE_SPLIT_RECOMMENDED (red). Store results in state.
|
|
111
113
|
|
|
112
114
|
### If guard fails (consumer projects):
|