sequant 2.9.0 → 2.11.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +20 -5
  4. package/dist/bin/cli.js +143 -18
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +56 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.d.ts +20 -1
  36. package/dist/src/commands/locks.js +208 -4
  37. package/dist/src/commands/logs.js +14 -5
  38. package/dist/src/commands/merge.js +154 -2
  39. package/dist/src/commands/ready-tui-adapter.js +6 -1
  40. package/dist/src/commands/ready.d.ts +31 -3
  41. package/dist/src/commands/ready.js +53 -13
  42. package/dist/src/commands/resume.d.ts +113 -0
  43. package/dist/src/commands/resume.js +193 -0
  44. package/dist/src/commands/run-display.js +38 -4
  45. package/dist/src/commands/run-flags.d.ts +42 -1
  46. package/dist/src/commands/run-flags.js +53 -1
  47. package/dist/src/commands/run-progress.js +26 -1
  48. package/dist/src/commands/run.js +11 -10
  49. package/dist/src/commands/state.js +7 -0
  50. package/dist/src/commands/status.d.ts +9 -0
  51. package/dist/src/commands/status.js +24 -1
  52. package/dist/src/commands/sync.js +90 -19
  53. package/dist/src/commands/update.js +28 -5
  54. package/dist/src/commands/worktree.d.ts +31 -0
  55. package/dist/src/commands/worktree.js +95 -0
  56. package/dist/src/lib/ac-parser.d.ts +14 -0
  57. package/dist/src/lib/ac-parser.js +99 -5
  58. package/dist/src/lib/assess/index.d.ts +10 -0
  59. package/dist/src/lib/assess/index.js +9 -0
  60. package/dist/src/lib/assess/renderer.d.ts +64 -0
  61. package/dist/src/lib/assess/renderer.js +481 -0
  62. package/dist/src/lib/assess/types.d.ts +224 -0
  63. package/dist/src/lib/assess/types.js +241 -0
  64. package/dist/src/lib/ci/outputs.d.ts +15 -2
  65. package/dist/src/lib/ci/outputs.js +17 -5
  66. package/dist/src/lib/cli-flags.d.ts +56 -0
  67. package/dist/src/lib/cli-flags.js +89 -0
  68. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
  69. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  70. package/dist/src/lib/cli-ui/run-renderer.js +69 -1
  71. package/dist/src/lib/errors.d.ts +78 -6
  72. package/dist/src/lib/errors.js +133 -15
  73. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  74. package/dist/src/lib/locks/checkout-lock.js +389 -0
  75. package/dist/src/lib/locks/index.d.ts +7 -4
  76. package/dist/src/lib/locks/index.js +5 -3
  77. package/dist/src/lib/locks/lock-manager.d.ts +101 -3
  78. package/dist/src/lib/locks/lock-manager.js +277 -8
  79. package/dist/src/lib/locks/types.d.ts +106 -3
  80. package/dist/src/lib/locks/types.js +44 -0
  81. package/dist/src/lib/mcp-config.d.ts +45 -0
  82. package/dist/src/lib/mcp-config.js +77 -1
  83. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  84. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  85. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  86. package/dist/src/lib/merge-check/command-result.js +56 -0
  87. package/dist/src/lib/merge-check/index.d.ts +4 -0
  88. package/dist/src/lib/merge-check/index.js +1 -1
  89. package/dist/src/lib/merge-check/report.js +5 -1
  90. package/dist/src/lib/merge-check/types.d.ts +9 -0
  91. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  92. package/dist/src/lib/merge-check/watch.js +215 -0
  93. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  94. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  95. package/dist/src/lib/settings.d.ts +87 -0
  96. package/dist/src/lib/settings.js +106 -8
  97. package/dist/src/lib/shutdown.d.ts +42 -2
  98. package/dist/src/lib/shutdown.js +75 -5
  99. package/dist/src/lib/skills-check.d.ts +26 -0
  100. package/dist/src/lib/skills-check.js +31 -0
  101. package/dist/src/lib/stacks.d.ts +110 -1
  102. package/dist/src/lib/stacks.js +244 -5
  103. package/dist/src/lib/templates.d.ts +74 -0
  104. package/dist/src/lib/templates.js +171 -16
  105. package/dist/src/lib/test-tautology-detector.d.ts +10 -2
  106. package/dist/src/lib/test-tautology-detector.js +213 -12
  107. package/dist/src/lib/version-check.d.ts +31 -0
  108. package/dist/src/lib/version-check.js +45 -2
  109. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  110. package/dist/src/lib/workflow/batch-executor.js +552 -60
  111. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  112. package/dist/src/lib/workflow/chain-resume.js +24 -20
  113. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  114. package/dist/src/lib/workflow/completed-status.js +57 -0
  115. package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
  116. package/dist/src/lib/workflow/config-resolver.js +143 -2
  117. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  118. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  119. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  120. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  121. package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
  122. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  123. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  124. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  125. package/dist/src/lib/workflow/error-classifier.js +4 -0
  126. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  127. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  128. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  129. package/dist/src/lib/workflow/heartbeat.js +90 -0
  130. package/dist/src/lib/workflow/log-writer.d.ts +54 -2
  131. package/dist/src/lib/workflow/log-writer.js +95 -6
  132. package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
  133. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  134. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  135. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  136. package/dist/src/lib/workflow/phase-detection.js +5 -1
  137. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  138. package/dist/src/lib/workflow/phase-executor.js +467 -52
  139. package/dist/src/lib/workflow/phase-registry.js +2 -1
  140. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  141. package/dist/src/lib/workflow/platforms/github.js +152 -0
  142. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  143. package/dist/src/lib/workflow/qa-cache.js +22 -9
  144. package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
  145. package/dist/src/lib/workflow/ready-gate.js +49 -12
  146. package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
  147. package/dist/src/lib/workflow/run-log-schema.js +78 -2
  148. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  149. package/dist/src/lib/workflow/run-orchestrator.js +168 -20
  150. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  151. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  152. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  153. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  154. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  155. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  156. package/dist/src/lib/workflow/state-manager.js +83 -0
  157. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  158. package/dist/src/lib/workflow/state-schema.js +47 -0
  159. package/dist/src/lib/workflow/types.d.ts +190 -4
  160. package/dist/src/lib/workflow/types.js +1 -0
  161. package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
  162. package/dist/src/lib/workflow/worktree-manager.js +111 -24
  163. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  164. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  165. package/dist/src/mcp/tools/run.d.ts +9 -1
  166. package/dist/src/mcp/tools/run.js +20 -1
  167. package/dist/src/mcp/tools/status.js +6 -0
  168. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  169. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  170. package/dist/src/ui/tui/IssueBox.js +10 -2
  171. package/dist/src/ui/tui/row-cap.js +2 -1
  172. package/dist/src/ui/tui/theme.d.ts +7 -0
  173. package/dist/src/ui/tui/theme.js +9 -0
  174. package/package.json +10 -8
  175. package/templates/hooks/parallel-marker.sh +50 -0
  176. package/templates/hooks/post-tool.sh +20 -7
  177. package/templates/hooks/pre-tool.sh +303 -10
  178. package/templates/scripts/cleanup-worktree.sh +217 -33
  179. package/templates/scripts/new-feature.sh +289 -35
  180. package/templates/skills/_shared/references/subagent-types.md +7 -18
  181. package/templates/skills/_shared/references/trust-model.md +18 -0
  182. package/templates/skills/assess/SKILL.md +220 -391
  183. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  184. package/templates/skills/docs/SKILL.md +4 -4
  185. package/templates/skills/exec/SKILL.md +87 -13
  186. package/templates/skills/fullsolve/SKILL.md +130 -22
  187. package/templates/skills/loop/SKILL.md +94 -13
  188. package/templates/skills/merger/SKILL.md +100 -12
  189. package/templates/skills/qa/SKILL.md +396 -63
  190. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  191. package/templates/skills/qa/references/call-site-review.md +2 -2
  192. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  193. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  194. package/templates/skills/qa/references/quality-gates.md +21 -7
  195. package/templates/skills/qa/references/testing-requirements.md +1 -1
  196. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  197. package/templates/skills/release/SKILL.md +80 -1
  198. package/templates/skills/setup/SKILL.md +5 -5
  199. package/templates/skills/spec/SKILL.md +33 -15
  200. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  201. package/templates/skills/test/SKILL.md +1 -1
  202. package/templates/skills/testgen/SKILL.md +23 -6
  203. package/dist/src/lib/phase-spinner.d.ts +0 -146
  204. package/dist/src/lib/phase-spinner.js +0 -255
  205. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  206. package/dist/src/lib/workflow/pr-operations.js +0 -326
  207. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  208. package/dist/src/lib/workflow/run-summary.js +0 -142
  209. package/templates/agents/sequant-explorer.md +0 -24
@@ -57,6 +57,23 @@ export interface AgentSettings {
57
57
  */
58
58
  isolateParallel: boolean;
59
59
  }
60
+ /**
61
+ * The Agent SDK's closed reasoning-effort enum (#914). Single source of
62
+ * truth — reused by `PhasePolicySchema`'s zod validation below and by
63
+ * `cli-flags.ts:parsePhaseSpecFlag` for the `--efforts` CLI boundary, so the
64
+ * two validation points cannot drift apart on which values are accepted.
65
+ */
66
+ export declare const EFFORT_LEVELS: readonly ["low", "medium", "high", "xhigh", "max"];
67
+ /**
68
+ * A single phase's `model`/`effort` override for the claude-code driver
69
+ * (#914). See `RunSettings.phases`.
70
+ */
71
+ export interface PhasePolicy {
72
+ /** Model alias/ID, passed through unvalidated to the Agent SDK. */
73
+ model?: string;
74
+ /** Reasoning effort — validated against the SDK's closed enum. */
75
+ effort?: (typeof EFFORT_LEVELS)[number];
76
+ }
60
77
  /**
61
78
  * Aider-specific settings for the aider agent driver.
62
79
  */
@@ -112,6 +129,18 @@ export interface RunSettings {
112
129
  * Default: true
113
130
  */
114
131
  retry: boolean;
132
+ /**
133
+ * Total minutes willing to wait for an exhausted rate-limit window to reopen
134
+ * before halting the run (#804).
135
+ *
136
+ * `0` (default) disables auto-wait: a window-exhausted rate limit halts
137
+ * immediately, exactly as it did before #804. Any positive value is a TOTAL
138
+ * budget per issue, not a per-occurrence allowance.
139
+ *
140
+ * The wait is in-process — it does not survive closing the terminal.
141
+ * Overridable per-invocation with `--auto-wait <minutes>`.
142
+ */
143
+ autoWaitMinutes: number;
115
144
  /**
116
145
  * Threshold for stale branch detection in pre-flight checks.
117
146
  * If feature branch is more than this many commits behind main,
@@ -154,6 +183,21 @@ export interface RunSettings {
154
183
  * Default: true.
155
184
  */
156
185
  relay?: boolean;
186
+ /**
187
+ * Per-phase `model`/`effort` overrides for the claude-code driver (#914),
188
+ * keyed by phase name. Absent by default — zero behavior change until
189
+ * opted in. Overridable per-invocation with `--models`/`--efforts`
190
+ * (CLI > settings > absent, resolved by `resolvePhasePolicies` in
191
+ * `config-resolver.ts`).
192
+ */
193
+ phases?: Record<string, PhasePolicy>;
194
+ /**
195
+ * Evidence-based effort escalation on quality-loop retries (#915). Default
196
+ * `false` — raising effort raises token spend, which is the user's call.
197
+ * Overridable per-invocation with `--escalate-effort` (CLI > settings >
198
+ * default). See `effort-escalation.ts` for the resolver.
199
+ */
200
+ effortEscalation: boolean;
157
201
  }
158
202
  /**
159
203
  * Scope assessment threshold configuration
@@ -292,6 +336,25 @@ export declare const AgentSettingsSchema: z.ZodObject<{
292
336
  }>>;
293
337
  isolateParallel: z.ZodDefault<z.ZodBoolean>;
294
338
  }, z.core.$strip>;
339
+ /**
340
+ * Zod schema for a single phase's model/effort override (#914).
341
+ *
342
+ * Model aliases/IDs pass through unvalidated — they churn independently of
343
+ * sequant releases, and the Agent SDK's `query()` call errors clearly on a
344
+ * bad one. Effort validates against the SDK's closed enum at settings-parse
345
+ * time since that set is stable and a typo here would otherwise silently
346
+ * fall through to the SDK default.
347
+ */
348
+ export declare const PhasePolicySchema: z.ZodObject<{
349
+ model: z.ZodOptional<z.ZodString>;
350
+ effort: z.ZodOptional<z.ZodEnum<{
351
+ low: "low";
352
+ medium: "medium";
353
+ high: "high";
354
+ xhigh: "xhigh";
355
+ max: "max";
356
+ }>>;
357
+ }, z.core.$strip>;
295
358
  /** Zod schema for RunSettings */
296
359
  export declare const RunSettingsSchema: z.ZodObject<{
297
360
  logJson: z.ZodDefault<z.ZodBoolean>;
@@ -311,6 +374,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
311
374
  defaultBase: z.ZodOptional<z.ZodString>;
312
375
  mcp: z.ZodDefault<z.ZodBoolean>;
313
376
  retry: z.ZodDefault<z.ZodBoolean>;
377
+ autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
314
378
  staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
315
379
  resolvedIssueTTL: z.ZodDefault<z.ZodNumber>;
316
380
  pmRun: z.ZodOptional<z.ZodString>;
@@ -322,6 +386,17 @@ export declare const RunSettingsSchema: z.ZodObject<{
322
386
  extraArgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
323
387
  }, z.core.$strip>>;
324
388
  relay: z.ZodDefault<z.ZodBoolean>;
389
+ phases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
390
+ model: z.ZodOptional<z.ZodString>;
391
+ effort: z.ZodOptional<z.ZodEnum<{
392
+ low: "low";
393
+ medium: "medium";
394
+ high: "high";
395
+ xhigh: "xhigh";
396
+ max: "max";
397
+ }>>;
398
+ }, z.core.$strip>>>;
399
+ effortEscalation: z.ZodDefault<z.ZodBoolean>;
325
400
  }, z.core.$strip>;
326
401
  /** Zod schema for ScopeThreshold (base — fields required, no defaults) */
327
402
  export declare const ScopeThresholdSchema: z.ZodObject<{
@@ -403,6 +478,7 @@ export declare const SettingsSchema: z.ZodObject<{
403
478
  defaultBase: z.ZodOptional<z.ZodString>;
404
479
  mcp: z.ZodDefault<z.ZodBoolean>;
405
480
  retry: z.ZodDefault<z.ZodBoolean>;
481
+ autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
406
482
  staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
407
483
  resolvedIssueTTL: z.ZodDefault<z.ZodNumber>;
408
484
  pmRun: z.ZodOptional<z.ZodString>;
@@ -414,6 +490,17 @@ export declare const SettingsSchema: z.ZodObject<{
414
490
  extraArgs: z.ZodOptional<z.ZodArray<z.ZodString>>;
415
491
  }, z.core.$strip>>;
416
492
  relay: z.ZodDefault<z.ZodBoolean>;
493
+ phases: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
494
+ model: z.ZodOptional<z.ZodString>;
495
+ effort: z.ZodOptional<z.ZodEnum<{
496
+ low: "low";
497
+ medium: "medium";
498
+ high: "high";
499
+ xhigh: "xhigh";
500
+ max: "max";
501
+ }>>;
502
+ }, z.core.$strip>>>;
503
+ effortEscalation: z.ZodDefault<z.ZodBoolean>;
417
504
  }, z.core.$strip>>;
418
505
  agents: z.ZodDefault<z.ZodObject<{
419
506
  parallel: z.ZodDefault<z.ZodBoolean>;
@@ -13,10 +13,18 @@
13
13
  import { readFile, writeFile, fileExists, ensureDir } from "./fs.js";
14
14
  import { dirname } from "path";
15
15
  import { z } from "zod";
16
+ import { getPhaseNames } from "./workflow/phase-registry.js";
16
17
  /** Path to project-level settings file */
17
18
  export const SETTINGS_PATH = ".sequant/settings.json";
18
19
  /** Current settings schema version */
19
20
  export const SETTINGS_VERSION = "1.0";
21
+ /**
22
+ * The Agent SDK's closed reasoning-effort enum (#914). Single source of
23
+ * truth — reused by `PhasePolicySchema`'s zod validation below and by
24
+ * `cli-flags.ts:parsePhaseSpecFlag` for the `--efforts` CLI boundary, so the
25
+ * two validation points cannot drift apart on which values are accepted.
26
+ */
27
+ export const EFFORT_LEVELS = ["low", "medium", "high", "xhigh", "max"];
20
28
  // ─── Zod Schemas (AC-1, AC-5) ────────────────────────────────────────────────
21
29
  /** Zod schema for RotationSettings */
22
30
  export const RotationSettingsSchema = z.object({
@@ -36,21 +44,42 @@ export const AgentSettingsSchema = z.object({
36
44
  model: z.enum(["haiku", "sonnet", "opus"]).default("haiku"),
37
45
  isolateParallel: z.boolean().default(false),
38
46
  });
47
+ /**
48
+ * Zod schema for a single phase's model/effort override (#914).
49
+ *
50
+ * Model aliases/IDs pass through unvalidated — they churn independently of
51
+ * sequant releases, and the Agent SDK's `query()` call errors clearly on a
52
+ * bad one. Effort validates against the SDK's closed enum at settings-parse
53
+ * time since that set is stable and a typo here would otherwise silently
54
+ * fall through to the SDK default.
55
+ */
56
+ export const PhasePolicySchema = z.object({
57
+ model: z.string().optional(),
58
+ effort: z.enum(EFFORT_LEVELS).optional(),
59
+ });
39
60
  /** Zod schema for RunSettings */
40
61
  export const RunSettingsSchema = z.object({
41
62
  logJson: z.boolean().default(true),
42
63
  logPath: z.string().default(".sequant/logs"),
43
64
  autoDetectPhases: z.boolean().default(true),
44
- timeout: z.number().default(1800),
65
+ // #833: `.positive()` on the three that feed a timer or a loop bound. A `0`
66
+ // or negative here is not a weaker setting — it is an instant abort
67
+ // (`setTimeout` clamps a non-positive delay to 0) or a loop that never runs
68
+ // (`while (iteration < maxIterations)` is false on entry). The resolvers now
69
+ // guard these too; the schema is what gets the bad key *named* at load
70
+ // instead of silently swapped for a default. Left unconstrained elsewhere
71
+ // (`staleBranchThreshold`, `resolvedIssueTTL`) where 0 is meaningful.
72
+ timeout: z.number().positive().default(1800),
45
73
  sequential: z.boolean().default(false),
46
- concurrency: z.number().default(3),
74
+ concurrency: z.number().positive().default(3),
47
75
  qualityLoop: z.boolean().default(false),
48
- maxIterations: z.number().default(3),
76
+ maxIterations: z.number().positive().default(3),
49
77
  smartTests: z.boolean().default(true),
50
78
  rotation: RotationSettingsSchema.default(() => RotationSettingsSchema.parse({})),
51
79
  defaultBase: z.string().optional(),
52
80
  mcp: z.boolean().default(true),
53
81
  retry: z.boolean().default(true),
82
+ autoWaitMinutes: z.number().min(0).default(0),
54
83
  staleBranchThreshold: z.number().default(5),
55
84
  resolvedIssueTTL: z.number().default(7),
56
85
  pmRun: z.string().optional(),
@@ -58,6 +87,23 @@ export const RunSettingsSchema = z.object({
58
87
  agent: z.string().optional(),
59
88
  aider: AiderSettingsSchema.optional(),
60
89
  relay: z.boolean().default(true),
90
+ /**
91
+ * Per-phase `model`/`effort` overrides for the claude-code driver (#914).
92
+ * Absent by default — zero behavior change until opted in. Keyed by phase
93
+ * name (validated against `getPhaseNames()` via `KNOWN_KEYS["run.phases"]`
94
+ * as a non-fatal warning, not a schema-level rejection — a typo'd phase
95
+ * name here should not crash a run the way an invalid `effort` enum does).
96
+ */
97
+ phases: z.record(z.string(), PhasePolicySchema).optional(),
98
+ /**
99
+ * Evidence-based effort escalation on quality-loop retries (#915). Default
100
+ * `false` — raising effort raises token spend, which is the user's call.
101
+ * When enabled, a retried phase execution (loop iteration ≥ 2, or a
102
+ * `sequant ready` QA-pass loop re-run) resolves one effort tier above its
103
+ * configured/inherited base for that execution only. See
104
+ * `effort-escalation.ts` for the resolver.
105
+ */
106
+ effortEscalation: z.boolean().default(false),
61
107
  });
62
108
  /** Zod schema for ScopeThreshold (base — fields required, no defaults) */
63
109
  export const ScopeThresholdSchema = z.object({
@@ -178,7 +224,13 @@ const KNOWN_KEYS = {
178
224
  "agent",
179
225
  "aider",
180
226
  "relay",
227
+ "phases",
181
228
  ]),
229
+ // #914: keyed by real phase name so a typo (`run.phases.exce`) warns
230
+ // instead of silently resolving to nothing. Computed from the registry
231
+ // rather than hardcoded so a new phase registration doesn't need a
232
+ // matching edit here.
233
+ "run.phases": new Set(getPhaseNames()),
182
234
  agents: new Set(["parallel", "model", "isolateParallel"]),
183
235
  scopeAssessment: new Set([
184
236
  "enabled",
@@ -243,6 +295,43 @@ function formatZodErrors(error) {
243
295
  return { path, message };
244
296
  });
245
297
  }
298
+ /**
299
+ * Delete the values Zod rejected from a deep clone of `raw`, so a re-parse can
300
+ * fill just those with their defaults and keep every valid sibling (#833).
301
+ *
302
+ * An array index in the path means one element of an array failed. Deleting by
303
+ * index would leave a hole that re-parses as `undefined` and fails again, so
304
+ * the whole array is dropped and re-defaulted instead.
305
+ */
306
+ function stripInvalidPaths(raw, issues) {
307
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
308
+ return {};
309
+ const clone = structuredClone(raw);
310
+ for (const issue of issues) {
311
+ // An empty path means the root object itself was rejected — nothing to
312
+ // salvage, so fall back to a bare defaults parse.
313
+ if (issue.path.length === 0)
314
+ return {};
315
+ // Truncate at the first array index: drop the array, not an element.
316
+ const firstIndex = issue.path.findIndex((seg) => typeof seg === "number");
317
+ const path = firstIndex === -1 ? issue.path : issue.path.slice(0, firstIndex);
318
+ if (path.length === 0)
319
+ return {};
320
+ let cursor = clone;
321
+ for (const key of path.slice(0, -1)) {
322
+ const next = cursor?.[key];
323
+ cursor =
324
+ next && typeof next === "object" && !Array.isArray(next)
325
+ ? next
326
+ : undefined;
327
+ if (!cursor)
328
+ break;
329
+ }
330
+ if (cursor)
331
+ delete cursor[path[path.length - 1]];
332
+ }
333
+ return clone;
334
+ }
246
335
  /**
247
336
  * Validate a raw settings object against the Zod schema (AC-2).
248
337
  *
@@ -262,8 +351,15 @@ export function validateSettings(raw) {
262
351
  }
263
352
  // Zod validation failed — report errors as warnings and fall back to defaults
264
353
  warnings.push(...formatZodErrors(result.error));
265
- // Try to salvage what we can: parse with defaults for the invalid parts
266
- // by stripping invalid fields and re-parsing
354
+ // Salvage the valid settings by dropping only the rejected keys and
355
+ // re-parsing. #833: this previously re-parsed `{}` despite a comment saying
356
+ // it stripped invalid fields, so a single bad value — one typo in
357
+ // `run.timeout` — silently discarded the user's entire settings.json and
358
+ // reverted every unrelated key to its default.
359
+ const salvaged = SettingsSchema.safeParse(stripInvalidPaths(raw, result.error.issues));
360
+ if (salvaged.success) {
361
+ return { settings: salvaged.data, warnings };
362
+ }
267
363
  const fallback = SettingsSchema.safeParse({});
268
364
  const settings = (fallback.success ? fallback.data : DEFAULT_SETTINGS);
269
365
  return { settings, warnings };
@@ -350,9 +446,11 @@ export const DEFAULT_SETTINGS = {
350
446
  rotation: DEFAULT_ROTATION_SETTINGS,
351
447
  mcp: true, // Enable MCP servers by default in headless mode
352
448
  retry: true, // Enable automatic retry with MCP fallback by default
449
+ autoWaitMinutes: 0, // #804: auto-wait off by default — window exhaustion halts
353
450
  staleBranchThreshold: 5, // Block QA/test if feature is >5 commits behind main
354
451
  resolvedIssueTTL: 7, // Auto-prune resolved issues after 7 days
355
452
  relay: true, // Enable interactive relay (#383) by default
453
+ effortEscalation: false, // #915: off by default — raises token spend
356
454
  },
357
455
  agents: DEFAULT_AGENT_SETTINGS,
358
456
  scopeAssessment: DEFAULT_SCOPE_ASSESSMENT_SETTINGS,
@@ -592,11 +690,11 @@ Generated by \`sequant init\`. See defaults below.
592
690
  | \`logJson\` | boolean | \`true\` | Enable JSON logging |
593
691
  | \`logPath\` | string | \`".sequant/logs"\` | Path to log directory |
594
692
  | \`autoDetectPhases\` | boolean | \`true\` | Auto-detect phases from GitHub issue labels |
595
- | \`timeout\` | number | \`1800\` | Default timeout per phase in seconds |
693
+ | \`timeout\` | number (> 0) | \`1800\` | Default timeout per phase in seconds. Must be positive — 0 or negative is rejected with a warning and the default used (#833) |
596
694
  | \`sequential\` | boolean | \`false\` | Run issues sequentially by default |
597
- | \`concurrency\` | number | \`3\` | Max concurrent issues in parallel mode |
695
+ | \`concurrency\` | number (> 0) | \`3\` | Max concurrent issues in parallel mode. Must be positive (#833) |
598
696
  | \`qualityLoop\` | boolean | \`false\` | Enable quality loop by default |
599
- | \`maxIterations\` | number | \`3\` | Max iterations for quality loop |
697
+ | \`maxIterations\` | number (> 0) | \`3\` | Max iterations for quality loop. Must be positive (#833) |
600
698
  | \`smartTests\` | boolean | \`true\` | Enable smart test detection |
601
699
  | \`defaultBase\` | string | — | Default base branch for worktree creation |
602
700
  | \`mcp\` | boolean | \`true\` | Enable MCP servers in headless mode |
@@ -20,6 +20,36 @@
20
20
  * shutdown.dispose();
21
21
  * ```
22
22
  */
23
+ /**
24
+ * Why the process is shutting down, handed to every cleanup task so they can
25
+ * record the cause rather than silently persisting a truncated record (#856).
26
+ */
27
+ export interface AbortContext {
28
+ /** Signal that triggered the shutdown, e.g. `SIGTERM`. */
29
+ signal: string;
30
+ /** Human-readable cause, from `describeSignalCause`. */
31
+ reason: string;
32
+ }
33
+ /**
34
+ * Conventional shell exit code for death-by-signal: `128 + signum`
35
+ * (SIGINT → 130, SIGTERM → 143). Falls back to 1 for anything unmapped.
36
+ *
37
+ * #856: this used to be a flat `0`. A run killed from outside therefore
38
+ * exited *successfully*, so nothing downstream — CI, a wrapping script, the
39
+ * user reading `$?` — could tell a completed run from a terminated one.
40
+ */
41
+ export declare function exitCodeForSignal(signal: string): number;
42
+ /**
43
+ * Human-readable cause for a termination signal (#856, AC-4).
44
+ *
45
+ * SIGINT is the user pressing Ctrl+C — self-explanatory. SIGTERM is not:
46
+ * no interactive user sends it by hand, so it always means something else
47
+ * killed the run, and the user needs to be told that rather than left to
48
+ * infer it from truncated output. The known offender on macOS is Claude
49
+ * Code's `[bg-pty]` host-dead watchdog, which group-SIGKILLs a process tree
50
+ * containing the run; see `docs/incidents/856/`.
51
+ */
52
+ export declare function describeSignalCause(signal: string): string;
23
53
  /**
24
54
  * Options for ShutdownManager
25
55
  */
@@ -46,6 +76,8 @@ export interface ShutdownManagerOptions {
46
76
  export declare class ShutdownManager {
47
77
  private cleanupTasks;
48
78
  private _isShuttingDown;
79
+ /** Cause of the in-progress shutdown; null until a signal arrives (#856). */
80
+ private _abortContext;
49
81
  /** Active abort controllers — supports concurrent phase execution (#404) */
50
82
  private abortControllers;
51
83
  private forceExitTimeout;
@@ -63,6 +95,12 @@ export declare class ShutdownManager {
63
95
  * Alias for isShuttingDown (matches proposed API)
64
96
  */
65
97
  get shuttingDown(): boolean;
98
+ /**
99
+ * Why the process is shutting down, or `null` if it isn't (#856).
100
+ * Lets code outside a cleanup task — e.g. a `finally` block that also
101
+ * finalizes state — record the same cause the cleanup tasks saw.
102
+ */
103
+ get abortContext(): AbortContext | null;
66
104
  /**
67
105
  * Register an abort controller for a running phase.
68
106
  *
@@ -91,9 +129,11 @@ export declare class ShutdownManager {
91
129
  * This allows dependent cleanup to happen in correct order.
92
130
  *
93
131
  * @param name - Human-readable name for user feedback
94
- * @param task - Async function to execute during cleanup
132
+ * @param task - Async function to execute during cleanup. Receives the
133
+ * `AbortContext` when the shutdown was signal-triggered, or `null` on a
134
+ * programmatic teardown. Tasks that don't care may ignore the argument.
95
135
  */
96
- registerCleanup(name: string, task: () => Promise<void>): void;
136
+ registerCleanup(name: string, task: (abort: AbortContext | null) => Promise<void>): void;
97
137
  /**
98
138
  * Unregister a cleanup task by name
99
139
  *
@@ -21,6 +21,43 @@
21
21
  * ```
22
22
  */
23
23
  import chalk from "chalk";
24
+ /** POSIX signal numbers for the signals we install handlers for. */
25
+ const SIGNAL_NUMBERS = {
26
+ SIGINT: 2,
27
+ SIGTERM: 15,
28
+ };
29
+ /**
30
+ * Conventional shell exit code for death-by-signal: `128 + signum`
31
+ * (SIGINT → 130, SIGTERM → 143). Falls back to 1 for anything unmapped.
32
+ *
33
+ * #856: this used to be a flat `0`. A run killed from outside therefore
34
+ * exited *successfully*, so nothing downstream — CI, a wrapping script, the
35
+ * user reading `$?` — could tell a completed run from a terminated one.
36
+ */
37
+ export function exitCodeForSignal(signal) {
38
+ const num = SIGNAL_NUMBERS[signal];
39
+ return num === undefined ? 1 : 128 + num;
40
+ }
41
+ /**
42
+ * Human-readable cause for a termination signal (#856, AC-4).
43
+ *
44
+ * SIGINT is the user pressing Ctrl+C — self-explanatory. SIGTERM is not:
45
+ * no interactive user sends it by hand, so it always means something else
46
+ * killed the run, and the user needs to be told that rather than left to
47
+ * infer it from truncated output. The known offender on macOS is Claude
48
+ * Code's `[bg-pty]` host-dead watchdog, which group-SIGKILLs a process tree
49
+ * containing the run; see `docs/incidents/856/`.
50
+ */
51
+ export function describeSignalCause(signal) {
52
+ switch (signal) {
53
+ case "SIGINT":
54
+ return "interrupted by user (Ctrl+C)";
55
+ case "SIGTERM":
56
+ return "terminated by an external SIGTERM (not sent by sequant)";
57
+ default:
58
+ return `terminated by ${signal}`;
59
+ }
60
+ }
24
61
  /**
25
62
  * Manages graceful shutdown for sequant run
26
63
  *
@@ -34,6 +71,8 @@ import chalk from "chalk";
34
71
  export class ShutdownManager {
35
72
  cleanupTasks = [];
36
73
  _isShuttingDown = false;
74
+ /** Cause of the in-progress shutdown; null until a signal arrives (#856). */
75
+ _abortContext = null;
37
76
  /** Active abort controllers — supports concurrent phase execution (#404) */
38
77
  abortControllers = new Set();
39
78
  forceExitTimeout;
@@ -67,6 +106,14 @@ export class ShutdownManager {
67
106
  get shuttingDown() {
68
107
  return this._isShuttingDown;
69
108
  }
109
+ /**
110
+ * Why the process is shutting down, or `null` if it isn't (#856).
111
+ * Lets code outside a cleanup task — e.g. a `finally` block that also
112
+ * finalizes state — record the same cause the cleanup tasks saw.
113
+ */
114
+ get abortContext() {
115
+ return this._abortContext;
116
+ }
70
117
  /**
71
118
  * Register an abort controller for a running phase.
72
119
  *
@@ -104,7 +151,9 @@ export class ShutdownManager {
104
151
  * This allows dependent cleanup to happen in correct order.
105
152
  *
106
153
  * @param name - Human-readable name for user feedback
107
- * @param task - Async function to execute during cleanup
154
+ * @param task - Async function to execute during cleanup. Receives the
155
+ * `AbortContext` when the shutdown was signal-triggered, or `null` on a
156
+ * programmatic teardown. Tasks that don't care may ignore the argument.
108
157
  */
109
158
  registerCleanup(name, task) {
110
159
  this.cleanupTasks.push({ name, task });
@@ -138,6 +187,13 @@ export class ShutdownManager {
138
187
  return;
139
188
  }
140
189
  this._isShuttingDown = true;
190
+ // #856 AC-4: name the cause up front. A truncated run whose last words
191
+ // were "shutting down gracefully" reads as an orderly stop; it isn't one.
192
+ const abort = {
193
+ signal,
194
+ reason: describeSignalCause(signal),
195
+ };
196
+ this._abortContext = abort;
141
197
  this.output(chalk.yellow(`\n! Received ${signal}, shutting down gracefully...`));
142
198
  // Abort all in-flight phases immediately
143
199
  if (this.abortControllers.size > 0) {
@@ -157,7 +213,7 @@ export class ShutdownManager {
157
213
  const tasksToRun = [...this.cleanupTasks].reverse();
158
214
  for (const { name, task } of tasksToRun) {
159
215
  try {
160
- await task();
216
+ await task(abort);
161
217
  this.output(chalk.green(`✓ ${name}`));
162
218
  }
163
219
  catch (err) {
@@ -166,9 +222,22 @@ export class ShutdownManager {
166
222
  }
167
223
  }
168
224
  clearTimeout(forceExitTimer);
169
- // Print summary
170
- this.output(chalk.yellow("\nInterrupted. Cleanup complete."));
171
- this.exit(0);
225
+ // #856 AC-4: report the abort AS an abort, with its cause and a non-zero
226
+ // exit code. The old banner ("Interrupted. Cleanup complete.") plus
227
+ // `exit(0)` made an externally-killed run indistinguishable from a
228
+ // successful one — and because this path exits the process, the normal
229
+ // `displaySummary` / `abortReason` rendering in run-display.ts is never
230
+ // reached, so this banner is the only thing the user sees.
231
+ const code = exitCodeForSignal(signal);
232
+ this.errorOutput(chalk.red(`\n✗ Run aborted — ${abort.reason}.`));
233
+ this.output(chalk.yellow(` Cleanup completed. No phase results beyond this point were recorded.`));
234
+ if (signal === "SIGTERM") {
235
+ this.output(chalk.gray(` sequant does not send itself SIGTERM. If this run was launched as a\n` +
236
+ ` backgrounded task inside an interactive Claude Code session, see\n` +
237
+ ` docs/incidents/856/ — relaunch from a plain terminal instead.`));
238
+ }
239
+ this.output(chalk.gray(` Exit code: ${code}`));
240
+ this.exit(code);
172
241
  }
173
242
  /**
174
243
  * Remove signal handlers and clean up
@@ -181,6 +250,7 @@ export class ShutdownManager {
181
250
  process.removeListener("SIGTERM", this.sigtermHandler);
182
251
  this.cleanupTasks = [];
183
252
  this.abortControllers.clear();
253
+ this._abortContext = null;
184
254
  }
185
255
  }
186
256
  /**
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Skills installation check — single source of truth for the
3
+ * `.claude/skills/<skill>/SKILL.md` layout.
4
+ *
5
+ * Shared by `doctor` (Checks 2+3) and the `run` pre-flight (#813) so the two
6
+ * cannot drift. The claude-code driver loads skills from project scope only
7
+ * (`settingSources: ["project"]`, see #19 AC-3 / #711), which makes
8
+ * `.claude/skills/` a hard runtime dependency: a phase prompt like
9
+ * "Run the /spec workflow" can never resolve without it.
10
+ */
11
+ /** Directory (relative to a project root) that skills are resolved from. */
12
+ export declare const SKILLS_DIR = ".claude/skills";
13
+ export interface SkillsCheckResult {
14
+ /** True when the `.claude/skills/` directory exists. */
15
+ skillsDirExists: boolean;
16
+ /** Requested skills whose `SKILL.md` is missing, in input order. */
17
+ missingSkills: string[];
18
+ }
19
+ /**
20
+ * Check that each requested skill is installed at
21
+ * `<cwd>/.claude/skills/<skill>/SKILL.md`.
22
+ *
23
+ * @param skills - Skill directory names to require (e.g. `["spec", "exec"]`).
24
+ * @param cwd - Project root to check under (default: `process.cwd()`).
25
+ */
26
+ export declare function checkSkillsInstalled(skills: string[], cwd?: string): Promise<SkillsCheckResult>;
@@ -0,0 +1,31 @@
1
+ /**
2
+ * Skills installation check — single source of truth for the
3
+ * `.claude/skills/<skill>/SKILL.md` layout.
4
+ *
5
+ * Shared by `doctor` (Checks 2+3) and the `run` pre-flight (#813) so the two
6
+ * cannot drift. The claude-code driver loads skills from project scope only
7
+ * (`settingSources: ["project"]`, see #19 AC-3 / #711), which makes
8
+ * `.claude/skills/` a hard runtime dependency: a phase prompt like
9
+ * "Run the /spec workflow" can never resolve without it.
10
+ */
11
+ import { join } from "path";
12
+ import { fileExists } from "./fs.js";
13
+ /** Directory (relative to a project root) that skills are resolved from. */
14
+ export const SKILLS_DIR = ".claude/skills";
15
+ /**
16
+ * Check that each requested skill is installed at
17
+ * `<cwd>/.claude/skills/<skill>/SKILL.md`.
18
+ *
19
+ * @param skills - Skill directory names to require (e.g. `["spec", "exec"]`).
20
+ * @param cwd - Project root to check under (default: `process.cwd()`).
21
+ */
22
+ export async function checkSkillsInstalled(skills, cwd = process.cwd()) {
23
+ const skillsDirExists = await fileExists(join(cwd, SKILLS_DIR));
24
+ const missingSkills = [];
25
+ for (const skill of skills) {
26
+ if (!(await fileExists(join(cwd, SKILLS_DIR, skill, "SKILL.md")))) {
27
+ missingSkills.push(skill);
28
+ }
29
+ }
30
+ return { skillsDirExists, missingSkills };
31
+ }