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.
Files changed (61) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +13 -0
  4. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  5. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +331 -12
  7. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/subagent-types.md +7 -18
  8. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +5 -1
  9. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +62 -8
  10. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +187 -28
  11. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +127 -23
  12. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +130 -13
  13. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +306 -8
  14. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +79 -0
  15. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +40 -20
  16. package/dist/marketplace/external_plugins/sequant/skills/spec/references/recommended-workflow.md +14 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  18. package/dist/marketplace/external_plugins/sequant/skills/testgen/SKILL.md +23 -6
  19. package/dist/src/commands/doctor.js +20 -18
  20. package/dist/src/commands/ready.js +4 -0
  21. package/dist/src/lib/ac-linter.js +26 -0
  22. package/dist/src/lib/ac-parser.d.ts +40 -0
  23. package/dist/src/lib/ac-parser.js +202 -16
  24. package/dist/src/lib/markdown-fence.d.ts +24 -0
  25. package/dist/src/lib/markdown-fence.js +51 -0
  26. package/dist/src/lib/mcp-config.d.ts +24 -0
  27. package/dist/src/lib/mcp-config.js +51 -0
  28. package/dist/src/lib/scope/analyzer.d.ts +4 -0
  29. package/dist/src/lib/scope/analyzer.js +7 -1
  30. package/dist/src/lib/settings.d.ts +38 -1
  31. package/dist/src/lib/settings.js +14 -0
  32. package/dist/src/lib/system.d.ts +7 -3
  33. package/dist/src/lib/system.js +7 -3
  34. package/dist/src/lib/test-tautology-detector.js +50 -3
  35. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  36. package/dist/src/lib/workflow/batch-executor.js +77 -5
  37. package/dist/src/lib/workflow/config-resolver.js +1 -0
  38. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  39. package/dist/src/lib/workflow/drivers/claude-code.js +9 -3
  40. package/dist/src/lib/workflow/mutation-marker.d.ts +86 -0
  41. package/dist/src/lib/workflow/mutation-marker.js +97 -0
  42. package/dist/src/lib/workflow/phase-executor.d.ts +17 -0
  43. package/dist/src/lib/workflow/phase-executor.js +50 -4
  44. package/dist/src/lib/workflow/qa-gaps-marker.d.ts +38 -0
  45. package/dist/src/lib/workflow/qa-gaps-marker.js +66 -0
  46. package/dist/src/lib/workflow/ready-gate.d.ts +25 -1
  47. package/dist/src/lib/workflow/ready-gate.js +81 -11
  48. package/dist/src/lib/workflow/run-log-schema.d.ts +120 -0
  49. package/dist/src/lib/workflow/run-log-schema.js +40 -0
  50. package/dist/src/lib/workflow/state-schema.d.ts +5 -1
  51. package/dist/src/lib/workflow/state-schema.js +8 -1
  52. package/dist/src/lib/workflow/types.d.ts +14 -0
  53. package/package.json +2 -2
  54. package/templates/hooks/pre-tool.sh +108 -17
  55. package/templates/skills/exec/SKILL.md +1 -1
  56. package/templates/skills/fullsolve/SKILL.md +62 -9
  57. package/templates/skills/loop/SKILL.md +71 -12
  58. package/templates/skills/merger/SKILL.md +32 -3
  59. package/templates/skills/qa/SKILL.md +247 -2
  60. package/templates/skills/spec/SKILL.md +9 -5
  61. package/templates/skills/test/SKILL.md +1 -1
@@ -1,6 +1,17 @@
1
1
  # Recommended Workflow Format
2
2
 
3
- This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output. The `parseRecommendedWorkflow()` function parses this format to determine which phases to execute.
3
+ This document shows the expected output format for the `## Recommended Workflow` section in `/spec` output.
4
+
5
+ ## Resolution chain (#921)
6
+
7
+ `sequant run` resolves phases through an ordered chain, not `parseRecommendedWorkflow()` alone:
8
+
9
+ 1. **`SEQUANT_SPEC` marker** — a structured HTML comment in the posted plan comment, e.g. `<!-- SEQUANT_SPEC: {"phases":["testgen","exec","qa"],"qualityLoop":true} -->`. This is the primary, durable channel — always emit it alongside the prose section below.
10
+ 2. **Comment prose** — `parseRecommendedWorkflow()` applied to the plan comment body (same format as this doc).
11
+ 3. **Chat text** — the same parser applied to the spec agent's chat output. Nondeterministic: only present if the agent happens to restate the section in chat rather than posting via a body file (#814).
12
+ 4. **Label fallback** — `detectPhasesFromLabels()`. Can never produce `testgen` or `security-review`.
13
+
14
+ The marker's `phases` array excludes `spec` (it already ran) and must name only registered phases — an unknown phase name invalidates the whole marker and falls through to step 2.
4
15
 
5
16
  ## Format
6
17
 
@@ -10,6 +21,8 @@ This document shows the expected output format for the `## Recommended Workflow`
10
21
  **Phases:** spec → exec → qa
11
22
  **Quality Loop:** disabled
12
23
  **Reasoning:** Brief explanation of why this workflow was chosen.
24
+
25
+ <!-- SEQUANT_SPEC: {"phases":["exec","qa"],"qualityLoop":false} -->
13
26
  ```
14
27
 
15
28
  ## Examples
@@ -582,7 +582,7 @@ Create structured test results:
582
582
  ### 3.2 GitHub Comment
583
583
 
584
584
  **If orchestrated (SEQUANT_ORCHESTRATOR is set):**
585
- - Skip posting GitHub comment (orchestrator handles summary)
585
+ - Skip posting this skill's own GitHub comment no per-phase comment is posted under `sequant run`; test results surface through the run summary and the PR body (#964)
586
586
  - Include test summary in output for orchestrator to capture
587
587
  - Let orchestrator aggregate results across phases
588
588
 
@@ -6,6 +6,7 @@ metadata:
6
6
  author: sequant
7
7
  version: "1.0"
8
8
  allowed-tools:
9
+ - Bash(npx sequant worktree:*)
9
10
  - Read
10
11
  - Write
11
12
  - Edit
@@ -530,17 +531,33 @@ If an AC has verification method "N/A - Trivial", skip test generation and note
530
531
 
531
532
  ### Step 4: Locate Feature Worktree
532
533
 
533
- If generating file-based tests (Unit Test, Integration Test), find the worktree:
534
+ If generating file-based tests (Unit Test, Integration Test), find the worktree.
534
535
 
535
- ```bash
536
- git worktree list | grep -E "feature.*<issue-number>" || true
537
- ```
536
+ <!-- BEGIN: worktree-standalone-lookup (#899) -->
537
+
538
+ Resolve it through git, not the filesystem:
538
539
 
539
- Or check:
540
540
  ```bash
541
- ls ../worktrees/feature/<issue-number>-*/
541
+ WORKTREE="$(npx sequant worktree resolve <issue-number>)" || {
542
+ echo "❌ HALT: no worktree for #<issue-number> in this repository."
543
+ exit 1
544
+ }
545
+ cd "$WORKTREE"
542
546
  ```
543
547
 
548
+ `sequant worktree resolve` reads `git worktree list` in the current repository
549
+ — which reports only *this* repo's worktrees — and selects on the **branch**
550
+ git reports, not the directory name.
551
+
552
+ **Do not glob `../worktrees/feature/<issue-number>-*`, and do not grep
553
+ `git worktree list` for the issue number.** The first matches across sibling
554
+ repositories, which share that directory; the second matches the printed path,
555
+ so it keys on the directory slug — and a slug can drift from its own branch
556
+ after a rename. Because this skill **writes test files**, landing in the wrong
557
+ tree scatters stubs into an unrelated project.
558
+
559
+ <!-- END: worktree-standalone-lookup (#899) -->
560
+
544
561
  Create test directories if needed:
545
562
  ```bash
546
563
  mkdir -p __tests__/integration
@@ -8,7 +8,8 @@ import { GitHubProvider } from "../lib/workflow/platforms/github.js";
8
8
  import { fileExists, isExecutable } from "../lib/fs.js";
9
9
  import { checkSkillsInstalled } from "../lib/skills-check.js";
10
10
  import { getManifest } from "../lib/manifest.js";
11
- import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, getMcpServersConfig, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
11
+ import { commandExists, isGhAuthenticated, isNativeWindows, isWSL, checkOptionalMcpServers, OPTIONAL_MCP_SERVERS, } from "../lib/system.js";
12
+ import { getPhaseMcpServersConfig } from "../lib/mcp-config.js";
12
13
  import { getSettings, DEFAULT_AGENT_SETTINGS } from "../lib/settings.js";
13
14
  import { checkVersionThorough, getVersionWarning, resolveCliInvocation, } from "../lib/version-check.js";
14
15
  import { areSkillsOutdated } from "./sync.js";
@@ -435,23 +436,24 @@ export async function doctorCommand(options = {}) {
435
436
  message: "No optional MCPs configured (Sequant works without them, but they enhance functionality)",
436
437
  });
437
438
  }
438
- // Check: MCP availability for headless mode (sequant run)
439
- const mcpServersConfig = getMcpServersConfig();
440
- if (mcpServersConfig) {
441
- const serverCount = Object.keys(mcpServersConfig).length;
442
- checks.push({
443
- name: "MCP Servers (headless)",
444
- status: "pass",
445
- message: `Available for sequant run (${serverCount} server${serverCount !== 1 ? "s" : ""} configured)`,
446
- });
447
- }
448
- else {
449
- checks.push({
450
- name: "MCP Servers (headless)",
451
- status: "warn",
452
- message: "Not available for sequant run (no Claude Desktop config found or empty mcpServers)",
453
- });
454
- }
439
+ // Check: MCP availability for headless mode (sequant run) (#936)
440
+ //
441
+ // Phase agents read the project's .mcp.json + settings.run.mcpAllowlist,
442
+ // never Claude Desktop config wholesale — see getPhaseMcpServersConfig.
443
+ // The sequant server is always guaranteed, so this check reports what a
444
+ // phase will actually receive rather than pass/warn on presence.
445
+ const phaseServersConfig = getPhaseMcpServersConfig(process.cwd(), {
446
+ desktopAllowlist: settings.run.mcpAllowlist,
447
+ });
448
+ const phaseServerCount = Object.keys(phaseServersConfig).length;
449
+ const extraServerCount = phaseServerCount - 1; // minus the guaranteed sequant entry
450
+ checks.push({
451
+ name: "MCP Servers (headless)",
452
+ status: "pass",
453
+ message: extraServerCount > 0
454
+ ? `Available for sequant run (${phaseServerCount} servers: sequant + ${extraServerCount} from .mcp.json${settings.run.mcpAllowlist?.length ? "/mcpAllowlist" : ""})`
455
+ : "Available for sequant run (sequant only — add servers to .mcp.json, or settings.run.mcpAllowlist for desktop servers, for more)",
456
+ });
455
457
  // Check: Sequant MCP server health
456
458
  try {
457
459
  // Verify MCP server can be created (validates SDK availability)
@@ -200,11 +200,15 @@ export async function readyCommand(issueArg, options) {
200
200
  nonGoals,
201
201
  phaseTimeout,
202
202
  mcp,
203
+ mcpAllowlist: settings.run.mcpAllowlist,
203
204
  verbose: options.verbose,
204
205
  runPhase,
205
206
  onProgress,
206
207
  phasePolicies,
207
208
  effortEscalation,
209
+ // #937 AC-4: persist the final gap report so it survives the terminal
210
+ // closing (previously terminal-scrollback only under `ac` policy).
211
+ postReport: (body) => gh.postComment(String(issueNumber), body),
208
212
  });
209
213
  }
210
214
  catch (error) {
@@ -272,6 +272,29 @@ function detectTitleBodyTension(ac) {
272
272
  suggestion: "Two verification bars detected. Either (a) tighten the title to match the runtime body (e.g., 'Smoke test execution — capture evidence'), or (b) split the runtime requirement into a separate AC.",
273
273
  };
274
274
  }
275
+ /**
276
+ * Detect a test-type AC (unit/integration/browser) whose verification
277
+ * method came from keyword inference rather than a declared `Evidence:`
278
+ * clause (#938). `manual` ACs are exempt — docs/decision ACs legitimately
279
+ * have no runnable evidence.
280
+ *
281
+ * Warning-only, same convention as the regex-based DEFAULT_LINT_PATTERNS.
282
+ *
283
+ * @param ac - The acceptance criterion to check
284
+ * @returns A lint issue if evidence is missing, otherwise null
285
+ */
286
+ function detectMissingEvidence(ac) {
287
+ if (ac.evidence)
288
+ return null;
289
+ if (ac.verificationMethod === "manual")
290
+ return null;
291
+ return {
292
+ type: "incomplete",
293
+ matchedPattern: ac.verificationMethod,
294
+ problem: `Incomplete: verification not named — method "${ac.verificationMethod}" was inferred from keywords, not declared`,
295
+ suggestion: "Add a trailing `Evidence:` clause naming the command or check that verifies this AC (e.g., `Evidence: \\`npm test -- foo\\``).",
296
+ };
297
+ }
275
298
  /**
276
299
  * Lint a single acceptance criterion against all patterns
277
300
  *
@@ -296,6 +319,9 @@ export function lintAcceptanceCriterion(ac, patterns = DEFAULT_LINT_PATTERNS) {
296
319
  const tension = detectTitleBodyTension(ac);
297
320
  if (tension)
298
321
  issues.push(tension);
322
+ const missingEvidence = detectMissingEvidence(ac);
323
+ if (missingEvidence)
324
+ issues.push(missingEvidence);
299
325
  return {
300
326
  ac,
301
327
  issues,
@@ -30,6 +30,20 @@
30
30
  * ```
31
31
  */
32
32
  import { type AcceptanceCriterion, type AcceptanceCriteria, type ACVerificationMethod } from "./workflow/state-schema.js";
33
+ /**
34
+ * Build a word-boundary-anchored, case-insensitive matcher for a keyword.
35
+ * `\b` sits correctly around spaces and hyphens too, so this is safe for
36
+ * both single words ("ui") and phrases ("end-to-end", "unit test") — a
37
+ * phrase already reads as self-anchoring under plain substring matching,
38
+ * and anchoring it doesn't change that (#946).
39
+ *
40
+ * @param allowPlural - Also match a trailing "s" (e.g. `flag` -> `flags`).
41
+ * Only meaningful for single nouns/verbs that commonly appear inflected
42
+ * in prose; multi-word phrases never need it.
43
+ *
44
+ * @internal Exported for testing only — not part of the module's public API.
45
+ */
46
+ export declare function keywordMatcher(keyword: string, allowPlural?: boolean): RegExp;
33
47
  /**
34
48
  * Infer verification method from description text
35
49
  *
@@ -37,6 +51,32 @@ import { type AcceptanceCriterion, type AcceptanceCriteria, type ACVerificationM
37
51
  * @returns The inferred verification method (defaults to 'manual')
38
52
  */
39
53
  export declare function inferVerificationMethod(description: string): ACVerificationMethod;
54
+ /**
55
+ * Resolve the verification method for an AC, preferring a declared
56
+ * `Evidence:` clause over keyword inference (#938).
57
+ *
58
+ * - Evidence names a backtick-quoted command containing a unit-test
59
+ * token (`test`, `vitest`, `jest`) → `unit_test`.
60
+ * - Evidence names any other backtick-quoted command → `integration_test`.
61
+ * - Evidence is prose with no backtick command (e.g. "human review") →
62
+ * `manual`.
63
+ * - No evidence declared → falls back to {@link inferVerificationMethod}.
64
+ *
65
+ * @param description - The AC description text (evidence clause stripped)
66
+ * @param evidence - The declared evidence clause, if any
67
+ * @returns The resolved verification method
68
+ */
69
+ export declare function resolveVerificationMethod(description: string, evidence?: string): ACVerificationMethod;
70
+ /**
71
+ * Whether a declared `Evidence:` clause describes a CLAUDE.md-style gate
72
+ * test — a fixture-exists / section-present / flag-wired assertion — rather
73
+ * than an ordinary behavioral unit/integration test or a human sign-off.
74
+ *
75
+ * @param evidence - The declared evidence clause text (from {@link splitEvidenceClause})
76
+ * @returns True when the evidence text matches the gate-test keyword set and
77
+ * does not read as a manual-review attestation
78
+ */
79
+ export declare function isGateTestEvidence(evidence: string): boolean;
40
80
  /**
41
81
  * Parse acceptance criteria from GitHub issue markdown
42
82
  *
@@ -30,6 +30,7 @@
30
30
  * ```
31
31
  */
32
32
  import { createAcceptanceCriterion, createAcceptanceCriteria, } from "./workflow/state-schema.js";
33
+ import { computeFenceMask } from "./markdown-fence.js";
33
34
  /**
34
35
  * Regex patterns for AC extraction
35
36
  *
@@ -115,6 +116,39 @@ const VERIFICATION_KEYWORDS = {
115
116
  "manual test": "manual",
116
117
  verify: "manual",
117
118
  };
119
+ /**
120
+ * Escape regex metacharacters in a literal keyword before embedding it in a
121
+ * constructed RegExp.
122
+ */
123
+ function escapeRegExp(literal) {
124
+ return literal.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
125
+ }
126
+ /**
127
+ * Build a word-boundary-anchored, case-insensitive matcher for a keyword.
128
+ * `\b` sits correctly around spaces and hyphens too, so this is safe for
129
+ * both single words ("ui") and phrases ("end-to-end", "unit test") — a
130
+ * phrase already reads as self-anchoring under plain substring matching,
131
+ * and anchoring it doesn't change that (#946).
132
+ *
133
+ * @param allowPlural - Also match a trailing "s" (e.g. `flag` -> `flags`).
134
+ * Only meaningful for single nouns/verbs that commonly appear inflected
135
+ * in prose; multi-word phrases never need it.
136
+ *
137
+ * @internal Exported for testing only — not part of the module's public API.
138
+ */
139
+ export function keywordMatcher(keyword, allowPlural = false) {
140
+ const escaped = escapeRegExp(keyword);
141
+ const pattern = allowPlural ? `\\b${escaped}s?\\b` : `\\b${escaped}\\b`;
142
+ return new RegExp(pattern, "i");
143
+ }
144
+ /**
145
+ * Precompiled, longest-keyword-first matchers for {@link VERIFICATION_KEYWORDS}.
146
+ * Longest-first preserves the original precedence (e.g. "unit test" wins
147
+ * over a lone "unit").
148
+ */
149
+ const VERIFICATION_KEYWORD_MATCHERS = Object.entries(VERIFICATION_KEYWORDS)
150
+ .sort((a, b) => b[0].length - a[0].length)
151
+ .map(([keyword, method]) => ({ regex: keywordMatcher(keyword), method }));
118
152
  /**
119
153
  * Infer verification method from description text
120
154
  *
@@ -122,16 +156,156 @@ const VERIFICATION_KEYWORDS = {
122
156
  * @returns The inferred verification method (defaults to 'manual')
123
157
  */
124
158
  export function inferVerificationMethod(description) {
125
- const lowerDesc = description.toLowerCase();
126
- // Check for explicit keywords (longer phrases first)
127
- const sortedKeywords = Object.keys(VERIFICATION_KEYWORDS).sort((a, b) => b.length - a.length);
128
- for (const keyword of sortedKeywords) {
129
- if (lowerDesc.includes(keyword)) {
130
- return VERIFICATION_KEYWORDS[keyword];
159
+ for (const { regex, method } of VERIFICATION_KEYWORD_MATCHERS) {
160
+ if (regex.test(description)) {
161
+ return method;
131
162
  }
132
163
  }
133
164
  return "manual";
134
165
  }
166
+ /**
167
+ * Matches a trailing `Evidence:` clause on an AC line (#938). Only the
168
+ * LAST occurrence is honored — the clause is defined as trailing, and AC
169
+ * prose can legitimately contain the word "Evidence:" earlier in the
170
+ * sentence (e.g. "the report cites strong Evidence: peer review ..."
171
+ * before the real declaration). Splitting on the first match would corrupt
172
+ * extraction by swallowing the real trailing clause into the description.
173
+ */
174
+ const EVIDENCE_CLAUSE_RE = /\bEvidence:\s*/gi;
175
+ /**
176
+ * Split a trailing `Evidence:` clause out of an AC description (#938).
177
+ *
178
+ * @param description - The AC description text (post ID-stripping)
179
+ * @returns The description with the clause removed, plus the declared
180
+ * evidence text if present
181
+ */
182
+ function splitEvidenceClause(description) {
183
+ const matches = [...description.matchAll(EVIDENCE_CLAUSE_RE)];
184
+ if (matches.length === 0)
185
+ return { description };
186
+ const last = matches[matches.length - 1];
187
+ const before = description.slice(0, last.index).trim();
188
+ const evidence = description.slice(last.index + last[0].length).trim();
189
+ if (!before || !evidence)
190
+ return { description };
191
+ return { description: before, evidence };
192
+ }
193
+ /**
194
+ * Matches a backtick-quoted command inside a declared evidence clause.
195
+ * A command (vs. prose like "human review") is what makes evidence
196
+ * runnable/checkable rather than a manual attestation.
197
+ */
198
+ const EVIDENCE_COMMAND_RE = /`([^`]+)`/;
199
+ /**
200
+ * Command tokens that indicate a unit-test invocation. Anything else
201
+ * backtick-quoted (CLI commands, curl, scripts) is treated as an
202
+ * integration-level check.
203
+ */
204
+ const UNIT_TEST_COMMAND_RE = /\b(test|vitest|jest)\b/i;
205
+ /**
206
+ * Resolve the verification method for an AC, preferring a declared
207
+ * `Evidence:` clause over keyword inference (#938).
208
+ *
209
+ * - Evidence names a backtick-quoted command containing a unit-test
210
+ * token (`test`, `vitest`, `jest`) → `unit_test`.
211
+ * - Evidence names any other backtick-quoted command → `integration_test`.
212
+ * - Evidence is prose with no backtick command (e.g. "human review") →
213
+ * `manual`.
214
+ * - No evidence declared → falls back to {@link inferVerificationMethod}.
215
+ *
216
+ * @param description - The AC description text (evidence clause stripped)
217
+ * @param evidence - The declared evidence clause, if any
218
+ * @returns The resolved verification method
219
+ */
220
+ export function resolveVerificationMethod(description, evidence) {
221
+ if (evidence) {
222
+ const commandMatch = EVIDENCE_COMMAND_RE.exec(evidence);
223
+ if (commandMatch) {
224
+ return UNIT_TEST_COMMAND_RE.test(commandMatch[1])
225
+ ? "unit_test"
226
+ : "integration_test";
227
+ }
228
+ return "manual";
229
+ }
230
+ return inferVerificationMethod(description);
231
+ }
232
+ /**
233
+ * Keywords matching the CLAUDE.md "gate test" definition — a test whose job
234
+ * is to gate a claim that "a fixture exists, a skill section is present, a
235
+ * flag is wired" (#830, #939). Distinct from {@link VERIFICATION_KEYWORDS}:
236
+ * those classify *how* an AC is checked (unit/integration/browser/manual),
237
+ * this classifies *what kind of claim* the test makes, independent of
238
+ * verification method.
239
+ *
240
+ * A heuristic, not a hard classifier — same caveat {@link inferVerificationMethod}
241
+ * already carries. Over-firing sweeps ordinary tests into the gate-test
242
+ * population (inflating the mutation-verification gate's authoring burden);
243
+ * under-firing lets a real gate test slip through ungated, the exact defect
244
+ * class #830 exists to prevent.
245
+ */
246
+ /**
247
+ * Single-word gate-test keywords that commonly appear inflected/pluralized
248
+ * in evidence prose ("flags are wired", "fixtures exist", "the section
249
+ * presents..."). Matched with an optional trailing "s" (#946 AC-4) so
250
+ * anchoring to word boundaries doesn't lose those forms.
251
+ */
252
+ const GATE_TEST_PLURAL_KEYWORDS = ["fixture", "section", "flag", "present"];
253
+ /**
254
+ * Single-word gate-test keywords with no natural plural/inflection needed
255
+ * for this AC's evidence phrasing — matched as exact words only.
256
+ */
257
+ const GATE_TEST_SINGULAR_KEYWORDS = ["wired", "exists", "registered"];
258
+ /**
259
+ * Multi-word gate-test phrases, matched as substrings (already
260
+ * self-anchoring, same rationale as {@link VERIFICATION_KEYWORDS} phrases).
261
+ *
262
+ * The mutation-verification rule (CLAUDE.md, #830) IS the gate-test
263
+ * definition — an AC that already names its own mutation-verified record
264
+ * is self-identifying, even when it doesn't separately name a
265
+ * fixture/section/flag (e.g. "lint test fails when the §7 entry is
266
+ * deleted (mutation-verified)").
267
+ */
268
+ const GATE_TEST_PHRASE_KEYWORDS = [
269
+ "skill gate",
270
+ "mutation-verified",
271
+ "mutation test",
272
+ ];
273
+ const GATE_TEST_KEYWORD_MATCHERS = [
274
+ ...GATE_TEST_PLURAL_KEYWORDS.map((k) => keywordMatcher(k, true)),
275
+ ...GATE_TEST_SINGULAR_KEYWORDS.map((k) => keywordMatcher(k)),
276
+ ...GATE_TEST_PHRASE_KEYWORDS.map((k) => keywordMatcher(k)),
277
+ ];
278
+ /**
279
+ * Phrases that mark evidence as a human-review attestation rather than an
280
+ * automated gate test, even when a {@link GATE_TEST_KEYWORD_MATCHERS} term also
281
+ * appears (#939 QA finding: "reviewed manually, fixture exists in the demo
282
+ * env" false-positived on `fixture` alone). Checked first and short-circuits
283
+ * to `false` — a human sign-off is not the mutation-verifiable claim §6i
284
+ * gates on, regardless of which nouns it happens to mention.
285
+ */
286
+ const MANUAL_REVIEW_NEGATIVE_SIGNALS = [
287
+ "reviewed manually",
288
+ "manual review",
289
+ "human review",
290
+ "manually verified",
291
+ "manually confirmed",
292
+ ];
293
+ /**
294
+ * Whether a declared `Evidence:` clause describes a CLAUDE.md-style gate
295
+ * test — a fixture-exists / section-present / flag-wired assertion — rather
296
+ * than an ordinary behavioral unit/integration test or a human sign-off.
297
+ *
298
+ * @param evidence - The declared evidence clause text (from {@link splitEvidenceClause})
299
+ * @returns True when the evidence text matches the gate-test keyword set and
300
+ * does not read as a manual-review attestation
301
+ */
302
+ export function isGateTestEvidence(evidence) {
303
+ const lower = evidence.toLowerCase();
304
+ if (MANUAL_REVIEW_NEGATIVE_SIGNALS.some((signal) => lower.includes(signal))) {
305
+ return false;
306
+ }
307
+ return GATE_TEST_KEYWORD_MATCHERS.some((regex) => regex.test(evidence));
308
+ }
135
309
  /**
136
310
  * Parse a single line and extract AC if present
137
311
  *
@@ -146,12 +320,14 @@ function parseACLine(line) {
146
320
  if (match) {
147
321
  // Combine groups 2 and 3 for bold-wrapped format (Pattern 3)
148
322
  // where group 3 captures optional text after closing **
149
- const description = match[3]
323
+ const rawDescription = match[3]
150
324
  ? `${match[2].trim()} ${match[3].trim()}`.trim()
151
325
  : match[2].trim();
326
+ const { description, evidence } = splitEvidenceClause(rawDescription);
152
327
  return {
153
328
  id: match[1].toUpperCase(),
154
329
  description,
330
+ ...(evidence !== undefined ? { evidence } : {}),
155
331
  };
156
332
  }
157
333
  }
@@ -181,11 +357,11 @@ function parseACLine(line) {
181
357
  export function parseAcceptanceCriteria(issueBody) {
182
358
  const criteria = [];
183
359
  const seenIds = new Set();
184
- const push = (id, description) => {
360
+ const push = (id, description, evidence) => {
185
361
  if (seenIds.has(id))
186
362
  return;
187
363
  seenIds.add(id);
188
- criteria.push(createAcceptanceCriterion(id, description, inferVerificationMethod(description)));
364
+ criteria.push(createAcceptanceCriterion(id, description, resolveVerificationMethod(description, evidence), evidence));
189
365
  };
190
366
  // Split into lines and process each. `inAcSection` tracks whether the
191
367
  // current line falls under an `## Acceptance Criteria` heading; it toggles on
@@ -193,6 +369,10 @@ export function parseAcceptanceCriteria(issueBody) {
193
369
  // rule as `parseNonGoals` in ready-gate.ts). `bareCount` numbers synthesized
194
370
  // IDs for bare checkboxes in appearance order.
195
371
  const lines = issueBody.split("\n");
372
+ // Lines inside a fenced code block (#947) — an AC-authoring example shown
373
+ // in a fence must not be scanned as a real AC, or toggle the AC-section
374
+ // heading state, in either pass below.
375
+ const fenceMask = computeFenceMask(lines);
196
376
  let inAcSection = false;
197
377
  let bareCount = 0;
198
378
  // Pre-scan for explicit IDs anywhere in the body. Synthesis must skip IDs
@@ -201,12 +381,17 @@ export function parseAcceptanceCriteria(issueBody) {
201
381
  // synthesizes AC-1 first and the author's explicit AC-1 is silently dropped
202
382
  // by the first-occurrence dedupe.
203
383
  const explicitIds = new Set();
204
- for (const line of lines) {
205
- const parsed = parseACLine(line);
384
+ for (let i = 0; i < lines.length; i++) {
385
+ if (fenceMask[i])
386
+ continue;
387
+ const parsed = parseACLine(lines[i]);
206
388
  if (parsed)
207
389
  explicitIds.add(parsed.id);
208
390
  }
209
- for (const line of lines) {
391
+ for (let i = 0; i < lines.length; i++) {
392
+ const line = lines[i];
393
+ if (fenceMask[i])
394
+ continue;
210
395
  if (HEADING_RE.test(line)) {
211
396
  inAcSection = AC_HEADING_RE.test(line);
212
397
  continue;
@@ -215,14 +400,15 @@ export function parseAcceptanceCriteria(issueBody) {
215
400
  // so previously-parsable issues are unaffected (AC-4).
216
401
  const parsed = parseACLine(line);
217
402
  if (parsed) {
218
- push(parsed.id, parsed.description);
403
+ push(parsed.id, parsed.description, parsed.evidence);
219
404
  continue;
220
405
  }
221
406
  // Bare-checkbox fallback: only inside the AC section (AC-1/AC-2).
222
407
  if (inAcSection) {
223
408
  const bare = line.match(BARE_CHECKBOX_RE);
224
- const description = bare?.[1].trim();
225
- if (description) {
409
+ const bareText = bare?.[1].trim();
410
+ if (bareText) {
411
+ const { description, evidence } = splitEvidenceClause(bareText);
226
412
  // Synthesize `AC-<n>`, skipping any ID already taken by an explicit
227
413
  // marker — before OR after this line — so a synthesized ID can never
228
414
  // collide with (and be silently dropped against) a hand-written one.
@@ -230,7 +416,7 @@ export function parseAcceptanceCriteria(issueBody) {
230
416
  do {
231
417
  id = `AC-${++bareCount}`;
232
418
  } while (seenIds.has(id) || explicitIds.has(id));
233
- push(id, description);
419
+ push(id, description, evidence);
234
420
  }
235
421
  }
236
422
  }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Fenced-code-block tracking for line-oriented markdown scanners.
3
+ *
4
+ * Multiple parsers in this codebase scan an issue/PR body line-by-line
5
+ * looking for patterns (checkbox items, `## Non-Goals` bullets). Without
6
+ * fence awareness, a markdown-authoring example quoted inside a fence
7
+ * — showing what the pattern syntax looks like — gets scanned as if it
8
+ * were real content. See #947 (ac-parser.ts) and its sibling in
9
+ * scope/analyzer.ts's `parseNonGoals`.
10
+ */
11
+ /**
12
+ * Compute, for every line of a split markdown body, whether that line falls
13
+ * inside a fenced code block (CommonMark rules: matching delimiter
14
+ * character, closing fence length >= opening fence length; an unclosed
15
+ * fence runs to EOF). The delimiter lines themselves are marked `true` —
16
+ * they're fence syntax, not real content, so patterns should skip them too.
17
+ */
18
+ export declare function computeFenceMask(lines: string[]): boolean[];
19
+ /**
20
+ * Blank out every line that falls inside a fenced code block, preserving
21
+ * line count (and therefore `\n`-relative offsets) so callers that locate
22
+ * sections via newline-anchored regexes on the full body are unaffected.
23
+ */
24
+ export declare function stripFencedLines(body: string): string;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Fenced-code-block tracking for line-oriented markdown scanners.
3
+ *
4
+ * Multiple parsers in this codebase scan an issue/PR body line-by-line
5
+ * looking for patterns (checkbox items, `## Non-Goals` bullets). Without
6
+ * fence awareness, a markdown-authoring example quoted inside a fence
7
+ * — showing what the pattern syntax looks like — gets scanned as if it
8
+ * were real content. See #947 (ac-parser.ts) and its sibling in
9
+ * scope/analyzer.ts's `parseNonGoals`.
10
+ */
11
+ /** Matches a fenced-code-block delimiter line (` ``` ` or `~~~`, 3+ repeats). */
12
+ const FENCE_DELIMITER_RE = /^\s*(`{3,}|~{3,})/;
13
+ /**
14
+ * Compute, for every line of a split markdown body, whether that line falls
15
+ * inside a fenced code block (CommonMark rules: matching delimiter
16
+ * character, closing fence length >= opening fence length; an unclosed
17
+ * fence runs to EOF). The delimiter lines themselves are marked `true` —
18
+ * they're fence syntax, not real content, so patterns should skip them too.
19
+ */
20
+ export function computeFenceMask(lines) {
21
+ const mask = new Array(lines.length).fill(false);
22
+ let fenceChar = null;
23
+ let fenceLen = 0;
24
+ for (let i = 0; i < lines.length; i++) {
25
+ const match = lines[i].match(FENCE_DELIMITER_RE);
26
+ if (fenceChar === null) {
27
+ if (match) {
28
+ fenceChar = match[1][0];
29
+ fenceLen = match[1].length;
30
+ mask[i] = true;
31
+ }
32
+ continue;
33
+ }
34
+ mask[i] = true;
35
+ if (match && match[1][0] === fenceChar && match[1].length >= fenceLen) {
36
+ fenceChar = null;
37
+ fenceLen = 0;
38
+ }
39
+ }
40
+ return mask;
41
+ }
42
+ /**
43
+ * Blank out every line that falls inside a fenced code block, preserving
44
+ * line count (and therefore `\n`-relative offsets) so callers that locate
45
+ * sections via newline-anchored regexes on the full body are unaffected.
46
+ */
47
+ export function stripFencedLines(body) {
48
+ const lines = body.split("\n");
49
+ const mask = computeFenceMask(lines);
50
+ return lines.map((line, i) => (mask[i] ? "" : line)).join("\n");
51
+ }
@@ -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
  */