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.
Files changed (33) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +34 -3
  4. package/dist/dashboard/server.js +4 -0
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +9 -0
  8. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  9. package/dist/src/commands/ready.js +3 -1
  10. package/dist/src/commands/status.js +4 -0
  11. package/dist/src/lib/settings.d.ts +35 -13
  12. package/dist/src/lib/settings.js +31 -3
  13. package/dist/src/lib/workflow/batch-executor.js +4 -1
  14. package/dist/src/lib/workflow/config-resolver.d.ts +30 -2
  15. package/dist/src/lib/workflow/config-resolver.js +58 -2
  16. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  17. package/dist/src/lib/workflow/drivers/claude-code.js +27 -1
  18. package/dist/src/lib/workflow/metrics-schema.d.ts +10 -1
  19. package/dist/src/lib/workflow/metrics-schema.js +13 -1
  20. package/dist/src/lib/workflow/metrics-writer.d.ts +3 -1
  21. package/dist/src/lib/workflow/phase-executor.js +10 -2
  22. package/dist/src/lib/workflow/reconcile.js +4 -2
  23. package/dist/src/lib/workflow/run-orchestrator.d.ts +18 -0
  24. package/dist/src/lib/workflow/run-orchestrator.js +38 -2
  25. package/dist/src/lib/workflow/state-cleanup.d.ts +4 -4
  26. package/dist/src/lib/workflow/state-cleanup.js +9 -5
  27. package/dist/src/lib/workflow/state-schema.d.ts +5 -0
  28. package/dist/src/lib/workflow/state-schema.js +5 -0
  29. package/dist/src/lib/workflow/types.d.ts +6 -0
  30. package/dist/src/mcp/tools/run.js +10 -1
  31. package/package.json +12 -11
  32. package/templates/memory/constitution.md +112 -45
  33. 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.12.0",
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.12.0",
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
- **Spec-driven AI coding agents — every acceptance criterion verified, stops at the human merge gate.**
3
+ **AI coding agents that prove their work — every acceptance criterion verified, and you hold the merge button.**
4
4
 
5
- For teams that can't ship un-reviewed AI code. Solve GitHub issues with structured phases and quality gates from issue to merge-ready PR.
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
  [![CI](https://github.com/sequant-io/sequant/actions/workflows/ci.yml/badge.svg)](https://github.com/sequant-io/sequant/actions/workflows/ci.yml)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](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
@@ -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.12.0",
4
+ "version": "2.13.0",
5
5
  "author": {
6
6
  "name": "sequant-io",
7
7
  "email": "hello@sequant.io"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "sequant": {
3
3
  "command": "npx",
4
- "args": ["-y", "sequant@2.12.0", "serve"]
4
+ "args": ["-y", "sequant@2.13.0", "serve"]
5
5
  }
6
6
  }
@@ -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
- const phasePolicies = resolvePhasePolicies(options.models, options.efforts, settings.run.phases, getPhaseNames());
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: "haiku" | "sonnet" | "opus";
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.ZodEnum<{
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.ZodEnum<{
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<{
@@ -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.enum(["haiku", "sonnet", "opus"]).default("haiku"),
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 ("haiku", "sonnet", "opus") — currently inert per anthropics/claude-code#43869`);
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\` | enum | \`"haiku"\` | Default model: \`"haiku"\`, \`"sonnet"\`, or \`"opus"\`. **Currently inert** per [anthropics/claude-code#43869](https://github.com/anthropics/claude-code/issues/43869) — subagents inherit the parent session's model. Kept for forward compatibility. |
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
- ? "ready_for_merge"
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
- phasePolicies: resolvePhasePolicies(mergedOptions.models, mergedOptions.efforts, settings.run.phases, getPhaseNames()),
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({ model: z.string().optional(), effort: z.string().optional() }))
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
- return mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
1148
+ const result = mapAgentSuccessToPhaseResult(phase, agentResult, durationSeconds, cwd);
1149
+ return resolvedModel ? { ...result, resolvedModel } : result;
1143
1150
  }
1144
- return mapAgentFailureToPhaseResult(phase, agentResult, durationSeconds);
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} --phase ${failedPhase[0]}`;
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} --phase qa`;
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
- phasePolicies: config.phasePolicies,
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 `waiting_for_qa_gate`) */
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`, or `waiting_for_qa_gate`
72
- * state and advances them to `merged` if their PRs are merged or their branches
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`, or `waiting_for_qa_gate`
192
- * state and advances them to `merged` if their PRs are merged or their branches
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). Note this list is deliberately WIDER than `isCompletedIssueStatus`
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.12.0",
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.",
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.142",
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.12.1",
103
+ "hono": "^4.13.4",
103
104
  "ink": "^7.0.1",
104
- "inquirer": "^14.0.1",
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.1",
118
+ "@types/node": "^26.3.0",
118
119
  "@types/react": "^19.2.14",
119
- "@typescript-eslint/eslint-plugin": "^8.58.0",
120
- "@typescript-eslint/parser": "^8.58.0",
121
- "eslint": "^10.1.0",
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.58.0",
127
- "vitest": "^4.1.0"
127
+ "typescript-eslint": "^8.68.0",
128
+ "vitest": "^4.1.11"
128
129
  }
129
130
  }
@@ -1,64 +1,131 @@
1
- # {{PROJECT_NAME}} Constitution
1
+ # {{PROJECT_NAME}} Agent Contract
2
2
 
3
- This document defines the core principles and patterns for AI-assisted development in this project.
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
- ## Core Principles
5
+ ---
6
6
 
7
- 1. **Quality First** - Never sacrifice code quality for speed
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
- ## Workflow Phases
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
- ### Phase 1: Planning (`/spec`)
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
- ### Phase 2: Implementation (`/exec`)
21
- - Work in feature worktree
22
- - Follow established patterns
23
- - Keep commits atomic and well-described
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
- ### Phase 3: Quality (`/qa`)
26
- - Review against acceptance criteria
27
- - Run all quality checks
28
- - Address feedback before merge
40
+ ---
29
41
 
30
- ## Code Standards
42
+ ## 2. AC Authoring Standard
31
43
 
32
- ### Naming Conventions
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
- ### Error Handling
37
- - Handle errors gracefully
38
- - Log meaningful error messages
39
- - Don't swallow exceptions silently
46
+ ### Format rules
40
47
 
41
- ### Testing
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
- ## Commands Available
50
+ **Required fields for testable ACs:**
47
51
 
48
- | Command | Purpose |
49
- |---------|---------|
50
- | `/spec` | Plan implementation for an issue |
51
- | `/exec` | Implement a planned feature |
52
- | `/test` | Run browser-based UI tests |
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
- ## Stack-Specific Notes
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
- ## Project-Specific Notes
125
+ ---
62
126
 
63
- <!-- Add your project-specific guidelines below -->
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):