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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -4
- package/dist/bin/cli.js +99 -18
- package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
- package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
- package/dist/marketplace/external_plugins/sequant/README.md +58 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +38 -10
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +405 -67
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +250 -344
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +10 -7
- package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
- package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
- package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +4 -2
- package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
- package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
- package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
- package/dist/marketplace/external_plugins/sequant/skills/reflect/SKILL.md +27 -13
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/documentation-tiers.md +80 -68
- package/dist/marketplace/external_plugins/sequant/skills/reflect/references/phase-reflection.md +31 -15
- package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +11 -3
- package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
- package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
- package/dist/marketplace/external_plugins/sequant/skills/spec/references/verification-criteria.md +1 -1
- package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
- package/dist/src/commands/assess-render.d.ts +23 -0
- package/dist/src/commands/assess-render.js +60 -0
- package/dist/src/commands/doctor.js +8 -13
- package/dist/src/commands/init.js +13 -1
- package/dist/src/commands/locks.js +2 -0
- package/dist/src/commands/logs.js +20 -6
- package/dist/src/commands/merge.js +154 -2
- package/dist/src/commands/ready-tui-adapter.js +6 -1
- package/dist/src/commands/ready.d.ts +25 -3
- package/dist/src/commands/ready.js +39 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.d.ts +20 -0
- package/dist/src/commands/run-display.js +117 -5
- package/dist/src/commands/run-flags.d.ts +42 -1
- package/dist/src/commands/run-flags.js +53 -1
- package/dist/src/commands/run-progress.js +26 -1
- package/dist/src/commands/run.js +11 -10
- package/dist/src/commands/state.js +7 -0
- package/dist/src/commands/stats.js +47 -0
- package/dist/src/commands/status.d.ts +9 -0
- package/dist/src/commands/status.js +24 -1
- package/dist/src/commands/sync.js +90 -19
- package/dist/src/commands/update.js +28 -5
- package/dist/src/lib/ac-parser.d.ts +14 -0
- package/dist/src/lib/ac-parser.js +99 -5
- package/dist/src/lib/assess/index.d.ts +10 -0
- package/dist/src/lib/assess/index.js +9 -0
- package/dist/src/lib/assess/renderer.d.ts +64 -0
- package/dist/src/lib/assess/renderer.js +481 -0
- package/dist/src/lib/assess/types.d.ts +224 -0
- package/dist/src/lib/assess/types.js +241 -0
- package/dist/src/lib/assess-collision-detect.d.ts +19 -2
- package/dist/src/lib/assess-collision-detect.js +68 -4
- package/dist/src/lib/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +33 -0
- package/dist/src/lib/cli-flags.js +46 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +79 -9
- package/dist/src/lib/errors.d.ts +84 -6
- package/dist/src/lib/errors.js +142 -17
- package/dist/src/lib/locks/index.d.ts +3 -3
- package/dist/src/lib/locks/index.js +2 -2
- package/dist/src/lib/locks/lock-manager.d.ts +20 -2
- package/dist/src/lib/locks/lock-manager.js +47 -3
- package/dist/src/lib/locks/types.d.ts +34 -3
- package/dist/src/lib/locks/types.js +16 -0
- package/dist/src/lib/manifest.js +1 -17
- package/dist/src/lib/mcp-config.d.ts +45 -0
- package/dist/src/lib/mcp-config.js +77 -1
- package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
- package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
- package/dist/src/lib/merge-check/command-result.d.ts +36 -0
- package/dist/src/lib/merge-check/command-result.js +56 -0
- package/dist/src/lib/merge-check/index.d.ts +4 -0
- package/dist/src/lib/merge-check/index.js +1 -1
- package/dist/src/lib/merge-check/report.js +5 -1
- package/dist/src/lib/merge-check/types.d.ts +9 -0
- package/dist/src/lib/merge-check/watch.d.ts +121 -0
- package/dist/src/lib/merge-check/watch.js +215 -0
- package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
- package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
- package/dist/src/lib/settings.d.ts +14 -0
- package/dist/src/lib/settings.js +61 -8
- package/dist/src/lib/shutdown.d.ts +42 -2
- package/dist/src/lib/shutdown.js +75 -5
- package/dist/src/lib/skills-check.d.ts +26 -0
- package/dist/src/lib/skills-check.js +31 -0
- package/dist/src/lib/stacks.d.ts +110 -1
- package/dist/src/lib/stacks.js +244 -5
- package/dist/src/lib/templates.d.ts +74 -0
- package/dist/src/lib/templates.js +171 -16
- package/dist/src/lib/test-tautology-detector.d.ts +9 -2
- package/dist/src/lib/test-tautology-detector.js +153 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +46 -7
- package/dist/src/lib/workflow/batch-executor.d.ts +138 -6
- package/dist/src/lib/workflow/batch-executor.js +550 -54
- package/dist/src/lib/workflow/chain-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/chain-preflight.js +199 -0
- package/dist/src/lib/workflow/chain-resume.d.ts +118 -0
- package/dist/src/lib/workflow/chain-resume.js +170 -0
- package/dist/src/lib/workflow/completed-status.d.ts +52 -0
- package/dist/src/lib/workflow/completed-status.js +57 -0
- package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
- package/dist/src/lib/workflow/config-resolver.js +54 -2
- package/dist/src/lib/workflow/dependency-markers.d.ts +29 -0
- package/dist/src/lib/workflow/dependency-markers.js +79 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
- package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
- package/dist/src/lib/workflow/drivers/aider.js +2 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +21 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +50 -8
- package/dist/src/lib/workflow/error-classifier.d.ts +9 -2
- package/dist/src/lib/workflow/error-classifier.js +14 -1
- package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
- package/dist/src/lib/workflow/git-diff-utils.js +77 -4
- package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
- package/dist/src/lib/workflow/heartbeat.js +90 -0
- package/dist/src/lib/workflow/log-writer.d.ts +44 -1
- package/dist/src/lib/workflow/log-writer.js +81 -14
- package/dist/src/lib/workflow/metrics-schema.d.ts +42 -3
- package/dist/src/lib/workflow/metrics-schema.js +16 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +2 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +234 -13
- package/dist/src/lib/workflow/phase-executor.js +533 -56
- package/dist/src/lib/workflow/phase-registry.js +2 -1
- package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
- package/dist/src/lib/workflow/platforms/github.js +152 -0
- package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
- package/dist/src/lib/workflow/qa-cache.js +22 -9
- package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
- package/dist/src/lib/workflow/ready-gate.js +25 -9
- package/dist/src/lib/workflow/run-log-schema.d.ts +37 -0
- package/dist/src/lib/workflow/run-log-schema.js +92 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +33 -0
- package/dist/src/lib/workflow/run-orchestrator.js +429 -47
- package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
- package/dist/src/lib/workflow/skills-preflight.js +112 -0
- package/dist/src/lib/workflow/state-cleanup.js +9 -2
- package/dist/src/lib/workflow/state-manager.d.ts +33 -0
- package/dist/src/lib/workflow/state-manager.js +83 -0
- package/dist/src/lib/workflow/state-schema.d.ts +26 -0
- package/dist/src/lib/workflow/state-schema.js +47 -0
- package/dist/src/lib/workflow/status-derivation.d.ts +30 -0
- package/dist/src/lib/workflow/status-derivation.js +27 -0
- package/dist/src/lib/workflow/types.d.ts +149 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +70 -2
- package/dist/src/lib/workflow/worktree-manager.js +201 -52
- package/dist/src/mcp/tools/run.d.ts +11 -1
- package/dist/src/mcp/tools/run.js +22 -1
- package/dist/src/mcp/tools/status.js +6 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
- package/dist/src/ui/tui/ElapsedTimer.js +12 -10
- package/dist/src/ui/tui/IssueBox.js +10 -2
- package/dist/src/ui/tui/row-cap.js +2 -1
- package/dist/src/ui/tui/theme.d.ts +7 -0
- package/dist/src/ui/tui/theme.js +9 -0
- package/package.json +9 -10
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +38 -10
- package/templates/hooks/pre-tool.sh +405 -67
- package/templates/scripts/cleanup-worktree.sh +282 -30
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +250 -344
- package/templates/skills/assess/references/predicted-collision-detection.md +10 -7
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +26 -6
- package/templates/skills/fullsolve/SKILL.md +4 -2
- package/templates/skills/loop/SKILL.md +38 -2
- package/templates/skills/merger/SKILL.md +2 -2
- package/templates/skills/qa/SKILL.md +337 -57
- package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
- package/templates/skills/qa/references/call-site-review.md +2 -2
- package/templates/skills/qa/references/code-review-checklist.md +2 -2
- package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
- package/templates/skills/qa/references/quality-gates.md +21 -7
- package/templates/skills/qa/references/testing-requirements.md +1 -1
- package/templates/skills/qa/scripts/quality-checks.sh +11 -11
- package/templates/skills/reflect/SKILL.md +27 -13
- package/templates/skills/reflect/references/documentation-tiers.md +80 -68
- package/templates/skills/reflect/references/phase-reflection.md +31 -15
- package/templates/skills/release/SKILL.md +11 -3
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- package/templates/skills/spec/references/verification-criteria.md +1 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/dist/src/lib/phase-spinner.d.ts +0 -146
- package/dist/src/lib/phase-spinner.js +0 -255
- package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
- package/dist/src/lib/workflow/pr-operations.js +0 -326
- package/dist/src/lib/workflow/run-summary.d.ts +0 -36
- 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>;
|
package/dist/src/lib/settings.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
//
|
|
266
|
-
//
|
|
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 |
|