sequant 2.9.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 +14 -3
- package/dist/bin/cli.js +97 -17
- 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.js +2 -0
- 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 +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.js +37 -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/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 +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 +62 -0
- package/dist/src/lib/errors.d.ts +78 -6
- package/dist/src/lib/errors.js +133 -15
- 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/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 +45 -2
- package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
- package/dist/src/lib/workflow/batch-executor.js +477 -44
- 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 +26 -0
- package/dist/src/lib/workflow/config-resolver.js +54 -2
- 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 +14 -0
- package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
- 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 +75 -6
- package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
- package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
- package/dist/src/lib/workflow/phase-executor.js +457 -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 +17 -4
- package/dist/src/lib/workflow/ready-gate.js +25 -9
- package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
- package/dist/src/lib/workflow/run-log-schema.js +47 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
- package/dist/src/lib/workflow/run-orchestrator.js +141 -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/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 +126 -4
- package/dist/src/lib/workflow/types.js +1 -0
- package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
- package/dist/src/lib/workflow/worktree-manager.js +102 -23
- 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 +8 -7
- package/templates/hooks/parallel-marker.sh +50 -0
- package/templates/hooks/post-tool.sh +20 -7
- package/templates/hooks/pre-tool.sh +75 -10
- package/templates/scripts/cleanup-worktree.sh +182 -19
- package/templates/scripts/new-feature.sh +264 -16
- package/templates/skills/_shared/references/trust-model.md +18 -0
- package/templates/skills/assess/SKILL.md +215 -390
- 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 +26 -6
- package/templates/skills/fullsolve/SKILL.md +3 -1
- 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/release/SKILL.md +1 -1
- package/templates/skills/setup/SKILL.md +5 -5
- package/templates/skills/spec/SKILL.md +2 -0
- 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
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
* Async interface methods delegate to the sync implementations.
|
|
10
10
|
*/
|
|
11
11
|
import type { PlatformProvider, Issue, CreatePROptions, PRInfo, PRStatus, Comment } from "./platform-provider.js";
|
|
12
|
+
import type { AnnotatedCheck } from "../../qa/infra-blocked-ci.js";
|
|
12
13
|
/**
|
|
13
14
|
* PR merge status values (matches the casing returned by `gh pr view`).
|
|
14
15
|
*/
|
|
@@ -55,6 +56,30 @@ export interface BatchGitHubResult {
|
|
|
55
56
|
pullRequests: Record<number, BatchPRInfo>;
|
|
56
57
|
error?: string;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* A single entry in a PR's `statusCheckRollup`.
|
|
61
|
+
*
|
|
62
|
+
* `gh pr view N --json statusCheckRollup` returns a heterogeneous array of two
|
|
63
|
+
* shapes, discriminated by `__typename`:
|
|
64
|
+
* - `CheckRun` (GitHub Actions / Checks API): progress in `status`
|
|
65
|
+
* (QUEUED | IN_PROGRESS | COMPLETED) with the outcome in `conclusion`.
|
|
66
|
+
* - `StatusContext` (legacy commit statuses): combined state in `state`
|
|
67
|
+
* (EXPECTED | PENDING | SUCCESS | FAILURE | ERROR).
|
|
68
|
+
* Only the fields the watch loop consumes are typed; unknown fields are ignored.
|
|
69
|
+
*/
|
|
70
|
+
export interface RollupEntry {
|
|
71
|
+
__typename?: string;
|
|
72
|
+
name?: string;
|
|
73
|
+
context?: string;
|
|
74
|
+
/** CheckRun progress. */
|
|
75
|
+
status?: string;
|
|
76
|
+
/** CheckRun outcome. */
|
|
77
|
+
conclusion?: string;
|
|
78
|
+
/** StatusContext combined state. */
|
|
79
|
+
state?: string;
|
|
80
|
+
}
|
|
81
|
+
/** PR mergeability as reported by `gh pr view --json mergeable`. */
|
|
82
|
+
export type MergeableState = "MERGEABLE" | "CONFLICTING" | "UNKNOWN";
|
|
58
83
|
export declare class GitHubProvider implements PlatformProvider {
|
|
59
84
|
name: string;
|
|
60
85
|
/**
|
|
@@ -151,6 +176,45 @@ export declare class GitHubProvider implements PlatformProvider {
|
|
|
151
176
|
* Used by upstream/issues.ts.
|
|
152
177
|
*/
|
|
153
178
|
commentOnIssueWithBodyFileSync(repo: string, issueNumber: number, bodyFile: string): boolean;
|
|
179
|
+
/**
|
|
180
|
+
* Fetch a PR's status-check rollup via `statusCheckRollup`.
|
|
181
|
+
*
|
|
182
|
+
* Uses `statusCheckRollup`, NOT the known-broken `--json checks` field
|
|
183
|
+
* (#443 / #818). Returns the raw heterogeneous entries; callers classify
|
|
184
|
+
* terminal-ness with the helpers in `merge-check/watch.ts`. Returns `[]` on
|
|
185
|
+
* any error so a transient `gh` failure degrades to "no checks yet" rather
|
|
186
|
+
* than crashing a poll loop.
|
|
187
|
+
*/
|
|
188
|
+
getStatusCheckRollupSync(prNumber: number): RollupEntry[];
|
|
189
|
+
/**
|
|
190
|
+
* Fetch a PR's mergeability state via `--json mergeable`.
|
|
191
|
+
*
|
|
192
|
+
* Right after a push GitHub reports `UNKNOWN` while it recomputes, then
|
|
193
|
+
* settles to `MERGEABLE` or `CONFLICTING`. `merge --watch` treats
|
|
194
|
+
* `CONFLICTING` as a dispatch block: CI never starts against an unmergeable
|
|
195
|
+
* ref. Returns `UNKNOWN` on any error — the safe default that keeps the loop
|
|
196
|
+
* polling rather than falsely reporting a conflict.
|
|
197
|
+
*/
|
|
198
|
+
getMergeableStateSync(prNumber: number): MergeableState;
|
|
199
|
+
/**
|
|
200
|
+
* Get a PR's head commit SHA via `--json headRefOid`.
|
|
201
|
+
*
|
|
202
|
+
* Needed to query the commit's check-run annotations for the billing-lockout
|
|
203
|
+
* signature. Returns `null` on error.
|
|
204
|
+
*/
|
|
205
|
+
getPRHeadShaSync(prNumber: number): string | null;
|
|
206
|
+
/**
|
|
207
|
+
* Fetch check-run annotations for a commit, shaped for `detectInfraBlockedCi`.
|
|
208
|
+
*
|
|
209
|
+
* Lists the commit's check runs (per_page=100, no pagination needed for the
|
|
210
|
+
* billing-lockout case — every run carries the same annotation, so the first
|
|
211
|
+
* page is representative), then fetches annotations for each run that reports
|
|
212
|
+
* any. Used only when the whole board is failing, to recognise the "job was
|
|
213
|
+
* not started" signature (#820). Degrades to `[]` on any error (404, rate
|
|
214
|
+
* limit, revoked token) so a broken-CI situation never crashes the watch
|
|
215
|
+
* loop — `detectInfraBlockedCi` then reads that as "not infra-blocked".
|
|
216
|
+
*/
|
|
217
|
+
getCheckRunAnnotationsSync(headSha: string): AnnotatedCheck[];
|
|
154
218
|
fetchIssue(id: string): Promise<Issue>;
|
|
155
219
|
postComment(issueId: string, body: string): Promise<void>;
|
|
156
220
|
addLabel(issueId: string, label: string): Promise<void>;
|
|
@@ -414,6 +414,158 @@ export class GitHubProvider {
|
|
|
414
414
|
return false;
|
|
415
415
|
}
|
|
416
416
|
}
|
|
417
|
+
// ─── Watch / CI-rollup helpers (for `merge --watch`, #818) ─────────
|
|
418
|
+
/**
|
|
419
|
+
* Fetch a PR's status-check rollup via `statusCheckRollup`.
|
|
420
|
+
*
|
|
421
|
+
* Uses `statusCheckRollup`, NOT the known-broken `--json checks` field
|
|
422
|
+
* (#443 / #818). Returns the raw heterogeneous entries; callers classify
|
|
423
|
+
* terminal-ness with the helpers in `merge-check/watch.ts`. Returns `[]` on
|
|
424
|
+
* any error so a transient `gh` failure degrades to "no checks yet" rather
|
|
425
|
+
* than crashing a poll loop.
|
|
426
|
+
*/
|
|
427
|
+
getStatusCheckRollupSync(prNumber) {
|
|
428
|
+
try {
|
|
429
|
+
const result = spawnSync("gh", [
|
|
430
|
+
"pr",
|
|
431
|
+
"view",
|
|
432
|
+
String(prNumber),
|
|
433
|
+
"--json",
|
|
434
|
+
"statusCheckRollup",
|
|
435
|
+
"-q",
|
|
436
|
+
".statusCheckRollup",
|
|
437
|
+
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
|
|
438
|
+
if (result.status !== 0 || !result.stdout)
|
|
439
|
+
return [];
|
|
440
|
+
const parsed = JSON.parse(result.stdout);
|
|
441
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
442
|
+
}
|
|
443
|
+
catch {
|
|
444
|
+
return [];
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Fetch a PR's mergeability state via `--json mergeable`.
|
|
449
|
+
*
|
|
450
|
+
* Right after a push GitHub reports `UNKNOWN` while it recomputes, then
|
|
451
|
+
* settles to `MERGEABLE` or `CONFLICTING`. `merge --watch` treats
|
|
452
|
+
* `CONFLICTING` as a dispatch block: CI never starts against an unmergeable
|
|
453
|
+
* ref. Returns `UNKNOWN` on any error — the safe default that keeps the loop
|
|
454
|
+
* polling rather than falsely reporting a conflict.
|
|
455
|
+
*/
|
|
456
|
+
getMergeableStateSync(prNumber) {
|
|
457
|
+
try {
|
|
458
|
+
const result = spawnSync("gh", [
|
|
459
|
+
"pr",
|
|
460
|
+
"view",
|
|
461
|
+
String(prNumber),
|
|
462
|
+
"--json",
|
|
463
|
+
"mergeable",
|
|
464
|
+
"-q",
|
|
465
|
+
".mergeable",
|
|
466
|
+
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
|
|
467
|
+
if (result.status === 0 && result.stdout) {
|
|
468
|
+
const state = result.stdout.toString().trim().toUpperCase();
|
|
469
|
+
if (state === "MERGEABLE" ||
|
|
470
|
+
state === "CONFLICTING" ||
|
|
471
|
+
state === "UNKNOWN") {
|
|
472
|
+
return state;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
catch {
|
|
477
|
+
// fall through to UNKNOWN
|
|
478
|
+
}
|
|
479
|
+
return "UNKNOWN";
|
|
480
|
+
}
|
|
481
|
+
/**
|
|
482
|
+
* Get a PR's head commit SHA via `--json headRefOid`.
|
|
483
|
+
*
|
|
484
|
+
* Needed to query the commit's check-run annotations for the billing-lockout
|
|
485
|
+
* signature. Returns `null` on error.
|
|
486
|
+
*/
|
|
487
|
+
getPRHeadShaSync(prNumber) {
|
|
488
|
+
try {
|
|
489
|
+
const result = spawnSync("gh", [
|
|
490
|
+
"pr",
|
|
491
|
+
"view",
|
|
492
|
+
String(prNumber),
|
|
493
|
+
"--json",
|
|
494
|
+
"headRefOid",
|
|
495
|
+
"-q",
|
|
496
|
+
".headRefOid",
|
|
497
|
+
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 10000 });
|
|
498
|
+
if (result.status === 0 && result.stdout) {
|
|
499
|
+
const sha = result.stdout.toString().trim();
|
|
500
|
+
return sha || null;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
// fall through to null
|
|
505
|
+
}
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* Fetch check-run annotations for a commit, shaped for `detectInfraBlockedCi`.
|
|
510
|
+
*
|
|
511
|
+
* Lists the commit's check runs (per_page=100, no pagination needed for the
|
|
512
|
+
* billing-lockout case — every run carries the same annotation, so the first
|
|
513
|
+
* page is representative), then fetches annotations for each run that reports
|
|
514
|
+
* any. Used only when the whole board is failing, to recognise the "job was
|
|
515
|
+
* not started" signature (#820). Degrades to `[]` on any error (404, rate
|
|
516
|
+
* limit, revoked token) so a broken-CI situation never crashes the watch
|
|
517
|
+
* loop — `detectInfraBlockedCi` then reads that as "not infra-blocked".
|
|
518
|
+
*/
|
|
519
|
+
getCheckRunAnnotationsSync(headSha) {
|
|
520
|
+
try {
|
|
521
|
+
const listResult = spawnSync("gh", [
|
|
522
|
+
"api",
|
|
523
|
+
`repos/{owner}/{repo}/commits/${headSha}/check-runs?per_page=100`,
|
|
524
|
+
"-q",
|
|
525
|
+
".check_runs",
|
|
526
|
+
], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
|
|
527
|
+
if (listResult.status !== 0 || !listResult.stdout)
|
|
528
|
+
return [];
|
|
529
|
+
const runs = JSON.parse(listResult.stdout);
|
|
530
|
+
if (!Array.isArray(runs))
|
|
531
|
+
return [];
|
|
532
|
+
const annotated = [];
|
|
533
|
+
for (const run of runs) {
|
|
534
|
+
const checkName = typeof run?.name === "string" ? run.name : "unknown";
|
|
535
|
+
const id = run?.id;
|
|
536
|
+
if (typeof id !== "number" && typeof id !== "string")
|
|
537
|
+
continue;
|
|
538
|
+
// Skip runs that explicitly report zero annotations; keep fetching when
|
|
539
|
+
// the count is unknown, so a missing field never hides the signature.
|
|
540
|
+
const count = run?.output?.annotations_count;
|
|
541
|
+
if (typeof count === "number" && count === 0)
|
|
542
|
+
continue;
|
|
543
|
+
const annResult = spawnSync("gh", ["api", `repos/{owner}/{repo}/check-runs/${id}/annotations`], {
|
|
544
|
+
encoding: "utf-8",
|
|
545
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
546
|
+
timeout: 15000,
|
|
547
|
+
});
|
|
548
|
+
if (annResult.status !== 0 || !annResult.stdout) {
|
|
549
|
+
annotated.push({ checkName, annotations: [] });
|
|
550
|
+
continue;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const annotations = JSON.parse(annResult.stdout);
|
|
554
|
+
annotated.push({
|
|
555
|
+
checkName,
|
|
556
|
+
annotations: Array.isArray(annotations) ? annotations : [],
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
annotated.push({ checkName, annotations: [] });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
return annotated;
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
return [];
|
|
567
|
+
}
|
|
568
|
+
}
|
|
417
569
|
// ─── Async interface methods (PlatformProvider) ────────────────────
|
|
418
570
|
async fetchIssue(id) {
|
|
419
571
|
const result = spawnSync("gh", ["issue", "view", id, "--json", "number,title,body,labels,state"], { encoding: "utf-8", stdio: ["pipe", "pipe", "pipe"], timeout: 15000 });
|
|
@@ -130,6 +130,8 @@ export interface QACacheOptions {
|
|
|
130
130
|
defaultTtl?: number;
|
|
131
131
|
/** Enable verbose logging */
|
|
132
132
|
verbose?: boolean;
|
|
133
|
+
/** Repository directory for git operations (default: process.cwd()) */
|
|
134
|
+
cwd?: string;
|
|
133
135
|
}
|
|
134
136
|
/**
|
|
135
137
|
* Result of cache lookup
|
|
@@ -153,6 +155,7 @@ export declare class QACache {
|
|
|
153
155
|
private cacheDir;
|
|
154
156
|
private defaultTtl;
|
|
155
157
|
private verbose;
|
|
158
|
+
private cwd;
|
|
156
159
|
private cachedState;
|
|
157
160
|
constructor(options?: QACacheOptions);
|
|
158
161
|
/**
|
|
@@ -160,9 +163,11 @@ export declare class QACache {
|
|
|
160
163
|
*/
|
|
161
164
|
getCachePath(): string;
|
|
162
165
|
/**
|
|
163
|
-
* Compute hash of git diff between
|
|
166
|
+
* Compute hash of git diff between the resolved base and HEAD
|
|
164
167
|
*
|
|
165
168
|
* This provides a content-based cache key that changes when code changes.
|
|
169
|
+
* The base is resolved via resolveDiffBase so a stale local main does not
|
|
170
|
+
* leak merged-upstream content into the cache key (#890).
|
|
166
171
|
*/
|
|
167
172
|
computeDiffHash(): string;
|
|
168
173
|
/**
|
|
@@ -27,8 +27,9 @@
|
|
|
27
27
|
import * as fs from "fs";
|
|
28
28
|
import * as path from "path";
|
|
29
29
|
import * as crypto from "crypto";
|
|
30
|
-
import {
|
|
30
|
+
import { execFileSync } from "child_process";
|
|
31
31
|
import { z } from "zod";
|
|
32
|
+
import { resolveDiffBase } from "./git-diff-utils.js";
|
|
32
33
|
/**
|
|
33
34
|
* Check types that can be cached
|
|
34
35
|
*/
|
|
@@ -74,7 +75,7 @@ export const CachedCheckResultSchema = z.object({
|
|
|
74
75
|
export const QARunContextSchema = z.object({
|
|
75
76
|
/** Git HEAD SHA at the time of QA completion */
|
|
76
77
|
lastQACommitSHA: z.string(),
|
|
77
|
-
/** Diff hash (
|
|
78
|
+
/** Diff hash (resolved base...HEAD) at the time of QA */
|
|
78
79
|
lastQADiffHash: z.string(),
|
|
79
80
|
/** Map of AC ID → status from the last QA run */
|
|
80
81
|
acStatuses: z.record(z.string(), z.enum(["met", "not_met", "partially_met", "pending", "blocked"])),
|
|
@@ -139,11 +140,13 @@ export class QACache {
|
|
|
139
140
|
cacheDir;
|
|
140
141
|
defaultTtl;
|
|
141
142
|
verbose;
|
|
143
|
+
cwd;
|
|
142
144
|
cachedState = null;
|
|
143
145
|
constructor(options = {}) {
|
|
144
146
|
this.cacheDir = options.cacheDir ?? DEFAULT_CACHE_DIR;
|
|
145
147
|
this.defaultTtl = options.defaultTtl ?? DEFAULT_TTL;
|
|
146
148
|
this.verbose = options.verbose ?? false;
|
|
149
|
+
this.cwd = options.cwd ?? process.cwd();
|
|
147
150
|
}
|
|
148
151
|
/**
|
|
149
152
|
* Get the cache file path
|
|
@@ -152,14 +155,18 @@ export class QACache {
|
|
|
152
155
|
return path.join(this.cacheDir, "cache.json");
|
|
153
156
|
}
|
|
154
157
|
/**
|
|
155
|
-
* Compute hash of git diff between
|
|
158
|
+
* Compute hash of git diff between the resolved base and HEAD
|
|
156
159
|
*
|
|
157
160
|
* This provides a content-based cache key that changes when code changes.
|
|
161
|
+
* The base is resolved via resolveDiffBase so a stale local main does not
|
|
162
|
+
* leak merged-upstream content into the cache key (#890).
|
|
158
163
|
*/
|
|
159
164
|
computeDiffHash() {
|
|
160
165
|
try {
|
|
161
|
-
// Get the diff content between
|
|
162
|
-
const
|
|
166
|
+
// Get the diff content between the resolved base and HEAD
|
|
167
|
+
const base = resolveDiffBase(this.cwd, "main");
|
|
168
|
+
const diff = execFileSync("git", ["diff", `${base}...HEAD`], {
|
|
169
|
+
cwd: this.cwd,
|
|
163
170
|
encoding: "utf-8",
|
|
164
171
|
maxBuffer: 10 * 1024 * 1024, // 10MB buffer
|
|
165
172
|
});
|
|
@@ -236,7 +243,9 @@ export class QACache {
|
|
|
236
243
|
*/
|
|
237
244
|
async checkGlobalInvalidation() {
|
|
238
245
|
try {
|
|
239
|
-
const
|
|
246
|
+
const base = resolveDiffBase(this.cwd, "main");
|
|
247
|
+
const changedFiles = execFileSync("git", ["diff", `${base}...HEAD`, "--name-only"], {
|
|
248
|
+
cwd: this.cwd,
|
|
240
249
|
encoding: "utf-8",
|
|
241
250
|
})
|
|
242
251
|
.trim()
|
|
@@ -263,7 +272,9 @@ export class QACache {
|
|
|
263
272
|
return false;
|
|
264
273
|
}
|
|
265
274
|
try {
|
|
266
|
-
const
|
|
275
|
+
const base = resolveDiffBase(this.cwd, "main");
|
|
276
|
+
const changedFiles = execFileSync("git", ["diff", `${base}...HEAD`, "--name-only"], {
|
|
277
|
+
cwd: this.cwd,
|
|
267
278
|
encoding: "utf-8",
|
|
268
279
|
})
|
|
269
280
|
.trim()
|
|
@@ -432,7 +443,8 @@ export class QACache {
|
|
|
432
443
|
return null;
|
|
433
444
|
}
|
|
434
445
|
try {
|
|
435
|
-
const diff =
|
|
446
|
+
const diff = execFileSync("git", ["diff", `${sinceCommit}...HEAD`], {
|
|
447
|
+
cwd: this.cwd,
|
|
436
448
|
encoding: "utf-8",
|
|
437
449
|
maxBuffer: 10 * 1024 * 1024,
|
|
438
450
|
});
|
|
@@ -460,7 +472,8 @@ export class QACache {
|
|
|
460
472
|
return null;
|
|
461
473
|
}
|
|
462
474
|
try {
|
|
463
|
-
const output =
|
|
475
|
+
const output = execFileSync("git", ["diff", `${sinceCommit}...HEAD`, "--name-only"], {
|
|
476
|
+
cwd: this.cwd,
|
|
464
477
|
encoding: "utf-8",
|
|
465
478
|
});
|
|
466
479
|
return output.trim().split("\n").filter(Boolean);
|
|
@@ -26,6 +26,7 @@ import type { QaVerdict } from "./run-log-schema.js";
|
|
|
26
26
|
import type { ReadyPolicy } from "../settings.js";
|
|
27
27
|
import type { IssueStatus } from "./state-schema.js";
|
|
28
28
|
import { type LoopProgressSnapshot } from "./qa-stagnation.js";
|
|
29
|
+
import { type ExecChangeState } from "./phase-executor.js";
|
|
29
30
|
export type { ReadyPolicy } from "../settings.js";
|
|
30
31
|
/**
|
|
31
32
|
* Why the gate stopped. Drives `ready`, the persisted issue status, and the
|
|
@@ -44,8 +45,20 @@ export type ReadyTerminalReason =
|
|
|
44
45
|
| "LOOP_NO_DIFF"
|
|
45
46
|
/** Guard: `/loop` phase itself failed. Needs human. */
|
|
46
47
|
| "LOOP_FAILED"
|
|
47
|
-
/** #534: zero-diff exec
|
|
48
|
-
| "NO_IMPLEMENTATION"
|
|
48
|
+
/** #534: zero-diff exec worktree — nothing was built. Not ready. */
|
|
49
|
+
| "NO_IMPLEMENTATION"
|
|
50
|
+
/**
|
|
51
|
+
* #879: the worktree is dirty but has no commits. Distinct from
|
|
52
|
+
* NO_IMPLEMENTATION — work exists, but uncommitted work cannot rebase, push,
|
|
53
|
+
* or become a PR, so it is not certifiable as-is. Re-run once it is committed.
|
|
54
|
+
*/
|
|
55
|
+
| "UNCOMMITTED_ONLY"
|
|
56
|
+
/**
|
|
57
|
+
* #853: QA ran but produced no verdict (deferred its one-shot turn, or
|
|
58
|
+
* output was unparseable). Distinct from NO_IMPLEMENTATION — the
|
|
59
|
+
* implementation may be complete; it is the *review* that is missing.
|
|
60
|
+
*/
|
|
61
|
+
| "NO_VERDICT";
|
|
49
62
|
/** A single gap surfaced by QA, classified for the report. */
|
|
50
63
|
export interface ReadyGapItem {
|
|
51
64
|
/** Gap description as surfaced by QA. */
|
|
@@ -110,8 +123,8 @@ export interface RunReadyGateOptions {
|
|
|
110
123
|
onProgress?: ProgressCallback;
|
|
111
124
|
/** Injectable token reader — defaults to reading `<worktree>/.sequant`. */
|
|
112
125
|
readTokensUsed?: (worktreePath: string) => number;
|
|
113
|
-
/** Injectable change
|
|
114
|
-
|
|
126
|
+
/** Injectable change classifier — defaults to {@link classifyExecChanges}. */
|
|
127
|
+
classifyChangesFn?: (cwd: string) => ExecChangeState;
|
|
115
128
|
/** Injectable loop-progress snapshot — defaults to {@link snapshotLoopProgress}. */
|
|
116
129
|
snapshotFn?: (cwd: string) => LoopProgressSnapshot;
|
|
117
130
|
}
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* lives in `src/commands/ready.ts`.
|
|
23
23
|
*/
|
|
24
24
|
import { snapshotLoopProgress, compareLoopProgress, } from "./qa-stagnation.js";
|
|
25
|
-
import {
|
|
25
|
+
import { classifyExecChanges } from "./phase-executor.js";
|
|
26
26
|
import { readTokenUsageFiles, aggregateTokenUsage, TOKEN_USAGE_DIR, } from "./token-utils.js";
|
|
27
27
|
import * as path from "path";
|
|
28
28
|
/**
|
|
@@ -180,7 +180,11 @@ export function formatReadyReport(result) {
|
|
|
180
180
|
? "✅ READY — awaiting human merge decision"
|
|
181
181
|
: result.reason === "NO_IMPLEMENTATION"
|
|
182
182
|
? "⛔ NOT READY — no implementation detected"
|
|
183
|
-
:
|
|
183
|
+
: result.reason === "UNCOMMITTED_ONLY"
|
|
184
|
+
? "⛔ NOT READY — work is uncommitted"
|
|
185
|
+
: result.reason === "NO_VERDICT"
|
|
186
|
+
? "⛔ NOT READY — QA produced no verdict"
|
|
187
|
+
: "⚠️ NOT READY — needs human intervention";
|
|
184
188
|
const reasonText = {
|
|
185
189
|
AC_MET: "Acceptance Criteria are objectively met (no AC_NOT_MET). Remaining gaps are quality-only and reported below (policy `ac` does not auto-fix them).",
|
|
186
190
|
READY_FOR_MERGE: "QA returned READY_FOR_MERGE.",
|
|
@@ -188,7 +192,9 @@ export function formatReadyReport(result) {
|
|
|
188
192
|
TOKEN_BUDGET: "Token budget exhausted before reaching the policy threshold. A human should review the remaining gaps.",
|
|
189
193
|
LOOP_NO_DIFF: "The fix loop made no diff (stagnation guard). Manual intervention required.",
|
|
190
194
|
LOOP_FAILED: "The fix loop phase failed. A human should investigate before merging.",
|
|
191
|
-
NO_IMPLEMENTATION: "Zero-diff worktree
|
|
195
|
+
NO_IMPLEMENTATION: "Zero-diff worktree — there is nothing to certify (#534 guard).",
|
|
196
|
+
UNCOMMITTED_ONLY: "The worktree has uncommitted changes but no commits (#879 guard). Uncommitted work cannot rebase, push, or become a PR — commit it, then re-run the gate.",
|
|
197
|
+
NO_VERDICT: "QA ran but produced no verdict (deferred one-shot turn or unparseable output) — there is nothing to certify (#534/#853 guard). The implementation may exist; re-run the gate once QA emits a verdict.",
|
|
192
198
|
};
|
|
193
199
|
const lines = [];
|
|
194
200
|
lines.push(`## sequant ready — Issue #${result.issueNumber}`);
|
|
@@ -231,7 +237,7 @@ export function formatReadyReport(result) {
|
|
|
231
237
|
export async function runReadyGate(opts) {
|
|
232
238
|
const { issueNumber, worktreePath, policy, maxIterations, tokenBudget, nonGoals = [], } = opts;
|
|
233
239
|
const readTokensUsed = opts.readTokensUsed ?? defaultReadTokensUsed;
|
|
234
|
-
const
|
|
240
|
+
const classifyChangesFn = opts.classifyChangesFn ?? classifyExecChanges;
|
|
235
241
|
const snapshotFn = opts.snapshotFn ?? snapshotLoopProgress;
|
|
236
242
|
// #697: run a phase while emitting live-progress events around it. `iteration`
|
|
237
243
|
// is the 1-based QA-pass index so the renderer can render `loop N/M`. Emits
|
|
@@ -296,15 +302,25 @@ export async function runReadyGate(opts) {
|
|
|
296
302
|
const qaResult = await runPhaseTracked("qa", buildPhaseConfig(opts, { fullQa: true }), iterations);
|
|
297
303
|
tokensUsed = readTokensUsed(worktreePath);
|
|
298
304
|
const verdict = qaResult.verdict ?? null;
|
|
299
|
-
// #534 guard: a null
|
|
305
|
+
// #534 guard: a null verdict is never "ready". #853: report it as
|
|
306
|
+
// NO_VERDICT, not NO_IMPLEMENTATION — a deferred/unparseable QA turn says
|
|
307
|
+
// nothing about whether an implementation exists, and the misleading label
|
|
308
|
+
// sent debuggers to the wrong place (same class as the phase-executor
|
|
309
|
+
// "unparseable verdict" split).
|
|
300
310
|
if (!verdict) {
|
|
301
|
-
return finish("
|
|
311
|
+
return finish("NO_VERDICT");
|
|
302
312
|
}
|
|
303
|
-
// #534 guard:
|
|
304
|
-
//
|
|
305
|
-
|
|
313
|
+
// #534/#879 guard: a worktree with no commits is never "ready". Split by
|
|
314
|
+
// cause so the report is not misleading: `none` is the empty-branch class
|
|
315
|
+
// (#529/#570), `uncommitted` is a dirty tree whose work cannot become a PR
|
|
316
|
+
// until committed (#879). `commits`/`unknown` continue (unknown fails open).
|
|
317
|
+
const changes = classifyChangesFn(worktreePath);
|
|
318
|
+
if (changes.kind === "none") {
|
|
306
319
|
return finish("NO_IMPLEMENTATION");
|
|
307
320
|
}
|
|
321
|
+
if (changes.kind === "uncommitted") {
|
|
322
|
+
return finish("UNCOMMITTED_ONLY");
|
|
323
|
+
}
|
|
308
324
|
finalVerdict = verdict;
|
|
309
325
|
const gaps = qaResult.summary?.gaps ?? [];
|
|
310
326
|
remaining = classifyGaps(gaps, nonGoals);
|
|
@@ -260,6 +260,8 @@ export declare const IssueLogSchema: z.ZodObject<{
|
|
|
260
260
|
}, z.core.$strip>>;
|
|
261
261
|
}, z.core.$strip>>;
|
|
262
262
|
totalDurationSeconds: z.ZodNumber;
|
|
263
|
+
aborted: z.ZodOptional<z.ZodBoolean>;
|
|
264
|
+
abortReason: z.ZodOptional<z.ZodString>;
|
|
263
265
|
prNumber: z.ZodOptional<z.ZodNumber>;
|
|
264
266
|
prUrl: z.ZodOptional<z.ZodString>;
|
|
265
267
|
}, z.core.$strip>;
|
|
@@ -284,6 +286,7 @@ export declare const RunSummarySchema: z.ZodObject<{
|
|
|
284
286
|
passed: z.ZodNumber;
|
|
285
287
|
failed: z.ZodNumber;
|
|
286
288
|
partial: z.ZodDefault<z.ZodNumber>;
|
|
289
|
+
aborted: z.ZodDefault<z.ZodNumber>;
|
|
287
290
|
totalDurationSeconds: z.ZodNumber;
|
|
288
291
|
}, z.core.$strip>;
|
|
289
292
|
export type RunSummary = z.infer<typeof RunSummarySchema>;
|
|
@@ -381,6 +384,8 @@ export declare const RunLogSchema: z.ZodObject<{
|
|
|
381
384
|
}, z.core.$strip>>;
|
|
382
385
|
}, z.core.$strip>>;
|
|
383
386
|
totalDurationSeconds: z.ZodNumber;
|
|
387
|
+
aborted: z.ZodOptional<z.ZodBoolean>;
|
|
388
|
+
abortReason: z.ZodOptional<z.ZodString>;
|
|
384
389
|
prNumber: z.ZodOptional<z.ZodNumber>;
|
|
385
390
|
prUrl: z.ZodOptional<z.ZodString>;
|
|
386
391
|
}, z.core.$strip>>;
|
|
@@ -389,10 +394,12 @@ export declare const RunLogSchema: z.ZodObject<{
|
|
|
389
394
|
passed: z.ZodNumber;
|
|
390
395
|
failed: z.ZodNumber;
|
|
391
396
|
partial: z.ZodDefault<z.ZodNumber>;
|
|
397
|
+
aborted: z.ZodDefault<z.ZodNumber>;
|
|
392
398
|
totalDurationSeconds: z.ZodNumber;
|
|
393
399
|
}, z.core.$strip>;
|
|
394
400
|
startCommit: z.ZodOptional<z.ZodString>;
|
|
395
401
|
endCommit: z.ZodOptional<z.ZodString>;
|
|
402
|
+
abortedBy: z.ZodOptional<z.ZodString>;
|
|
396
403
|
}, z.core.$strip>;
|
|
397
404
|
export type RunLog = z.infer<typeof RunLogSchema>;
|
|
398
405
|
/**
|
|
@@ -421,6 +428,7 @@ export declare function generateLogFilename(runId: string, startTime: Date): str
|
|
|
421
428
|
*/
|
|
422
429
|
export declare function createEmptyRunLog(config: RunConfig, options?: {
|
|
423
430
|
startCommit?: string;
|
|
431
|
+
startTime?: Date;
|
|
424
432
|
}): Omit<RunLog, "endTime">;
|
|
425
433
|
/**
|
|
426
434
|
* Create a phase log entry
|
|
@@ -448,6 +456,7 @@ export declare function completePhaseLog(phaseLog: Omit<PhaseLog, "endTime" | "d
|
|
|
448
456
|
*/
|
|
449
457
|
export declare function finalizeRunLog(runLog: Omit<RunLog, "endTime">, options?: {
|
|
450
458
|
endCommit?: string;
|
|
459
|
+
abortedBy?: string;
|
|
451
460
|
}): RunLog;
|
|
452
461
|
/**
|
|
453
462
|
* Derive an issue's overall log status from its phase log entries (#766).
|
|
@@ -460,5 +469,10 @@ export declare function finalizeRunLog(runLog: Omit<RunLog, "endTime">, options?
|
|
|
460
469
|
* verdict (mirrors the live-card rule); an unrecovered failure still leaves a
|
|
461
470
|
* non-loop phase failed. Priority among latest attempts: failure > timeout >
|
|
462
471
|
* success.
|
|
472
|
+
*
|
|
473
|
+
* An issue with no phase entries is a `failure`, not a `success` (#856). The
|
|
474
|
+
* only way to reach that state is for `startIssue` to have run while no phase
|
|
475
|
+
* ever completed — i.e. the run was cut short. Returning `success` there is
|
|
476
|
+
* what let a SIGTERM'd run persist as a pass with an empty `phases[]`.
|
|
463
477
|
*/
|
|
464
478
|
export declare function deriveIssueLogStatus(phases: PhaseLog[]): IssueStatus;
|
|
@@ -180,6 +180,20 @@ export const IssueLogSchema = z.object({
|
|
|
180
180
|
phases: z.array(PhaseLogSchema),
|
|
181
181
|
/** Total execution time in seconds */
|
|
182
182
|
totalDurationSeconds: z.number().nonnegative(),
|
|
183
|
+
/**
|
|
184
|
+
* Set when this issue never finished because the run was terminated from
|
|
185
|
+
* outside (#856). Reuses the `"failure"` status — additive boolean rather
|
|
186
|
+
* than a new `IssueStatus` enum value, matching the `capped` precedent
|
|
187
|
+
* above, so the persisted-log schema stays stable at `version: 1`.
|
|
188
|
+
*
|
|
189
|
+
* Without this, a run killed mid-flight persisted as `success`: `startIssue`
|
|
190
|
+
* seeds `status: "success"` and only `logPhase` ever revises it, so an issue
|
|
191
|
+
* whose first phase never completed was written out as a pass with an empty
|
|
192
|
+
* `phases[]`.
|
|
193
|
+
*/
|
|
194
|
+
aborted: z.boolean().optional(),
|
|
195
|
+
/** Human-readable cause of the abort (e.g. `terminated by SIGTERM`). */
|
|
196
|
+
abortReason: z.string().optional(),
|
|
183
197
|
/** PR number if created after successful QA */
|
|
184
198
|
prNumber: z.number().int().positive().optional(),
|
|
185
199
|
/** PR URL if created after successful QA */
|
|
@@ -219,6 +233,14 @@ export const RunSummarySchema = z.object({
|
|
|
219
233
|
* `.default(0)` keeps logs written before this field parseable.
|
|
220
234
|
*/
|
|
221
235
|
partial: z.number().int().nonnegative().default(0),
|
|
236
|
+
/**
|
|
237
|
+
* Number of issues cut short by an external signal (#856). A sub-count of
|
|
238
|
+
* `failed`, not a fourth disjoint bucket — aborted issues carry
|
|
239
|
+
* `status: "failure"` so existing consumers keep working, and this field
|
|
240
|
+
* says whether the failure was the run's own or something killing it.
|
|
241
|
+
* `.default(0)` keeps logs written before this field parseable.
|
|
242
|
+
*/
|
|
243
|
+
aborted: z.number().int().nonnegative().default(0),
|
|
222
244
|
/** Total execution time in seconds */
|
|
223
245
|
totalDurationSeconds: z.number().nonnegative(),
|
|
224
246
|
});
|
|
@@ -246,6 +268,11 @@ export const RunLogSchema = z.object({
|
|
|
246
268
|
startCommit: z.string().optional(),
|
|
247
269
|
/** Git commit SHA at run end (AC-2) */
|
|
248
270
|
endCommit: z.string().optional(),
|
|
271
|
+
/**
|
|
272
|
+
* Signal that terminated the run, when it ended by external signal rather
|
|
273
|
+
* than by finishing (#856). Present iff at least one issue is `aborted`.
|
|
274
|
+
*/
|
|
275
|
+
abortedBy: z.string().optional(),
|
|
249
276
|
});
|
|
250
277
|
/**
|
|
251
278
|
* Default log directory paths
|
|
@@ -276,7 +303,9 @@ export function generateLogFilename(runId, startTime) {
|
|
|
276
303
|
*/
|
|
277
304
|
export function createEmptyRunLog(config, options) {
|
|
278
305
|
const runId = randomUUID();
|
|
279
|
-
|
|
306
|
+
// #867: use the caller-supplied run origin when provided so the log's wall
|
|
307
|
+
// clock shares the orchestrator's start; fall back to now for standalone use.
|
|
308
|
+
const startTime = (options?.startTime ?? new Date()).toISOString();
|
|
280
309
|
return {
|
|
281
310
|
version: 1,
|
|
282
311
|
runId,
|
|
@@ -288,6 +317,7 @@ export function createEmptyRunLog(config, options) {
|
|
|
288
317
|
passed: 0,
|
|
289
318
|
failed: 0,
|
|
290
319
|
partial: 0,
|
|
320
|
+
aborted: 0,
|
|
291
321
|
totalDurationSeconds: 0,
|
|
292
322
|
},
|
|
293
323
|
startCommit: options?.startCommit,
|
|
@@ -343,6 +373,11 @@ export function finalizeRunLog(runLog, options) {
|
|
|
343
373
|
// #766: `partial` gets its own bucket so an all-partial run isn't counted as
|
|
344
374
|
// `0 passed · 0 failed` — it landed in neither before.
|
|
345
375
|
const partial = runLog.issues.filter((i) => i.status === "partial").length;
|
|
376
|
+
// #856: aborted issues are also counted in `failed` (they carry
|
|
377
|
+
// `status: "failure"`); this is the sub-count that says *why* they failed,
|
|
378
|
+
// so a run killed from outside is distinguishable from one whose phases
|
|
379
|
+
// genuinely failed.
|
|
380
|
+
const aborted = runLog.issues.filter((i) => i.aborted).length;
|
|
346
381
|
return {
|
|
347
382
|
...runLog,
|
|
348
383
|
endTime: endTime.toISOString(),
|
|
@@ -351,9 +386,13 @@ export function finalizeRunLog(runLog, options) {
|
|
|
351
386
|
passed,
|
|
352
387
|
failed,
|
|
353
388
|
partial,
|
|
389
|
+
aborted,
|
|
354
390
|
totalDurationSeconds,
|
|
355
391
|
},
|
|
356
392
|
endCommit: options?.endCommit ?? runLog.endCommit,
|
|
393
|
+
...((options?.abortedBy ?? runLog.abortedBy) && {
|
|
394
|
+
abortedBy: options?.abortedBy ?? runLog.abortedBy,
|
|
395
|
+
}),
|
|
357
396
|
};
|
|
358
397
|
}
|
|
359
398
|
/**
|
|
@@ -367,6 +406,11 @@ export function finalizeRunLog(runLog, options) {
|
|
|
367
406
|
* verdict (mirrors the live-card rule); an unrecovered failure still leaves a
|
|
368
407
|
* non-loop phase failed. Priority among latest attempts: failure > timeout >
|
|
369
408
|
* success.
|
|
409
|
+
*
|
|
410
|
+
* An issue with no phase entries is a `failure`, not a `success` (#856). The
|
|
411
|
+
* only way to reach that state is for `startIssue` to have run while no phase
|
|
412
|
+
* ever completed — i.e. the run was cut short. Returning `success` there is
|
|
413
|
+
* what let a SIGTERM'd run persist as a pass with an empty `phases[]`.
|
|
370
414
|
*/
|
|
371
415
|
export function deriveIssueLogStatus(phases) {
|
|
372
416
|
const latest = new Map();
|
|
@@ -376,6 +420,8 @@ export function deriveIssueLogStatus(phases) {
|
|
|
376
420
|
latest.set(p.phase, p.status);
|
|
377
421
|
}
|
|
378
422
|
const statuses = [...latest.values()];
|
|
423
|
+
if (statuses.length === 0)
|
|
424
|
+
return "failure";
|
|
379
425
|
if (statuses.some((s) => s === "failure"))
|
|
380
426
|
return "failure";
|
|
381
427
|
if (statuses.some((s) => s === "timeout"))
|