sequant 2.11.0 → 2.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +47 -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/hooks/pre-tool.sh +331 -12
  9. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  10. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  11. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  12. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  13. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  14. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  15. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  16. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  17. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +42 -20
  18. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  19. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  20. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  21. package/dist/src/commands/doctor.js +20 -18
  22. package/dist/src/commands/ready.js +7 -1
  23. package/dist/src/commands/status.js +4 -0
  24. package/dist/src/lib/ac-linter.js +26 -0
  25. package/dist/src/lib/ac-parser.d.ts +40 -0
  26. package/dist/src/lib/ac-parser.js +202 -16
  27. package/dist/src/lib/markdown-fence.d.ts +24 -0
  28. package/dist/src/lib/markdown-fence.js +51 -0
  29. package/dist/src/lib/mcp-config.d.ts +24 -0
  30. package/dist/src/lib/mcp-config.js +51 -0
  31. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  32. package/dist/src/lib/scope/analyzer.js +7 -1
  33. package/dist/src/lib/settings.d.ts +73 -14
  34. package/dist/src/lib/settings.js +45 -3
  35. package/dist/src/lib/system.d.ts +7 -3
  36. package/dist/src/lib/system.js +7 -3
  37. package/dist/src/lib/test-tautology-detector.js +50 -3
  38. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  39. package/dist/src/lib/workflow/batch-executor.js +81 -6
  40. package/dist/src/lib/workflow/config-resolver.d.ts +30 -2
  41. package/dist/src/lib/workflow/config-resolver.js +59 -2
  42. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +14 -0
  43. package/dist/src/lib/workflow/drivers/claude-code.js +36 -4
  44. package/dist/src/lib/workflow/metrics-schema.d.ts +10 -1
  45. package/dist/src/lib/workflow/metrics-schema.js +13 -1
  46. package/dist/src/lib/workflow/metrics-writer.d.ts +3 -1
  47. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  48. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  49. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  50. package/dist/src/lib/workflow/phase-executor.js +60 -6
  51. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  52. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  53. package/dist/src/lib/workflow/ready-gate.d.ts +25 -1
  54. package/dist/src/lib/workflow/ready-gate.js +81 -11
  55. package/dist/src/lib/workflow/reconcile.js +4 -2
  56. package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
  57. package/dist/src/lib/workflow/run-log-schema.js +40 -0
  58. package/dist/src/lib/workflow/run-orchestrator.d.ts +18 -0
  59. package/dist/src/lib/workflow/run-orchestrator.js +38 -2
  60. package/dist/src/lib/workflow/state-cleanup.d.ts +4 -4
  61. package/dist/src/lib/workflow/state-cleanup.js +9 -5
  62. package/dist/src/lib/workflow/state-schema.d.ts +10 -1
  63. package/dist/src/lib/workflow/state-schema.js +13 -1
  64. package/dist/src/lib/workflow/types.d.ts +20 -0
  65. package/dist/src/mcp/tools/run.js +10 -1
  66. package/package.json +13 -12
  67. package/templates/hooks/pre-tool.sh +108 -17
  68. package/templates/memory/constitution.md +112 -45
  69. package/templates/skills/exec/SKILL.md +1 -1
  70. package/templates/skills/fullsolve/SKILL.md +62 -9
  71. package/templates/skills/loop/SKILL.md +71 -12
  72. package/templates/skills/merger/SKILL.md +32 -3
  73. package/templates/skills/qa/SKILL.md +247 -2
  74. package/templates/skills/spec/SKILL.md +11 -5
  75. package/templates/skills/test/SKILL.md +1 -1
@@ -4,6 +4,7 @@
4
4
  * Detects installed MCP clients (Claude Desktop, Cursor, VS Code)
5
5
  * and generates appropriate configuration entries for Sequant MCP server.
6
6
  */
7
+ import { type McpServerConfig } from "./system.js";
7
8
  /** Path to the project-level MCP config file used by Claude Code */
8
9
  export declare const PROJECT_MCP_JSON = ".mcp.json";
9
10
  /**
@@ -38,6 +39,29 @@ export declare function getSequantMcpConfig(options?: {
38
39
  projectDir?: string;
39
40
  clientType?: McpClientType;
40
41
  }): Record<string, unknown>;
42
+ /**
43
+ * Build the MCP server set for an autonomous phase agent (#936).
44
+ *
45
+ * Phase agents are a different trust domain from the interactive Claude
46
+ * Desktop app: they run unattended, and Claude Desktop configs cannot use
47
+ * `${VAR}` references, so they hold literal secrets that the SDK would
48
+ * otherwise serialize verbatim into the child process's `--mcp-config`
49
+ * argv. This builder allowlists instead of passing through — it unions the
50
+ * project's own `.mcp.json` (secret-free by convention, committed to git)
51
+ * with a guaranteed sequant server entry, and never reads
52
+ * `claude_desktop_config.json` **unless** a server name is explicitly
53
+ * listed in `opts.desktopAllowlist` (from `settings.run.mcpAllowlist`) —
54
+ * the deliberate per-server opt-in for a desktop-only server. A name not
55
+ * present in the desktop config is silently ignored.
56
+ *
57
+ * @param cwd - Directory to resolve `.mcp.json` from (the phase worktree)
58
+ * @param opts.desktopAllowlist - Exact `mcpServers` keys to pass through
59
+ * from Claude Desktop config, despite the default exclusion
60
+ * @returns MCP server configurations for the phase agent
61
+ */
62
+ export declare function getPhaseMcpServersConfig(cwd?: string, opts?: {
63
+ desktopAllowlist?: string[];
64
+ }): Record<string, McpServerConfig>;
41
65
  /**
42
66
  * Detect which MCP-compatible clients are installed
43
67
  */
@@ -8,6 +8,7 @@ import * as fs from "fs";
8
8
  import * as os from "os";
9
9
  import * as path from "path";
10
10
  import { getVersion } from "./version.js";
11
+ import { getMcpServersConfig } from "./system.js";
11
12
  /** Path to the project-level MCP config file used by Claude Code */
12
13
  export const PROJECT_MCP_JSON = ".mcp.json";
13
14
  /**
@@ -59,6 +60,56 @@ export function getSequantMcpConfig(options) {
59
60
  }
60
61
  return config;
61
62
  }
63
+ /**
64
+ * Build the MCP server set for an autonomous phase agent (#936).
65
+ *
66
+ * Phase agents are a different trust domain from the interactive Claude
67
+ * Desktop app: they run unattended, and Claude Desktop configs cannot use
68
+ * `${VAR}` references, so they hold literal secrets that the SDK would
69
+ * otherwise serialize verbatim into the child process's `--mcp-config`
70
+ * argv. This builder allowlists instead of passing through — it unions the
71
+ * project's own `.mcp.json` (secret-free by convention, committed to git)
72
+ * with a guaranteed sequant server entry, and never reads
73
+ * `claude_desktop_config.json` **unless** a server name is explicitly
74
+ * listed in `opts.desktopAllowlist` (from `settings.run.mcpAllowlist`) —
75
+ * the deliberate per-server opt-in for a desktop-only server. A name not
76
+ * present in the desktop config is silently ignored.
77
+ *
78
+ * @param cwd - Directory to resolve `.mcp.json` from (the phase worktree)
79
+ * @param opts.desktopAllowlist - Exact `mcpServers` keys to pass through
80
+ * from Claude Desktop config, despite the default exclusion
81
+ * @returns MCP server configurations for the phase agent
82
+ */
83
+ export function getPhaseMcpServersConfig(cwd, opts) {
84
+ const mcpJsonPath = path.resolve(cwd ?? ".", PROJECT_MCP_JSON);
85
+ let servers = {};
86
+ try {
87
+ const content = fs.readFileSync(mcpJsonPath, "utf-8");
88
+ const config = JSON.parse(content);
89
+ if (config.mcpServers &&
90
+ typeof config.mcpServers === "object" &&
91
+ !Array.isArray(config.mcpServers)) {
92
+ servers = config.mcpServers;
93
+ }
94
+ }
95
+ catch {
96
+ // .mcp.json doesn't exist or is invalid — sequant entry still applies
97
+ }
98
+ const allowedDesktopServers = {};
99
+ if (opts?.desktopAllowlist && opts.desktopAllowlist.length > 0) {
100
+ const desktopServers = getMcpServersConfig() ?? {};
101
+ for (const name of opts.desktopAllowlist) {
102
+ if (desktopServers[name]) {
103
+ allowedDesktopServers[name] = desktopServers[name];
104
+ }
105
+ }
106
+ }
107
+ return {
108
+ ...allowedDesktopServers,
109
+ ...servers,
110
+ sequant: getSequantMcpConfig(),
111
+ };
112
+ }
62
113
  /**
63
114
  * Detect which MCP-compatible clients are installed
64
115
  */
@@ -73,6 +73,10 @@ export declare function detectFeatures(criteria: AcceptanceCriterion[], title: s
73
73
  *
74
74
  * Looks for a "Non-Goals" or "Out of Scope" section with checkbox items.
75
75
  *
76
+ * Fenced code blocks are excluded before matching (#947 sibling) — a
77
+ * Non-Goals example shown inside a fence (e.g. illustrating checkbox
78
+ * syntax) must not itself be read as a real non-goal item.
79
+ *
76
80
  * @param issueBody - Full issue body markdown
77
81
  * @returns Non-goals extraction result
78
82
  */
@@ -16,6 +16,7 @@
16
16
  * ```
17
17
  */
18
18
  import { DEFAULT_SCOPE_CONFIG } from "./types.js";
19
+ import { stripFencedLines } from "../markdown-fence.js";
19
20
  /**
20
21
  * Keywords for clustering AC items by functional area
21
22
  */
@@ -247,14 +248,19 @@ export function detectFeatures(criteria, title) {
247
248
  *
248
249
  * Looks for a "Non-Goals" or "Out of Scope" section with checkbox items.
249
250
  *
251
+ * Fenced code blocks are excluded before matching (#947 sibling) — a
252
+ * Non-Goals example shown inside a fence (e.g. illustrating checkbox
253
+ * syntax) must not itself be read as a real non-goal item.
254
+ *
250
255
  * @param issueBody - Full issue body markdown
251
256
  * @returns Non-goals extraction result
252
257
  */
253
258
  export function parseNonGoals(issueBody) {
254
259
  const items = [];
260
+ const strippedBody = stripFencedLines(issueBody);
255
261
  // Find Non-Goals section (case-insensitive)
256
262
  const sectionPattern = /##\s*(?:Non[- ]?Goals|Out\s+of\s+Scope|Scope\s+Boundaries)\s*\n([\s\S]*?)(?=\n##|\n---|$)/i;
257
- const sectionMatch = issueBody.match(sectionPattern);
263
+ const sectionMatch = strippedBody.match(sectionPattern);
258
264
  if (!sectionMatch) {
259
265
  return {
260
266
  items: [],
@@ -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`.
@@ -117,11 +134,35 @@ export interface RunSettings {
117
134
  defaultBase?: string;
118
135
  /**
119
136
  * Enable MCP servers in headless mode.
120
- * When true, reads MCP config from Claude Desktop and passes to SDK.
137
+ * When true, injects the sequant MCP server plus any servers declared in
138
+ * the project's own `.mcp.json` — never a passthrough of the user's Claude
139
+ * Desktop config, which is a different trust domain and may carry literal
140
+ * secrets (#936), unless a server is explicitly named in `mcpAllowlist`
141
+ * below.
121
142
  * When false or --no-mcp flag is used, MCPs are disabled.
122
143
  * Default: true
123
144
  */
124
145
  mcp: boolean;
146
+ /**
147
+ * Explicit per-server opt-in to pass specific Claude Desktop MCP servers
148
+ * through to phase execution (#936).
149
+ *
150
+ * `mcp` above never reads Claude Desktop config by default — phase agents
151
+ * get the sequant server plus the project's own `.mcp.json` only. This is
152
+ * the deliberate escape hatch for a server that exists only in Claude
153
+ * Desktop config (e.g. never committed to git): list its exact
154
+ * `mcpServers` key here and phase agents additionally receive it. A name
155
+ * not present in the desktop config is silently ignored — this is a
156
+ * filter, not a requirement. Unset or empty (default): no desktop servers
157
+ * pass through, matching `mcp`'s secure-by-default behavior.
158
+ *
159
+ * ⚠️ Desktop Claude configs cannot use `${VAR}` references, so a server you
160
+ * allowlist here may carry a literal secret that reaches the phase
161
+ * process's argv (the SDK serializes `mcpServers` into `--mcp-config`).
162
+ * Only allowlist a server with no credential, or one whose credential you
163
+ * accept exposing to phase agents and to `ps`.
164
+ */
165
+ mcpAllowlist?: string[];
125
166
  /**
126
167
  * Enable automatic retry with MCP fallback.
127
168
  * When true (default), failed phases are retried with MCP disabled.
@@ -198,6 +239,26 @@ export interface RunSettings {
198
239
  * default). See `effort-escalation.ts` for the resolver.
199
240
  */
200
241
  effortEscalation: boolean;
242
+ /**
243
+ * Whether `/fullsolve`'s Phase 5.3 merges the PR automatically once QA
244
+ * passes (#958). Default `false` — the workflow stops at PR creation +
245
+ * final summary, preserving the human merge gate kept by #817–#819.
246
+ * Overridable per-invocation with `--auto-merge`. Read directly by the
247
+ * `/fullsolve` skill prose (not by any runtime code path — `sequant run`
248
+ * never merges regardless of this setting).
249
+ */
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;
201
262
  }
202
263
  /**
203
264
  * Scope assessment threshold configuration
@@ -329,11 +390,7 @@ export declare const AiderSettingsSchema: z.ZodObject<{
329
390
  /** Zod schema for AgentSettings */
330
391
  export declare const AgentSettingsSchema: z.ZodObject<{
331
392
  parallel: z.ZodDefault<z.ZodBoolean>;
332
- model: z.ZodDefault<z.ZodEnum<{
333
- haiku: "haiku";
334
- sonnet: "sonnet";
335
- opus: "opus";
336
- }>>;
393
+ model: z.ZodDefault<z.ZodString>;
337
394
  isolateParallel: z.ZodDefault<z.ZodBoolean>;
338
395
  }, z.core.$strip>;
339
396
  /**
@@ -373,6 +430,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
373
430
  }, z.core.$strip>>;
374
431
  defaultBase: z.ZodOptional<z.ZodString>;
375
432
  mcp: z.ZodDefault<z.ZodBoolean>;
433
+ mcpAllowlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
376
434
  retry: z.ZodDefault<z.ZodBoolean>;
377
435
  autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
378
436
  staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
@@ -397,6 +455,8 @@ export declare const RunSettingsSchema: z.ZodObject<{
397
455
  }>>;
398
456
  }, z.core.$strip>>>;
399
457
  effortEscalation: z.ZodDefault<z.ZodBoolean>;
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>]>>>;
400
460
  }, z.core.$strip>;
401
461
  /** Zod schema for ScopeThreshold (base — fields required, no defaults) */
402
462
  export declare const ScopeThresholdSchema: z.ZodObject<{
@@ -477,6 +537,7 @@ export declare const SettingsSchema: z.ZodObject<{
477
537
  }, z.core.$strip>>;
478
538
  defaultBase: z.ZodOptional<z.ZodString>;
479
539
  mcp: z.ZodDefault<z.ZodBoolean>;
540
+ mcpAllowlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
480
541
  retry: z.ZodDefault<z.ZodBoolean>;
481
542
  autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
482
543
  staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
@@ -501,14 +562,12 @@ export declare const SettingsSchema: z.ZodObject<{
501
562
  }>>;
502
563
  }, z.core.$strip>>>;
503
564
  effortEscalation: z.ZodDefault<z.ZodBoolean>;
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>]>>>;
504
567
  }, z.core.$strip>>;
505
568
  agents: z.ZodDefault<z.ZodObject<{
506
569
  parallel: z.ZodDefault<z.ZodBoolean>;
507
- model: z.ZodDefault<z.ZodEnum<{
508
- haiku: "haiku";
509
- sonnet: "sonnet";
510
- opus: "opus";
511
- }>>;
570
+ model: z.ZodDefault<z.ZodString>;
512
571
  isolateParallel: z.ZodDefault<z.ZodBoolean>;
513
572
  }, z.core.$strip>>;
514
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
  /**
@@ -78,6 +98,7 @@ export const RunSettingsSchema = z.object({
78
98
  rotation: RotationSettingsSchema.default(() => RotationSettingsSchema.parse({})),
79
99
  defaultBase: z.string().optional(),
80
100
  mcp: z.boolean().default(true),
101
+ mcpAllowlist: z.array(z.string()).optional(),
81
102
  retry: z.boolean().default(true),
82
103
  autoWaitMinutes: z.number().min(0).default(0),
83
104
  staleBranchThreshold: z.number().default(5),
@@ -104,6 +125,18 @@ export const RunSettingsSchema = z.object({
104
125
  * `effort-escalation.ts` for the resolver.
105
126
  */
106
127
  effortEscalation: z.boolean().default(false),
128
+ /**
129
+ * Whether `/fullsolve`'s Phase 5.3 merges the PR automatically once QA
130
+ * passes (#958). Default `false` — preserves the human merge gate kept by
131
+ * #817–#819. Overridable per-invocation with `--auto-merge`.
132
+ */
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 })),
107
140
  });
108
141
  /** Zod schema for ScopeThreshold (base — fields required, no defaults) */
109
142
  export const ScopeThresholdSchema = z.object({
@@ -216,6 +249,7 @@ const KNOWN_KEYS = {
216
249
  "rotation",
217
250
  "defaultBase",
218
251
  "mcp",
252
+ "mcpAllowlist",
219
253
  "retry",
220
254
  "staleBranchThreshold",
221
255
  "resolvedIssueTTL",
@@ -225,6 +259,7 @@ const KNOWN_KEYS = {
225
259
  "aider",
226
260
  "relay",
227
261
  "phases",
262
+ "modelRoles",
228
263
  ]),
229
264
  // #914: keyed by real phase name so a typo (`run.phases.exce`) warns
230
265
  // instead of silently resolving to nothing. Computed from the registry
@@ -451,6 +486,8 @@ export const DEFAULT_SETTINGS = {
451
486
  resolvedIssueTTL: 7, // Auto-prune resolved issues after 7 days
452
487
  relay: true, // Enable interactive relay (#383) by default
453
488
  effortEscalation: false, // #915: off by default — raises token spend
489
+ autoMerge: false, // #958: off by default — preserves the human merge gate
490
+ modelRoles: DEFAULT_MODEL_ROLES, // #975: shipped defaults; absent key → these
454
491
  },
455
492
  agents: DEFAULT_AGENT_SETTINGS,
456
493
  scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
@@ -581,6 +618,10 @@ export function generateSettingsJsonc(settings) {
581
618
  lines.push(` "smartTests": ${JSON.stringify(settings.run.smartTests)},`);
582
619
  lines.push(` // Enable MCP servers in headless mode`);
583
620
  lines.push(` "mcp": ${JSON.stringify(settings.run.mcp)},`);
621
+ if (settings.run.mcpAllowlist !== undefined) {
622
+ lines.push(` // Desktop MCP servers explicitly allowlisted for phases`);
623
+ lines.push(` "mcpAllowlist": ${JSON.stringify(settings.run.mcpAllowlist)},`);
624
+ }
584
625
  lines.push(` // Enable automatic retry with MCP fallback`);
585
626
  lines.push(` "retry": ${JSON.stringify(settings.run.retry)},`);
586
627
  lines.push(` // Commits behind main before warning`);
@@ -603,7 +644,7 @@ export function generateSettingsJsonc(settings) {
603
644
  lines.push(` "agents": {`);
604
645
  lines.push(` // Run agents in parallel (faster, higher token usage)`);
605
646
  lines.push(` "parallel": ${JSON.stringify(settings.agents.parallel)},`);
606
- 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`);
607
648
  lines.push(` "model": ${JSON.stringify(settings.agents.model)},`);
608
649
  lines.push(` // Isolate parallel agent groups in separate worktrees`);
609
650
  lines.push(` "isolateParallel": ${JSON.stringify(settings.agents.isolateParallel)}`);
@@ -698,6 +739,7 @@ Generated by \`sequant init\`. See defaults below.
698
739
  | \`smartTests\` | boolean | \`true\` | Enable smart test detection |
699
740
  | \`defaultBase\` | string | — | Default base branch for worktree creation |
700
741
  | \`mcp\` | boolean | \`true\` | Enable MCP servers in headless mode |
742
+ | \`mcpAllowlist\` | string[] | — | Desktop MCP server names to pass through to phases despite \`mcp\`'s default exclusion (#936) |
701
743
  | \`retry\` | boolean | \`true\` | Enable automatic retry with MCP fallback |
702
744
  | \`staleBranchThreshold\` | number | \`5\` | Commits behind main before warning |
703
745
  | \`resolvedIssueTTL\` | number | \`7\` | Days before resolved issues auto-prune (0=never, -1=immediate) |
@@ -724,7 +766,7 @@ Generated by \`sequant init\`. See defaults below.
724
766
  | Key | Type | Default | Description |
725
767
  |-----|------|---------|-------------|
726
768
  | \`parallel\` | boolean | \`false\` | Run agents in parallel (faster, higher token usage) |
727
- | \`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). |
728
770
  | \`isolateParallel\` | boolean | \`false\` | Isolate parallel agents in separate worktrees |
729
771
 
730
772
  ## \`scopeAssessment\` — Scope Assessment Settings
@@ -54,9 +54,13 @@ export interface McpServerConfig {
54
54
  /**
55
55
  * Get full MCP server configurations from Claude Desktop config
56
56
  *
57
- * Returns the complete mcpServers object suitable for passing to the
58
- * Claude Agent SDK query() options. Returns undefined if config doesn't
59
- * exist or is invalid (graceful degradation for AC-3).
57
+ * For interactive `doctor` diagnostics only reports what's configured for
58
+ * the human user. Never pass this to phase execution (#936): Claude Desktop
59
+ * is a different trust domain (interactive, human-supervised) than an
60
+ * autonomous phase agent, and desktop configs carry literal secrets since
61
+ * they cannot use `${VAR}` references. Phase agents use
62
+ * `getPhaseMcpServersConfig` from `mcp-config.ts` instead, which allowlists
63
+ * from the project's own `.mcp.json`.
60
64
  *
61
65
  * @returns MCP server configurations or undefined
62
66
  */
@@ -136,9 +136,13 @@ export function getConfiguredMcpServers() {
136
136
  /**
137
137
  * Get full MCP server configurations from Claude Desktop config
138
138
  *
139
- * Returns the complete mcpServers object suitable for passing to the
140
- * Claude Agent SDK query() options. Returns undefined if config doesn't
141
- * exist or is invalid (graceful degradation for AC-3).
139
+ * For interactive `doctor` diagnostics only reports what's configured for
140
+ * the human user. Never pass this to phase execution (#936): Claude Desktop
141
+ * is a different trust domain (interactive, human-supervised) than an
142
+ * autonomous phase agent, and desktop configs carry literal secrets since
143
+ * they cannot use `${VAR}` references. Phase agents use
144
+ * `getPhaseMcpServersConfig` from `mcp-config.ts` instead, which allowlists
145
+ * from the project's own `.mcp.json`.
142
146
  *
143
147
  * @returns MCP server configurations or undefined
144
148
  */
@@ -390,12 +390,20 @@ const PROJECT_SCRIPT_PATTERN = /\b(?:hooks\/[\w.-]+\.sh|scripts\/[\w./-]+)/;
390
390
  * across declarations, and must reach one of the two markers. Spawning a
391
391
  * *system* binary (`git`, `bash` with a temp fixture) matches neither, which
392
392
  * is the intended exclusion — those are not this project's code.
393
+ *
394
+ * An optional type annotation is allowed between the name and `=`, e.g.
395
+ * const HOOK_COPIES: Array<[label: string, path: string]> = [...];
396
+ * The annotation submatch (`[^=;]*`) stops at the first `=`, so an arrow-
397
+ * function-typed annotation (`const f: (x: string) => void = ...`) breaks
398
+ * the match at its `=>` instead of reaching the real assignment — accepted
399
+ * as a narrow miss; typed function-value declarations are not the shape this
400
+ * collector targets (path-bearing table/tuple declarations are).
393
401
  */
394
402
  function collectBuildOutputVars(content) {
395
403
  const names = new Set();
396
404
  const patterns = [
397
- /(?:const|let|var)\s+(\w+)\s*=\s*[^;]*?\bdist\//g,
398
- /(?:const|let|var)\s+(\w+)\s*=\s*[^;]*?\b(?:hooks\/[\w.-]+\.sh|scripts\/[\w./-]+)/g,
405
+ /(?:const|let|var)\s+(\w+)\s*(?::[^=;]*)?=\s*[^;]*?\bdist\//g,
406
+ /(?:const|let|var)\s+(\w+)\s*(?::[^=;]*)?=\s*[^;]*?\b(?:hooks\/[\w.-]+\.sh|scripts\/[\w./-]+)/g,
399
407
  ];
400
408
  for (const pattern of patterns) {
401
409
  let match;
@@ -405,6 +413,44 @@ function collectBuildOutputVars(content) {
405
413
  }
406
414
  return [...names];
407
415
  }
416
+ /**
417
+ * Collect callback parameter names bound to a build-output table's rows via
418
+ * `describe.each(X)("...", (a, b) => {...})`, when `X` is itself a
419
+ * build-output source: a var already collected by collectBuildOutputVars, or
420
+ * an inline array literal containing a build-output token directly.
421
+ *
422
+ * `describe.each` destructures each table row into positional callback
423
+ * params. A helper spawning `hookPath` — the param, not the table var — is
424
+ * exercising production code just as much as one spawning `HOOK_COPIES`
425
+ * directly; static analysis of the table alone misses it entirely. The
426
+ * string-title argument is matched by quote char (not `[^()]*`) because test
427
+ * titles routinely contain literal parens (e.g. `"... (#564) [%s]"`), which
428
+ * a paren-excluding class would truncate on.
429
+ *
430
+ * Bounded like the rest of this file's helper matchers: no nested parens in
431
+ * the table-var/params captures, so a callback with a destructured or
432
+ * default-valued param is skipped rather than mis-parsed.
433
+ */
434
+ function collectDescribeEachParams(content, buildOutputVars) {
435
+ const names = new Set();
436
+ const pattern = /describe\.each\(\s*([^()]*?)\s*\)\s*\(\s*(['"`])(?:(?!\2)[\s\S])*?\2\s*,\s*(?:async\s+)?\(([^()]*)\)\s*=>/g;
437
+ let match;
438
+ while ((match = pattern.exec(content)) !== null) {
439
+ const tableArg = match[1].trim();
440
+ const isKnownVar = /^\w+$/.test(tableArg) && buildOutputVars.includes(tableArg);
441
+ const isInlineSource = BUILD_OUTPUT_PATTERN.test(tableArg) ||
442
+ PROJECT_SCRIPT_PATTERN.test(tableArg);
443
+ if (!isKnownVar && !isInlineSource)
444
+ continue;
445
+ for (const param of match[3].split(",")) {
446
+ const name = param.trim().split(":")[0].trim();
447
+ if (/^\w+$/.test(name)) {
448
+ names.add(name);
449
+ }
450
+ }
451
+ }
452
+ return [...names];
453
+ }
408
454
  /**
409
455
  * Whether a code body references a build-output token: either the literal
410
456
  * `dist/` marker or one of the collected build-path variable names.
@@ -588,7 +634,8 @@ export function analyzeTestFile(content, filePath) {
588
634
  }
589
635
  try {
590
636
  const importedFunctions = extractImports(content);
591
- const buildOutputVars = collectBuildOutputVars(content);
637
+ const declaredBuildOutputVars = collectBuildOutputVars(content);
638
+ const buildOutputVars = declaredBuildOutputVars.concat(collectDescribeEachParams(content, declaredBuildOutputVars));
592
639
  const productionHandles = collectProductionHandles(content, buildOutputVars, importedFunctions);
593
640
  const testBlocks = extractTestBlocks(content);
594
641
  const analyzedBlocks = testBlocks.map((block) => ({
@@ -8,7 +8,7 @@
8
8
  * creation).
9
9
  */
10
10
  import { LogWriter } from "./log-writer.js";
11
- import { ExecutionConfig, PhaseResult, IssueResult, type RunOptions, type IssueExecutionContext, type BatchExecutionContext, type ProgressCallback } from "./types.js";
11
+ import { ExecutionConfig, PhaseResult, IssueResult, type RunOptions, type IssueExecutionContext, type BatchExecutionContext, type ProgressCallback, type QaVerdict, type QaSummary } from "./types.js";
12
12
  import { type ErrorCategory } from "./error-classifier.js";
13
13
  export type { RunOptions, ProgressCallback, IssueExecutionContext, BatchExecutionContext, } from "./types.js";
14
14
  /**
@@ -205,4 +205,23 @@ export declare function billingHaltReason(result: PhaseResult): string;
205
205
  * @internal Exported for testing
206
206
  */
207
207
  export declare function windowHaltResumeAtMs(result: PhaseResult): number | null;
208
+ /**
209
+ * Build the comment body for a standard-qa-phase verdict post (#964).
210
+ * Includes AC coverage and any gaps/suggestions from the parsed `QaSummary`,
211
+ * plus a machine marker so a future dedup pass has an anchor.
212
+ * @internal Exported for testing.
213
+ */
214
+ export declare function buildQaVerdictComment(verdict: QaVerdict, summary: QaSummary | undefined, commitHash: string | undefined, iteration: number): string;
215
+ /**
216
+ * Post the qa-verdict comment for a standard (non-ready-gate) qa phase under
217
+ * orchestrated `sequant run` (#964). This is the channel `qa/SKILL.md` §9
218
+ * promises ("orchestrator handles aggregated summary") but batch-executor
219
+ * never backed — a re-run producing a fresh, different verdict left a stale,
220
+ * contradicted comment as the only externally-visible one.
221
+ *
222
+ * Best-effort: a post failure is caught and logged, never fails the run —
223
+ * mirrors {@link runReadyGateForIssue}'s `postReport` contract (#937 AC-4).
224
+ * @internal Exported for testing.
225
+ */
226
+ export declare function postQaVerdictComment(issueNumber: number, verdict: QaVerdict, summary: QaSummary | undefined, commitHash: string | undefined, iteration: number, log: (msg: string) => void, postComment?: (issueNumber: number, body: string) => Promise<void>): Promise<void>;
208
227
  export declare function runIssueWithLogging(ctx: IssueExecutionContext): Promise<IssueResult>;