sequant 2.11.0 → 2.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +13 -0
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
- package/dist/src/commands/doctor.js +20 -18
- package/dist/src/commands/ready.js +4 -0
- package/dist/src/lib/ac-linter.js +26 -0
- package/dist/src/lib/ac-parser.d.ts +40 -0
- package/dist/src/lib/ac-parser.js +202 -16
- package/dist/src/lib/markdown-fence.d.ts +24 -0
- package/dist/src/lib/markdown-fence.js +51 -0
- package/dist/src/lib/mcp-config.d.ts +24 -0
- package/dist/src/lib/mcp-config.js +51 -0
- package/dist/src/lib/scope/analyzer.d.ts +4 -0
- package/dist/src/lib/scope/analyzer.js +7 -1
- package/dist/src/lib/settings.d.ts +38 -1
- package/dist/src/lib/settings.js +14 -0
- package/dist/src/lib/system.d.ts +7 -3
- package/dist/src/lib/system.js +7 -3
- package/dist/src/lib/test-tautology-detector.js +50 -3
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +77 -5
- package/dist/src/lib/workflow/config-resolver.js +1 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +9 -3
- package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
- package/dist/src/lib/workflow/mutation-marker.js +97 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
- package/dist/src/lib/workflow/phase-executor.js +50 -4
- package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
- package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
- package/dist/src/lib/workflow/ready-gate.d.ts +25 -1
- package/dist/src/lib/workflow/ready-gate.js +81 -11
- package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
- package/dist/src/lib/workflow/run-log-schema.js +40 -0
- package/dist/src/lib/workflow/state-schema.d.ts +5 -1
- package/dist/src/lib/workflow/state-schema.js +8 -1
- package/dist/src/lib/workflow/types.d.ts +14 -0
- package/package.json +2 -2
- package/templates/hooks/pre-tool.sh +108 -17
- package/templates/skills/exec/SKILL.md +1 -1
- package/templates/skills/fullsolve/SKILL.md +62 -9
- package/templates/skills/loop/SKILL.md +71 -12
- package/templates/skills/merger/SKILL.md +32 -3
- package/templates/skills/qa/SKILL.md +247 -2
- package/templates/skills/spec/SKILL.md +9 -5
- package/templates/skills/test/SKILL.md +1 -1
|
@@ -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 =
|
|
263
|
+
const sectionMatch = strippedBody.match(sectionPattern);
|
|
258
264
|
if (!sectionMatch) {
|
|
259
265
|
return {
|
|
260
266
|
items: [],
|
|
@@ -117,11 +117,35 @@ export interface RunSettings {
|
|
|
117
117
|
defaultBase?: string;
|
|
118
118
|
/**
|
|
119
119
|
* Enable MCP servers in headless mode.
|
|
120
|
-
* When true,
|
|
120
|
+
* When true, injects the sequant MCP server plus any servers declared in
|
|
121
|
+
* the project's own `.mcp.json` — never a passthrough of the user's Claude
|
|
122
|
+
* Desktop config, which is a different trust domain and may carry literal
|
|
123
|
+
* secrets (#936), unless a server is explicitly named in `mcpAllowlist`
|
|
124
|
+
* below.
|
|
121
125
|
* When false or --no-mcp flag is used, MCPs are disabled.
|
|
122
126
|
* Default: true
|
|
123
127
|
*/
|
|
124
128
|
mcp: boolean;
|
|
129
|
+
/**
|
|
130
|
+
* Explicit per-server opt-in to pass specific Claude Desktop MCP servers
|
|
131
|
+
* through to phase execution (#936).
|
|
132
|
+
*
|
|
133
|
+
* `mcp` above never reads Claude Desktop config by default — phase agents
|
|
134
|
+
* get the sequant server plus the project's own `.mcp.json` only. This is
|
|
135
|
+
* the deliberate escape hatch for a server that exists only in Claude
|
|
136
|
+
* Desktop config (e.g. never committed to git): list its exact
|
|
137
|
+
* `mcpServers` key here and phase agents additionally receive it. A name
|
|
138
|
+
* not present in the desktop config is silently ignored — this is a
|
|
139
|
+
* filter, not a requirement. Unset or empty (default): no desktop servers
|
|
140
|
+
* pass through, matching `mcp`'s secure-by-default behavior.
|
|
141
|
+
*
|
|
142
|
+
* ⚠️ Desktop Claude configs cannot use `${VAR}` references, so a server you
|
|
143
|
+
* allowlist here may carry a literal secret that reaches the phase
|
|
144
|
+
* process's argv (the SDK serializes `mcpServers` into `--mcp-config`).
|
|
145
|
+
* Only allowlist a server with no credential, or one whose credential you
|
|
146
|
+
* accept exposing to phase agents and to `ps`.
|
|
147
|
+
*/
|
|
148
|
+
mcpAllowlist?: string[];
|
|
125
149
|
/**
|
|
126
150
|
* Enable automatic retry with MCP fallback.
|
|
127
151
|
* When true (default), failed phases are retried with MCP disabled.
|
|
@@ -198,6 +222,15 @@ export interface RunSettings {
|
|
|
198
222
|
* default). See `effort-escalation.ts` for the resolver.
|
|
199
223
|
*/
|
|
200
224
|
effortEscalation: boolean;
|
|
225
|
+
/**
|
|
226
|
+
* Whether `/fullsolve`'s Phase 5.3 merges the PR automatically once QA
|
|
227
|
+
* passes (#958). Default `false` — the workflow stops at PR creation +
|
|
228
|
+
* final summary, preserving the human merge gate kept by #817–#819.
|
|
229
|
+
* Overridable per-invocation with `--auto-merge`. Read directly by the
|
|
230
|
+
* `/fullsolve` skill prose (not by any runtime code path — `sequant run`
|
|
231
|
+
* never merges regardless of this setting).
|
|
232
|
+
*/
|
|
233
|
+
autoMerge: boolean;
|
|
201
234
|
}
|
|
202
235
|
/**
|
|
203
236
|
* Scope assessment threshold configuration
|
|
@@ -373,6 +406,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
|
|
|
373
406
|
}, z.core.$strip>>;
|
|
374
407
|
defaultBase: z.ZodOptional<z.ZodString>;
|
|
375
408
|
mcp: z.ZodDefault<z.ZodBoolean>;
|
|
409
|
+
mcpAllowlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
376
410
|
retry: z.ZodDefault<z.ZodBoolean>;
|
|
377
411
|
autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
|
|
378
412
|
staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
|
|
@@ -397,6 +431,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
|
|
|
397
431
|
}>>;
|
|
398
432
|
}, z.core.$strip>>>;
|
|
399
433
|
effortEscalation: z.ZodDefault<z.ZodBoolean>;
|
|
434
|
+
autoMerge: z.ZodDefault<z.ZodBoolean>;
|
|
400
435
|
}, z.core.$strip>;
|
|
401
436
|
/** Zod schema for ScopeThreshold (base — fields required, no defaults) */
|
|
402
437
|
export declare const ScopeThresholdSchema: z.ZodObject<{
|
|
@@ -477,6 +512,7 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
477
512
|
}, z.core.$strip>>;
|
|
478
513
|
defaultBase: z.ZodOptional<z.ZodString>;
|
|
479
514
|
mcp: z.ZodDefault<z.ZodBoolean>;
|
|
515
|
+
mcpAllowlist: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
480
516
|
retry: z.ZodDefault<z.ZodBoolean>;
|
|
481
517
|
autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
|
|
482
518
|
staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
|
|
@@ -501,6 +537,7 @@ export declare const SettingsSchema: z.ZodObject<{
|
|
|
501
537
|
}>>;
|
|
502
538
|
}, z.core.$strip>>>;
|
|
503
539
|
effortEscalation: z.ZodDefault<z.ZodBoolean>;
|
|
540
|
+
autoMerge: z.ZodDefault<z.ZodBoolean>;
|
|
504
541
|
}, z.core.$strip>>;
|
|
505
542
|
agents: z.ZodDefault<z.ZodObject<{
|
|
506
543
|
parallel: z.ZodDefault<z.ZodBoolean>;
|
package/dist/src/lib/settings.js
CHANGED
|
@@ -78,6 +78,7 @@ export const RunSettingsSchema = z.object({
|
|
|
78
78
|
rotation: RotationSettingsSchema.default(() => RotationSettingsSchema.parse({})),
|
|
79
79
|
defaultBase: z.string().optional(),
|
|
80
80
|
mcp: z.boolean().default(true),
|
|
81
|
+
mcpAllowlist: z.array(z.string()).optional(),
|
|
81
82
|
retry: z.boolean().default(true),
|
|
82
83
|
autoWaitMinutes: z.number().min(0).default(0),
|
|
83
84
|
staleBranchThreshold: z.number().default(5),
|
|
@@ -104,6 +105,12 @@ export const RunSettingsSchema = z.object({
|
|
|
104
105
|
* `effort-escalation.ts` for the resolver.
|
|
105
106
|
*/
|
|
106
107
|
effortEscalation: z.boolean().default(false),
|
|
108
|
+
/**
|
|
109
|
+
* Whether `/fullsolve`'s Phase 5.3 merges the PR automatically once QA
|
|
110
|
+
* passes (#958). Default `false` — preserves the human merge gate kept by
|
|
111
|
+
* #817–#819. Overridable per-invocation with `--auto-merge`.
|
|
112
|
+
*/
|
|
113
|
+
autoMerge: z.boolean().default(false),
|
|
107
114
|
});
|
|
108
115
|
/** Zod schema for ScopeThreshold (base — fields required, no defaults) */
|
|
109
116
|
export const ScopeThresholdSchema = z.object({
|
|
@@ -216,6 +223,7 @@ const KNOWN_KEYS = {
|
|
|
216
223
|
"rotation",
|
|
217
224
|
"defaultBase",
|
|
218
225
|
"mcp",
|
|
226
|
+
"mcpAllowlist",
|
|
219
227
|
"retry",
|
|
220
228
|
"staleBranchThreshold",
|
|
221
229
|
"resolvedIssueTTL",
|
|
@@ -451,6 +459,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
451
459
|
resolvedIssueTTL: 7, // Auto-prune resolved issues after 7 days
|
|
452
460
|
relay: true, // Enable interactive relay (#383) by default
|
|
453
461
|
effortEscalation: false, // #915: off by default — raises token spend
|
|
462
|
+
autoMerge: false, // #958: off by default — preserves the human merge gate
|
|
454
463
|
},
|
|
455
464
|
agents: DEFAULT_AGENT_SETTINGS,
|
|
456
465
|
scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
|
|
@@ -581,6 +590,10 @@ export function generateSettingsJsonc(settings) {
|
|
|
581
590
|
lines.push(` "smartTests": ${JSON.stringify(settings.run.smartTests)},`);
|
|
582
591
|
lines.push(` // Enable MCP servers in headless mode`);
|
|
583
592
|
lines.push(` "mcp": ${JSON.stringify(settings.run.mcp)},`);
|
|
593
|
+
if (settings.run.mcpAllowlist !== undefined) {
|
|
594
|
+
lines.push(` // Desktop MCP servers explicitly allowlisted for phases`);
|
|
595
|
+
lines.push(` "mcpAllowlist": ${JSON.stringify(settings.run.mcpAllowlist)},`);
|
|
596
|
+
}
|
|
584
597
|
lines.push(` // Enable automatic retry with MCP fallback`);
|
|
585
598
|
lines.push(` "retry": ${JSON.stringify(settings.run.retry)},`);
|
|
586
599
|
lines.push(` // Commits behind main before warning`);
|
|
@@ -698,6 +711,7 @@ Generated by \`sequant init\`. See defaults below.
|
|
|
698
711
|
| \`smartTests\` | boolean | \`true\` | Enable smart test detection |
|
|
699
712
|
| \`defaultBase\` | string | — | Default base branch for worktree creation |
|
|
700
713
|
| \`mcp\` | boolean | \`true\` | Enable MCP servers in headless mode |
|
|
714
|
+
| \`mcpAllowlist\` | string[] | — | Desktop MCP server names to pass through to phases despite \`mcp\`'s default exclusion (#936) |
|
|
701
715
|
| \`retry\` | boolean | \`true\` | Enable automatic retry with MCP fallback |
|
|
702
716
|
| \`staleBranchThreshold\` | number | \`5\` | Commits behind main before warning |
|
|
703
717
|
| \`resolvedIssueTTL\` | number | \`7\` | Days before resolved issues auto-prune (0=never, -1=immediate) |
|
package/dist/src/lib/system.d.ts
CHANGED
|
@@ -54,9 +54,13 @@ export interface McpServerConfig {
|
|
|
54
54
|
/**
|
|
55
55
|
* Get full MCP server configurations from Claude Desktop config
|
|
56
56
|
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
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
|
*/
|
package/dist/src/lib/system.js
CHANGED
|
@@ -136,9 +136,13 @@ export function getConfiguredMcpServers() {
|
|
|
136
136
|
/**
|
|
137
137
|
* Get full MCP server configurations from Claude Desktop config
|
|
138
138
|
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
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
|
|
398
|
-
/(?:const|let|var)\s+(\w+)\s
|
|
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
|
|
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>;
|
|
@@ -14,7 +14,7 @@ import { withEscalatedEffort } from "./effort-escalation.js";
|
|
|
14
14
|
import { classifyError, errorTypeToCategory, } from "./error-classifier.js";
|
|
15
15
|
import { getGitDiffStats, getCommitHash, resolveDiffBase, } from "./git-diff-utils.js";
|
|
16
16
|
import { createCheckpointCommit, rebaseBeforePR, createPR, readCacheMetrics, filterResumedPhases, } from "./worktree-manager.js";
|
|
17
|
-
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, hasExecChanges, isWindowExhaustedRateLimit, } from "./phase-executor.js";
|
|
17
|
+
import { AUTO_WAIT_BUFFER_MS, createAutoWaitLedger, executePhaseWithRetry, hasExecChanges, isWindowExhaustedRateLimit, selectFixableGaps, } from "./phase-executor.js";
|
|
18
18
|
import { BillingError, RateLimitError, resetsAtToMs } from "../errors.js";
|
|
19
19
|
import { parseBodyDependencyMarkers } from "./dependency-markers.js";
|
|
20
20
|
import { determinePhasesForIssue, DOCS_LABELS } from "./phase-mapper.js";
|
|
@@ -135,8 +135,11 @@ export function buildLoopContext(failedResult) {
|
|
|
135
135
|
if (failedResult.verdict) {
|
|
136
136
|
parts.push(`QA Verdict: ${failedResult.verdict}`);
|
|
137
137
|
}
|
|
138
|
-
|
|
139
|
-
|
|
138
|
+
// #937 AC-3: exclude findings marked `document`/`pause_for_human` — those
|
|
139
|
+
// are QA-real but not something a fix loop should chase.
|
|
140
|
+
const fixableGaps = selectFixableGaps(failedResult.summary);
|
|
141
|
+
if (fixableGaps.length) {
|
|
142
|
+
parts.push(`QA Gaps:\n${fixableGaps.map((gap) => `- ${gap}`).join("\n")}`);
|
|
140
143
|
}
|
|
141
144
|
if (failedResult.summary?.suggestions?.length) {
|
|
142
145
|
parts.push(`Suggestions:\n${failedResult.summary.suggestions.map((s) => `- ${s}`).join("\n")}`);
|
|
@@ -564,6 +567,50 @@ async function recordWindowHaltState(stateManager, issueNumber, phase, result) {
|
|
|
564
567
|
// State tracking errors shouldn't stop execution
|
|
565
568
|
}
|
|
566
569
|
}
|
|
570
|
+
/**
|
|
571
|
+
* Build the comment body for a standard-qa-phase verdict post (#964).
|
|
572
|
+
* Includes AC coverage and any gaps/suggestions from the parsed `QaSummary`,
|
|
573
|
+
* plus a machine marker so a future dedup pass has an anchor.
|
|
574
|
+
* @internal Exported for testing.
|
|
575
|
+
*/
|
|
576
|
+
export function buildQaVerdictComment(verdict, summary, commitHash, iteration) {
|
|
577
|
+
const lines = [`## QA Verdict: ${verdict}`];
|
|
578
|
+
if (summary) {
|
|
579
|
+
lines.push("", `AC coverage: ${summary.acMet}/${summary.acTotal} met`);
|
|
580
|
+
if (summary.gaps.length > 0) {
|
|
581
|
+
lines.push("", "**Gaps:**", ...summary.gaps.map((g) => `- ${g}`));
|
|
582
|
+
}
|
|
583
|
+
if (summary.suggestions.length > 0) {
|
|
584
|
+
lines.push("", "**Suggestions:**", ...summary.suggestions.map((s) => `- ${s}`));
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
lines.push("", `<!-- SEQUANT_QA_VERDICT: ${JSON.stringify({
|
|
588
|
+
verdict,
|
|
589
|
+
commit: commitHash ?? null,
|
|
590
|
+
iteration,
|
|
591
|
+
})} -->`);
|
|
592
|
+
return lines.join("\n");
|
|
593
|
+
}
|
|
594
|
+
/**
|
|
595
|
+
* Post the qa-verdict comment for a standard (non-ready-gate) qa phase under
|
|
596
|
+
* orchestrated `sequant run` (#964). This is the channel `qa/SKILL.md` §9
|
|
597
|
+
* promises ("orchestrator handles aggregated summary") but batch-executor
|
|
598
|
+
* never backed — a re-run producing a fresh, different verdict left a stale,
|
|
599
|
+
* contradicted comment as the only externally-visible one.
|
|
600
|
+
*
|
|
601
|
+
* Best-effort: a post failure is caught and logged, never fails the run —
|
|
602
|
+
* mirrors {@link runReadyGateForIssue}'s `postReport` contract (#937 AC-4).
|
|
603
|
+
* @internal Exported for testing.
|
|
604
|
+
*/
|
|
605
|
+
export async function postQaVerdictComment(issueNumber, verdict, summary, commitHash, iteration, log, postComment = (n, body) => new GitHubProvider().postComment(String(n), body)) {
|
|
606
|
+
try {
|
|
607
|
+
const body = buildQaVerdictComment(verdict, summary, commitHash, iteration);
|
|
608
|
+
await postComment(issueNumber, body);
|
|
609
|
+
}
|
|
610
|
+
catch (err) {
|
|
611
|
+
log(chalk.yellow(` ! Failed to post QA verdict comment: ${err}`));
|
|
612
|
+
}
|
|
613
|
+
}
|
|
567
614
|
/**
|
|
568
615
|
* Run the post-QA ready gate (#817) for a single issue at the run path's
|
|
569
616
|
* post-success / pre-PR seam.
|
|
@@ -594,6 +641,8 @@ async function runReadyGateForIssue(args) {
|
|
|
594
641
|
const getSettingsFn = args.getSettingsFn ?? getSettings;
|
|
595
642
|
const fetchBody = args.fetchBody ??
|
|
596
643
|
((n) => new GitHubProvider().fetchIssueBodySync(String(n)));
|
|
644
|
+
const postComment = args.postComment ??
|
|
645
|
+
((n, body) => new GitHubProvider().postComment(String(n), body));
|
|
597
646
|
try {
|
|
598
647
|
const settings = await getSettingsFn();
|
|
599
648
|
const policy = settings.ready.policy;
|
|
@@ -620,6 +669,8 @@ async function runReadyGateForIssue(args) {
|
|
|
620
669
|
verbose: config.verbose,
|
|
621
670
|
runPhase,
|
|
622
671
|
onProgress,
|
|
672
|
+
// #937 AC-4: persist the final gap report as an issue comment.
|
|
673
|
+
postReport: (body) => postComment(issueNumber, body),
|
|
623
674
|
});
|
|
624
675
|
log(result.ready
|
|
625
676
|
? chalk.green(` ✓ Ready gate: ${result.reason} — awaiting human merge (never merged)`)
|
|
@@ -636,7 +687,7 @@ async function runReadyGateForIssue(args) {
|
|
|
636
687
|
}
|
|
637
688
|
export async function runIssueWithLogging(ctx) {
|
|
638
689
|
// Destructure context for use throughout the function
|
|
639
|
-
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, } = ctx;
|
|
690
|
+
const { issueNumber, config, options, title: issueTitle, labels, services: { logWriter, stateManager, shutdownManager }, worktree, chain, packageManager, baseBranch, onProgress, onPhasePlan, phasePauseHandle, postComment: injectedPostComment, } = ctx;
|
|
640
691
|
const worktreePath = worktree?.path;
|
|
641
692
|
const branch = worktree?.branch;
|
|
642
693
|
const chainMode = chain?.enabled;
|
|
@@ -1103,6 +1154,23 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1103
1154
|
/* progress errors must not halt */
|
|
1104
1155
|
}
|
|
1105
1156
|
}
|
|
1157
|
+
// #964: post the verdict from a standard (non-ready-gate) qa phase.
|
|
1158
|
+
// qa/SKILL.md §9 promises "the orchestrator handles aggregated summary"
|
|
1159
|
+
// under SEQUANT_ORCHESTRATOR, but nothing backed that promise — a
|
|
1160
|
+
// re-run producing a fresh, different verdict left the stale prior
|
|
1161
|
+
// comment as the only externally-visible one. Gating on
|
|
1162
|
+
// `result.success && result.verdict` also excludes turn-capped and
|
|
1163
|
+
// unparseable-verdict phases (AC-4) without extra bookkeeping, since
|
|
1164
|
+
// both already flow through the `else` branch above.
|
|
1165
|
+
if (phase === "qa" && result.success && result.verdict) {
|
|
1166
|
+
const verdictDiffBase = worktreePath
|
|
1167
|
+
? resolveDiffBase(worktreePath, baseBranch ?? "main")
|
|
1168
|
+
: undefined;
|
|
1169
|
+
const verdictCommitHash = worktreePath && verdictDiffBase
|
|
1170
|
+
? getCommitHash(worktreePath, verdictDiffBase)
|
|
1171
|
+
: undefined;
|
|
1172
|
+
await postQaVerdictComment(issueNumber, result.verdict, result.summary, verdictCommitHash, iteration, log, injectedPostComment);
|
|
1173
|
+
}
|
|
1106
1174
|
// Log phase result with observability data (AC-1, AC-2, AC-3, AC-7)
|
|
1107
1175
|
if (logWriter) {
|
|
1108
1176
|
// Resolve the diff base once (#878): worktrees branch from
|
|
@@ -1222,10 +1290,14 @@ export async function runIssueWithLogging(ctx) {
|
|
|
1222
1290
|
// Build enriched config for loop phase with QA context (#488).
|
|
1223
1291
|
// Pass verdict, failed ACs, and error directly so the /loop skill
|
|
1224
1292
|
// doesn't need to reconstruct context from GitHub comments.
|
|
1293
|
+
// #937 AC-3: exclude `document`/`pause_for_human`-tagged findings
|
|
1294
|
+
// from what the loop is told to fix (same filter as ready-gate's
|
|
1295
|
+
// fixableGaps).
|
|
1296
|
+
const fixableGaps = selectFixableGaps(result.summary);
|
|
1225
1297
|
const loopConfig = {
|
|
1226
1298
|
...issueConfig,
|
|
1227
1299
|
lastVerdict: result.verdict ?? undefined,
|
|
1228
|
-
failedAcs:
|
|
1300
|
+
failedAcs: fixableGaps.length ? fixableGaps.join("; ") : undefined,
|
|
1229
1301
|
promptContext: buildLoopContext(result),
|
|
1230
1302
|
};
|
|
1231
1303
|
const loopStartTime = new Date();
|
|
@@ -285,6 +285,7 @@ export function buildExecutionConfig(mergedOptions, settings, issueCount) {
|
|
|
285
285
|
maxIterations: positiveOr(mergedOptions.maxIterations, positiveOr(settings.run.maxIterations, DEFAULT_CONFIG.maxIterations)),
|
|
286
286
|
noSmartTests: mergedOptions.noSmartTests ?? false,
|
|
287
287
|
mcp: mcpEnabled,
|
|
288
|
+
mcpAllowlist: settings.run.mcpAllowlist,
|
|
288
289
|
retry: retryEnabled,
|
|
289
290
|
// #804: default 0 (off) — the whole regression contract for auto-wait is
|
|
290
291
|
// that an unset flag leaves the #761/#799 halt path untouched.
|
|
@@ -32,6 +32,13 @@ export interface AgentExecutionConfig {
|
|
|
32
32
|
phaseTimeout: number;
|
|
33
33
|
verbose: boolean;
|
|
34
34
|
mcp: boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Claude Desktop MCP server names explicitly opted in to pass through to
|
|
37
|
+
* this phase, despite `mcp`'s default exclusion (#936). Forwarded
|
|
38
|
+
* verbatim to `getPhaseMcpServersConfig`'s `desktopAllowlist` by
|
|
39
|
+
* ClaudeCodeDriver; ignored by drivers without an MCP concept.
|
|
40
|
+
*/
|
|
41
|
+
mcpAllowlist?: string[];
|
|
35
42
|
/**
|
|
36
43
|
* Resume a previous session (driver-specific; ignored if unsupported).
|
|
37
44
|
*
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* orchestration layer should import the SDK directly.
|
|
6
6
|
*/
|
|
7
7
|
import { query } from "@anthropic-ai/claude-agent-sdk";
|
|
8
|
-
import {
|
|
8
|
+
import { getPhaseMcpServersConfig } from "../../mcp-config.js";
|
|
9
9
|
import { RateLimitError, BillingError, createRateLimitError, isWaitableWindow, isRateLimitFailureInfo, } from "../../errors.js";
|
|
10
10
|
import { RingBuffer } from "../ring-buffer.js";
|
|
11
11
|
export class ClaudeCodeDriver {
|
|
@@ -80,8 +80,14 @@ export class ClaudeCodeDriver {
|
|
|
80
80
|
resumeToken = undefined;
|
|
81
81
|
}
|
|
82
82
|
try {
|
|
83
|
-
// Get MCP servers config if enabled
|
|
84
|
-
|
|
83
|
+
// Get MCP servers config if enabled — allowlisted, not passed through
|
|
84
|
+
// from Claude Desktop config (#936), except for servers explicitly
|
|
85
|
+
// named in config.mcpAllowlist (settings.run.mcpAllowlist).
|
|
86
|
+
const mcpServers = config.mcp
|
|
87
|
+
? getPhaseMcpServersConfig(config.cwd, {
|
|
88
|
+
desktopAllowlist: config.mcpAllowlist,
|
|
89
|
+
})
|
|
90
|
+
: undefined;
|
|
85
91
|
const queryInstance = query({
|
|
86
92
|
prompt,
|
|
87
93
|
options: {
|