sequant 2.8.0 → 2.10.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 (211) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +23 -4
  4. package/dist/bin/cli.js +99 -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 +58 -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 +38 -10
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
  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 +250 -344
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
  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 +4 -2
  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/reflect/SKILL.md +27 -13
  28. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
  29. package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
  30. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +11 -3
  31. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  32. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  33. package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
  34. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  35. package/dist/src/commands/assess-render.d.ts +23 -0
  36. package/dist/src/commands/assess-render.js +60 -0
  37. package/dist/src/commands/doctor.js +8 -13
  38. package/dist/src/commands/init.js +13 -1
  39. package/dist/src/commands/locks.js +2 -0
  40. package/dist/src/commands/logs.js +20 -6
  41. package/dist/src/commands/merge.js +154 -2
  42. package/dist/src/commands/ready-tui-adapter.js +6 -1
  43. package/dist/src/commands/ready.d.ts +25 -3
  44. package/dist/src/commands/ready.js +39 -13
  45. package/dist/src/commands/resume.d.ts +113 -0
  46. package/dist/src/commands/resume.js +193 -0
  47. package/dist/src/commands/run-display.d.ts +20 -0
  48. package/dist/src/commands/run-display.js +117 -5
  49. package/dist/src/commands/run-flags.d.ts +42 -1
  50. package/dist/src/commands/run-flags.js +53 -1
  51. package/dist/src/commands/run-progress.js +26 -1
  52. package/dist/src/commands/run.js +11 -10
  53. package/dist/src/commands/state.js +7 -0
  54. package/dist/src/commands/stats.js +47 -0
  55. package/dist/src/commands/status.d.ts +9 -0
  56. package/dist/src/commands/status.js +24 -1
  57. package/dist/src/commands/sync.js +90 -19
  58. package/dist/src/commands/update.js +28 -5
  59. package/dist/src/lib/ac-parser.d.ts +14 -0
  60. package/dist/src/lib/ac-parser.js +99 -5
  61. package/dist/src/lib/assess/index.d.ts +10 -0
  62. package/dist/src/lib/assess/index.js +9 -0
  63. package/dist/src/lib/assess/renderer.d.ts +64 -0
  64. package/dist/src/lib/assess/renderer.js +481 -0
  65. package/dist/src/lib/assess/types.d.ts +224 -0
  66. package/dist/src/lib/assess/types.js +241 -0
  67. package/dist/src/lib/assess-collision-detect.d.ts +19 -2
  68. package/dist/src/lib/assess-collision-detect.js +68 -4
  69. package/dist/src/lib/ci/outputs.d.ts +15 -2
  70. package/dist/src/lib/ci/outputs.js +17 -5
  71. package/dist/src/lib/cli-flags.d.ts +33 -0
  72. package/dist/src/lib/cli-flags.js +46 -0
  73. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  74. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  75. package/dist/src/lib/cli-ui/run-renderer.js +79 -9
  76. package/dist/src/lib/errors.d.ts +84 -6
  77. package/dist/src/lib/errors.js +142 -17
  78. package/dist/src/lib/locks/index.d.ts +3 -3
  79. package/dist/src/lib/locks/index.js +2 -2
  80. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  81. package/dist/src/lib/locks/lock-manager.js +47 -3
  82. package/dist/src/lib/locks/types.d.ts +34 -3
  83. package/dist/src/lib/locks/types.js +16 -0
  84. package/dist/src/lib/manifest.js +1 -17
  85. package/dist/src/lib/mcp-config.d.ts +45 -0
  86. package/dist/src/lib/mcp-config.js +77 -1
  87. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  88. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  89. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  90. package/dist/src/lib/merge-check/command-result.js +56 -0
  91. package/dist/src/lib/merge-check/index.d.ts +4 -0
  92. package/dist/src/lib/merge-check/index.js +1 -1
  93. package/dist/src/lib/merge-check/report.js +5 -1
  94. package/dist/src/lib/merge-check/types.d.ts +9 -0
  95. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  96. package/dist/src/lib/merge-check/watch.js +215 -0
  97. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  98. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  99. package/dist/src/lib/settings.d.ts +14 -0
  100. package/dist/src/lib/settings.js +61 -8
  101. package/dist/src/lib/shutdown.d.ts +42 -2
  102. package/dist/src/lib/shutdown.js +75 -5
  103. package/dist/src/lib/skills-check.d.ts +26 -0
  104. package/dist/src/lib/skills-check.js +31 -0
  105. package/dist/src/lib/stacks.d.ts +110 -1
  106. package/dist/src/lib/stacks.js +244 -5
  107. package/dist/src/lib/templates.d.ts +74 -0
  108. package/dist/src/lib/templates.js +171 -16
  109. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  110. package/dist/src/lib/test-tautology-detector.js +153 -12
  111. package/dist/src/lib/version-check.d.ts +31 -0
  112. package/dist/src/lib/version-check.js +46 -7
  113. package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
  114. package/dist/src/lib/workflow/batch-executor.js +550 -54
  115. package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
  116. package/dist/src/lib/workflow/chain-preflight.js +199 -0
  117. package/dist/src/lib/workflow/chain-resume.d.ts +118 -0
  118. package/dist/src/lib/workflow/chain-resume.js +170 -0
  119. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  120. package/dist/src/lib/workflow/completed-status.js +57 -0
  121. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  122. package/dist/src/lib/workflow/config-resolver.js +54 -2
  123. package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
  124. package/dist/src/lib/workflow/dependency-markers.js +79 -0
  125. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  126. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  127. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  128. package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
  129. package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
  130. package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
  131. package/dist/src/lib/workflow/error-classifier.js +14 -1
  132. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  133. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  134. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  135. package/dist/src/lib/workflow/heartbeat.js +90 -0
  136. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  137. package/dist/src/lib/workflow/log-writer.js +81 -14
  138. package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
  139. package/dist/src/lib/workflow/metrics-schema.js +16 -0
  140. package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
  141. package/dist/src/lib/workflow/phase-executor.d.ts +234 -13
  142. package/dist/src/lib/workflow/phase-executor.js +533 -56
  143. package/dist/src/lib/workflow/phase-registry.js +2 -1
  144. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  145. package/dist/src/lib/workflow/platforms/github.js +152 -0
  146. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  147. package/dist/src/lib/workflow/qa-cache.js +22 -9
  148. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  149. package/dist/src/lib/workflow/ready-gate.js +25 -9
  150. package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
  151. package/dist/src/lib/workflow/run-log-schema.js +92 -2
  152. package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
  153. package/dist/src/lib/workflow/run-orchestrator.js +429 -47
  154. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  155. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  156. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  157. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  158. package/dist/src/lib/workflow/state-manager.js +83 -0
  159. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  160. package/dist/src/lib/workflow/state-schema.js +47 -0
  161. package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
  162. package/dist/src/lib/workflow/status-derivation.js +27 -0
  163. package/dist/src/lib/workflow/types.d.ts +149 -4
  164. package/dist/src/lib/workflow/types.js +1 -0
  165. package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
  166. package/dist/src/lib/workflow/worktree-manager.js +201 -52
  167. package/dist/src/mcp/tools/run.d.ts +11 -1
  168. package/dist/src/mcp/tools/run.js +22 -1
  169. package/dist/src/mcp/tools/status.js +6 -0
  170. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  171. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  172. package/dist/src/ui/tui/IssueBox.js +10 -2
  173. package/dist/src/ui/tui/row-cap.js +2 -1
  174. package/dist/src/ui/tui/theme.d.ts +7 -0
  175. package/dist/src/ui/tui/theme.js +9 -0
  176. package/package.json +9 -10
  177. package/templates/hooks/parallel-marker.sh +50 -0
  178. package/templates/hooks/post-tool.sh +38 -10
  179. package/templates/hooks/pre-tool.sh +405 -67
  180. package/templates/scripts/cleanup-worktree.sh +282 -30
  181. package/templates/scripts/new-feature.sh +264 -16
  182. package/templates/skills/_shared/references/trust-model.md +18 -0
  183. package/templates/skills/assess/SKILL.md +250 -344
  184. package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
  185. package/templates/skills/docs/SKILL.md +4 -4
  186. package/templates/skills/exec/SKILL.md +26 -6
  187. package/templates/skills/fullsolve/SKILL.md +4 -2
  188. package/templates/skills/loop/SKILL.md +38 -2
  189. package/templates/skills/merger/SKILL.md +2 -2
  190. package/templates/skills/qa/SKILL.md +337 -57
  191. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  192. package/templates/skills/qa/references/call-site-review.md +2 -2
  193. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  194. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  195. package/templates/skills/qa/references/quality-gates.md +21 -7
  196. package/templates/skills/qa/references/testing-requirements.md +1 -1
  197. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  198. package/templates/skills/reflect/SKILL.md +27 -13
  199. package/templates/skills/reflect/references/documentation-tiers.md +80 -68
  200. package/templates/skills/reflect/references/phase-reflection.md +31 -15
  201. package/templates/skills/release/SKILL.md +11 -3
  202. package/templates/skills/setup/SKILL.md +5 -5
  203. package/templates/skills/spec/SKILL.md +2 -0
  204. package/templates/skills/spec/references/verification-criteria.md +1 -1
  205. package/templates/skills/test/SKILL.md +1 -1
  206. package/dist/src/lib/phase-spinner.d.ts +0 -146
  207. package/dist/src/lib/phase-spinner.js +0 -255
  208. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  209. package/dist/src/lib/workflow/pr-operations.js +0 -326
  210. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  211. package/dist/src/lib/workflow/run-summary.js +0 -142
@@ -171,6 +171,15 @@ export interface MergeCommandOptions {
171
171
  json?: boolean;
172
172
  /** Enable verbose output */
173
173
  verbose?: boolean;
174
+ /**
175
+ * Watch mode (#818): poll each resolved PR's CI rollup until every check is
176
+ * terminal, then run the normal merge-check report. Never merges.
177
+ */
178
+ watch?: boolean;
179
+ /** Watch poll interval in seconds (default 30). Only used with `watch`. */
180
+ interval?: number;
181
+ /** Watch give-up timeout in seconds (default 1800). Only used with `watch`. */
182
+ timeout?: number;
174
183
  }
175
184
  /**
176
185
  * Configuration for paired directories (template mirroring)
@@ -0,0 +1,121 @@
1
+ /**
2
+ * `sequant merge --watch` — poll a PR's CI rollup until it is terminal, then let
3
+ * the existing merge-check report run. This module owns only the *wait*: it
4
+ * never merges and never runs the checks itself (merge.ts orchestrates that).
5
+ *
6
+ * See #818. Design contract:
7
+ * - foreground `await sleep` loop — no daemon, no child process, no OS notifier;
8
+ * - all `gh` access through `GitHubProvider` (#443), using `statusCheckRollup`
9
+ * (the `--json checks` field is known-broken);
10
+ * - three dispatch-block signatures short-circuit to BLOCKED instead of polling
11
+ * until timeout (AC-3): merge conflict, zero-checks-because-conflicting, and a
12
+ * uniformly-failing board whose annotations show the runner never started.
13
+ */
14
+ import type { RollupEntry, MergeableState } from "../workflow/platforms/github.js";
15
+ import { type AnnotatedCheck } from "../qa/infra-blocked-ci.js";
16
+ /**
17
+ * The subset of `GitHubProvider` the watch loop depends on. Declared as an
18
+ * interface so tests inject a fake without spawning real `gh` calls.
19
+ */
20
+ export interface WatchGitHub {
21
+ getMergeableStateSync(prNumber: number): MergeableState;
22
+ getStatusCheckRollupSync(prNumber: number): RollupEntry[];
23
+ getPRHeadShaSync(prNumber: number): string | null;
24
+ getCheckRunAnnotationsSync(headSha: string): AnnotatedCheck[];
25
+ }
26
+ /**
27
+ * Map one rollup entry to a `gh`-style bucket (`pass | fail | pending |
28
+ * skipping`), normalising the two entry shapes (CheckRun vs StatusContext) so
29
+ * `allChecksFailing` — which the billing-lockout detector gates on — can be
30
+ * reused verbatim.
31
+ */
32
+ export declare function rollupEntryBucket(entry: RollupEntry): string;
33
+ /** Is a rollup entry in a terminal (no-longer-changing) state? */
34
+ export declare function isRollupEntryTerminal(entry: RollupEntry): boolean;
35
+ /** Outcome of classifying a single poll tick. */
36
+ export type TickOutcome = {
37
+ kind: "terminal";
38
+ } | {
39
+ kind: "blocked";
40
+ reason: string;
41
+ checkName?: string;
42
+ } | {
43
+ kind: "pending";
44
+ pending: number;
45
+ total: number;
46
+ };
47
+ /**
48
+ * Classify a single poll of `(mergeable, rollup)`.
49
+ *
50
+ * Pure except for the lazily-invoked `fetchAnnotations`, which is called ONLY
51
+ * when the whole board is terminal-and-failing — so healthy or partial boards
52
+ * never pay for the extra check-run annotation API calls.
53
+ */
54
+ export declare function classifyTick(mergeable: MergeableState, rollup: RollupEntry[], fetchAnnotations: () => AnnotatedCheck[]): TickOutcome;
55
+ /** Terminal status of a watch. */
56
+ export type WatchStatus = "terminal" | "blocked" | "timeout";
57
+ /** Result of watching a single PR's checks to completion. */
58
+ export interface WatchResult {
59
+ status: WatchStatus;
60
+ /** Human-readable cause for `blocked`/`timeout`; undefined when `terminal`. */
61
+ reason?: string;
62
+ /** Check name whose annotation matched, for a billing-lockout BLOCKED. */
63
+ checkName?: string;
64
+ /** Number of polls performed. */
65
+ polls: number;
66
+ /** Elapsed wall-clock time in milliseconds. */
67
+ elapsedMs: number;
68
+ }
69
+ /** Options for {@link waitForChecks}. */
70
+ export interface WatchOptions {
71
+ /** Delay between polls, in milliseconds. */
72
+ intervalMs: number;
73
+ /** Give-up deadline measured from the first poll, in milliseconds. */
74
+ timeoutMs: number;
75
+ /** Optional per-poll progress callback (terminal output). */
76
+ onPoll?: (message: string) => void;
77
+ }
78
+ /** Default watch poll interval (seconds) when `--interval` is not given. */
79
+ export declare const DEFAULT_WATCH_INTERVAL_S = 30;
80
+ /** Default watch timeout (seconds) when `--timeout` is not given: 30 minutes. */
81
+ export declare const DEFAULT_WATCH_TIMEOUT_S = 1800;
82
+ /**
83
+ * Smallest poll interval we will ever use, in milliseconds.
84
+ *
85
+ * A zero/sub-second interval turns the gate into a tight `gh`-spawning loop that
86
+ * can trip GitHub rate limiting, so the floor is enforced even against an
87
+ * explicit request.
88
+ */
89
+ export declare const MIN_WATCH_INTERVAL_MS = 1000;
90
+ /**
91
+ * Convert user-supplied `--interval`/`--timeout` *seconds* into validated
92
+ * milliseconds, falling back to the defaults for anything unusable.
93
+ *
94
+ * Why this exists: `??` only defends against `null`/`undefined`, and
95
+ * `parseInt("abc", 10)` yields `NaN`, which is neither. A `NaN` timeout makes
96
+ * `start + timeoutMs` `NaN`, so `now >= deadline` is permanently false and the
97
+ * watch loop never gives up; a `NaN`/`0` interval makes `setTimeout` fire
98
+ * immediately, so it never sleeps either. The two combine into an unbounded,
99
+ * zero-delay poll loop. This mirrors the numeric-input guards used elsewhere in
100
+ * the codebase (`batch-executor.ts` `maxIter`/`autoWait`, `merge.ts` issue-number
101
+ * parsing) rather than trusting the raw flag value.
102
+ *
103
+ * The CLI rejects malformed values outright (see `bin/cli.ts`); this is the
104
+ * defence for programmatic callers that build `MergeCommandOptions` directly.
105
+ */
106
+ export declare function resolveWatchTiming(options: {
107
+ interval?: number;
108
+ timeout?: number;
109
+ }): {
110
+ intervalMs: number;
111
+ timeoutMs: number;
112
+ };
113
+ /**
114
+ * Poll a PR's rollup until it is terminal, blocked, or timed out.
115
+ *
116
+ * `sleepFn`/`nowFn` are injectable so tests run instantly and deterministically
117
+ * without real timers. The loop always classifies a *fresh* poll before it can
118
+ * declare a timeout, so a check that turns terminal on the final interval is
119
+ * never swallowed by the deadline.
120
+ */
121
+ export declare function waitForChecks(prNumber: number, options: WatchOptions, gh: WatchGitHub, sleepFn?: (ms: number) => Promise<void>, nowFn?: () => number): Promise<WatchResult>;
@@ -0,0 +1,215 @@
1
+ /**
2
+ * `sequant merge --watch` — poll a PR's CI rollup until it is terminal, then let
3
+ * the existing merge-check report run. This module owns only the *wait*: it
4
+ * never merges and never runs the checks itself (merge.ts orchestrates that).
5
+ *
6
+ * See #818. Design contract:
7
+ * - foreground `await sleep` loop — no daemon, no child process, no OS notifier;
8
+ * - all `gh` access through `GitHubProvider` (#443), using `statusCheckRollup`
9
+ * (the `--json checks` field is known-broken);
10
+ * - three dispatch-block signatures short-circuit to BLOCKED instead of polling
11
+ * until timeout (AC-3): merge conflict, zero-checks-because-conflicting, and a
12
+ * uniformly-failing board whose annotations show the runner never started.
13
+ */
14
+ import { allChecksFailing, detectInfraBlockedCi, } from "../qa/infra-blocked-ci.js";
15
+ /**
16
+ * Map one rollup entry to a `gh`-style bucket (`pass | fail | pending |
17
+ * skipping`), normalising the two entry shapes (CheckRun vs StatusContext) so
18
+ * `allChecksFailing` — which the billing-lockout detector gates on — can be
19
+ * reused verbatim.
20
+ */
21
+ export function rollupEntryBucket(entry) {
22
+ // CheckRun: progress in `status`, outcome in `conclusion`.
23
+ if (entry.status !== undefined || entry.conclusion !== undefined) {
24
+ const status = (entry.status ?? "").toUpperCase();
25
+ if (status !== "COMPLETED")
26
+ return "pending"; // QUEUED | IN_PROGRESS | ...
27
+ const conclusion = (entry.conclusion ?? "").toUpperCase();
28
+ switch (conclusion) {
29
+ case "SUCCESS":
30
+ return "pass";
31
+ case "NEUTRAL":
32
+ case "SKIPPED":
33
+ return "skipping";
34
+ default:
35
+ // FAILURE | TIMED_OUT | CANCELLED | ACTION_REQUIRED | STARTUP_FAILURE | STALE
36
+ return "fail";
37
+ }
38
+ }
39
+ // StatusContext: combined state.
40
+ const state = (entry.state ?? "").toUpperCase();
41
+ switch (state) {
42
+ case "SUCCESS":
43
+ return "pass";
44
+ case "FAILURE":
45
+ case "ERROR":
46
+ return "fail";
47
+ default:
48
+ // PENDING | EXPECTED
49
+ return "pending";
50
+ }
51
+ }
52
+ /** Is a rollup entry in a terminal (no-longer-changing) state? */
53
+ export function isRollupEntryTerminal(entry) {
54
+ return rollupEntryBucket(entry) !== "pending";
55
+ }
56
+ /**
57
+ * Classify a single poll of `(mergeable, rollup)`.
58
+ *
59
+ * Pure except for the lazily-invoked `fetchAnnotations`, which is called ONLY
60
+ * when the whole board is terminal-and-failing — so healthy or partial boards
61
+ * never pay for the extra check-run annotation API calls.
62
+ */
63
+ export function classifyTick(mergeable, rollup, fetchAnnotations) {
64
+ // (1) Merge conflict blocks CI dispatch entirely — CI never starts against an
65
+ // unmergeable ref, so waiting on checks is futile. Fire regardless of check
66
+ // count: this also covers the "zero checks because CONFLICTING" trap (AC-3a,
67
+ // AC-3b) — the case the issue calls out where 0 checks after a push means
68
+ // dispatch was blocked, not that CI is slow.
69
+ if (mergeable === "CONFLICTING") {
70
+ return {
71
+ kind: "blocked",
72
+ reason: "PR is not mergeable (CONFLICTING) — merge conflicts block CI dispatch. " +
73
+ "Resolve conflicts and re-push before running merge-check.",
74
+ };
75
+ }
76
+ // No checks yet. While GitHub is still computing mergeability or hasn't
77
+ // dispatched, keep polling; a genuine no-CI PR falls through to the timeout,
78
+ // whose message names the zero-checks case. We deliberately do NOT declare
79
+ // terminal here — proceeding on an empty board would skip the very CI the
80
+ // user asked us to wait for.
81
+ if (rollup.length === 0) {
82
+ return { kind: "pending", pending: 0, total: 0 };
83
+ }
84
+ const pending = rollup.filter((e) => !isRollupEntryTerminal(e));
85
+ if (pending.length > 0) {
86
+ return { kind: "pending", pending: pending.length, total: rollup.length };
87
+ }
88
+ // (3) Every check is terminal. If the board is uniformly failing it may be a
89
+ // billing lockout (runner never started) rather than real test failures —
90
+ // scan the head-SHA annotations for the not-started signature (AC-3c). Only
91
+ // then; a healthy or partially-failing board is a real verdict, not a block.
92
+ const buckets = rollup.map((e) => ({
93
+ bucket: rollupEntryBucket(e),
94
+ }));
95
+ if (allChecksFailing(buckets)) {
96
+ const infra = detectInfraBlockedCi(fetchAnnotations());
97
+ if (infra.blocked) {
98
+ return {
99
+ kind: "blocked",
100
+ reason: infra.message ??
101
+ "CI runner was not started (infrastructure blocked).",
102
+ checkName: infra.checkName,
103
+ };
104
+ }
105
+ }
106
+ return { kind: "terminal" };
107
+ }
108
+ /** Default watch poll interval (seconds) when `--interval` is not given. */
109
+ export const DEFAULT_WATCH_INTERVAL_S = 30;
110
+ /** Default watch timeout (seconds) when `--timeout` is not given: 30 minutes. */
111
+ export const DEFAULT_WATCH_TIMEOUT_S = 1800;
112
+ /**
113
+ * Smallest poll interval we will ever use, in milliseconds.
114
+ *
115
+ * A zero/sub-second interval turns the gate into a tight `gh`-spawning loop that
116
+ * can trip GitHub rate limiting, so the floor is enforced even against an
117
+ * explicit request.
118
+ */
119
+ export const MIN_WATCH_INTERVAL_MS = 1000;
120
+ /**
121
+ * Convert user-supplied `--interval`/`--timeout` *seconds* into validated
122
+ * milliseconds, falling back to the defaults for anything unusable.
123
+ *
124
+ * Why this exists: `??` only defends against `null`/`undefined`, and
125
+ * `parseInt("abc", 10)` yields `NaN`, which is neither. A `NaN` timeout makes
126
+ * `start + timeoutMs` `NaN`, so `now >= deadline` is permanently false and the
127
+ * watch loop never gives up; a `NaN`/`0` interval makes `setTimeout` fire
128
+ * immediately, so it never sleeps either. The two combine into an unbounded,
129
+ * zero-delay poll loop. This mirrors the numeric-input guards used elsewhere in
130
+ * the codebase (`batch-executor.ts` `maxIter`/`autoWait`, `merge.ts` issue-number
131
+ * parsing) rather than trusting the raw flag value.
132
+ *
133
+ * The CLI rejects malformed values outright (see `bin/cli.ts`); this is the
134
+ * defence for programmatic callers that build `MergeCommandOptions` directly.
135
+ */
136
+ export function resolveWatchTiming(options) {
137
+ const seconds = (value, fallback) => typeof value === "number" && Number.isFinite(value) && value > 0
138
+ ? value
139
+ : fallback;
140
+ return {
141
+ intervalMs: Math.max(MIN_WATCH_INTERVAL_MS, seconds(options.interval, DEFAULT_WATCH_INTERVAL_S) * 1000),
142
+ timeoutMs: seconds(options.timeout, DEFAULT_WATCH_TIMEOUT_S) * 1000,
143
+ };
144
+ }
145
+ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
146
+ /**
147
+ * Poll a PR's rollup until it is terminal, blocked, or timed out.
148
+ *
149
+ * `sleepFn`/`nowFn` are injectable so tests run instantly and deterministically
150
+ * without real timers. The loop always classifies a *fresh* poll before it can
151
+ * declare a timeout, so a check that turns terminal on the final interval is
152
+ * never swallowed by the deadline.
153
+ */
154
+ export async function waitForChecks(prNumber, options, gh, sleepFn = defaultSleep, nowFn = Date.now) {
155
+ const { onPoll } = options;
156
+ // Defensive normalisation. `resolveWatchTiming` already sanitises the CLI
157
+ // path, but the loop itself must be incapable of running away: a non-finite
158
+ // deadline makes `now >= deadline` permanently false (never times out) and a
159
+ // non-finite/zero interval makes `setTimeout` fire immediately (never
160
+ // sleeps), which together spawn `gh` in a tight loop forever. `timeoutMs: 0`
161
+ // is deliberately preserved — callers use it to mean "deadline already
162
+ // exhausted, poll once then give up" (see the shared deadline in
163
+ // `runWatchGate`).
164
+ const intervalMs = Math.max(MIN_WATCH_INTERVAL_MS, Number.isFinite(options.intervalMs) ? options.intervalMs : 0);
165
+ const timeoutMs = Number.isFinite(options.timeoutMs) && options.timeoutMs >= 0
166
+ ? options.timeoutMs
167
+ : DEFAULT_WATCH_TIMEOUT_S * 1000;
168
+ const start = nowFn();
169
+ const deadline = start + timeoutMs;
170
+ let polls = 0;
171
+ // Lazy annotation fetch — only reached from classifyTick's all-failing branch.
172
+ const fetchAnnotations = () => {
173
+ const sha = gh.getPRHeadShaSync(prNumber);
174
+ if (!sha)
175
+ return [];
176
+ return gh.getCheckRunAnnotationsSync(sha);
177
+ };
178
+ for (;;) {
179
+ polls++;
180
+ const mergeable = gh.getMergeableStateSync(prNumber);
181
+ const rollup = gh.getStatusCheckRollupSync(prNumber);
182
+ const tick = classifyTick(mergeable, rollup, fetchAnnotations);
183
+ const elapsedMs = nowFn() - start;
184
+ if (tick.kind === "blocked") {
185
+ return {
186
+ status: "blocked",
187
+ reason: tick.reason,
188
+ checkName: tick.checkName,
189
+ polls,
190
+ elapsedMs,
191
+ };
192
+ }
193
+ if (tick.kind === "terminal") {
194
+ return { status: "terminal", polls, elapsedMs };
195
+ }
196
+ // Pending: report progress.
197
+ if (onPoll) {
198
+ onPoll(tick.total === 0
199
+ ? "No checks reported yet…"
200
+ : `${tick.total - tick.pending}/${tick.total} checks terminal…`);
201
+ }
202
+ // Deadline reached. The classify above was a fresh poll, so timing out now
203
+ // cannot swallow a late terminal.
204
+ if (nowFn() >= deadline) {
205
+ const timeoutSec = Math.round(timeoutMs / 1000);
206
+ const reason = tick.total === 0
207
+ ? `Watch timed out after ${timeoutSec}s: no CI checks ever appeared for PR #${prNumber} ` +
208
+ `(CI may not be configured, or dispatch was blocked).`
209
+ : `Watch timed out after ${timeoutSec}s: ${tick.pending}/${tick.total} checks still pending on PR #${prNumber}.`;
210
+ return { status: "timeout", reason, polls, elapsedMs };
211
+ }
212
+ const remaining = deadline - nowFn();
213
+ await sleepFn(Math.min(intervalMs, Math.max(0, remaining)));
214
+ }
215
+ }
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Infra-blocked CI detection helpers used by the `/qa` Phase 1 CI status check.
3
+ *
4
+ * A repository that has hit its GitHub Actions spending limit fails **every**
5
+ * check within seconds of starting: no runner is ever allocated, so no step
6
+ * executes. `gh pr checks` and `gh run view` report these as ordinary failures,
7
+ * and the default status mapping (`FAILURE → NOT_MET → blocks merge`) then marks
8
+ * CI-dependent ACs as unmet for a condition no code change can fix — findings
9
+ * that would go on to feed `/loop` and burn iterations "fixing" working code.
10
+ *
11
+ * The real cause exists only as a check-run annotation. `/qa` uses these helpers
12
+ * to recognise that signature and reclassify CI as infra-blocked instead.
13
+ *
14
+ * Both functions are pure predicates over already-fetched API data; the `gh api`
15
+ * plumbing lives in the skill prompt, mirroring `./markdown-only-ci.ts`.
16
+ */
17
+ /** Minimal shape of a `gh pr checks --json name,state,bucket` entry. */
18
+ export interface CiCheckBucket {
19
+ /** `gh` rollup bucket: `pass` | `fail` | `pending` | `skipping`. */
20
+ bucket: string;
21
+ }
22
+ /**
23
+ * Predicate: is *every* check in the `fail` bucket?
24
+ *
25
+ * This is the gate for the annotation query. A mix of real failures and
26
+ * fail-fast ones is not the billing-lockout signature — only a uniformly red
27
+ * board is — so healthy and partially-failing PRs never pay for the extra API
28
+ * calls, and their status mapping is untouched.
29
+ *
30
+ * An empty list means "no CI configured", which is a separate case handled by
31
+ * the skill's existing empty-response branch, so it returns `false` here rather
32
+ * than vacuously true.
33
+ *
34
+ * @param checks - Entries from `gh pr checks --json name,state,bucket`.
35
+ * @returns `true` only if `checks` is non-empty and every entry is `fail`.
36
+ */
37
+ export declare function allChecksFailing(checks: readonly CiCheckBucket[]): boolean;
38
+ /**
39
+ * The runner-never-started signature.
40
+ *
41
+ * Deliberately matches on the annotation **message only**, with no
42
+ * `annotation_level` condition. The captured fixture does carry
43
+ * `annotation_level: "failure"`, but gating on it would add a requirement the
44
+ * acceptance criteria never state, and a `warning`-level variant of the same
45
+ * message would then be silently missed — the exact class of failure this
46
+ * detection exists to prevent.
47
+ */
48
+ export declare const NOT_STARTED_SIGNATURE: RegExp;
49
+ /** Minimal shape of a GitHub check-run annotation. */
50
+ export interface CheckAnnotation {
51
+ message?: string | null;
52
+ annotation_level?: string | null;
53
+ path?: string | null;
54
+ }
55
+ /** A check paired with the annotations fetched from its `annotations_url`. */
56
+ export interface AnnotatedCheck {
57
+ checkName: string;
58
+ annotations?: readonly CheckAnnotation[] | null;
59
+ }
60
+ /** Outcome of scanning failing checks for the not-started signature. */
61
+ export interface InfraBlockedResult {
62
+ /** `true` when CI is red for infrastructure reasons, not code reasons. */
63
+ blocked: boolean;
64
+ /**
65
+ * The matching annotation message, **verbatim**. `/qa` surfaces this as the
66
+ * action item rather than paraphrasing it — the message names the remedy
67
+ * (e.g. the billing settings page), and a paraphrase would lose it.
68
+ */
69
+ message?: string;
70
+ /** Name of the check whose annotation matched, for the QA report. */
71
+ checkName?: string;
72
+ }
73
+ /**
74
+ * Scan failing checks' annotations for the not-started signature.
75
+ *
76
+ * Returns on the first match: when the board is uniformly red for a billing
77
+ * lockout, every check carries the same annotation, so one is representative
78
+ * and the caller can stop fetching.
79
+ *
80
+ * Deliberately defensive about its input shape. Callers feed this raw `gh api`
81
+ * output, and a failed annotations request (404, rate limit, revoked token)
82
+ * yields an *object* — `{"message":"Not Found"}` — where an array is expected.
83
+ * Iterating that throws. Since the whole point of this detector is to keep a
84
+ * broken-CI situation from derailing the QA phase, a malformed payload must
85
+ * degrade to "not infra-blocked", never crash the run.
86
+ *
87
+ * @param checks - Failing checks with their fetched annotations.
88
+ * @returns `{ blocked: true, message, checkName }` on a match, else `{ blocked: false }`.
89
+ */
90
+ export declare function detectInfraBlockedCi(checks: readonly AnnotatedCheck[]): InfraBlockedResult;
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Infra-blocked CI detection helpers used by the `/qa` Phase 1 CI status check.
3
+ *
4
+ * A repository that has hit its GitHub Actions spending limit fails **every**
5
+ * check within seconds of starting: no runner is ever allocated, so no step
6
+ * executes. `gh pr checks` and `gh run view` report these as ordinary failures,
7
+ * and the default status mapping (`FAILURE → NOT_MET → blocks merge`) then marks
8
+ * CI-dependent ACs as unmet for a condition no code change can fix — findings
9
+ * that would go on to feed `/loop` and burn iterations "fixing" working code.
10
+ *
11
+ * The real cause exists only as a check-run annotation. `/qa` uses these helpers
12
+ * to recognise that signature and reclassify CI as infra-blocked instead.
13
+ *
14
+ * Both functions are pure predicates over already-fetched API data; the `gh api`
15
+ * plumbing lives in the skill prompt, mirroring `./markdown-only-ci.ts`.
16
+ */
17
+ /**
18
+ * Predicate: is *every* check in the `fail` bucket?
19
+ *
20
+ * This is the gate for the annotation query. A mix of real failures and
21
+ * fail-fast ones is not the billing-lockout signature — only a uniformly red
22
+ * board is — so healthy and partially-failing PRs never pay for the extra API
23
+ * calls, and their status mapping is untouched.
24
+ *
25
+ * An empty list means "no CI configured", which is a separate case handled by
26
+ * the skill's existing empty-response branch, so it returns `false` here rather
27
+ * than vacuously true.
28
+ *
29
+ * @param checks - Entries from `gh pr checks --json name,state,bucket`.
30
+ * @returns `true` only if `checks` is non-empty and every entry is `fail`.
31
+ */
32
+ export function allChecksFailing(checks) {
33
+ if (checks.length === 0)
34
+ return false;
35
+ return checks.every((check) => check.bucket === "fail");
36
+ }
37
+ /**
38
+ * The runner-never-started signature.
39
+ *
40
+ * Deliberately matches on the annotation **message only**, with no
41
+ * `annotation_level` condition. The captured fixture does carry
42
+ * `annotation_level: "failure"`, but gating on it would add a requirement the
43
+ * acceptance criteria never state, and a `warning`-level variant of the same
44
+ * message would then be silently missed — the exact class of failure this
45
+ * detection exists to prevent.
46
+ */
47
+ export const NOT_STARTED_SIGNATURE = /job was not started/i;
48
+ /**
49
+ * Scan failing checks' annotations for the not-started signature.
50
+ *
51
+ * Returns on the first match: when the board is uniformly red for a billing
52
+ * lockout, every check carries the same annotation, so one is representative
53
+ * and the caller can stop fetching.
54
+ *
55
+ * Deliberately defensive about its input shape. Callers feed this raw `gh api`
56
+ * output, and a failed annotations request (404, rate limit, revoked token)
57
+ * yields an *object* — `{"message":"Not Found"}` — where an array is expected.
58
+ * Iterating that throws. Since the whole point of this detector is to keep a
59
+ * broken-CI situation from derailing the QA phase, a malformed payload must
60
+ * degrade to "not infra-blocked", never crash the run.
61
+ *
62
+ * @param checks - Failing checks with their fetched annotations.
63
+ * @returns `{ blocked: true, message, checkName }` on a match, else `{ blocked: false }`.
64
+ */
65
+ export function detectInfraBlockedCi(checks) {
66
+ if (!Array.isArray(checks))
67
+ return { blocked: false };
68
+ for (const check of checks) {
69
+ const annotations = check?.annotations;
70
+ if (!Array.isArray(annotations))
71
+ continue;
72
+ for (const annotation of annotations) {
73
+ const message = annotation?.message;
74
+ if (typeof message === "string" && NOT_STARTED_SIGNATURE.test(message)) {
75
+ return { blocked: true, message, checkName: check.checkName };
76
+ }
77
+ }
78
+ }
79
+ return { blocked: false };
80
+ }
@@ -112,6 +112,18 @@ export interface RunSettings {
112
112
  * Default: true
113
113
  */
114
114
  retry: boolean;
115
+ /**
116
+ * Total minutes willing to wait for an exhausted rate-limit window to reopen
117
+ * before halting the run (#804).
118
+ *
119
+ * `0` (default) disables auto-wait: a window-exhausted rate limit halts
120
+ * immediately, exactly as it did before #804. Any positive value is a TOTAL
121
+ * budget per issue, not a per-occurrence allowance.
122
+ *
123
+ * The wait is in-process — it does not survive closing the terminal.
124
+ * Overridable per-invocation with `--auto-wait <minutes>`.
125
+ */
126
+ autoWaitMinutes: number;
115
127
  /**
116
128
  * Threshold for stale branch detection in pre-flight checks.
117
129
  * If feature branch is more than this many commits behind main,
@@ -311,6 +323,7 @@ export declare const RunSettingsSchema: z.ZodObject<{
311
323
  defaultBase: z.ZodOptional<z.ZodString>;
312
324
  mcp: z.ZodDefault<z.ZodBoolean>;
313
325
  retry: z.ZodDefault<z.ZodBoolean>;
326
+ autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
314
327
  staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
315
328
  resolvedIssueTTL: z.ZodDefault<z.ZodNumber>;
316
329
  pmRun: z.ZodOptional<z.ZodString>;
@@ -403,6 +416,7 @@ export declare const SettingsSchema: z.ZodObject<{
403
416
  defaultBase: z.ZodOptional<z.ZodString>;
404
417
  mcp: z.ZodDefault<z.ZodBoolean>;
405
418
  retry: z.ZodDefault<z.ZodBoolean>;
419
+ autoWaitMinutes: z.ZodDefault<z.ZodNumber>;
406
420
  staleBranchThreshold: z.ZodDefault<z.ZodNumber>;
407
421
  resolvedIssueTTL: z.ZodDefault<z.ZodNumber>;
408
422
  pmRun: z.ZodOptional<z.ZodString>;
@@ -41,16 +41,24 @@ export const RunSettingsSchema = z.object({
41
41
  logJson: z.boolean().default(true),
42
42
  logPath: z.string().default(".sequant/logs"),
43
43
  autoDetectPhases: z.boolean().default(true),
44
- timeout: z.number().default(1800),
44
+ // #833: `.positive()` on the three that feed a timer or a loop bound. A `0`
45
+ // or negative here is not a weaker setting — it is an instant abort
46
+ // (`setTimeout` clamps a non-positive delay to 0) or a loop that never runs
47
+ // (`while (iteration < maxIterations)` is false on entry). The resolvers now
48
+ // guard these too; the schema is what gets the bad key *named* at load
49
+ // instead of silently swapped for a default. Left unconstrained elsewhere
50
+ // (`staleBranchThreshold`, `resolvedIssueTTL`) where 0 is meaningful.
51
+ timeout: z.number().positive().default(1800),
45
52
  sequential: z.boolean().default(false),
46
- concurrency: z.number().default(3),
53
+ concurrency: z.number().positive().default(3),
47
54
  qualityLoop: z.boolean().default(false),
48
- maxIterations: z.number().default(3),
55
+ maxIterations: z.number().positive().default(3),
49
56
  smartTests: z.boolean().default(true),
50
57
  rotation: RotationSettingsSchema.default(() => RotationSettingsSchema.parse({})),
51
58
  defaultBase: z.string().optional(),
52
59
  mcp: z.boolean().default(true),
53
60
  retry: z.boolean().default(true),
61
+ autoWaitMinutes: z.number().min(0).default(0),
54
62
  staleBranchThreshold: z.number().default(5),
55
63
  resolvedIssueTTL: z.number().default(7),
56
64
  pmRun: z.string().optional(),
@@ -243,6 +251,43 @@ function formatZodErrors(error) {
243
251
  return { path, message };
244
252
  });
245
253
  }
254
+ /**
255
+ * Delete the values Zod rejected from a deep clone of `raw`, so a re-parse can
256
+ * fill just those with their defaults and keep every valid sibling (#833).
257
+ *
258
+ * An array index in the path means one element of an array failed. Deleting by
259
+ * index would leave a hole that re-parses as `undefined` and fails again, so
260
+ * the whole array is dropped and re-defaulted instead.
261
+ */
262
+ function stripInvalidPaths(raw, issues) {
263
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
264
+ return {};
265
+ const clone = structuredClone(raw);
266
+ for (const issue of issues) {
267
+ // An empty path means the root object itself was rejected — nothing to
268
+ // salvage, so fall back to a bare defaults parse.
269
+ if (issue.path.length === 0)
270
+ return {};
271
+ // Truncate at the first array index: drop the array, not an element.
272
+ const firstIndex = issue.path.findIndex((seg) => typeof seg === "number");
273
+ const path = firstIndex === -1 ? issue.path : issue.path.slice(0, firstIndex);
274
+ if (path.length === 0)
275
+ return {};
276
+ let cursor = clone;
277
+ for (const key of path.slice(0, -1)) {
278
+ const next = cursor?.[key];
279
+ cursor =
280
+ next && typeof next === "object" && !Array.isArray(next)
281
+ ? next
282
+ : undefined;
283
+ if (!cursor)
284
+ break;
285
+ }
286
+ if (cursor)
287
+ delete cursor[path[path.length - 1]];
288
+ }
289
+ return clone;
290
+ }
246
291
  /**
247
292
  * Validate a raw settings object against the Zod schema (AC-2).
248
293
  *
@@ -262,8 +307,15 @@ export function validateSettings(raw) {
262
307
  }
263
308
  // Zod validation failed — report errors as warnings and fall back to defaults
264
309
  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
310
+ // Salvage the valid settings by dropping only the rejected keys and
311
+ // re-parsing. #833: this previously re-parsed `{}` despite a comment saying
312
+ // it stripped invalid fields, so a single bad value — one typo in
313
+ // `run.timeout` — silently discarded the user's entire settings.json and
314
+ // reverted every unrelated key to its default.
315
+ const salvaged = SettingsSchema.safeParse(stripInvalidPaths(raw, result.error.issues));
316
+ if (salvaged.success) {
317
+ return { settings: salvaged.data, warnings };
318
+ }
267
319
  const fallback = SettingsSchema.safeParse({});
268
320
  const settings = (fallback.success ? fallback.data : DEFAULT_SETTINGS);
269
321
  return { settings, warnings };
@@ -350,6 +402,7 @@ export const DEFAULT_SETTINGS = {
350
402
  rotation: DEFAULT_ROTATION_SETTINGS,
351
403
  mcp: true, // Enable MCP servers by default in headless mode
352
404
  retry: true, // Enable automatic retry with MCP fallback by default
405
+ autoWaitMinutes: 0, // #804: auto-wait off by default — window exhaustion halts
353
406
  staleBranchThreshold: 5, // Block QA/test if feature is >5 commits behind main
354
407
  resolvedIssueTTL: 7, // Auto-prune resolved issues after 7 days
355
408
  relay: true, // Enable interactive relay (#383) by default
@@ -592,11 +645,11 @@ Generated by \`sequant init\`. See defaults below.
592
645
  | \`logJson\` | boolean | \`true\` | Enable JSON logging |
593
646
  | \`logPath\` | string | \`".sequant/logs"\` | Path to log directory |
594
647
  | \`autoDetectPhases\` | boolean | \`true\` | Auto-detect phases from GitHub issue labels |
595
- | \`timeout\` | number | \`1800\` | Default timeout per phase in seconds |
648
+ | \`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
649
  | \`sequential\` | boolean | \`false\` | Run issues sequentially by default |
597
- | \`concurrency\` | number | \`3\` | Max concurrent issues in parallel mode |
650
+ | \`concurrency\` | number (> 0) | \`3\` | Max concurrent issues in parallel mode. Must be positive (#833) |
598
651
  | \`qualityLoop\` | boolean | \`false\` | Enable quality loop by default |
599
- | \`maxIterations\` | number | \`3\` | Max iterations for quality loop |
652
+ | \`maxIterations\` | number (> 0) | \`3\` | Max iterations for quality loop. Must be positive (#833) |
600
653
  | \`smartTests\` | boolean | \`true\` | Enable smart test detection |
601
654
  | \`defaultBase\` | string | — | Default base branch for worktree creation |
602
655
  | \`mcp\` | boolean | \`true\` | Enable MCP servers in headless mode |