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
|
@@ -8,7 +8,8 @@ import chalk from "chalk";
|
|
|
8
8
|
import { spawnSync } from "child_process";
|
|
9
9
|
import { existsSync, readFileSync } from "fs";
|
|
10
10
|
import path from "path";
|
|
11
|
-
import {
|
|
11
|
+
import { resolvePackageManager, resolvePackageManagerConfig, } from "../stacks.js";
|
|
12
|
+
import { resolveDiffBase } from "./git-diff-utils.js";
|
|
12
13
|
import { getResumablePhasesForIssue } from "./phase-detection.js";
|
|
13
14
|
import { GitHubProvider } from "./platforms/github.js";
|
|
14
15
|
/**
|
|
@@ -155,7 +156,11 @@ export function checkWorktreeFreshness(worktreePath, verbose, baseBranch = "main
|
|
|
155
156
|
const countResult = spawnSync("git", ["-C", worktreePath, "rev-list", "--count", `${mergeBase}..${baseHead}`], { stdio: "pipe" });
|
|
156
157
|
if (countResult.status === 0) {
|
|
157
158
|
result.commitsBehind = parseInt(countResult.stdout.toString().trim(), 10);
|
|
158
|
-
// Consider stale if more than 5 commits behind
|
|
159
|
+
// Consider stale if more than 5 commits behind. NOT configurable — the
|
|
160
|
+
// threshold is the literal below, and a stale-but-clean worktree is
|
|
161
|
+
// force-removed and rebuilt on the strength of it (see the recreate
|
|
162
|
+
// branch in `ensureWorktree`). Surfaced in #810; left as a literal here
|
|
163
|
+
// because making it configurable is a behavior change, not a comment fix.
|
|
159
164
|
result.isStale = result.commitsBehind > 5;
|
|
160
165
|
}
|
|
161
166
|
}
|
|
@@ -231,7 +236,8 @@ export function listWorktrees() {
|
|
|
231
236
|
* Get changed files in a worktree compared to main
|
|
232
237
|
*/
|
|
233
238
|
export function getWorktreeChangedFiles(worktreePath) {
|
|
234
|
-
const
|
|
239
|
+
const diffBase = resolveDiffBase(worktreePath, "main");
|
|
240
|
+
const result = spawnSync("git", ["-C", worktreePath, "diff", "--name-only", `${diffBase}...HEAD`], { stdio: "pipe" });
|
|
235
241
|
if (result.status !== 0)
|
|
236
242
|
return [];
|
|
237
243
|
return result.stdout
|
|
@@ -245,7 +251,8 @@ export function getWorktreeChangedFiles(worktreePath) {
|
|
|
245
251
|
* Returns aggregate metrics only - no file paths to preserve privacy
|
|
246
252
|
*/
|
|
247
253
|
export function getWorktreeDiffStats(worktreePath) {
|
|
248
|
-
const
|
|
254
|
+
const diffBase = resolveDiffBase(worktreePath, "main");
|
|
255
|
+
const result = spawnSync("git", ["-C", worktreePath, "diff", "--stat", `${diffBase}...HEAD`], { stdio: "pipe" });
|
|
249
256
|
if (result.status !== 0) {
|
|
250
257
|
return { filesChanged: 0, linesAdded: 0 };
|
|
251
258
|
}
|
|
@@ -314,6 +321,63 @@ export function filterResumedPhases(issueNumber, phases, resume) {
|
|
|
314
321
|
const skipped = phases.filter((p) => !resumable.includes(p));
|
|
315
322
|
return { phases: resumable, skipped };
|
|
316
323
|
}
|
|
324
|
+
/**
|
|
325
|
+
* Run the frozen dependency install for a freshly provisioned worktree and
|
|
326
|
+
* surface (rather than swallow) a failed install.
|
|
327
|
+
*
|
|
328
|
+
* Mirrors the status check in `reinstallIfLockfileChanged` (#846): a plain
|
|
329
|
+
* `npm install` self-heals a stale/absent lockfile, but the frozen `npm ci`
|
|
330
|
+
* hard-fails (exit 1, no node_modules). Without this check a failing install
|
|
331
|
+
* left a silently dependency-less worktree whose breakage only surfaced later
|
|
332
|
+
* as a confusing phase error. The warning names the resolved command so the
|
|
333
|
+
* user can rerun it by hand.
|
|
334
|
+
*
|
|
335
|
+
* @returns true if the install succeeded, false if it failed (warn-and-continue)
|
|
336
|
+
* @internal Exported for testing
|
|
337
|
+
*/
|
|
338
|
+
export function installWorktreeDeps(worktreePath, packageManager, verbose) {
|
|
339
|
+
if (verbose) {
|
|
340
|
+
console.log(chalk.gray(` Installing dependencies...`));
|
|
341
|
+
}
|
|
342
|
+
// Two independent questions, resolved in order against the worktree itself.
|
|
343
|
+
//
|
|
344
|
+
// WHICH manager: the manifest's packageManager is a snapshot and may be
|
|
345
|
+
// absent, so fall back to the worktree's own lockfile — that keeps this in
|
|
346
|
+
// agreement with what new-feature.sh would run for the same project (#870).
|
|
347
|
+
//
|
|
348
|
+
// WHICH commands for it: yarn's frozen install differs between classic
|
|
349
|
+
// (`--frozen-lockfile`) and berry (`--immutable`), and both majors use
|
|
350
|
+
// `yarn.lock`, so the manager's identity alone cannot say which (#871).
|
|
351
|
+
//
|
|
352
|
+
// The worktree is checked out by now, so its package.json / .yarnrc.yml /
|
|
353
|
+
// yarn.lock are all readable here.
|
|
354
|
+
const pmConfig = resolvePackageManagerConfig(resolvePackageManager(packageManager, worktreePath), worktreePath);
|
|
355
|
+
// ciInstall, not installSilent: a plain `npm install` normalizes and
|
|
356
|
+
// rewrites package-lock.json (observed: npm 10 strips the `libc` fields a
|
|
357
|
+
// newer npm committed), so every provisioned worktree started dirty. That
|
|
358
|
+
// one unstaged file cascaded: rebaseBeforePR refused to run, stale
|
|
359
|
+
// worktrees read as having "uncommitted changes" and were never recreated,
|
|
360
|
+
// and chain checkpoints skipped on an out-of-scope dirty file. A frozen
|
|
361
|
+
// install never touches the lockfile. Same substitution #803 made for
|
|
362
|
+
// merge-check's combined-branch test. The `!existsSync(node_modules)`
|
|
363
|
+
// guard at the call site means `npm ci`'s wipe-and-reinstall has nothing
|
|
364
|
+
// to wipe.
|
|
365
|
+
const [cmd, ...args] = pmConfig.ciInstall.split(" ");
|
|
366
|
+
const command = [cmd, ...args].join(" ");
|
|
367
|
+
const installResult = spawnSync(cmd, args, {
|
|
368
|
+
cwd: worktreePath,
|
|
369
|
+
stdio: "pipe",
|
|
370
|
+
});
|
|
371
|
+
if (installResult.status !== 0) {
|
|
372
|
+
// stderr may be empty on some failures — the resolved command alone is
|
|
373
|
+
// enough to act on, so always name it (AC-2).
|
|
374
|
+
const error = installResult.stderr?.toString().trim() ?? "";
|
|
375
|
+
const detail = error ? `: ${error}` : "";
|
|
376
|
+
console.log(chalk.yellow(` ! Dependency install failed (${command})${detail}`));
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
317
381
|
/**
|
|
318
382
|
* Create or reuse a worktree for an issue
|
|
319
383
|
* @param baseBranch - Optional branch to use as base instead of origin/main (for chain mode)
|
|
@@ -365,21 +429,14 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
|
|
|
365
429
|
if (verbose) {
|
|
366
430
|
console.log(chalk.gray(` Rebasing existing worktree onto chain base (${baseBranch})...`));
|
|
367
431
|
}
|
|
368
|
-
const
|
|
369
|
-
if (
|
|
370
|
-
|
|
371
|
-
// Check if it's a conflict
|
|
372
|
-
if (rebaseError.includes("CONFLICT") ||
|
|
373
|
-
rebaseError.includes("could not apply")) {
|
|
432
|
+
const rebase = rebaseOntoLocalBranch(existingPath, baseBranch, verbose);
|
|
433
|
+
if (!rebase.success) {
|
|
434
|
+
if (rebase.conflict) {
|
|
374
435
|
console.log(chalk.yellow(` ! Rebase conflict detected. Aborting rebase and keeping original branch state.`));
|
|
375
436
|
console.log(chalk.yellow(` ℹ️ Branch ${branch} is not properly chained. Manual rebase may be required.`));
|
|
376
|
-
// Abort the rebase to restore branch state
|
|
377
|
-
spawnSync("git", ["-C", existingPath, "rebase", "--abort"], {
|
|
378
|
-
stdio: "pipe",
|
|
379
|
-
});
|
|
380
437
|
}
|
|
381
438
|
else {
|
|
382
|
-
console.log(chalk.yellow(` ! Rebase failed: ${
|
|
439
|
+
console.log(chalk.yellow(` ! Rebase failed: ${rebase.error ?? ""}`));
|
|
383
440
|
console.log(chalk.yellow(` ℹ️ Continuing with branch in its original state.`));
|
|
384
441
|
}
|
|
385
442
|
return {
|
|
@@ -390,9 +447,6 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
|
|
|
390
447
|
rebased: false,
|
|
391
448
|
};
|
|
392
449
|
}
|
|
393
|
-
if (verbose) {
|
|
394
|
-
console.log(chalk.green(` ✔ Existing worktree rebased onto ${baseBranch}`));
|
|
395
|
-
}
|
|
396
450
|
return {
|
|
397
451
|
issue: issueNumber,
|
|
398
452
|
path: existingPath,
|
|
@@ -523,17 +577,7 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
|
|
|
523
577
|
// Install dependencies if needed
|
|
524
578
|
const nodeModulesPath = path.join(worktreePath, "node_modules");
|
|
525
579
|
if (!existsSync(nodeModulesPath)) {
|
|
526
|
-
|
|
527
|
-
console.log(chalk.gray(` Installing dependencies...`));
|
|
528
|
-
}
|
|
529
|
-
// Use detected package manager or default to npm
|
|
530
|
-
const pm = packageManager || "npm";
|
|
531
|
-
const pmConfig = PM_CONFIG[pm];
|
|
532
|
-
const [cmd, ...args] = pmConfig.installSilent.split(" ");
|
|
533
|
-
spawnSync(cmd, args, {
|
|
534
|
-
cwd: worktreePath,
|
|
535
|
-
stdio: "pipe",
|
|
536
|
-
});
|
|
580
|
+
installWorktreeDeps(worktreePath, packageManager, verbose);
|
|
537
581
|
}
|
|
538
582
|
if (verbose) {
|
|
539
583
|
console.log(chalk.green(` ✔ Worktree ready: ${worktreePath}`));
|
|
@@ -700,9 +744,10 @@ passed QA in chain mode. It serves as a recovery point if later issues fail.`;
|
|
|
700
744
|
const commitResult = spawnSync("git", ["-C", worktreePath, "commit", "-m", commitMessage], { stdio: "pipe" });
|
|
701
745
|
if (commitResult.status !== 0) {
|
|
702
746
|
const error = commitResult.stderr.toString();
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
747
|
+
// #760: chain resume rebases the next link onto this checkpoint, so a
|
|
748
|
+
// commit failure is surfaced regardless of --verbose (the call site adds
|
|
749
|
+
// the resume-impact warning; here we show the underlying git error).
|
|
750
|
+
console.log(chalk.yellow(` ! Could not create checkpoint commit for #${issueNumber}: ${error.trim()}`));
|
|
706
751
|
return false;
|
|
707
752
|
}
|
|
708
753
|
console.log(chalk.green(` 📌 Checkpoint commit created for #${issueNumber}`));
|
|
@@ -751,9 +796,14 @@ export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose
|
|
|
751
796
|
}
|
|
752
797
|
// Re-run install to sync node_modules with updated lockfile
|
|
753
798
|
console.log(chalk.blue(` Reinstalling dependencies (lockfile changed)...`));
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
const
|
|
799
|
+
// Same two-step resolution as installWorktreeDeps: manifest-or-lockfile for
|
|
800
|
+
// the manager (#870), then classic-vs-berry for its commands (#871).
|
|
801
|
+
const pmConfig = resolvePackageManagerConfig(resolvePackageManager(packageManager, worktreePath), worktreePath);
|
|
802
|
+
// ciInstall for the same reason as provisioning (see ensureWorktree): this
|
|
803
|
+
// reinstall exists because a rebase pulled in a NEW lockfile, so installing
|
|
804
|
+
// exactly what that lockfile says — never rewriting it — is the semantic
|
|
805
|
+
// the function's own name promises.
|
|
806
|
+
const [cmd, ...args] = pmConfig.ciInstall.split(" ");
|
|
757
807
|
const installResult = spawnSync(cmd, args, {
|
|
758
808
|
cwd: worktreePath,
|
|
759
809
|
stdio: "pipe",
|
|
@@ -766,6 +816,39 @@ export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose
|
|
|
766
816
|
console.log(chalk.green(` ✔ Dependencies reinstalled`));
|
|
767
817
|
return true;
|
|
768
818
|
}
|
|
819
|
+
/**
|
|
820
|
+
* Rebase a worktree's branch onto a *local* branch ref (e.g. a chain
|
|
821
|
+
* predecessor's feature branch), NOT origin/main. Used to chain a successor
|
|
822
|
+
* onto its predecessor's committed work at execution time (#748), and to
|
|
823
|
+
* re-chain a pre-existing worktree onto its chain base.
|
|
824
|
+
*
|
|
825
|
+
* On conflict, aborts the rebase to restore the original branch state and
|
|
826
|
+
* returns `{ success: false, conflict: true }` so callers can warn rather than
|
|
827
|
+
* silently treat a broken link as healthy.
|
|
828
|
+
*/
|
|
829
|
+
export function rebaseOntoLocalBranch(worktreePath, ontoBranch, verbose = false) {
|
|
830
|
+
const rebaseResult = spawnSync("git", ["-C", worktreePath, "rebase", ontoBranch], { stdio: "pipe" });
|
|
831
|
+
if (rebaseResult.status === 0) {
|
|
832
|
+
if (verbose) {
|
|
833
|
+
console.log(chalk.green(` ✔ Rebased worktree onto ${ontoBranch}`));
|
|
834
|
+
}
|
|
835
|
+
return { performed: true, success: true, conflict: false };
|
|
836
|
+
}
|
|
837
|
+
const rebaseError = rebaseResult.stderr.toString();
|
|
838
|
+
const isConflict = rebaseError.includes("CONFLICT") || rebaseError.includes("could not apply");
|
|
839
|
+
if (isConflict) {
|
|
840
|
+
// Abort to restore the original branch state.
|
|
841
|
+
spawnSync("git", ["-C", worktreePath, "rebase", "--abort"], {
|
|
842
|
+
stdio: "pipe",
|
|
843
|
+
});
|
|
844
|
+
}
|
|
845
|
+
return {
|
|
846
|
+
performed: true,
|
|
847
|
+
success: false,
|
|
848
|
+
conflict: isConflict,
|
|
849
|
+
error: rebaseError.trim(),
|
|
850
|
+
};
|
|
851
|
+
}
|
|
769
852
|
/**
|
|
770
853
|
* Rebase the worktree branch onto the base branch before PR creation.
|
|
771
854
|
* This ensures the branch is up-to-date and prevents lockfile drift.
|
|
@@ -832,11 +915,84 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
|
|
|
832
915
|
reinstalled,
|
|
833
916
|
};
|
|
834
917
|
}
|
|
918
|
+
/**
|
|
919
|
+
* Render a non-A+ QA verdict as a PR-body note, or `null` for an A+ / absent
|
|
920
|
+
* verdict.
|
|
921
|
+
*
|
|
922
|
+
* Only stopping states that still break to a PR reach this path
|
|
923
|
+
* (`AC_MET_BUT_NOT_A_PLUS`, `NEEDS_VERIFICATION`); `READY_FOR_MERGE` and an
|
|
924
|
+
* absent verdict produce no note. (#749)
|
|
925
|
+
*/
|
|
926
|
+
function qaVerdictNote(verdict) {
|
|
927
|
+
if (!verdict || verdict === "READY_FOR_MERGE")
|
|
928
|
+
return null;
|
|
929
|
+
switch (verdict) {
|
|
930
|
+
case "AC_MET_BUT_NOT_A_PLUS":
|
|
931
|
+
return `> **QA verdict: AC_MET_BUT_NOT_A_PLUS** — acceptance criteria met, but not A+. The run broke to this PR for review rather than entering the quality loop; see the QA review comment for improvement notes.`;
|
|
932
|
+
case "NEEDS_VERIFICATION":
|
|
933
|
+
return `> **QA verdict: NEEDS_VERIFICATION** — acceptance criteria met, pending external verification (e.g. CI or a manual test). See the QA review comment.`;
|
|
934
|
+
default:
|
|
935
|
+
return `> **QA verdict: ${verdict}** — see the QA review comment.`;
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
/**
|
|
939
|
+
* Build the automated PR body for `sequant run`.
|
|
940
|
+
*
|
|
941
|
+
* Pure and I/O-free, kept separate from {@link createPR} (which shells out to
|
|
942
|
+
* `gh`) so the body — including the non-A+ QA note (#749) — is unit-testable
|
|
943
|
+
* directly.
|
|
944
|
+
*
|
|
945
|
+
* @param issueNumber Issue the PR closes
|
|
946
|
+
* @param opts.stackManifest Appended before the trailer under --stacked (#605)
|
|
947
|
+
* @param opts.qaVerdict QA verdict for the run; a non-A+ stopping state
|
|
948
|
+
* (anything other than `READY_FOR_MERGE`) surfaces a note so a human reviewer
|
|
949
|
+
* sees why the run broke to PR without reaching A+ (#749).
|
|
950
|
+
* @param opts.readyGateReport Rendered `sequant ready` gap report (#817). Set
|
|
951
|
+
* only when the run used `--ready-gate`; surfaces the gate outcome (threshold
|
|
952
|
+
* reached vs guard halt) in the PR body the same way `sequant ready` does.
|
|
953
|
+
* @internal Exported for testing
|
|
954
|
+
*/
|
|
955
|
+
export function buildAutomatedPRBody(issueNumber, opts) {
|
|
956
|
+
const bodyLines = [
|
|
957
|
+
`## Summary`,
|
|
958
|
+
``,
|
|
959
|
+
`Automated PR for issue #${issueNumber}.`,
|
|
960
|
+
``,
|
|
961
|
+
`Fixes #${issueNumber}`,
|
|
962
|
+
``,
|
|
963
|
+
];
|
|
964
|
+
// #749: surface a non-A+ QA verdict in the PR body (not just the run log) so
|
|
965
|
+
// a reviewer sees why the run broke to PR rather than reaching A+.
|
|
966
|
+
//
|
|
967
|
+
// #817: when the ready gate ran, its report already carries the terminal
|
|
968
|
+
// verdict and stop reason (a superset of the #749 note), so prefer it and
|
|
969
|
+
// suppress the standalone note to avoid a contradictory double-headline.
|
|
970
|
+
if (opts?.readyGateReport) {
|
|
971
|
+
bodyLines.push(opts.readyGateReport, ``);
|
|
972
|
+
}
|
|
973
|
+
else {
|
|
974
|
+
const note = qaVerdictNote(opts?.qaVerdict);
|
|
975
|
+
if (note) {
|
|
976
|
+
bodyLines.push(note, ``);
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
// #605 AC-4: emit stack manifest before the trailer so reviewers see the
|
|
980
|
+
// chain at the top of the body. Manifest is only set under --stacked.
|
|
981
|
+
if (opts?.stackManifest) {
|
|
982
|
+
bodyLines.push(opts.stackManifest, ``);
|
|
983
|
+
}
|
|
984
|
+
bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
|
|
985
|
+
return bodyLines.join("\n");
|
|
986
|
+
}
|
|
835
987
|
/**
|
|
836
988
|
* Push branch and create a PR after successful QA.
|
|
837
989
|
*
|
|
838
990
|
* Handles both fresh PR creation and detection of existing PRs.
|
|
839
|
-
*
|
|
991
|
+
*
|
|
992
|
+
* Returns `{ success: false, error }` on failure. As of #879 the caller
|
|
993
|
+
* (`runIssueWithLogging`) treats an attempted-but-failed PR as a run failure:
|
|
994
|
+
* a passing run that produced no PR is not a success. This function itself
|
|
995
|
+
* still only reports — it never throws — so relay teardown runs regardless.
|
|
840
996
|
*
|
|
841
997
|
* @param worktreePath Path to the worktree
|
|
842
998
|
* @param issueNumber Issue number
|
|
@@ -847,10 +1003,13 @@ export function rebaseBeforePR(worktreePath, issueNumber, packageManager, verbos
|
|
|
847
1003
|
* @param stackOptions When set under --stacked, `prBase` overrides the default
|
|
848
1004
|
* PR target (otherwise gh defaults to the repo's default branch) and
|
|
849
1005
|
* `stackManifest` is appended to the PR body. (#605)
|
|
1006
|
+
* @param qaVerdict QA verdict surfaced in the PR body when non-A+ (#749).
|
|
1007
|
+
* @param readyGateReport Rendered `sequant ready` gap report, set only when the
|
|
1008
|
+
* run used `--ready-gate` (#817).
|
|
850
1009
|
* @returns PRCreationResult with PR info or error
|
|
851
1010
|
* @internal Exported for testing
|
|
852
1011
|
*/
|
|
853
|
-
export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions) {
|
|
1012
|
+
export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions, qaVerdict, readyGateReport) {
|
|
854
1013
|
const github = new GitHubProvider();
|
|
855
1014
|
// Step 1: Check for existing PR on this branch
|
|
856
1015
|
const existingPRInfo = github.viewPRByBranchSync(branch, worktreePath);
|
|
@@ -886,21 +1045,11 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
|
|
|
886
1045
|
const isBug = labels?.some((l) => /^bug/i.test(l));
|
|
887
1046
|
const prefix = isBug ? "fix" : "feat";
|
|
888
1047
|
const prTitle = `${prefix}(#${issueNumber}): ${issueTitle}`;
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
`Fixes #${issueNumber}`,
|
|
895
|
-
``,
|
|
896
|
-
];
|
|
897
|
-
// #605 AC-4: emit stack manifest before the trailer so reviewers see the
|
|
898
|
-
// chain at the top of the body. Manifest is only set under --stacked.
|
|
899
|
-
if (stackOptions?.stackManifest) {
|
|
900
|
-
bodyLines.push(stackOptions.stackManifest, ``);
|
|
901
|
-
}
|
|
902
|
-
bodyLines.push(`---`, `🤖 Generated by \`sequant run\``);
|
|
903
|
-
const prBody = bodyLines.join("\n");
|
|
1048
|
+
const prBody = buildAutomatedPRBody(issueNumber, {
|
|
1049
|
+
stackManifest: stackOptions?.stackManifest,
|
|
1050
|
+
qaVerdict,
|
|
1051
|
+
readyGateReport,
|
|
1052
|
+
});
|
|
904
1053
|
const prResult = github.createPRCliSync(prTitle, prBody, branch, worktreePath, stackOptions?.prBase);
|
|
905
1054
|
if (prResult.exitCode !== 0) {
|
|
906
1055
|
const prError = prResult.stderr.trim() || "Unknown error";
|
|
@@ -32,6 +32,8 @@ interface RunToolResponse {
|
|
|
32
32
|
total: number;
|
|
33
33
|
passed: number;
|
|
34
34
|
failed: number;
|
|
35
|
+
/** Issues that ended `partial` — timed out, no failure, no recovery (#766). */
|
|
36
|
+
partial: number;
|
|
35
37
|
durationSeconds: number;
|
|
36
38
|
};
|
|
37
39
|
phases: string;
|
|
@@ -92,13 +94,21 @@ export declare function parseRunIdLine(line: string): string | null;
|
|
|
92
94
|
export interface ProgressEvent {
|
|
93
95
|
issue: number;
|
|
94
96
|
phase: string;
|
|
95
|
-
event: "start" | "complete" | "failed";
|
|
97
|
+
event: "start" | "complete" | "failed" | "waiting";
|
|
96
98
|
durationSeconds?: number;
|
|
97
99
|
error?: string;
|
|
100
|
+
/** #860: epoch ms of the auto-wait wake; absent on the terminal notice. */
|
|
101
|
+
wakeAtMs?: number;
|
|
102
|
+
/** #860: ms remaining in the auto-wait (0 on the terminal notice). */
|
|
103
|
+
remainingMs?: number;
|
|
98
104
|
}
|
|
99
105
|
/**
|
|
100
106
|
* Parse a SEQUANT_PROGRESS line emitted by the batch executor.
|
|
101
107
|
* Returns the parsed event or null if the line isn't a progress line.
|
|
108
|
+
*
|
|
109
|
+
* `waiting` events (#860) are the auto-wait liveness signal: a run paused on
|
|
110
|
+
* a rate-limit window emits one per minute so an MCP client sees a deliberate
|
|
111
|
+
* pause with a wake time rather than silence indistinguishable from a hang.
|
|
102
112
|
*/
|
|
103
113
|
export declare function parseProgressLine(line: string): ProgressEvent | null;
|
|
104
114
|
/**
|
|
@@ -11,6 +11,7 @@ import { resolve, dirname, join } from "path";
|
|
|
11
11
|
import { existsSync } from "fs";
|
|
12
12
|
import { readdir, readFile } from "fs/promises";
|
|
13
13
|
import { homedir } from "os";
|
|
14
|
+
import { formatResetTime } from "../../lib/errors.js";
|
|
14
15
|
import { LOG_PATHS, RunLogSchema } from "../../lib/workflow/run-log-schema.js";
|
|
15
16
|
import { registerRun, unregisterRun } from "../run-registry.js";
|
|
16
17
|
/** Maximum total response size in bytes (64 KB) */
|
|
@@ -182,6 +183,7 @@ export function buildStructuredResponse(runLog, rawOutput, overallStatus, exitCo
|
|
|
182
183
|
total: runLog.summary.totalIssues,
|
|
183
184
|
passed: runLog.summary.passed,
|
|
184
185
|
failed: runLog.summary.failed,
|
|
186
|
+
partial: runLog.summary.partial ?? 0,
|
|
185
187
|
durationSeconds: runLog.summary.totalDurationSeconds,
|
|
186
188
|
},
|
|
187
189
|
phases: phasesRan || runLog.config.phases.join(","),
|
|
@@ -232,6 +234,7 @@ function buildFallbackResponse(stdout, issueNumbers, overallStatus, phases, exit
|
|
|
232
234
|
total: issueNumbers.length,
|
|
233
235
|
passed: overallStatus === "success" ? issueNumbers.length : 0,
|
|
234
236
|
failed: overallStatus === "failure" ? issueNumbers.length : 0,
|
|
237
|
+
partial: 0,
|
|
235
238
|
durationSeconds: 0,
|
|
236
239
|
},
|
|
237
240
|
phases,
|
|
@@ -259,6 +262,10 @@ export function parseRunIdLine(line) {
|
|
|
259
262
|
/**
|
|
260
263
|
* Parse a SEQUANT_PROGRESS line emitted by the batch executor.
|
|
261
264
|
* Returns the parsed event or null if the line isn't a progress line.
|
|
265
|
+
*
|
|
266
|
+
* `waiting` events (#860) are the auto-wait liveness signal: a run paused on
|
|
267
|
+
* a rate-limit window emits one per minute so an MCP client sees a deliberate
|
|
268
|
+
* pause with a wake time rather than silence indistinguishable from a hang.
|
|
262
269
|
*/
|
|
263
270
|
export function parseProgressLine(line) {
|
|
264
271
|
if (!line.startsWith(PROGRESS_LINE_PREFIX))
|
|
@@ -270,7 +277,8 @@ export function parseProgressLine(line) {
|
|
|
270
277
|
typeof json.event === "string" &&
|
|
271
278
|
(json.event === "start" ||
|
|
272
279
|
json.event === "complete" ||
|
|
273
|
-
json.event === "failed"
|
|
280
|
+
json.event === "failed" ||
|
|
281
|
+
json.event === "waiting")) {
|
|
274
282
|
const result = {
|
|
275
283
|
issue: json.issue,
|
|
276
284
|
phase: json.phase,
|
|
@@ -282,6 +290,12 @@ export function parseProgressLine(line) {
|
|
|
282
290
|
if (typeof json.error === "string") {
|
|
283
291
|
result.error = json.error;
|
|
284
292
|
}
|
|
293
|
+
if (typeof json.wakeAtMs === "number") {
|
|
294
|
+
result.wakeAtMs = json.wakeAtMs;
|
|
295
|
+
}
|
|
296
|
+
if (typeof json.remainingMs === "number") {
|
|
297
|
+
result.remainingMs = json.remainingMs;
|
|
298
|
+
}
|
|
285
299
|
return result;
|
|
286
300
|
}
|
|
287
301
|
return null;
|
|
@@ -339,6 +353,13 @@ export function formatProgressMessage(event) {
|
|
|
339
353
|
const reason = event.error ? ` \u2014 ${event.error}` : "";
|
|
340
354
|
return `${prefix}: ${event.phase} \u2717${reason}`;
|
|
341
355
|
}
|
|
356
|
+
case "waiting": {
|
|
357
|
+
// #860: name the wake time so a paused run reads as deliberate.
|
|
358
|
+
if (event.wakeAtMs === undefined) {
|
|
359
|
+
return `${prefix}: ${event.phase} auto-wait complete \u2014 resuming`;
|
|
360
|
+
}
|
|
361
|
+
return `${prefix}: ${event.phase} \u23f8 rate-limit window \u2014 resuming at ${formatResetTime(event.wakeAtMs)}`;
|
|
362
|
+
}
|
|
342
363
|
}
|
|
343
364
|
}
|
|
344
365
|
/**
|
|
@@ -97,6 +97,12 @@ export function registerStatusTool(server) {
|
|
|
97
97
|
phases: issueState.phases,
|
|
98
98
|
worktree: issueState.worktree,
|
|
99
99
|
pr: issueState.pr,
|
|
100
|
+
// #860: surfaced so MCP pollers see a deliberate rate-limit
|
|
101
|
+
// pause (with its wake time) instead of a stale in-progress.
|
|
102
|
+
autoWait: issueState.autoWait,
|
|
103
|
+
// #892: a durable window halt — resumable via `sequant
|
|
104
|
+
// resume` after `resumeAt` — is a pause, not a failure.
|
|
105
|
+
windowHalt: issueState.windowHalt,
|
|
100
106
|
lastActivity: issueState.lastActivity,
|
|
101
107
|
nextAction: getNextActionHint(issueState),
|
|
102
108
|
lastSynced: reconcileResult.lastSynced,
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { type JSX } from "react";
|
|
2
2
|
/**
|
|
3
|
-
* Per-issue elapsed timer.
|
|
4
|
-
*
|
|
3
|
+
* Per-issue elapsed timer. Reads the shared 1 Hz `now` that `App` already owns
|
|
4
|
+
* and threads through `IssueBox`, rather than running its own interval: `App`'s
|
|
5
|
+
* snapshot poller re-renders the whole tree every tick regardless, so a
|
|
6
|
+
* per-issue clock bought no re-render scoping.
|
|
7
|
+
*
|
|
8
|
+
* When `completedAt` is set, the duration freezes at `completedAt - startedAt`.
|
|
9
|
+
* This is computed in render (not by tearing down an interval on mount), so an
|
|
10
|
+
* issue that transitions running → passed on the same mounted component freezes
|
|
11
|
+
* at the transition without a remount.
|
|
5
12
|
*/
|
|
6
|
-
export declare function ElapsedTimer({ startedAt }: {
|
|
13
|
+
export declare function ElapsedTimer({ startedAt, completedAt, now, }: {
|
|
7
14
|
startedAt?: Date;
|
|
15
|
+
completedAt?: Date;
|
|
16
|
+
now: number;
|
|
8
17
|
}): JSX.Element;
|
|
9
18
|
/** Format an absolute timestamp as the "last activity Xs ago" stamp. */
|
|
10
19
|
export declare function formatSinceActivity(now: number, activityAt: Date): string;
|
|
@@ -1,19 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { useEffect, useState } from "react";
|
|
3
2
|
import { Text } from "ink";
|
|
4
3
|
/**
|
|
5
|
-
* Per-issue elapsed timer.
|
|
6
|
-
*
|
|
4
|
+
* Per-issue elapsed timer. Reads the shared 1 Hz `now` that `App` already owns
|
|
5
|
+
* and threads through `IssueBox`, rather than running its own interval: `App`'s
|
|
6
|
+
* snapshot poller re-renders the whole tree every tick regardless, so a
|
|
7
|
+
* per-issue clock bought no re-render scoping.
|
|
8
|
+
*
|
|
9
|
+
* When `completedAt` is set, the duration freezes at `completedAt - startedAt`.
|
|
10
|
+
* This is computed in render (not by tearing down an interval on mount), so an
|
|
11
|
+
* issue that transitions running → passed on the same mounted component freezes
|
|
12
|
+
* at the transition without a remount.
|
|
7
13
|
*/
|
|
8
|
-
export function ElapsedTimer({ startedAt }) {
|
|
9
|
-
const [now, setNow] = useState(() => Date.now());
|
|
10
|
-
useEffect(() => {
|
|
11
|
-
const id = setInterval(() => setNow(Date.now()), 1000);
|
|
12
|
-
return () => clearInterval(id);
|
|
13
|
-
}, []);
|
|
14
|
+
export function ElapsedTimer({ startedAt, completedAt, now, }) {
|
|
14
15
|
if (!startedAt)
|
|
15
16
|
return _jsx(Text, { children: "--:--" });
|
|
16
|
-
const
|
|
17
|
+
const end = completedAt?.getTime() ?? now;
|
|
18
|
+
const secs = Math.max(0, Math.floor((end - startedAt.getTime()) / 1000));
|
|
17
19
|
const mm = Math.floor(secs / 60)
|
|
18
20
|
.toString()
|
|
19
21
|
.padStart(2, "0");
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from "ink";
|
|
3
|
-
import { ACTIVE_PHASE_COLOR, DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, phaseStatusColor, } from "./theme.js";
|
|
3
|
+
import { ACTIVE_PHASE_COLOR, DIVIDER_COLOR, PHASE_GLYPHS, borderColorForIssue, isTerminalStatus, phaseStatusColor, } from "./theme.js";
|
|
4
4
|
import { Spinner } from "./Spinner.js";
|
|
5
5
|
import { ElapsedTimer, formatSinceActivity } from "./ElapsedTimer.js";
|
|
6
6
|
import { truncateToWidth } from "./truncate.js";
|
|
@@ -20,7 +20,15 @@ export function IssueBox({ state, slot, width, now, }) {
|
|
|
20
20
|
const displayPhaseN = activePhaseIndex >= 0 ? activePhaseIndex + 1 : doneCount;
|
|
21
21
|
const total = state.phases.length;
|
|
22
22
|
const headerTitle = truncateToWidth(`#${state.number} ${state.title}`, Math.max(10, innerWidth - 20));
|
|
23
|
-
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt
|
|
23
|
+
return (_jsxs(Box, { flexDirection: "column", borderStyle: "round", borderColor: border, paddingX: 1, marginBottom: 1, width: width, children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Text, { color: border, children: headerTitle }), _jsxs(Text, { color: DIVIDER_COLOR, children: ["phase ", displayPhaseN, "/", total, " \u2022", " ", _jsx(ElapsedTimer, { startedAt: state.startedAt,
|
|
24
|
+
// #866: only a terminal status may freeze the clock. Mirrors the
|
|
25
|
+
// plain renderer, which branches on status *before* reading
|
|
26
|
+
// `completedAt` (`run-renderer.ts` `statusHeader`) and so always
|
|
27
|
+
// measures a running issue against the live clock. Without this
|
|
28
|
+
// gate a stale `completedAt` on a running issue — what the #766
|
|
29
|
+
// quality-loop recovery used to publish — freezes the header while
|
|
30
|
+
// the issue is visibly working.
|
|
31
|
+
completedAt: isTerminalStatus(state.status) ? state.completedAt : undefined, now: now })] })] }), _jsx(Divider, { width: innerWidth }), _jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "branch " }), _jsx(Text, { children: truncateToWidth(state.branch, innerWidth - 8) })] }), _jsx(PhaseProgression, { phases: state.phases, activeColor: ACTIVE_PHASE_COLOR }), state.currentPhase?.logPath ? (_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "log " }), _jsx(Text, { children: truncateToWidth(state.currentPhase.logPath, innerWidth - 8) })] })) : null] }), _jsx(Divider, { width: innerWidth }), _jsx(Box, { flexDirection: "column", children: state.currentPhase ? (_jsxs(_Fragment, { children: [_jsxs(Box, { children: [_jsx(Text, { color: DIVIDER_COLOR, children: "now " }), _jsx(Spinner, { color: ACTIVE_PHASE_COLOR }), _jsxs(Text, { children: [" ", truncateToWidth(state.currentPhase.nowLine, innerWidth - 12)] })] }), _jsx(Box, { children: _jsxs(Text, { color: DIVIDER_COLOR, children: [" └ last activity ", formatSinceActivity(now, state.currentPhase.lastActivityAt)] }) })] })) : (_jsx(Text, { color: DIVIDER_COLOR, children: statusLine(state) })) })] }));
|
|
24
32
|
}
|
|
25
33
|
function statusLine(state) {
|
|
26
34
|
switch (state.status) {
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* bottom margin). `LINES_PER_BOX` is sized accordingly so the dynamic cap
|
|
13
13
|
* reflects how many boxes actually fit.
|
|
14
14
|
*/
|
|
15
|
+
import { isTerminalStatus } from "./theme.js";
|
|
15
16
|
/** Static row cap (matches the plain renderer's default). */
|
|
16
17
|
export const DEFAULT_TUI_ROW_CAP = 10;
|
|
17
18
|
/**
|
|
@@ -27,7 +28,7 @@ export const TUI_LINES_PER_BOX = 11;
|
|
|
27
28
|
const FIXED_OVERHEAD = 4;
|
|
28
29
|
/** A queued or running issue is "active"; passed/failed are terminal. */
|
|
29
30
|
function isActive(issue) {
|
|
30
|
-
return issue.status
|
|
31
|
+
return !isTerminalStatus(issue.status);
|
|
31
32
|
}
|
|
32
33
|
/**
|
|
33
34
|
* Effective cap: the smaller of the static cap and a dynamic terminal-height
|
|
@@ -46,6 +46,13 @@ export declare const ACTIVE_PHASE_COLOR: "#FF8012";
|
|
|
46
46
|
/** Brand green for the rolled-up `✔ N done` summary line (#699, parity with the
|
|
47
47
|
* plain renderer's #624 rollup). */
|
|
48
48
|
export declare const ROLLUP_COLOR: "#10b981";
|
|
49
|
+
/**
|
|
50
|
+
* `passed`/`failed` are terminal; `queued`/`running` mean the issue is still
|
|
51
|
+
* live. The single definition of that split for the TUI — `row-cap` uses it to
|
|
52
|
+
* decide which issues can roll up, and `IssueBox` uses it to decide whether the
|
|
53
|
+
* elapsed clock may freeze (#866).
|
|
54
|
+
*/
|
|
55
|
+
export declare function isTerminalStatus(status: IssueStatus): boolean;
|
|
49
56
|
/**
|
|
50
57
|
* Pick the border color for an issue.
|
|
51
58
|
* Failed / passed states win over rotation; otherwise rotate by slot.
|
package/dist/src/ui/tui/theme.js
CHANGED
|
@@ -44,6 +44,15 @@ export const ACTIVE_PHASE_COLOR = BRAND_ORANGE;
|
|
|
44
44
|
/** Brand green for the rolled-up `✔ N done` summary line (#699, parity with the
|
|
45
45
|
* plain renderer's #624 rollup). */
|
|
46
46
|
export const ROLLUP_COLOR = BRAND_GREEN;
|
|
47
|
+
/**
|
|
48
|
+
* `passed`/`failed` are terminal; `queued`/`running` mean the issue is still
|
|
49
|
+
* live. The single definition of that split for the TUI — `row-cap` uses it to
|
|
50
|
+
* decide which issues can roll up, and `IssueBox` uses it to decide whether the
|
|
51
|
+
* elapsed clock may freeze (#866).
|
|
52
|
+
*/
|
|
53
|
+
export function isTerminalStatus(status) {
|
|
54
|
+
return status === "passed" || status === "failed";
|
|
55
|
+
}
|
|
47
56
|
/**
|
|
48
57
|
* Pick the border color for an issue.
|
|
49
58
|
* Failed / passed states win over rotation; otherwise rotate by slot.
|