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
|
@@ -73,13 +73,73 @@ function parseNameStatus(output) {
|
|
|
73
73
|
}
|
|
74
74
|
return result;
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Resolve the ref diff stats should compare against (#878).
|
|
78
|
+
*
|
|
79
|
+
* Worktrees are created from `origin/<base>` (worktree-manager), but callers
|
|
80
|
+
* historically passed the bare branch name and the diff ran against the
|
|
81
|
+
* *local* ref. When local `<base>` lags the remote — routine, since nothing
|
|
82
|
+
* in the run path updates it — `<base>...HEAD` attributes commits the run
|
|
83
|
+
* never made (phantom filesModified).
|
|
84
|
+
*
|
|
85
|
+
* Candidates are the origin-qualified and bare forms of `baseBranch`
|
|
86
|
+
* (already-remote-qualified input keeps a single candidate). Among the
|
|
87
|
+
* candidates that resolve to a commit, pick the one nearest to HEAD
|
|
88
|
+
* (smallest `rev-list --count <cand>..HEAD`), preferring the
|
|
89
|
+
* origin-qualified form on a tie. Nearest-wins matches the worktree's
|
|
90
|
+
* actual creation point in both staleness directions: a stale local
|
|
91
|
+
* default branch (this issue) and a chain-mode worktree branched from a
|
|
92
|
+
* local base that is ahead of its pushed counterpart. Falls back to
|
|
93
|
+
* `baseBranch` verbatim when no candidate resolves (e.g. remote-less repo
|
|
94
|
+
* with a missing branch) — the diff then fails gracefully to empty, the
|
|
95
|
+
* pre-#878 behavior.
|
|
96
|
+
*/
|
|
97
|
+
export function resolveDiffBase(worktreePath, baseBranch) {
|
|
98
|
+
const candidates = baseBranch.startsWith("origin/")
|
|
99
|
+
? [baseBranch]
|
|
100
|
+
: [`origin/${baseBranch}`, baseBranch];
|
|
101
|
+
let best;
|
|
102
|
+
let bestCount = Number.POSITIVE_INFINITY;
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
const verify = spawnSync("git", [
|
|
105
|
+
"-C",
|
|
106
|
+
worktreePath,
|
|
107
|
+
"rev-parse",
|
|
108
|
+
"--verify",
|
|
109
|
+
"--quiet",
|
|
110
|
+
`${candidate}^{commit}`,
|
|
111
|
+
], { stdio: "pipe", encoding: "utf-8" });
|
|
112
|
+
if (verify.status !== 0)
|
|
113
|
+
continue;
|
|
114
|
+
const count = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${candidate}..HEAD`], { stdio: "pipe", encoding: "utf-8" });
|
|
115
|
+
if (count.status !== 0)
|
|
116
|
+
continue;
|
|
117
|
+
const n = Number.parseInt(count.stdout.trim(), 10);
|
|
118
|
+
if (Number.isNaN(n))
|
|
119
|
+
continue;
|
|
120
|
+
// Strict < keeps the earlier (origin-qualified) candidate on a tie.
|
|
121
|
+
if (n < bestCount) {
|
|
122
|
+
best = candidate;
|
|
123
|
+
bestCount = n;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return best ?? baseBranch;
|
|
127
|
+
}
|
|
76
128
|
/**
|
|
77
129
|
* Get git commit SHA for a worktree (AC-2)
|
|
78
130
|
*
|
|
131
|
+
* When `baseRef` is provided, returns undefined if HEAD has no commits
|
|
132
|
+
* unique to it (#878) — a branch that never moved off its base would
|
|
133
|
+
* otherwise log the base tip as if it were the phase's commit. Callers
|
|
134
|
+
* recording plain "where is HEAD" markers (run start/end) omit `baseRef`.
|
|
135
|
+
*
|
|
79
136
|
* @param worktreePath - Path to the git worktree
|
|
80
|
-
* @
|
|
137
|
+
* @param baseRef - Optional resolved base ref (see resolveDiffBase); when
|
|
138
|
+
* given, a HEAD with zero commits past it yields undefined
|
|
139
|
+
* @returns The current HEAD commit SHA, or undefined on error / no unique
|
|
140
|
+
* commits
|
|
81
141
|
*/
|
|
82
|
-
export function getCommitHash(worktreePath) {
|
|
142
|
+
export function getCommitHash(worktreePath, baseRef) {
|
|
83
143
|
const result = spawnSync("git", ["-C", worktreePath, "rev-parse", "HEAD"], {
|
|
84
144
|
stdio: "pipe",
|
|
85
145
|
encoding: "utf-8",
|
|
@@ -87,20 +147,33 @@ export function getCommitHash(worktreePath) {
|
|
|
87
147
|
if (result.status !== 0) {
|
|
88
148
|
return undefined;
|
|
89
149
|
}
|
|
150
|
+
if (baseRef !== undefined) {
|
|
151
|
+
const count = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${baseRef}..HEAD`], { stdio: "pipe", encoding: "utf-8" });
|
|
152
|
+
// Fail open on git errors: a transient failure should not erase a real
|
|
153
|
+
// commit hash from the log — only a confirmed zero suppresses it.
|
|
154
|
+
if (count.status === 0) {
|
|
155
|
+
const n = Number.parseInt(count.stdout.trim(), 10);
|
|
156
|
+
if (n === 0) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
90
161
|
return result.stdout.trim();
|
|
91
162
|
}
|
|
92
163
|
/**
|
|
93
164
|
* Get git diff statistics for a worktree (AC-1, AC-3, AC-4)
|
|
94
165
|
*
|
|
95
166
|
* Efficiently captures both filesModified and fileDiffStats using
|
|
96
|
-
* minimal git commands.
|
|
167
|
+
* minimal git commands. The base is resolved via resolveDiffBase (#878) so
|
|
168
|
+
* the comparison targets the ref the worktree was actually created from
|
|
169
|
+
* (origin/<base> in the common case) rather than a possibly-stale local ref.
|
|
97
170
|
*
|
|
98
171
|
* @param worktreePath - Path to the git worktree
|
|
99
172
|
* @param baseBranch - Branch to compare against (default: "main")
|
|
100
173
|
* @returns GitDiffStatsResult with files, stats, and totals
|
|
101
174
|
*/
|
|
102
175
|
export function getGitDiffStats(worktreePath, baseBranch = "main") {
|
|
103
|
-
const diffRef = `${baseBranch}...HEAD`;
|
|
176
|
+
const diffRef = `${resolveDiffBase(worktreePath, baseBranch)}...HEAD`;
|
|
104
177
|
// Get numstat for additions/deletions
|
|
105
178
|
const numstatResult = spawnSync("git", ["-C", worktreePath, "diff", "--numstat", diffRef], { stdio: "pipe", encoding: "utf-8" });
|
|
106
179
|
// Get name-status for file status (added/modified/deleted/renamed)
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { ShutdownManager } from "../shutdown.js";
|
|
2
|
+
/**
|
|
3
|
+
* Cadence of the non-TTY auto-wait notice (#860 AC-6). Non-TTY output is
|
|
4
|
+
* append-only (no `\r` rewrite), so the TTY heartbeat's every-tick refresh
|
|
5
|
+
* would log ~600 lines over a five-hour wait. Five minutes keeps an unattended
|
|
6
|
+
* log legibly alive (~60 lines per five-hour window) without drowning it.
|
|
7
|
+
*
|
|
8
|
+
* @internal Exported for testing only
|
|
9
|
+
*/
|
|
10
|
+
export declare const NON_TTY_WAIT_NOTICE_INTERVAL_MS: number;
|
|
2
11
|
export interface LivenessHeartbeatOptions {
|
|
3
12
|
/** Polling cadence for heartbeat ticks. Default: 30_000ms */
|
|
4
13
|
pollIntervalMs?: number;
|
|
@@ -60,10 +69,39 @@ export declare class LivenessHeartbeat {
|
|
|
60
69
|
* Dispose all tracked phases and clear the timer. Idempotent.
|
|
61
70
|
*/
|
|
62
71
|
dispose(): void;
|
|
72
|
+
/**
|
|
73
|
+
* Mark a phase as auto-waiting for a rate-limit window to reopen (#804 AC-7).
|
|
74
|
+
*
|
|
75
|
+
* This is not cosmetic. The stall detector's activity proxy is the mtime of
|
|
76
|
+
* `.sequant/state.json`, and an auto-wait produces no writes by definition —
|
|
77
|
+
* so a multi-hour wait would otherwise trip the "no log activity" warning
|
|
78
|
+
* within minutes and report an entirely expected pause as a stall. Marking
|
|
79
|
+
* the phase both relabels the heartbeat line with the wake time and
|
|
80
|
+
* suppresses that false warning.
|
|
81
|
+
*
|
|
82
|
+
* No-op for an untracked phase, so an out-of-order or late notice cannot
|
|
83
|
+
* resurrect a stopped entry.
|
|
84
|
+
*/
|
|
85
|
+
pauseForWait(key: PhaseKey, wakeAtMs: number): void;
|
|
86
|
+
/** Clear the auto-wait marker set by {@link pauseForWait} (#804). */
|
|
87
|
+
resumeFromWait(key: PhaseKey): void;
|
|
63
88
|
/** Test hook: drive a poll synchronously without waiting on real timers. */
|
|
64
89
|
tickNow(): void;
|
|
65
90
|
private tick;
|
|
66
91
|
private writeHeartbeat;
|
|
92
|
+
/**
|
|
93
|
+
* Heartbeat line for a phase paused on an auto-wait (#804). Names the wake
|
|
94
|
+
* time and the remaining wait so a multi-hour pause is legibly deliberate
|
|
95
|
+
* rather than indistinguishable from a hang.
|
|
96
|
+
*/
|
|
97
|
+
private writeWaitHeartbeat;
|
|
98
|
+
/**
|
|
99
|
+
* Append-only auto-wait notice for non-TTY runs (#860 AC-6). Same facts as
|
|
100
|
+
* {@link writeWaitHeartbeat} — wake time and remaining wait — but a plain
|
|
101
|
+
* `\n`-terminated line with no cursor control, throttled by the caller to
|
|
102
|
+
* {@link NON_TTY_WAIT_NOTICE_INTERVAL_MS} so an unattended log stays legible.
|
|
103
|
+
*/
|
|
104
|
+
private writeNonTtyWaitNotice;
|
|
67
105
|
private writeStallWarning;
|
|
68
106
|
}
|
|
69
107
|
/** Convenience factory mirroring `phaseSpinner()`. */
|
|
@@ -12,10 +12,20 @@
|
|
|
12
12
|
*/
|
|
13
13
|
import * as fs from "fs";
|
|
14
14
|
import { formatElapsedTime } from "../cli-ui/format.js";
|
|
15
|
+
import { formatResetTime } from "../errors.js";
|
|
15
16
|
const DEFAULT_POLL_INTERVAL_MS = 30_000;
|
|
16
17
|
const DEFAULT_STALL_THRESHOLD_MS = 5 * 60_000;
|
|
17
18
|
const DEFAULT_LIVENESS_FILE = ".sequant/state.json";
|
|
18
19
|
const CLEANUP_NAME = "liveness-heartbeat";
|
|
20
|
+
/**
|
|
21
|
+
* Cadence of the non-TTY auto-wait notice (#860 AC-6). Non-TTY output is
|
|
22
|
+
* append-only (no `\r` rewrite), so the TTY heartbeat's every-tick refresh
|
|
23
|
+
* would log ~600 lines over a five-hour wait. Five minutes keeps an unattended
|
|
24
|
+
* log legibly alive (~60 lines per five-hour window) without drowning it.
|
|
25
|
+
*
|
|
26
|
+
* @internal Exported for testing only
|
|
27
|
+
*/
|
|
28
|
+
export const NON_TTY_WAIT_NOTICE_INTERVAL_MS = 5 * 60_000;
|
|
19
29
|
function keyFor(k) {
|
|
20
30
|
return `${k.issueNumber}:${k.phase}`;
|
|
21
31
|
}
|
|
@@ -118,6 +128,39 @@ export class LivenessHeartbeat {
|
|
|
118
128
|
this.cleanupRegistered = false;
|
|
119
129
|
}
|
|
120
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Mark a phase as auto-waiting for a rate-limit window to reopen (#804 AC-7).
|
|
133
|
+
*
|
|
134
|
+
* This is not cosmetic. The stall detector's activity proxy is the mtime of
|
|
135
|
+
* `.sequant/state.json`, and an auto-wait produces no writes by definition —
|
|
136
|
+
* so a multi-hour wait would otherwise trip the "no log activity" warning
|
|
137
|
+
* within minutes and report an entirely expected pause as a stall. Marking
|
|
138
|
+
* the phase both relabels the heartbeat line with the wake time and
|
|
139
|
+
* suppresses that false warning.
|
|
140
|
+
*
|
|
141
|
+
* No-op for an untracked phase, so an out-of-order or late notice cannot
|
|
142
|
+
* resurrect a stopped entry.
|
|
143
|
+
*/
|
|
144
|
+
pauseForWait(key, wakeAtMs) {
|
|
145
|
+
const entry = this.phases.get(keyFor(key));
|
|
146
|
+
if (!entry)
|
|
147
|
+
return;
|
|
148
|
+
entry.waitingUntil = wakeAtMs;
|
|
149
|
+
// Clear any warning already fired so a genuine stall after the wait can
|
|
150
|
+
// still warn once.
|
|
151
|
+
entry.warningFired = false;
|
|
152
|
+
// #860 AC-6: a fresh wait announces itself on the next non-TTY tick.
|
|
153
|
+
entry.lastWaitNoticeAt = undefined;
|
|
154
|
+
}
|
|
155
|
+
/** Clear the auto-wait marker set by {@link pauseForWait} (#804). */
|
|
156
|
+
resumeFromWait(key) {
|
|
157
|
+
const entry = this.phases.get(keyFor(key));
|
|
158
|
+
if (!entry)
|
|
159
|
+
return;
|
|
160
|
+
entry.waitingUntil = undefined;
|
|
161
|
+
entry.warningFired = false;
|
|
162
|
+
entry.lastWaitNoticeAt = undefined;
|
|
163
|
+
}
|
|
121
164
|
/** Test hook: drive a poll synchronously without waiting on real timers. */
|
|
122
165
|
tickNow() {
|
|
123
166
|
this.tick();
|
|
@@ -127,6 +170,23 @@ export class LivenessHeartbeat {
|
|
|
127
170
|
return;
|
|
128
171
|
const now = this.now();
|
|
129
172
|
for (const entry of this.phases.values()) {
|
|
173
|
+
// #804: an auto-waiting phase has its own liveness story. Handled before
|
|
174
|
+
// the mtime read because the wait produces no file activity at all — the
|
|
175
|
+
// stall detector's proxy is meaningless here, and the `mtimeMs === null`
|
|
176
|
+
// early-continue below would otherwise leave the wait completely silent.
|
|
177
|
+
if (entry.waitingUntil !== undefined) {
|
|
178
|
+
if (this.tty) {
|
|
179
|
+
this.writeWaitHeartbeat(entry, Math.max(0, now - entry.startedAt));
|
|
180
|
+
}
|
|
181
|
+
else if (entry.lastWaitNoticeAt === undefined ||
|
|
182
|
+
now - entry.lastWaitNoticeAt >= NON_TTY_WAIT_NOTICE_INTERVAL_MS) {
|
|
183
|
+
// #860 AC-6: without this, a non-TTY wait emits nothing for hours —
|
|
184
|
+
// indistinguishable from the #856 hang. Append-only line, throttled.
|
|
185
|
+
this.writeNonTtyWaitNotice(entry);
|
|
186
|
+
entry.lastWaitNoticeAt = now;
|
|
187
|
+
}
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
130
190
|
let mtimeMs;
|
|
131
191
|
try {
|
|
132
192
|
const stat = fs.statSync(this.livenessFile);
|
|
@@ -168,6 +228,36 @@ export class LivenessHeartbeat {
|
|
|
168
228
|
const line = `\r ▸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, last log update ${sinceActivity} ago)[K`;
|
|
169
229
|
this.stdoutWrite(line);
|
|
170
230
|
}
|
|
231
|
+
/**
|
|
232
|
+
* Heartbeat line for a phase paused on an auto-wait (#804). Names the wake
|
|
233
|
+
* time and the remaining wait so a multi-hour pause is legibly deliberate
|
|
234
|
+
* rather than indistinguishable from a hang.
|
|
235
|
+
*/
|
|
236
|
+
writeWaitHeartbeat(entry, elapsedSinceStartMs) {
|
|
237
|
+
if (this.stopped || entry.waitingUntil === undefined)
|
|
238
|
+
return;
|
|
239
|
+
const elapsed = formatElapsedTime(Math.floor(elapsedSinceStartMs / 1000));
|
|
240
|
+
const remainingMs = Math.max(0, entry.waitingUntil - this.now());
|
|
241
|
+
const remaining = formatElapsedTime(Math.floor(remainingMs / 1000));
|
|
242
|
+
const wake = formatResetTime(entry.waitingUntil);
|
|
243
|
+
const line = `\r ⏸ #${entry.issueNumber} ${entry.phase} (${elapsed} elapsed, rate-limit window — resuming at ${wake}, ${remaining} left)\x1b[K`;
|
|
244
|
+
this.stdoutWrite(line);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Append-only auto-wait notice for non-TTY runs (#860 AC-6). Same facts as
|
|
248
|
+
* {@link writeWaitHeartbeat} — wake time and remaining wait — but a plain
|
|
249
|
+
* `\n`-terminated line with no cursor control, throttled by the caller to
|
|
250
|
+
* {@link NON_TTY_WAIT_NOTICE_INTERVAL_MS} so an unattended log stays legible.
|
|
251
|
+
*/
|
|
252
|
+
writeNonTtyWaitNotice(entry) {
|
|
253
|
+
if (this.stopped || entry.waitingUntil === undefined)
|
|
254
|
+
return;
|
|
255
|
+
const remainingMs = Math.max(0, entry.waitingUntil - this.now());
|
|
256
|
+
const remaining = formatElapsedTime(Math.floor(remainingMs / 1000));
|
|
257
|
+
const wake = formatResetTime(entry.waitingUntil);
|
|
258
|
+
const line = ` ⏸ #${entry.issueNumber} ${entry.phase} rate-limit window — resuming at ${wake} (${remaining} left)\n`;
|
|
259
|
+
this.stdoutWrite(line);
|
|
260
|
+
}
|
|
171
261
|
writeStallWarning(entry, sinceActivityMs, elapsedSinceStartMs) {
|
|
172
262
|
if (this.stopped)
|
|
173
263
|
return;
|
|
@@ -26,6 +26,13 @@ export interface LogWriterOptions {
|
|
|
26
26
|
rotation?: RotationSettings;
|
|
27
27
|
/** Git commit SHA at run start (AC-2) */
|
|
28
28
|
startCommit?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Run start timestamp (#867). When provided, the log's `startTime` uses this
|
|
31
|
+
* origin instead of self-stamping `new Date()` at initialize(), so the log's
|
|
32
|
+
* stored wall clock and the orchestrator's summary derive from one shared
|
|
33
|
+
* start. Defaults to now when omitted.
|
|
34
|
+
*/
|
|
35
|
+
startTime?: Date;
|
|
29
36
|
}
|
|
30
37
|
/**
|
|
31
38
|
* Manages writing structured run logs to disk
|
|
@@ -36,11 +43,20 @@ export declare class LogWriter {
|
|
|
36
43
|
private activeIssues;
|
|
37
44
|
/** @deprecated Single-issue slot for backwards compatibility — use activeIssues */
|
|
38
45
|
private currentIssue;
|
|
46
|
+
/**
|
|
47
|
+
* Issue slots forced to `failure` by {@link markIssueFailed} (#879). Keyed on
|
|
48
|
+
* the slot object so it covers both the numbered and legacy `currentIssue`
|
|
49
|
+
* paths. `completeIssue` re-derives status from phases (#856), so a seeded
|
|
50
|
+
* `issue.status = "failure"` alone would be overwritten back to `success`;
|
|
51
|
+
* this marker survives that re-derivation.
|
|
52
|
+
*/
|
|
53
|
+
private forcedFailures;
|
|
39
54
|
private logPath;
|
|
40
55
|
private writeToUserLogs;
|
|
41
56
|
private verbose;
|
|
42
57
|
private rotation;
|
|
43
58
|
private startCommit?;
|
|
59
|
+
private startTime?;
|
|
44
60
|
constructor(options?: LogWriterOptions);
|
|
45
61
|
/**
|
|
46
62
|
* Initialize a new run log
|
|
@@ -66,10 +82,28 @@ export declare class LogWriter {
|
|
|
66
82
|
* Set PR info on the current issue (call before completeIssue)
|
|
67
83
|
*/
|
|
68
84
|
setPRInfo(prNumber: number, prUrl: string, issueNumber?: number): void;
|
|
85
|
+
/**
|
|
86
|
+
* Force the in-flight issue's status to `failure` (#879).
|
|
87
|
+
*
|
|
88
|
+
* `deriveIssueLogStatus` runs at phase-log time, so an issue whose phases all
|
|
89
|
+
* passed but whose PR creation then failed would otherwise be recorded as
|
|
90
|
+
* `success`. Call this after the last phase is logged and before
|
|
91
|
+
* {@link completeIssue} to count it under `failed`. No-op if the issue is not
|
|
92
|
+
* active. Reuses the existing `failure` enum — no schema change.
|
|
93
|
+
*/
|
|
94
|
+
markIssueFailed(issueNumber?: number): void;
|
|
69
95
|
/**
|
|
70
96
|
* Complete the current issue and add it to the run log
|
|
97
|
+
*
|
|
98
|
+
* @param issueNumber - Issue to complete (defaults to the legacy single slot)
|
|
99
|
+
* @param abort - Set when the run is being torn down by an external signal
|
|
100
|
+
* (#856). Marks the issue aborted with its cause instead of persisting
|
|
101
|
+
* whatever verdict the incomplete phase list happens to imply.
|
|
71
102
|
*/
|
|
72
|
-
completeIssue(issueNumber?: number
|
|
103
|
+
completeIssue(issueNumber?: number, abort?: {
|
|
104
|
+
signal: string;
|
|
105
|
+
reason: string;
|
|
106
|
+
}): void;
|
|
73
107
|
/**
|
|
74
108
|
* Finalize the run log and write to disk
|
|
75
109
|
*
|
|
@@ -77,10 +111,19 @@ export declare class LogWriter {
|
|
|
77
111
|
*
|
|
78
112
|
* @param options - Optional finalization options
|
|
79
113
|
* @param options.endCommit - Git commit SHA at run end (AC-2)
|
|
114
|
+
* @param options.aborted - Set when finalizing because the run was
|
|
115
|
+
* terminated by an external signal (#856). Every still-in-flight issue is
|
|
116
|
+
* recorded as an abort naming its cause, and the run log carries
|
|
117
|
+
* `abortedBy`. Without it, a killed run's log is indistinguishable from a
|
|
118
|
+
* clean one that happened to do nothing.
|
|
80
119
|
* @returns Path to the written log file
|
|
81
120
|
*/
|
|
82
121
|
finalize(options?: {
|
|
83
122
|
endCommit?: string;
|
|
123
|
+
aborted?: {
|
|
124
|
+
signal: string;
|
|
125
|
+
reason: string;
|
|
126
|
+
};
|
|
84
127
|
}): Promise<string>;
|
|
85
128
|
/**
|
|
86
129
|
* Get the current run log (for inspection)
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
import * as fs from "fs";
|
|
17
17
|
import * as path from "path";
|
|
18
18
|
import * as os from "os";
|
|
19
|
-
import { createEmptyRunLog, finalizeRunLog, generateLogFilename, LOG_PATHS, } from "./run-log-schema.js";
|
|
19
|
+
import { createEmptyRunLog, finalizeRunLog, deriveIssueLogStatus, generateLogFilename, LOG_PATHS, } from "./run-log-schema.js";
|
|
20
20
|
import { rotateIfNeeded, DEFAULT_ROTATION_SETTINGS, } from "./log-rotation.js";
|
|
21
21
|
/**
|
|
22
22
|
* Manages writing structured run logs to disk
|
|
@@ -27,17 +27,27 @@ export class LogWriter {
|
|
|
27
27
|
activeIssues = new Map();
|
|
28
28
|
/** @deprecated Single-issue slot for backwards compatibility — use activeIssues */
|
|
29
29
|
currentIssue = null;
|
|
30
|
+
/**
|
|
31
|
+
* Issue slots forced to `failure` by {@link markIssueFailed} (#879). Keyed on
|
|
32
|
+
* the slot object so it covers both the numbered and legacy `currentIssue`
|
|
33
|
+
* paths. `completeIssue` re-derives status from phases (#856), so a seeded
|
|
34
|
+
* `issue.status = "failure"` alone would be overwritten back to `success`;
|
|
35
|
+
* this marker survives that re-derivation.
|
|
36
|
+
*/
|
|
37
|
+
forcedFailures = new WeakSet();
|
|
30
38
|
logPath;
|
|
31
39
|
writeToUserLogs;
|
|
32
40
|
verbose;
|
|
33
41
|
rotation;
|
|
34
42
|
startCommit;
|
|
43
|
+
startTime;
|
|
35
44
|
constructor(options = {}) {
|
|
36
45
|
this.logPath = options.logPath ?? LOG_PATHS.project;
|
|
37
46
|
this.writeToUserLogs = options.writeToUserLogs ?? false;
|
|
38
47
|
this.verbose = options.verbose ?? false;
|
|
39
48
|
this.rotation = options.rotation ?? DEFAULT_ROTATION_SETTINGS;
|
|
40
49
|
this.startCommit = options.startCommit;
|
|
50
|
+
this.startTime = options.startTime;
|
|
41
51
|
}
|
|
42
52
|
/**
|
|
43
53
|
* Initialize a new run log
|
|
@@ -45,7 +55,10 @@ export class LogWriter {
|
|
|
45
55
|
* @param config - Run configuration
|
|
46
56
|
*/
|
|
47
57
|
async initialize(config) {
|
|
48
|
-
this.runLog = createEmptyRunLog(config, {
|
|
58
|
+
this.runLog = createEmptyRunLog(config, {
|
|
59
|
+
startCommit: this.startCommit,
|
|
60
|
+
startTime: this.startTime,
|
|
61
|
+
});
|
|
49
62
|
// Ensure log directory exists
|
|
50
63
|
await this.ensureLogDirectory(this.logPath);
|
|
51
64
|
if (this.writeToUserLogs) {
|
|
@@ -67,12 +80,17 @@ export class LogWriter {
|
|
|
67
80
|
if (!this.runLog) {
|
|
68
81
|
throw new Error("LogWriter not initialized. Call initialize() first.");
|
|
69
82
|
}
|
|
83
|
+
// #856: seed pessimistically. This slot is only revised by `logPhase`, so
|
|
84
|
+
// an optimistic `"success"` seed became the persisted verdict for any
|
|
85
|
+
// issue whose first phase never completed — the exact shape of a run
|
|
86
|
+
// killed mid-flight. `completeIssue` re-derives from `phases` regardless,
|
|
87
|
+
// but the seed should not itself assert a pass that never happened.
|
|
70
88
|
const issueData = {
|
|
71
89
|
issueNumber,
|
|
72
90
|
title,
|
|
73
91
|
labels,
|
|
74
92
|
phases: [],
|
|
75
|
-
status: "
|
|
93
|
+
status: "failure",
|
|
76
94
|
totalDurationSeconds: 0,
|
|
77
95
|
};
|
|
78
96
|
this.activeIssues.set(issueNumber, issueData);
|
|
@@ -94,13 +112,11 @@ export class LogWriter {
|
|
|
94
112
|
throw new Error(`No active issue #${phaseLog.issueNumber}. Call startIssue() first.`);
|
|
95
113
|
}
|
|
96
114
|
issue.phases = [...(issue.phases ?? []), phaseLog];
|
|
97
|
-
//
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
issue.status = "partial";
|
|
103
|
-
}
|
|
115
|
+
// #766: derive from the latest attempt of each phase (loop excluded) rather
|
|
116
|
+
// than pinning failure/partial forever. A timeout or failure that a later
|
|
117
|
+
// quality-loop iteration recovers from no longer sticks, so the JSON log
|
|
118
|
+
// agrees with the live card and summary table (AC-3/AC-5).
|
|
119
|
+
issue.status = deriveIssueLogStatus(issue.phases);
|
|
104
120
|
if (this.verbose) {
|
|
105
121
|
console.log(`Log phase: ${phaseLog.phase} (${phaseLog.status}) - ${phaseLog.durationSeconds.toFixed(1)}s`);
|
|
106
122
|
}
|
|
@@ -118,10 +134,36 @@ export class LogWriter {
|
|
|
118
134
|
issue.prNumber = prNumber;
|
|
119
135
|
issue.prUrl = prUrl;
|
|
120
136
|
}
|
|
137
|
+
/**
|
|
138
|
+
* Force the in-flight issue's status to `failure` (#879).
|
|
139
|
+
*
|
|
140
|
+
* `deriveIssueLogStatus` runs at phase-log time, so an issue whose phases all
|
|
141
|
+
* passed but whose PR creation then failed would otherwise be recorded as
|
|
142
|
+
* `success`. Call this after the last phase is logged and before
|
|
143
|
+
* {@link completeIssue} to count it under `failed`. No-op if the issue is not
|
|
144
|
+
* active. Reuses the existing `failure` enum — no schema change.
|
|
145
|
+
*/
|
|
146
|
+
markIssueFailed(issueNumber) {
|
|
147
|
+
const issue = issueNumber
|
|
148
|
+
? (this.activeIssues.get(issueNumber) ?? this.currentIssue)
|
|
149
|
+
: this.currentIssue;
|
|
150
|
+
if (!issue) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
issue.status = "failure";
|
|
154
|
+
// completeIssue re-derives status from the phase list (#856), so the seed
|
|
155
|
+
// above is not enough on its own — mark the slot so completion honors it.
|
|
156
|
+
this.forcedFailures.add(issue);
|
|
157
|
+
}
|
|
121
158
|
/**
|
|
122
159
|
* Complete the current issue and add it to the run log
|
|
160
|
+
*
|
|
161
|
+
* @param issueNumber - Issue to complete (defaults to the legacy single slot)
|
|
162
|
+
* @param abort - Set when the run is being torn down by an external signal
|
|
163
|
+
* (#856). Marks the issue aborted with its cause instead of persisting
|
|
164
|
+
* whatever verdict the incomplete phase list happens to imply.
|
|
123
165
|
*/
|
|
124
|
-
completeIssue(issueNumber) {
|
|
166
|
+
completeIssue(issueNumber, abort) {
|
|
125
167
|
if (!this.runLog) {
|
|
126
168
|
throw new Error("No run log. Call initialize() first.");
|
|
127
169
|
}
|
|
@@ -136,13 +178,31 @@ export class LogWriter {
|
|
|
136
178
|
}
|
|
137
179
|
// Calculate total duration from phases
|
|
138
180
|
const totalDurationSeconds = issue.phases?.reduce((sum, p) => sum + p.durationSeconds, 0) ?? 0;
|
|
181
|
+
// #856: derive from the phase list rather than trusting the slot's seed.
|
|
182
|
+
// An issue with no completed phase is a failure — the run was cut short.
|
|
183
|
+
//
|
|
184
|
+
// An abort forces `failure` even when every phase logged so far passed:
|
|
185
|
+
// the issue was still in flight when the signal arrived, so its pipeline
|
|
186
|
+
// never reached a terminal state and a partial prefix of green phases is
|
|
187
|
+
// not a pass. `abortReason` records why, so the log names its own cause
|
|
188
|
+
// instead of leaving a silently truncated record.
|
|
189
|
+
// #879: markIssueFailed forces `failure` when every phase passed but a
|
|
190
|
+
// later step (e.g. PR creation) failed — same override intent as `abort`,
|
|
191
|
+
// so it must win over the phase-derived status too.
|
|
192
|
+
const status = abort || this.forcedFailures.has(issue)
|
|
193
|
+
? "failure"
|
|
194
|
+
: deriveIssueLogStatus(issue.phases ?? []);
|
|
139
195
|
const issueLog = {
|
|
140
196
|
issueNumber: issue.issueNumber,
|
|
141
197
|
title: issue.title,
|
|
142
198
|
labels: issue.labels,
|
|
143
|
-
status
|
|
199
|
+
status,
|
|
144
200
|
phases: issue.phases,
|
|
145
201
|
totalDurationSeconds,
|
|
202
|
+
...(abort && {
|
|
203
|
+
aborted: true,
|
|
204
|
+
abortReason: abort.reason,
|
|
205
|
+
}),
|
|
146
206
|
...(issue.prNumber != null && {
|
|
147
207
|
prNumber: issue.prNumber,
|
|
148
208
|
}),
|
|
@@ -170,22 +230,29 @@ export class LogWriter {
|
|
|
170
230
|
*
|
|
171
231
|
* @param options - Optional finalization options
|
|
172
232
|
* @param options.endCommit - Git commit SHA at run end (AC-2)
|
|
233
|
+
* @param options.aborted - Set when finalizing because the run was
|
|
234
|
+
* terminated by an external signal (#856). Every still-in-flight issue is
|
|
235
|
+
* recorded as an abort naming its cause, and the run log carries
|
|
236
|
+
* `abortedBy`. Without it, a killed run's log is indistinguishable from a
|
|
237
|
+
* clean one that happened to do nothing.
|
|
173
238
|
* @returns Path to the written log file
|
|
174
239
|
*/
|
|
175
240
|
async finalize(options) {
|
|
176
241
|
if (!this.runLog) {
|
|
177
242
|
throw new Error("LogWriter not initialized.");
|
|
178
243
|
}
|
|
244
|
+
const abort = options?.aborted;
|
|
179
245
|
// Complete any pending issues (Map-based concurrent tracking)
|
|
180
246
|
for (const issueNum of [...this.activeIssues.keys()]) {
|
|
181
|
-
this.completeIssue(issueNum);
|
|
247
|
+
this.completeIssue(issueNum, abort);
|
|
182
248
|
}
|
|
183
249
|
// Fallback: complete legacy currentIssue if not already handled
|
|
184
250
|
if (this.currentIssue) {
|
|
185
|
-
this.completeIssue();
|
|
251
|
+
this.completeIssue(undefined, abort);
|
|
186
252
|
}
|
|
187
253
|
const finalLog = finalizeRunLog(this.runLog, {
|
|
188
254
|
endCommit: options?.endCommit,
|
|
255
|
+
abortedBy: abort?.signal,
|
|
189
256
|
});
|
|
190
257
|
const filename = generateLogFilename(finalLog.runId, new Date(finalLog.startTime));
|
|
191
258
|
// Write to project logs
|
|
@@ -25,15 +25,33 @@ export declare const RunOutcomeSchema: z.ZodEnum<{
|
|
|
25
25
|
failed: "failed";
|
|
26
26
|
}>;
|
|
27
27
|
export type RunOutcome = z.infer<typeof RunOutcomeSchema>;
|
|
28
|
+
/**
|
|
29
|
+
* Bounded-enum classification of the failure that ended a run (#761 AC-7).
|
|
30
|
+
*
|
|
31
|
+
* Sourced from `ERROR_CATEGORIES` so the metric can never carry free text —
|
|
32
|
+
* error *messages* stay excluded per the privacy contract above MetricRunSchema
|
|
33
|
+
* (they could contain sensitive info); a closed enum cannot.
|
|
34
|
+
*/
|
|
35
|
+
export declare const FailureCategorySchema: z.ZodEnum<{
|
|
36
|
+
unknown: "unknown";
|
|
37
|
+
timeout: "timeout";
|
|
38
|
+
context_overflow: "context_overflow";
|
|
39
|
+
api_error: "api_error";
|
|
40
|
+
hook_failure: "hook_failure";
|
|
41
|
+
build_error: "build_error";
|
|
42
|
+
rate_limit: "rate_limit";
|
|
43
|
+
billing: "billing";
|
|
44
|
+
}>;
|
|
45
|
+
export type FailureCategory = z.infer<typeof FailureCategorySchema>;
|
|
28
46
|
/**
|
|
29
47
|
* Available phases (aligned with run-log-schema.ts)
|
|
30
48
|
*/
|
|
31
49
|
export declare const MetricPhaseSchema: z.ZodEnum<{
|
|
50
|
+
exec: "exec";
|
|
32
51
|
qa: "qa";
|
|
33
52
|
loop: "loop";
|
|
34
53
|
spec: "spec";
|
|
35
54
|
"security-review": "security-review";
|
|
36
|
-
exec: "exec";
|
|
37
55
|
testgen: "testgen";
|
|
38
56
|
test: "test";
|
|
39
57
|
}>;
|
|
@@ -67,11 +85,11 @@ export declare const MetricRunSchema: z.ZodObject<{
|
|
|
67
85
|
date: z.ZodString;
|
|
68
86
|
issues: z.ZodArray<z.ZodNumber>;
|
|
69
87
|
phases: z.ZodArray<z.ZodEnum<{
|
|
88
|
+
exec: "exec";
|
|
70
89
|
qa: "qa";
|
|
71
90
|
loop: "loop";
|
|
72
91
|
spec: "spec";
|
|
73
92
|
"security-review": "security-review";
|
|
74
|
-
exec: "exec";
|
|
75
93
|
testgen: "testgen";
|
|
76
94
|
test: "test";
|
|
77
95
|
}>>;
|
|
@@ -83,6 +101,16 @@ export declare const MetricRunSchema: z.ZodObject<{
|
|
|
83
101
|
duration: z.ZodNumber;
|
|
84
102
|
model: z.ZodString;
|
|
85
103
|
flags: z.ZodArray<z.ZodString>;
|
|
104
|
+
failureCategory: z.ZodOptional<z.ZodEnum<{
|
|
105
|
+
unknown: "unknown";
|
|
106
|
+
timeout: "timeout";
|
|
107
|
+
context_overflow: "context_overflow";
|
|
108
|
+
api_error: "api_error";
|
|
109
|
+
hook_failure: "hook_failure";
|
|
110
|
+
build_error: "build_error";
|
|
111
|
+
rate_limit: "rate_limit";
|
|
112
|
+
billing: "billing";
|
|
113
|
+
}>>;
|
|
86
114
|
metrics: z.ZodObject<{
|
|
87
115
|
tokensUsed: z.ZodNumber;
|
|
88
116
|
filesChanged: z.ZodNumber;
|
|
@@ -107,11 +135,11 @@ export declare const MetricsSchema: z.ZodObject<{
|
|
|
107
135
|
date: z.ZodString;
|
|
108
136
|
issues: z.ZodArray<z.ZodNumber>;
|
|
109
137
|
phases: z.ZodArray<z.ZodEnum<{
|
|
138
|
+
exec: "exec";
|
|
110
139
|
qa: "qa";
|
|
111
140
|
loop: "loop";
|
|
112
141
|
spec: "spec";
|
|
113
142
|
"security-review": "security-review";
|
|
114
|
-
exec: "exec";
|
|
115
143
|
testgen: "testgen";
|
|
116
144
|
test: "test";
|
|
117
145
|
}>>;
|
|
@@ -123,6 +151,16 @@ export declare const MetricsSchema: z.ZodObject<{
|
|
|
123
151
|
duration: z.ZodNumber;
|
|
124
152
|
model: z.ZodString;
|
|
125
153
|
flags: z.ZodArray<z.ZodString>;
|
|
154
|
+
failureCategory: z.ZodOptional<z.ZodEnum<{
|
|
155
|
+
unknown: "unknown";
|
|
156
|
+
timeout: "timeout";
|
|
157
|
+
context_overflow: "context_overflow";
|
|
158
|
+
api_error: "api_error";
|
|
159
|
+
hook_failure: "hook_failure";
|
|
160
|
+
build_error: "build_error";
|
|
161
|
+
rate_limit: "rate_limit";
|
|
162
|
+
billing: "billing";
|
|
163
|
+
}>>;
|
|
126
164
|
metrics: z.ZodObject<{
|
|
127
165
|
tokensUsed: z.ZodNumber;
|
|
128
166
|
filesChanged: z.ZodNumber;
|
|
@@ -154,6 +192,7 @@ export declare function createMetricRun(options: {
|
|
|
154
192
|
duration: number;
|
|
155
193
|
model?: string;
|
|
156
194
|
flags?: string[];
|
|
195
|
+
failureCategory?: FailureCategory;
|
|
157
196
|
metrics?: Partial<RunMetrics>;
|
|
158
197
|
}): MetricRun;
|
|
159
198
|
/**
|