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
|
@@ -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)
|
|
@@ -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);
|
|
@@ -116,10 +134,36 @@ export class LogWriter {
|
|
|
116
134
|
issue.prNumber = prNumber;
|
|
117
135
|
issue.prUrl = prUrl;
|
|
118
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
|
+
}
|
|
119
158
|
/**
|
|
120
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.
|
|
121
165
|
*/
|
|
122
|
-
completeIssue(issueNumber) {
|
|
166
|
+
completeIssue(issueNumber, abort) {
|
|
123
167
|
if (!this.runLog) {
|
|
124
168
|
throw new Error("No run log. Call initialize() first.");
|
|
125
169
|
}
|
|
@@ -134,13 +178,31 @@ export class LogWriter {
|
|
|
134
178
|
}
|
|
135
179
|
// Calculate total duration from phases
|
|
136
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 ?? []);
|
|
137
195
|
const issueLog = {
|
|
138
196
|
issueNumber: issue.issueNumber,
|
|
139
197
|
title: issue.title,
|
|
140
198
|
labels: issue.labels,
|
|
141
|
-
status
|
|
199
|
+
status,
|
|
142
200
|
phases: issue.phases,
|
|
143
201
|
totalDurationSeconds,
|
|
202
|
+
...(abort && {
|
|
203
|
+
aborted: true,
|
|
204
|
+
abortReason: abort.reason,
|
|
205
|
+
}),
|
|
144
206
|
...(issue.prNumber != null && {
|
|
145
207
|
prNumber: issue.prNumber,
|
|
146
208
|
}),
|
|
@@ -168,22 +230,29 @@ export class LogWriter {
|
|
|
168
230
|
*
|
|
169
231
|
* @param options - Optional finalization options
|
|
170
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.
|
|
171
238
|
* @returns Path to the written log file
|
|
172
239
|
*/
|
|
173
240
|
async finalize(options) {
|
|
174
241
|
if (!this.runLog) {
|
|
175
242
|
throw new Error("LogWriter not initialized.");
|
|
176
243
|
}
|
|
244
|
+
const abort = options?.aborted;
|
|
177
245
|
// Complete any pending issues (Map-based concurrent tracking)
|
|
178
246
|
for (const issueNum of [...this.activeIssues.keys()]) {
|
|
179
|
-
this.completeIssue(issueNum);
|
|
247
|
+
this.completeIssue(issueNum, abort);
|
|
180
248
|
}
|
|
181
249
|
// Fallback: complete legacy currentIssue if not already handled
|
|
182
250
|
if (this.currentIssue) {
|
|
183
|
-
this.completeIssue();
|
|
251
|
+
this.completeIssue(undefined, abort);
|
|
184
252
|
}
|
|
185
253
|
const finalLog = finalizeRunLog(this.runLog, {
|
|
186
254
|
endCommit: options?.endCommit,
|
|
255
|
+
abortedBy: abort?.signal,
|
|
187
256
|
});
|
|
188
257
|
const filename = generateLogFilename(finalLog.runId, new Date(finalLog.startTime));
|
|
189
258
|
// Write to project logs
|
|
@@ -47,11 +47,11 @@ export type FailureCategory = z.infer<typeof FailureCategorySchema>;
|
|
|
47
47
|
* Available phases (aligned with run-log-schema.ts)
|
|
48
48
|
*/
|
|
49
49
|
export declare const MetricPhaseSchema: z.ZodEnum<{
|
|
50
|
+
exec: "exec";
|
|
50
51
|
qa: "qa";
|
|
51
52
|
loop: "loop";
|
|
52
53
|
spec: "spec";
|
|
53
54
|
"security-review": "security-review";
|
|
54
|
-
exec: "exec";
|
|
55
55
|
testgen: "testgen";
|
|
56
56
|
test: "test";
|
|
57
57
|
}>;
|
|
@@ -85,11 +85,11 @@ export declare const MetricRunSchema: z.ZodObject<{
|
|
|
85
85
|
date: z.ZodString;
|
|
86
86
|
issues: z.ZodArray<z.ZodNumber>;
|
|
87
87
|
phases: z.ZodArray<z.ZodEnum<{
|
|
88
|
+
exec: "exec";
|
|
88
89
|
qa: "qa";
|
|
89
90
|
loop: "loop";
|
|
90
91
|
spec: "spec";
|
|
91
92
|
"security-review": "security-review";
|
|
92
|
-
exec: "exec";
|
|
93
93
|
testgen: "testgen";
|
|
94
94
|
test: "test";
|
|
95
95
|
}>>;
|
|
@@ -135,11 +135,11 @@ export declare const MetricsSchema: z.ZodObject<{
|
|
|
135
135
|
date: z.ZodString;
|
|
136
136
|
issues: z.ZodArray<z.ZodNumber>;
|
|
137
137
|
phases: z.ZodArray<z.ZodEnum<{
|
|
138
|
+
exec: "exec";
|
|
138
139
|
qa: "qa";
|
|
139
140
|
loop: "loop";
|
|
140
141
|
spec: "spec";
|
|
141
142
|
"security-review": "security-review";
|
|
142
|
-
exec: "exec";
|
|
143
143
|
testgen: "testgen";
|
|
144
144
|
test: "test";
|
|
145
145
|
}>>;
|