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
@@ -129,6 +129,50 @@ export declare const ErrorContextSchema: z.ZodObject<{
129
129
  isRetryable: z.ZodOptional<z.ZodBoolean>;
130
130
  }, z.core.$strip>;
131
131
  export type ErrorContext = z.infer<typeof ErrorContextSchema>;
132
+ /**
133
+ * Finite taxonomy for a single gap finding surfaced by `/qa` (#937).
134
+ *
135
+ * Replaces the lossy prose-scrape gap channel (`parseListSection` against
136
+ * `**Issues:**`/`**Gaps**` headers, which misses AC-table NOT_MET rows, §6d
137
+ * Adversarial Re-Read findings, and §5 Risk Assessment). `evidence` is
138
+ * required so a finding can't be speculative — see #608's 0%-action-rate
139
+ * result for open-ended "what might we be missing" findings.
140
+ */
141
+ export declare const GapCategorySchema: z.ZodEnum<{
142
+ requirement_gap: "requirement_gap";
143
+ dependency_gap: "dependency_gap";
144
+ test_gap: "test_gap";
145
+ repository_gap: "repository_gap";
146
+ risk_gap: "risk_gap";
147
+ execution_gap: "execution_gap";
148
+ }>;
149
+ export type GapCategory = z.infer<typeof GapCategorySchema>;
150
+ export declare const GapActionSchema: z.ZodEnum<{
151
+ fix_now: "fix_now";
152
+ document: "document";
153
+ pause_for_human: "pause_for_human";
154
+ }>;
155
+ export type GapAction = z.infer<typeof GapActionSchema>;
156
+ export declare const GapFindingSchema: z.ZodObject<{
157
+ category: z.ZodEnum<{
158
+ requirement_gap: "requirement_gap";
159
+ dependency_gap: "dependency_gap";
160
+ test_gap: "test_gap";
161
+ repository_gap: "repository_gap";
162
+ risk_gap: "risk_gap";
163
+ execution_gap: "execution_gap";
164
+ }>;
165
+ evidence: z.ZodString;
166
+ description: z.ZodString;
167
+ recommendedAction: z.ZodEnum<{
168
+ fix_now: "fix_now";
169
+ document: "document";
170
+ pause_for_human: "pause_for_human";
171
+ }>;
172
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
173
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
174
+ }, z.core.$strip>;
175
+ export type GapFinding = z.infer<typeof GapFindingSchema>;
132
176
  /**
133
177
  * Condensed QA verdict summary for structured log output (#434).
134
178
  *
@@ -141,6 +185,25 @@ export declare const QaSummarySchema: z.ZodObject<{
141
185
  acTotal: z.ZodNumber;
142
186
  gaps: z.ZodArray<z.ZodString>;
143
187
  suggestions: z.ZodArray<z.ZodString>;
188
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
189
+ category: z.ZodEnum<{
190
+ requirement_gap: "requirement_gap";
191
+ dependency_gap: "dependency_gap";
192
+ test_gap: "test_gap";
193
+ repository_gap: "repository_gap";
194
+ risk_gap: "risk_gap";
195
+ execution_gap: "execution_gap";
196
+ }>;
197
+ evidence: z.ZodString;
198
+ description: z.ZodString;
199
+ recommendedAction: z.ZodEnum<{
200
+ fix_now: "fix_now";
201
+ document: "document";
202
+ pause_for_human: "pause_for_human";
203
+ }>;
204
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
205
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
206
+ }, z.core.$strip>>>;
144
207
  }, z.core.$strip>;
145
208
  export type QaSummary = z.infer<typeof QaSummarySchema>;
146
209
  /**
@@ -175,6 +238,25 @@ export declare const PhaseLogSchema: z.ZodObject<{
175
238
  acTotal: z.ZodNumber;
176
239
  gaps: z.ZodArray<z.ZodString>;
177
240
  suggestions: z.ZodArray<z.ZodString>;
241
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
242
+ category: z.ZodEnum<{
243
+ requirement_gap: "requirement_gap";
244
+ dependency_gap: "dependency_gap";
245
+ test_gap: "test_gap";
246
+ repository_gap: "repository_gap";
247
+ risk_gap: "risk_gap";
248
+ execution_gap: "execution_gap";
249
+ }>;
250
+ evidence: z.ZodString;
251
+ description: z.ZodString;
252
+ recommendedAction: z.ZodEnum<{
253
+ fix_now: "fix_now";
254
+ document: "document";
255
+ pause_for_human: "pause_for_human";
256
+ }>;
257
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
258
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
259
+ }, z.core.$strip>>>;
178
260
  }, z.core.$strip>>;
179
261
  commitHash: z.ZodOptional<z.ZodString>;
180
262
  fileDiffStats: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -255,6 +337,25 @@ export declare const IssueLogSchema: z.ZodObject<{
255
337
  acTotal: z.ZodNumber;
256
338
  gaps: z.ZodArray<z.ZodString>;
257
339
  suggestions: z.ZodArray<z.ZodString>;
340
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
341
+ category: z.ZodEnum<{
342
+ requirement_gap: "requirement_gap";
343
+ dependency_gap: "dependency_gap";
344
+ test_gap: "test_gap";
345
+ repository_gap: "repository_gap";
346
+ risk_gap: "risk_gap";
347
+ execution_gap: "execution_gap";
348
+ }>;
349
+ evidence: z.ZodString;
350
+ description: z.ZodString;
351
+ recommendedAction: z.ZodEnum<{
352
+ fix_now: "fix_now";
353
+ document: "document";
354
+ pause_for_human: "pause_for_human";
355
+ }>;
356
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
357
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
358
+ }, z.core.$strip>>>;
258
359
  }, z.core.$strip>>;
259
360
  commitHash: z.ZodOptional<z.ZodString>;
260
361
  fileDiffStats: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -390,6 +491,25 @@ export declare const RunLogSchema: z.ZodObject<{
390
491
  acTotal: z.ZodNumber;
391
492
  gaps: z.ZodArray<z.ZodString>;
392
493
  suggestions: z.ZodArray<z.ZodString>;
494
+ findings: z.ZodOptional<z.ZodArray<z.ZodObject<{
495
+ category: z.ZodEnum<{
496
+ requirement_gap: "requirement_gap";
497
+ dependency_gap: "dependency_gap";
498
+ test_gap: "test_gap";
499
+ repository_gap: "repository_gap";
500
+ risk_gap: "risk_gap";
501
+ execution_gap: "execution_gap";
502
+ }>;
503
+ evidence: z.ZodString;
504
+ description: z.ZodString;
505
+ recommendedAction: z.ZodEnum<{
506
+ fix_now: "fix_now";
507
+ document: "document";
508
+ pause_for_human: "pause_for_human";
509
+ }>;
510
+ affectedAcs: z.ZodOptional<z.ZodArray<z.ZodString>>;
511
+ nonGoal: z.ZodOptional<z.ZodBoolean>;
512
+ }, z.core.$strip>>>;
393
513
  }, z.core.$strip>>;
394
514
  commitHash: z.ZodOptional<z.ZodString>;
395
515
  fileDiffStats: z.ZodOptional<z.ZodArray<z.ZodObject<{
@@ -129,6 +129,39 @@ export const ErrorContextSchema = z.object({
129
129
  /** Whether this error type is retryable (AC-9) */
130
130
  isRetryable: z.boolean().optional(),
131
131
  });
132
+ /**
133
+ * Finite taxonomy for a single gap finding surfaced by `/qa` (#937).
134
+ *
135
+ * Replaces the lossy prose-scrape gap channel (`parseListSection` against
136
+ * `**Issues:**`/`**Gaps**` headers, which misses AC-table NOT_MET rows, §6d
137
+ * Adversarial Re-Read findings, and §5 Risk Assessment). `evidence` is
138
+ * required so a finding can't be speculative — see #608's 0%-action-rate
139
+ * result for open-ended "what might we be missing" findings.
140
+ */
141
+ export const GapCategorySchema = z.enum([
142
+ "requirement_gap",
143
+ "dependency_gap",
144
+ "test_gap",
145
+ "repository_gap",
146
+ "risk_gap",
147
+ "execution_gap",
148
+ ]);
149
+ export const GapActionSchema = z.enum([
150
+ "fix_now",
151
+ "document",
152
+ "pause_for_human",
153
+ ]);
154
+ export const GapFindingSchema = z.object({
155
+ category: GapCategorySchema,
156
+ /** Concrete observation grounding the finding — never speculation. */
157
+ evidence: z.string().min(1),
158
+ description: z.string().min(1),
159
+ recommendedAction: GapActionSchema,
160
+ /** ACs this finding relates to, e.g. ["AC-3"]. */
161
+ affectedAcs: z.array(z.string()).optional(),
162
+ /** True when the finding overlaps one of the issue's Non-Goals. */
163
+ nonGoal: z.boolean().optional(),
164
+ });
132
165
  /**
133
166
  * Condensed QA verdict summary for structured log output (#434).
134
167
  *
@@ -145,6 +178,13 @@ export const QaSummarySchema = z.object({
145
178
  gaps: z.array(z.string()),
146
179
  /** List of improvement suggestions from QA */
147
180
  suggestions: z.array(z.string()),
181
+ /**
182
+ * Structured gap findings parsed from the `SEQUANT_QA_GAPS` marker (#937).
183
+ * Present only when the marker was found and validated; `gaps` above
184
+ * always carries the union of marker + prose descriptions (dedupe'd) so
185
+ * marker-unaware consumers never regress.
186
+ */
187
+ findings: z.array(GapFindingSchema).optional(),
148
188
  });
149
189
  /**
150
190
  * Log entry for a single phase execution
@@ -149,6 +149,7 @@ export declare const AcceptanceCriterionSchema: z.ZodObject<{
149
149
  }>;
150
150
  verifiedAt: z.ZodOptional<z.ZodString>;
151
151
  notes: z.ZodOptional<z.ZodString>;
152
+ evidence: z.ZodOptional<z.ZodString>;
152
153
  }, z.core.$strip>;
153
154
  export type AcceptanceCriterion = z.infer<typeof AcceptanceCriterionSchema>;
154
155
  /**
@@ -172,6 +173,7 @@ export declare const AcceptanceCriteriaSchema: z.ZodObject<{
172
173
  }>;
173
174
  verifiedAt: z.ZodOptional<z.ZodString>;
174
175
  notes: z.ZodOptional<z.ZodString>;
176
+ evidence: z.ZodOptional<z.ZodString>;
175
177
  }, z.core.$strip>>;
176
178
  extractedAt: z.ZodString;
177
179
  summary: z.ZodObject<{
@@ -281,6 +283,7 @@ export declare const IssueStateSchema: z.ZodObject<{
281
283
  }>;
282
284
  verifiedAt: z.ZodOptional<z.ZodString>;
283
285
  notes: z.ZodOptional<z.ZodString>;
286
+ evidence: z.ZodOptional<z.ZodString>;
284
287
  }, z.core.$strip>>;
285
288
  extractedAt: z.ZodString;
286
289
  summary: z.ZodObject<{
@@ -430,6 +433,7 @@ export declare const WorkflowStateSchema: z.ZodObject<{
430
433
  }>;
431
434
  verifiedAt: z.ZodOptional<z.ZodString>;
432
435
  notes: z.ZodOptional<z.ZodString>;
436
+ evidence: z.ZodOptional<z.ZodString>;
433
437
  }, z.core.$strip>>;
434
438
  extractedAt: z.ZodString;
435
439
  summary: z.ZodObject<{
@@ -538,7 +542,7 @@ export declare function createPhaseState(status?: PhaseStatus): PhaseState;
538
542
  /**
539
543
  * Create acceptance criterion
540
544
  */
541
- export declare function createAcceptanceCriterion(id: string, description: string, verificationMethod?: ACVerificationMethod): AcceptanceCriterion;
545
+ export declare function createAcceptanceCriterion(id: string, description: string, verificationMethod?: ACVerificationMethod, evidence?: string): AcceptanceCriterion;
542
546
  /**
543
547
  * Create acceptance criteria tracking structure
544
548
  */
@@ -149,6 +149,12 @@ export const AcceptanceCriterionSchema = z.object({
149
149
  verifiedAt: z.string().datetime().optional(),
150
150
  /** Additional notes about verification */
151
151
  notes: z.string().optional(),
152
+ /**
153
+ * Explicit `Evidence:` clause declared on the AC line (#938). When present,
154
+ * this is what verificationMethod was resolved from instead of keyword
155
+ * inference — see `resolveVerificationMethod` in ac-parser.ts.
156
+ */
157
+ evidence: z.string().optional(),
152
158
  });
153
159
  /**
154
160
  * Acceptance criteria tracking for an issue
@@ -369,12 +375,13 @@ export function createPhaseState(status = "pending") {
369
375
  /**
370
376
  * Create acceptance criterion
371
377
  */
372
- export function createAcceptanceCriterion(id, description, verificationMethod = "manual") {
378
+ export function createAcceptanceCriterion(id, description, verificationMethod = "manual", evidence) {
373
379
  return {
374
380
  id,
375
381
  description,
376
382
  verificationMethod,
377
383
  status: "pending",
384
+ ...(evidence !== undefined ? { evidence } : {}),
378
385
  };
379
386
  }
380
387
  /**
@@ -83,6 +83,15 @@ export interface ExecutionConfig {
83
83
  dryRun: boolean;
84
84
  /** Enable MCP servers in headless mode (true by default, false if --no-mcp flag used) */
85
85
  mcp: boolean;
86
+ /**
87
+ * Claude Desktop MCP server names explicitly opted in via
88
+ * `settings.run.mcpAllowlist` (#936). Both `ExecutionConfig` producers
89
+ * (`buildExecutionConfig`, `ready-gate.ts`'s `buildPhaseConfig`) read
90
+ * this from settings the same way, so it cannot drift between them the
91
+ * way #833's `phaseTimeout` once did. `phase-executor.ts` forwards it
92
+ * verbatim onto `AgentExecutionConfig.mcpAllowlist`.
93
+ */
94
+ mcpAllowlist?: string[];
86
95
  /**
87
96
  * Enable automatic retry with MCP fallback.
88
97
  * When true (default), failed phases are retried with MCP disabled.
@@ -705,4 +714,9 @@ export interface IssueExecutionContext {
705
714
  * around verbose Claude streaming.
706
715
  */
707
716
  phasePauseHandle?: PhasePauseHandle;
717
+ /**
718
+ * @internal Test seam for the #964 qa-verdict comment post. Defaults to a
719
+ * real `GitHubProvider().postComment` when unset.
720
+ */
721
+ postComment?: (issueNumber: number, body: string) => Promise<void>;
708
722
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sequant",
3
- "version": "2.11.0",
3
+ "version": "2.12.0",
4
4
  "description": "AI coding agent orchestrator — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -94,7 +94,7 @@
94
94
  "@anthropic-ai/claude-agent-sdk": "^0.3.142",
95
95
  "@hono/node-server": "^2.0.0",
96
96
  "boxen": "^8.0.1",
97
- "chalk": "^5.3.0",
97
+ "chalk": "^6.0.0",
98
98
  "chokidar": "^5.0.0",
99
99
  "cli-table3": "^0.6.5",
100
100
  "commander": "^14.0.3",
@@ -39,7 +39,55 @@ else
39
39
  HOOK_CWD=$(echo "$INPUT_JSON" | grep -oE '"cwd"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
40
40
  # For Bash tool, extract command from tool_input; for others, extract the whole object
41
41
  if [[ "$TOOL_NAME" == "Bash" ]]; then
42
- TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"command"\s*:\s*"[^"]+"' | head -1 | cut -d'"' -f4)
42
+ # Escape-aware extraction (#963 gap B): the naive `grep -oE '"[^"]+"'`
43
+ # form (still used for the simpler fields above, where an embedded
44
+ # `\"` is implausible) stops at the FIRST escaped quote inside the
45
+ # JSON string, truncating any command containing one — e.g.
46
+ # `git commit -m "msg with \"quotes\""` would be cut down to just
47
+ # `git commit -m ` before it ever reaches the guards below.
48
+ #
49
+ # sed's `(([^"\\]|\\.)*)` captures the full escaped run — any run of
50
+ # non-quote/non-backslash chars, or a backslash paired with whatever
51
+ # follows it — up to the closing unescaped `"`. `JSON.stringify`
52
+ # guarantees the whole payload is one physical line, so a single
53
+ # sed pass is enough (no multi-line `-z` needed, which BSD sed lacks
54
+ # anyway).
55
+ #
56
+ # The captured text still carries JSON string escapes literally
57
+ # (`\"`, `\\`, `\n`, `\t`, ...); the awk pass resolves them in ONE
58
+ # left-to-right scan, consuming two characters per recognized
59
+ # escape. That ordering is what a chain of separate sed/tr
60
+ # substitutions cannot get right without a placeholder: a literal
61
+ # `\\n` in the original command is an escaped backslash (`\\`)
62
+ # immediately followed by a literal `n`, and must stay a backslash
63
+ # plus 'n' — not become an escaped-newline (`\n`) if the two escapes
64
+ # were resolved out of order. Scanning once and advancing past both
65
+ # characters of whichever escape is recognized sidesteps that
66
+ # ambiguity entirely.
67
+ TOOL_INPUT=$(printf '%s' "$INPUT_JSON" \
68
+ | sed -E -n 's/.*"command"[[:space:]]*:[[:space:]]*"(([^"\\]|\\.)*)".*/\1/p' \
69
+ | head -1 \
70
+ | awk '
71
+ {
72
+ s = $0; out = ""; n = length(s)
73
+ for (i = 1; i <= n; i++) {
74
+ c = substr(s, i, 1)
75
+ if (c == "\\" && i < n) {
76
+ nc = substr(s, i + 1, 1)
77
+ if (nc == "\"") { out = out "\""; i++ }
78
+ else if (nc == "\\") { out = out "\\"; i++ }
79
+ else if (nc == "n") { out = out "\n"; i++ }
80
+ else if (nc == "t") { out = out "\t"; i++ }
81
+ else if (nc == "r") { out = out "\r"; i++ }
82
+ else if (nc == "/") { out = out "/"; i++ }
83
+ else { out = out c }
84
+ } else {
85
+ out = out c
86
+ }
87
+ }
88
+ print out
89
+ }
90
+ ')
43
91
  else
44
92
  TOOL_INPUT=$(echo "$INPUT_JSON" | grep -oE '"tool_input"\s*:\s*\{[^}]+\}' | head -1)
45
93
  fi
@@ -258,6 +306,43 @@ seg_match() {
258
306
  [[ -n "$SEGMENTS" ]] && grep -qE "$1" <<< "$SEGMENTS"
259
307
  }
260
308
 
309
+ # resolve_cd_target <tool_input> — print the target directory of the LAST
310
+ # `cd <path>` line in a (possibly multi-line) Bash command, if and only if
311
+ # the path is a static literal (quoted or unquoted) that resolves to an
312
+ # existing directory. Scans the raw command, not $SEGMENTS — emit_segments
313
+ # drops double-quoted regions, so `cd "$WT"` would vanish there before this
314
+ # ever saw it. Prints nothing when there is no `cd` line, the target is
315
+ # dynamic (contains `$` or a backtick), or the path doesn't exist — callers
316
+ # must treat empty output as "fail open", never as license to guess a
317
+ # directory (#963).
318
+ resolve_cd_target() {
319
+ local input="$1" line target
320
+ line=$(printf '%s\n' "$input" | grep -E '^[[:space:]]*cd[[:space:]]+' | tail -1)
321
+ [[ -z "$line" ]] && return 0
322
+
323
+ target=$(printf '%s' "$line" | sed -E 's/^[[:space:]]*cd[[:space:]]+//; s/[[:space:]]*[;&|].*$//; s/[[:space:]]+$//')
324
+
325
+ # Strip one layer of surrounding matching quotes.
326
+ case "$target" in
327
+ \"*\") target="${target#\"}"; target="${target%\"}" ;;
328
+ \'*\') target="${target#\'}"; target="${target%\'}" ;;
329
+ esac
330
+
331
+ # Fail open on anything dynamic — resolving shell expansions means
332
+ # reimplementing the shell, which is disproportionate; `git commit`
333
+ # itself already rejects a genuinely empty commit. A backslash is
334
+ # rejected too: it can escape a following `$`/`` ` `` into a form this
335
+ # literal-string check would otherwise miss, and a backslash also carries
336
+ # its own shell meaning (line continuation, escaped chars) that this
337
+ # function does not attempt to resolve — failing open is the safe
338
+ # direction either way (#963).
339
+ case "$target" in
340
+ *'$'*|*'`'*|*'\'*) return 0 ;;
341
+ esac
342
+
343
+ [[ -n "$target" && -d "$target" ]] && printf '%s' "$target"
344
+ }
345
+
261
346
  # Path of the session->issue binding the checkout guard maintains (#906).
262
347
  # $1 = repo toplevel, $2 = session id. The id is opaque, so squash everything
263
348
  # outside a filename-safe set — it must not be able to escape the directory.
@@ -552,11 +637,10 @@ if [[ -z "${SEQUANT_ORCHESTRATOR:-}" ]] \
552
637
  # `.cwd` is part of Claude Code's PreToolUse envelope (verified against a
553
638
  # live payload alongside `session_id`), with $PWD as the fallback.
554
639
  _CO_CWD="${HOOK_CWD:-$PWD}"
555
- # Honor a leading `cd <dir>` the same way the commit guard below does.
556
- if echo "$TOOL_INPUT" | grep -qE '^cd [^;&|]+'; then
557
- _CO_CD=$(echo "$TOOL_INPUT" | grep -oE '^cd [^;&|]+' | head -1 | sed 's/^cd //' | sed 's/[[:space:]]*$//')
558
- [[ -n "$_CO_CD" && -d "$_CO_CD" ]] && _CO_CWD="$_CO_CD"
559
- fi
640
+ # Honor a `cd <dir>` the same way the commit guard below does — including
641
+ # multi-line commands and quoted/dynamic targets (#963).
642
+ _CO_CD=$(resolve_cd_target "$TOOL_INPUT")
643
+ [[ -n "$_CO_CD" ]] && _CO_CWD="$_CO_CD"
560
644
 
561
645
  # A linked worktree's toplevel has `.git` as a FILE; the main checkout has
562
646
  # it as a directory.
@@ -782,18 +866,25 @@ fi
782
866
  # Skips for --amend since amending doesn't require new changes
783
867
  if [[ "$TOOL_NAME" == "Bash" ]] && seg_match 'git commit'; then
784
868
  if ! echo "$TOOL_INPUT" | grep -qE -- '--amend|--allow-empty'; then
785
- # Extract target directory from cd command if present (for worktree commits)
786
- # Handles: "cd /path && git commit" or "cd /path; git commit"
787
- TARGET_DIR=""
788
- if echo "$TOOL_INPUT" | grep -qE '^cd [^;&|]+'; then
789
- TARGET_DIR=$(echo "$TOOL_INPUT" | grep -oE '^cd [^;&|]+' | head -1 | sed 's/^cd //' | tr -d ' ')
790
- fi
791
-
792
- # Check for changes in the target directory (or current if no cd)
793
- if [[ -n "$TARGET_DIR" && -d "$TARGET_DIR" ]]; then
794
- CHANGES=$(cd "$TARGET_DIR" && git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
869
+ # Resolve where to check for changes: the last resolvable `cd`
870
+ # target if the command has one (multi-line commands included
871
+ # #963), else the command's own cwd from the hook payload (never
872
+ # this hook process's own cwd, which need not match).
873
+ TARGET_DIR=$(resolve_cd_target "$TOOL_INPUT")
874
+ HAS_CD_LINE=false
875
+ echo "$TOOL_INPUT" | grep -qE '^[[:space:]]*cd[[:space:]]+' && HAS_CD_LINE=true
876
+
877
+ if [[ -n "$TARGET_DIR" ]]; then
878
+ CHANGES=$(git -C "$TARGET_DIR" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
879
+ elif [[ "$HAS_CD_LINE" == true ]]; then
880
+ # A `cd` line is present but its target is dynamic (a shell
881
+ # variable/command substitution) or doesn't exist as a
882
+ # directory — fail open rather than check the wrong
883
+ # directory. `git commit` itself already rejects a genuinely
884
+ # empty commit, so this costs one harmless git error (#963).
885
+ CHANGES=1
795
886
  else
796
- CHANGES=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
887
+ CHANGES=$(git -C "${HOOK_CWD:-$PWD}" status --porcelain 2>/dev/null | wc -l | tr -d ' ')
797
888
  fi
798
889
 
799
890
  if [[ "$CHANGES" -eq 0 ]]; then
@@ -1955,7 +1955,7 @@ The goal is to satisfy AC with the smallest, safest change possible.
1955
1955
  ### 6. Progress Summary and Draft Issue Update
1956
1956
 
1957
1957
  **If orchestrated (SEQUANT_ORCHESTRATOR is set):**
1958
- - Skip posting progress comments to GitHub (orchestrator handles summary)
1958
+ - Skip posting progress comments to GitHub no per-phase comment is posted under `sequant run`; progress surfaces through the run summary and the PR body (#964)
1959
1959
  - Still provide AC coverage summary in output for orchestrator to capture
1960
1960
  - Let orchestrator handle final GitHub update
1961
1961
 
@@ -23,7 +23,6 @@ allowed-tools:
23
23
  - Bash(gh issue edit:*)
24
24
  - Bash(gh pr create:*)
25
25
  - Bash(gh pr list:*)
26
- - Bash(gh pr merge:*)
27
26
  - Bash(npm test:*)
28
27
  - Bash(npm run build:*)
29
28
  - Bash(git diff:*)
@@ -52,22 +51,28 @@ When invoked as `/fullsolve <issue-number>`, execute the complete issue resoluti
52
51
 
53
52
  ## CRITICAL: Auto-Progression Between Phases
54
53
 
55
- **DO NOT wait for user confirmation between phases.** This is an autonomous workflow.
54
+ **DO NOT wait for user confirmation between phases.** This is an autonomous workflow — through PR creation.
56
55
 
57
56
  After each phase completes successfully, **immediately proceed** to the next phase:
58
57
  1. `/spec` completes → **immediately** invoke `/exec`
59
58
  2. `/exec` completes → **immediately** invoke `/test` (if UI) or `/qa`
60
59
  3. `/test` completes → **immediately** invoke `/qa`
61
- 4. `/qa` completes → **immediately** create PR
60
+ 4. `/qa` completes → **immediately** create the PR and post the final summary
62
61
 
63
- **The user invoked `/fullsolve` expecting end-to-end automation.** Only stop for:
62
+ <!-- BEGIN: merge-gate (#958) -->
63
+ **The workflow's terminal state is PR created + final summary posted — not merged.** Merging (§5.3), post-merge verification (§5.4), and the auto-merge-path lock release (§5.5) run **only** when `--auto-merge` is passed, `run.autoMerge` is `true` in `.sequant/settings.json`, or the user has explicitly instructed a merge in this conversation (invoking `/fullsolve` alone does not count). Without one of those, `/fullsolve` stops after the final summary — the PR is left open for human review. See "Merge Gate" below for how this is resolved, and §5.3 for the gate itself.
64
+
65
+ **The user invoked `/fullsolve` expecting end-to-end automation up to a mergeable PR.** Only stop for:
64
66
  - Unrecoverable errors (after retry attempts exhausted)
65
- - Final summary after PR creation
67
+ - Final summary after PR creation — **this is the workflow's terminal state**, not a pause
66
68
  - Explicit user interruption
69
+ <!-- END: merge-gate (#958) -->
67
70
 
68
71
  ```
69
72
  WRONG: "Spec complete. Ready for exec phase." [waits]
70
73
  RIGHT: "Spec complete. Proceeding to exec..." [invokes /exec immediately]
74
+ WRONG (no --auto-merge): [creates PR, immediately runs `gh pr merge`]
75
+ RIGHT (no --auto-merge): [creates PR, posts final summary, stops]
71
76
  ```
72
77
 
73
78
  ## Workflow Overview
@@ -125,6 +130,7 @@ RIGHT: "Spec complete. Proceeding to exec..." [invokes /exec immediately]
125
130
  /fullsolve 218 --max-iterations 5 # Override max fix iterations
126
131
  /fullsolve 218 --parallel # Force parallel agent execution (faster, higher token usage)
127
132
  /fullsolve 218 --sequential # Force sequential agent execution (slower, lower token usage)
133
+ /fullsolve 218 --auto-merge # Merge the PR automatically once QA passes (default: off)
128
134
  ```
129
135
 
130
136
  ## Agent Execution Mode
@@ -151,6 +157,30 @@ When spawning sub-agents for quality checks, determine the execution mode:
151
157
 
152
158
  **Pass execution mode to child skills:** When invoking `/qa` or other skills that spawn agents, pass the `--parallel` or `--sequential` flag to maintain consistency.
153
159
 
160
+ ## Merge Gate (#958)
161
+
162
+ Determine whether Phase 5.3–5.5's merge workflow runs at all. This mirrors the Agent Execution Mode resolution above — flag first, then settings, defaulting closed.
163
+
164
+ 1. **Check for CLI flag override:**
165
+ - `--auto-merge` → run the merge workflow (§5.3) after the final summary
166
+ - No flag → do not merge automatically; fall through to step 2
167
+
168
+ 2. **If no flag, read project settings:**
169
+ Use the Read tool to check project settings:
170
+ ```
171
+ Read(file_path=".sequant/settings.json")
172
+ # Parse JSON and extract run.autoMerge (default: false)
173
+ ```
174
+
175
+ 3. **Default:** off. `/fullsolve` ends at PR creation + final summary — this
176
+ preserves the human merge gate recorded in #817–#819 (`sequant ready`
177
+ drives an issue to merge-*readiness*; a human runs `sequant merge` to
178
+ actually merge it).
179
+
180
+ **Explicit user instruction overrides the gate independent of the flag or setting.** If the user has told you in this conversation to merge once ready (not merely "run `/fullsolve`"), treat that as satisfying the gate for this run.
181
+
182
+ **If the gate does not fire:** skip §5.3 and §5.4 entirely. Release the concurrency locks immediately after §5.2 (see §5.5) and stop — do not attempt `gh pr merge` under any circumstance without one of the three conditions above.
183
+
154
184
  ## Orchestration Context
155
185
 
156
186
  This skill acts as an **orchestrator** and sets environment variables for child skills to optimize their behavior:
@@ -306,7 +336,7 @@ npx sequant locks checkout acquire \
306
336
 
307
337
  Release it alongside the per-issue lock: `npx sequant locks checkout release --issue=<issue-number> || true`. **`--issue` is mandatory** (#906) — it is what proves you are the holder. `--skip-pid-check` means the acquiring shell's PID is already dead, so PID identity is unavailable and a release without `--issue` is refused, not merely ineffective. Stale recovery is therefore age-based only for this lock: the 6h `SEQUANT_SKILL_LOCK_TTL_MS` and the 24h `SEQUANT_MAX_LOCK_AGE_MS` ceiling, *not* same-host dead-PID recovery, which `--skip-pid-check` disables by definition. An abandoned holder still cannot wedge the checkout permanently.
308
338
 
309
- **Release contract:** Phase 5.5 releases both locks on the happy path. On ANY branch that **exits the workflow without reaching Phase 5** — spec failure, exec iterations exhausted, unrecoverable error — you MUST run `npx sequant locks release <issue-number> || true` and `npx sequant locks checkout release --issue=<issue-number> || true` **before** printing the halt message. The explicit release calls below cover the known branches; if you add a new early-exit path, add a release call there too.
339
+ **Release contract (#958):** the happy path releases both locks **right after §5.2's final summary** — that is the workflow's default terminal state, since §5.3–5.4 do not run without the Merge Gate firing. Only when the Merge Gate *does* fire does release move to §5.5, after merge (§5.3) and post-merge verification (§5.4) complete. On ANY branch that **exits the workflow without reaching Phase 5** — spec failure, exec iterations exhausted, unrecoverable error — you MUST run `npx sequant locks release <issue-number> || true` and `npx sequant locks checkout release --issue=<issue-number> || true` **before** printing the halt message. The explicit release calls below cover the known branches; if you add a new early-exit path, add a release call there too.
310
340
 
311
341
  **Do NOT release at a branch that continues to Phase 5.** QA-loop exhaustion and the stagnation halt both fall through to PR creation, which still runs git in this tree — releasing there would leave Phase 5 unprotected.
312
342
 
@@ -763,9 +793,17 @@ Post completion comment to issue with:
763
793
  - PR link
764
794
  - Quality metrics
765
795
 
766
- ### 5.3 Merge Workflow (Correct Order)
796
+ ### 5.3 Merge Workflow (Opt-In Only) (#958)
767
797
 
768
- **IMPORTANT:** Merge the PR first, then clean up the worktree.
798
+ **STOP do not run this section unless the Merge Gate above fired.** That
799
+ means one of: `--auto-merge` was passed on this `/fullsolve` invocation,
800
+ `run.autoMerge` is `true` in `.sequant/settings.json`, or the user
801
+ explicitly instructed a merge in this conversation. If none of those hold,
802
+ **do not run `gh pr merge`.** Stop after §5.2's final summary instead — the
803
+ PR stays open, awaiting human review. That is the default terminal state,
804
+ not a fallback.
805
+
806
+ **IMPORTANT (once the gate above has fired):** Merge the PR first, then clean up the worktree.
769
807
 
770
808
  ```bash
771
809
  # 1. Merge PR (without --delete-branch; cleanup happens after success)
@@ -783,6 +821,8 @@ gh pr merge <N> --squash
783
821
 
784
822
  ### 5.4 Post-Merge Verification
785
823
 
824
+ **Skip this section if §5.3 did not run.** Nothing to verify post-merge when there was no merge.
825
+
786
826
  **Recommended:** After merge, verify the build and CLI still work:
787
827
 
788
828
  ```bash
@@ -800,7 +840,14 @@ If any command fails, fix immediately on main before continuing. This catches is
800
840
 
801
841
  ### 5.5 Release Concurrency Locks (#625, #901)
802
842
 
803
- After the PR is created (or earlier if the workflow exits gracefully), release both locks so other sessions can claim them:
843
+ **Default path (Merge Gate did not fire):** release runs immediately after
844
+ §5.2's final summary — that is the happy path, since §5.3–5.4 never execute.
845
+ Run the release calls below there, not here.
846
+
847
+ **Auto-merge path (Merge Gate fired):** release runs here, after §5.3
848
+ (merge) and §5.4 (post-merge verification) complete.
849
+
850
+ Either way, release both locks so other sessions can claim them:
804
851
 
805
852
  ```bash
806
853
  npx sequant locks release <issue-number> || true
@@ -949,6 +996,7 @@ Do **not** run it on the two branches below that continue to Phase 5 — "test l
949
996
  | MAX_QA_ITERATIONS | 2 | Max fix loops for QA phase |
950
997
  | SKIP_TEST | false | Skip testing phase |
951
998
  | AUTO_PR | true | Create PR automatically |
999
+ | AUTO_MERGE | false | Merge the PR automatically once QA passes (#958) |
952
1000
 
953
1001
  ## Smart Tests Integration
954
1002
 
@@ -993,6 +1041,11 @@ npx tsx scripts/dev/analyze-hook-logs.ts --tests
993
1041
  /fullsolve 218 --max-iterations 5
994
1042
  ```
995
1043
 
1044
+ **End-to-end including merge:**
1045
+ ```
1046
+ /fullsolve 218 --auto-merge
1047
+ ```
1048
+
996
1049
  ## Batch Processing
997
1050
 
998
1051
  For multiple issues, run `/fullsolve` on each sequentially: