sequant 2.9.0 → 2.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +20 -5
- package/dist/bin/cli.js +143 -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 +56 -2
- package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
- package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
- package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
- package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
- package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
- package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
- 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 +3 -1
- 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/release/SKILL.md +1 -1
- 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/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.d.ts +20 -1
- package/dist/src/commands/locks.js +208 -4
- package/dist/src/commands/logs.js +14 -5
- 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 +31 -3
- package/dist/src/commands/ready.js +53 -13
- package/dist/src/commands/resume.d.ts +113 -0
- package/dist/src/commands/resume.js +193 -0
- package/dist/src/commands/run-display.js +38 -4
- 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/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/commands/worktree.d.ts +31 -0
- package/dist/src/commands/worktree.js +95 -0
- 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/ci/outputs.d.ts +15 -2
- package/dist/src/lib/ci/outputs.js +17 -5
- package/dist/src/lib/cli-flags.d.ts +56 -0
- package/dist/src/lib/cli-flags.js +89 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
- package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
- package/dist/src/lib/cli-ui/run-renderer.js +69 -1
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
- package/dist/src/lib/locks/checkout-lock.js +389 -0
- package/dist/src/lib/locks/index.d.ts +7 -4
- package/dist/src/lib/locks/index.js +5 -3
- package/dist/src/lib/locks/lock-manager.d.ts +101 -3
- package/dist/src/lib/locks/lock-manager.js +277 -8
- package/dist/src/lib/locks/types.d.ts +106 -3
- package/dist/src/lib/locks/types.js +44 -0
- 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 +87 -0
- package/dist/src/lib/settings.js +106 -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 +10 -2
- package/dist/src/lib/test-tautology-detector.js +213 -12
- package/dist/src/lib/version-check.d.ts +31 -0
- package/dist/src/lib/version-check.js +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +552 -60
- package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
- package/dist/src/lib/workflow/chain-resume.js +24 -20
- 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 +51 -0
- package/dist/src/lib/workflow/config-resolver.js +143 -2
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -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 +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
- package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
- package/dist/src/lib/workflow/effort-escalation.js +82 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
- package/dist/src/lib/workflow/error-classifier.js +4 -0
- 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 +54 -2
- package/dist/src/lib/workflow/log-writer.js +95 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
- package/dist/src/lib/workflow/metrics-schema.js +33 -0
- package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
- package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
- package/dist/src/lib/workflow/phase-detection.js +5 -1
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +467 -52
- 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 +45 -4
- package/dist/src/lib/workflow/ready-gate.js +49 -12
- package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
- package/dist/src/lib/workflow/run-log-schema.js +78 -2
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +168 -20
- 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/spec-recommendation.d.ts +71 -0
- package/dist/src/lib/workflow/spec-recommendation.js +142 -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/types.d.ts +190 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
- package/dist/src/lib/workflow/worktree-manager.js +111 -24
- package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
- package/dist/src/lib/workflow/worktree-resolver.js +126 -0
- package/dist/src/mcp/tools/run.d.ts +9 -1
- package/dist/src/mcp/tools/run.js +20 -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 +10 -8
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +303 -10
- package/templates/scripts/cleanup-worktree.sh +217 -33
- package/templates/scripts/new-feature.sh +289 -35
- package/templates/skills/_shared/references/subagent-types.md +7 -18
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +220 -391
- package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
- package/templates/skills/docs/SKILL.md +4 -4
- package/templates/skills/exec/SKILL.md +87 -13
- package/templates/skills/fullsolve/SKILL.md +130 -22
- package/templates/skills/loop/SKILL.md +94 -13
- package/templates/skills/merger/SKILL.md +100 -12
- package/templates/skills/qa/SKILL.md +396 -63
- 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/release/SKILL.md +80 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +33 -15
- package/templates/skills/spec/references/recommended-workflow.md +14 -1
- package/templates/skills/test/SKILL.md +1 -1
- package/templates/skills/testgen/SKILL.md +23 -6
- 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
- package/templates/agents/sequant-explorer.md +0 -24
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized spawn results and never-empty failure reasons (#803)
|
|
3
|
+
*
|
|
4
|
+
* Shared by every merge-check module that reports on a spawned command. The
|
|
5
|
+
* motivating defect: a failure reason built from `stderr` alone renders as
|
|
6
|
+
* `... failed: ` with nothing after it whenever the command wrote its
|
|
7
|
+
* diagnostics to stdout — which is where vitest, tsc, and npm put theirs.
|
|
8
|
+
*/
|
|
9
|
+
import type { SpawnSyncReturns } from "child_process";
|
|
10
|
+
/** Max characters of captured output included in a failure message. */
|
|
11
|
+
export declare const REASON_MAX_CHARS = 500;
|
|
12
|
+
/**
|
|
13
|
+
* Outcome of a spawned command, normalized across success, non-zero exit,
|
|
14
|
+
* signal termination, and spawn failure.
|
|
15
|
+
*/
|
|
16
|
+
export interface CommandResult {
|
|
17
|
+
ok: boolean;
|
|
18
|
+
stdout: string;
|
|
19
|
+
stderr: string;
|
|
20
|
+
status: number | null;
|
|
21
|
+
signal: string | null;
|
|
22
|
+
spawnError?: string;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Normalize a raw `spawnSync` return into a {@link CommandResult}.
|
|
26
|
+
*/
|
|
27
|
+
export declare function toCommandResult(result: SpawnSyncReturns<string>): CommandResult;
|
|
28
|
+
/**
|
|
29
|
+
* Produce a human-readable, never-empty reason for a failed command.
|
|
30
|
+
*
|
|
31
|
+
* Falls through: stderr → stdout tail → spawn error → signal → exit code. The
|
|
32
|
+
* stdout *tail* is used because test runners put the failure summary last,
|
|
33
|
+
* while the stderr *head* is kept because the first error is usually the
|
|
34
|
+
* proximate cause.
|
|
35
|
+
*/
|
|
36
|
+
export declare function resolveFailureReason(result: CommandResult): string;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalized spawn results and never-empty failure reasons (#803)
|
|
3
|
+
*
|
|
4
|
+
* Shared by every merge-check module that reports on a spawned command. The
|
|
5
|
+
* motivating defect: a failure reason built from `stderr` alone renders as
|
|
6
|
+
* `... failed: ` with nothing after it whenever the command wrote its
|
|
7
|
+
* diagnostics to stdout — which is where vitest, tsc, and npm put theirs.
|
|
8
|
+
*/
|
|
9
|
+
/** Max characters of captured output included in a failure message. */
|
|
10
|
+
export const REASON_MAX_CHARS = 500;
|
|
11
|
+
/**
|
|
12
|
+
* Normalize a raw `spawnSync` return into a {@link CommandResult}.
|
|
13
|
+
*/
|
|
14
|
+
export function toCommandResult(result) {
|
|
15
|
+
return {
|
|
16
|
+
ok: result.status === 0,
|
|
17
|
+
stdout: result.stdout?.trim() ?? "",
|
|
18
|
+
stderr: result.stderr?.trim() ?? "",
|
|
19
|
+
status: result.status ?? null,
|
|
20
|
+
signal: result.signal ?? null,
|
|
21
|
+
spawnError: result.error?.message,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Produce a human-readable, never-empty reason for a failed command.
|
|
26
|
+
*
|
|
27
|
+
* Falls through: stderr → stdout tail → spawn error → signal → exit code. The
|
|
28
|
+
* stdout *tail* is used because test runners put the failure summary last,
|
|
29
|
+
* while the stderr *head* is kept because the first error is usually the
|
|
30
|
+
* proximate cause.
|
|
31
|
+
*/
|
|
32
|
+
export function resolveFailureReason(result) {
|
|
33
|
+
if (result.stderr) {
|
|
34
|
+
return truncateHead(result.stderr);
|
|
35
|
+
}
|
|
36
|
+
if (result.stdout) {
|
|
37
|
+
return truncateTail(result.stdout);
|
|
38
|
+
}
|
|
39
|
+
if (result.spawnError) {
|
|
40
|
+
return result.spawnError;
|
|
41
|
+
}
|
|
42
|
+
if (result.signal) {
|
|
43
|
+
return `no output; killed by signal ${result.signal} (likely a timeout)`;
|
|
44
|
+
}
|
|
45
|
+
return `no output; exited with code ${result.status ?? "unknown"}`;
|
|
46
|
+
}
|
|
47
|
+
function truncateHead(text) {
|
|
48
|
+
return text.length > REASON_MAX_CHARS
|
|
49
|
+
? `${text.slice(0, REASON_MAX_CHARS)}…`
|
|
50
|
+
: text;
|
|
51
|
+
}
|
|
52
|
+
function truncateTail(text) {
|
|
53
|
+
return text.length > REASON_MAX_CHARS
|
|
54
|
+
? `…${text.slice(-REASON_MAX_CHARS)}`
|
|
55
|
+
: text;
|
|
56
|
+
}
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
import { type RunLog } from "../workflow/run-log-schema.js";
|
|
8
8
|
import type { BranchInfo, CheckResult, MergeCommandOptions, MergeReport } from "./types.js";
|
|
9
9
|
import { formatReportMarkdown, formatBranchReportMarkdown, postReportToGitHub } from "./report.js";
|
|
10
|
+
/**
|
|
11
|
+
* Resolve log directory path
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveLogDir(customPath?: string): string;
|
|
10
14
|
/**
|
|
11
15
|
* Find the most recent run log file
|
|
12
16
|
*/
|
|
@@ -20,7 +20,7 @@ import { buildReport, formatReportMarkdown, formatBranchReportMarkdown, postRepo
|
|
|
20
20
|
/**
|
|
21
21
|
* Resolve log directory path
|
|
22
22
|
*/
|
|
23
|
-
function resolveLogDir(customPath) {
|
|
23
|
+
export function resolveLogDir(customPath) {
|
|
24
24
|
if (customPath) {
|
|
25
25
|
return customPath.replace("~", os.homedir());
|
|
26
26
|
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* batch-level verdict. Optionally posts to GitHub as PR comment.
|
|
6
6
|
*/
|
|
7
7
|
import { spawnSync } from "child_process";
|
|
8
|
+
import { resolveFailureReason, toCommandResult } from "./command-result.js";
|
|
8
9
|
/**
|
|
9
10
|
* Compute per-issue verdicts from check results
|
|
10
11
|
*/
|
|
@@ -232,7 +233,10 @@ export function postReportToGitHub(report) {
|
|
|
232
233
|
const markdown = formatBranchReportMarkdown(report, branch.issueNumber);
|
|
233
234
|
const result = spawnSync("gh", ["pr", "comment", String(branch.prNumber), "--body", markdown], { stdio: "pipe", encoding: "utf-8" });
|
|
234
235
|
if (result.status !== 0) {
|
|
235
|
-
|
|
236
|
+
// Same never-empty reason resolution as the combined-branch test (#803):
|
|
237
|
+
// `gh` usually writes to stderr, but a killed or unspawnable process
|
|
238
|
+
// leaves it empty, which would print a message with nothing after it.
|
|
239
|
+
console.error(`Failed to post comment on PR #${branch.prNumber}: ${resolveFailureReason(toCommandResult(result))}`);
|
|
236
240
|
}
|
|
237
241
|
}
|
|
238
242
|
}
|
|
@@ -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
|
+
}
|