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
|
@@ -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)
|
|
@@ -513,17 +577,7 @@ export async function ensureWorktree(issueNumber, title, verbose, packageManager
|
|
|
513
577
|
// Install dependencies if needed
|
|
514
578
|
const nodeModulesPath = path.join(worktreePath, "node_modules");
|
|
515
579
|
if (!existsSync(nodeModulesPath)) {
|
|
516
|
-
|
|
517
|
-
console.log(chalk.gray(` Installing dependencies...`));
|
|
518
|
-
}
|
|
519
|
-
// Use detected package manager or default to npm
|
|
520
|
-
const pm = packageManager || "npm";
|
|
521
|
-
const pmConfig = PM_CONFIG[pm];
|
|
522
|
-
const [cmd, ...args] = pmConfig.installSilent.split(" ");
|
|
523
|
-
spawnSync(cmd, args, {
|
|
524
|
-
cwd: worktreePath,
|
|
525
|
-
stdio: "pipe",
|
|
526
|
-
});
|
|
580
|
+
installWorktreeDeps(worktreePath, packageManager, verbose);
|
|
527
581
|
}
|
|
528
582
|
if (verbose) {
|
|
529
583
|
console.log(chalk.green(` ✔ Worktree ready: ${worktreePath}`));
|
|
@@ -742,9 +796,14 @@ export function reinstallIfLockfileChanged(worktreePath, packageManager, verbose
|
|
|
742
796
|
}
|
|
743
797
|
// Re-run install to sync node_modules with updated lockfile
|
|
744
798
|
console.log(chalk.blue(` Reinstalling dependencies (lockfile changed)...`));
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
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(" ");
|
|
748
807
|
const installResult = spawnSync(cmd, args, {
|
|
749
808
|
cwd: worktreePath,
|
|
750
809
|
stdio: "pipe",
|
|
@@ -888,6 +947,9 @@ function qaVerdictNote(verdict) {
|
|
|
888
947
|
* @param opts.qaVerdict QA verdict for the run; a non-A+ stopping state
|
|
889
948
|
* (anything other than `READY_FOR_MERGE`) surfaces a note so a human reviewer
|
|
890
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.
|
|
891
953
|
* @internal Exported for testing
|
|
892
954
|
*/
|
|
893
955
|
export function buildAutomatedPRBody(issueNumber, opts) {
|
|
@@ -901,9 +963,18 @@ export function buildAutomatedPRBody(issueNumber, opts) {
|
|
|
901
963
|
];
|
|
902
964
|
// #749: surface a non-A+ QA verdict in the PR body (not just the run log) so
|
|
903
965
|
// a reviewer sees why the run broke to PR rather than reaching A+.
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
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
|
+
}
|
|
907
978
|
}
|
|
908
979
|
// #605 AC-4: emit stack manifest before the trailer so reviewers see the
|
|
909
980
|
// chain at the top of the body. Manifest is only set under --stacked.
|
|
@@ -917,7 +988,11 @@ export function buildAutomatedPRBody(issueNumber, opts) {
|
|
|
917
988
|
* Push branch and create a PR after successful QA.
|
|
918
989
|
*
|
|
919
990
|
* Handles both fresh PR creation and detection of existing PRs.
|
|
920
|
-
*
|
|
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.
|
|
921
996
|
*
|
|
922
997
|
* @param worktreePath Path to the worktree
|
|
923
998
|
* @param issueNumber Issue number
|
|
@@ -928,10 +1003,13 @@ export function buildAutomatedPRBody(issueNumber, opts) {
|
|
|
928
1003
|
* @param stackOptions When set under --stacked, `prBase` overrides the default
|
|
929
1004
|
* PR target (otherwise gh defaults to the repo's default branch) and
|
|
930
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).
|
|
931
1009
|
* @returns PRCreationResult with PR info or error
|
|
932
1010
|
* @internal Exported for testing
|
|
933
1011
|
*/
|
|
934
|
-
export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions, qaVerdict) {
|
|
1012
|
+
export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose, labels, stackOptions, qaVerdict, readyGateReport) {
|
|
935
1013
|
const github = new GitHubProvider();
|
|
936
1014
|
// Step 1: Check for existing PR on this branch
|
|
937
1015
|
const existingPRInfo = github.viewPRByBranchSync(branch, worktreePath);
|
|
@@ -970,6 +1048,7 @@ export function createPR(worktreePath, issueNumber, issueTitle, branch, verbose,
|
|
|
970
1048
|
const prBody = buildAutomatedPRBody(issueNumber, {
|
|
971
1049
|
stackManifest: stackOptions?.stackManifest,
|
|
972
1050
|
qaVerdict,
|
|
1051
|
+
readyGateReport,
|
|
973
1052
|
});
|
|
974
1053
|
const prResult = github.createPRCliSync(prTitle, prBody, branch, worktreePath, stackOptions?.prBase);
|
|
975
1054
|
if (prResult.exitCode !== 0) {
|
|
@@ -94,13 +94,21 @@ export declare function parseRunIdLine(line: string): string | null;
|
|
|
94
94
|
export interface ProgressEvent {
|
|
95
95
|
issue: number;
|
|
96
96
|
phase: string;
|
|
97
|
-
event: "start" | "complete" | "failed";
|
|
97
|
+
event: "start" | "complete" | "failed" | "waiting";
|
|
98
98
|
durationSeconds?: number;
|
|
99
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;
|
|
100
104
|
}
|
|
101
105
|
/**
|
|
102
106
|
* Parse a SEQUANT_PROGRESS line emitted by the batch executor.
|
|
103
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.
|
|
104
112
|
*/
|
|
105
113
|
export declare function parseProgressLine(line: string): ProgressEvent | null;
|
|
106
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) */
|
|
@@ -261,6 +262,10 @@ export function parseRunIdLine(line) {
|
|
|
261
262
|
/**
|
|
262
263
|
* Parse a SEQUANT_PROGRESS line emitted by the batch executor.
|
|
263
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.
|
|
264
269
|
*/
|
|
265
270
|
export function parseProgressLine(line) {
|
|
266
271
|
if (!line.startsWith(PROGRESS_LINE_PREFIX))
|
|
@@ -272,7 +277,8 @@ export function parseProgressLine(line) {
|
|
|
272
277
|
typeof json.event === "string" &&
|
|
273
278
|
(json.event === "start" ||
|
|
274
279
|
json.event === "complete" ||
|
|
275
|
-
json.event === "failed"
|
|
280
|
+
json.event === "failed" ||
|
|
281
|
+
json.event === "waiting")) {
|
|
276
282
|
const result = {
|
|
277
283
|
issue: json.issue,
|
|
278
284
|
phase: json.phase,
|
|
@@ -284,6 +290,12 @@ export function parseProgressLine(line) {
|
|
|
284
290
|
if (typeof json.error === "string") {
|
|
285
291
|
result.error = json.error;
|
|
286
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
|
+
}
|
|
287
299
|
return result;
|
|
288
300
|
}
|
|
289
301
|
return null;
|
|
@@ -341,6 +353,13 @@ export function formatProgressMessage(event) {
|
|
|
341
353
|
const reason = event.error ? ` \u2014 ${event.error}` : "";
|
|
342
354
|
return `${prefix}: ${event.phase} \u2717${reason}`;
|
|
343
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
|
+
}
|
|
344
363
|
}
|
|
345
364
|
}
|
|
346
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.
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sequant",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "AI coding agent orchestrator — resolve GitHub issues end-to-end with isolated git worktrees, quality gates, and an MCP server. Works with Claude Code or Aider.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sequant": "dist/bin/cli.js"
|
|
8
8
|
},
|
|
9
|
-
"main": "./dist/index.js",
|
|
10
|
-
"types": "./dist/index.d.ts",
|
|
9
|
+
"main": "./dist/src/index.js",
|
|
10
|
+
"types": "./dist/src/index.d.ts",
|
|
11
11
|
"exports": {
|
|
12
12
|
".": {
|
|
13
|
-
"
|
|
14
|
-
"
|
|
13
|
+
"types": "./dist/src/index.d.ts",
|
|
14
|
+
"import": "./dist/src/index.js"
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"files": [
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
".claude-plugin"
|
|
22
22
|
],
|
|
23
23
|
"scripts": {
|
|
24
|
-
"build": "tsc",
|
|
24
|
+
"build": "rm -rf dist/src dist/bin dist/dashboard && tsc && chmod +x dist/bin/cli.js",
|
|
25
25
|
"dev": "tsx bin/cli.ts",
|
|
26
26
|
"test": "vitest run",
|
|
27
27
|
"lint": "eslint src/ bin/ --max-warnings 0",
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
"sync:hooks": "bash scripts/sync-hooks.sh",
|
|
30
30
|
"validate:skills": "for skill in templates/skills/*/; do case \"$skill\" in *_shared*|*/references/*) continue;; esac; npx skills-ref validate \"$skill\"; done",
|
|
31
31
|
"lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
|
|
32
|
+
"lint:skill-gates": "npx tsx scripts/lint-skill-gates.ts",
|
|
32
33
|
"lint:skill-sync": "npx tsx scripts/check-skill-sync.ts",
|
|
33
34
|
"prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
|
|
34
35
|
"validate:marketplace": "npx tsx scripts/prepare-marketplace.ts --validate-only",
|
|
@@ -78,7 +79,7 @@
|
|
|
78
79
|
},
|
|
79
80
|
"homepage": "https://sequant.io",
|
|
80
81
|
"engines": {
|
|
81
|
-
"node": ">=22.
|
|
82
|
+
"node": ">=22.13.0"
|
|
82
83
|
},
|
|
83
84
|
"peerDependencies": {
|
|
84
85
|
"@modelcontextprotocol/sdk": "^1.27.1"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Shared naming contract for /exec parallel-group marker files (#881).
|
|
3
|
+
#
|
|
4
|
+
# One writer (the /exec skill) and two readers (pre-tool.sh, post-tool.sh) must
|
|
5
|
+
# agree on (a) the marker filename and (b) how the owning project is identified.
|
|
6
|
+
# Before #881 the filename was a global constant, so a parallel group in ONE
|
|
7
|
+
# project silently redirected worktree enforcement for EVERY concurrent Claude
|
|
8
|
+
# session on the machine — the first matching marker in the shared temp dir won,
|
|
9
|
+
# regardless of which project wrote it. Keeping the scheme in this single sourced
|
|
10
|
+
# file is what stops the writer and the readers from drifting apart (AC-4).
|
|
11
|
+
#
|
|
12
|
+
# Marker filename: ${TMPDIR}/claude-parallel-<project-hash>-<group-id>.marker
|
|
13
|
+
# Marker contents: line 1 = worktree path, line 2 = owning project root.
|
|
14
|
+
|
|
15
|
+
# parallel_marker_project_root — the canonical identity of the current project,
|
|
16
|
+
# derived identically in skill-context and hook-context. CLAUDE_PROJECT_DIR is
|
|
17
|
+
# set by Claude Code for both the skill's shell and the hook's shell within the
|
|
18
|
+
# same session; the git-toplevel fallback resolves to the same worktree when it
|
|
19
|
+
# is unset. A mismatch here would silently re-introduce the cross-project
|
|
20
|
+
# collision this fix exists to close, so the two contexts MUST agree.
|
|
21
|
+
parallel_marker_project_root() {
|
|
22
|
+
if [[ -n "${CLAUDE_PROJECT_DIR:-}" ]]; then
|
|
23
|
+
printf '%s' "$CLAUDE_PROJECT_DIR"
|
|
24
|
+
else
|
|
25
|
+
git rev-parse --show-toplevel 2>/dev/null || printf '%s' "$PWD"
|
|
26
|
+
fi
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
# parallel_marker_hash — a stable per-project hash used in the marker filename so
|
|
30
|
+
# two projects running parallel groups at once cannot collide (AC-1). Reuses the
|
|
31
|
+
# md5-with-md5sum-fallback idiom already used for the per-file lock in
|
|
32
|
+
# pre-tool.sh. printf (no trailing newline) keeps writer and reader identical.
|
|
33
|
+
parallel_marker_hash() {
|
|
34
|
+
local root
|
|
35
|
+
root="$(parallel_marker_project_root)"
|
|
36
|
+
printf '%s' "$root" | md5 -q 2>/dev/null || printf '%s' "$root" | md5sum | cut -d' ' -f1
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
# parallel_marker_prefix — the ${TMPDIR}-anchored, project-scoped filename prefix
|
|
40
|
+
# that both the writer and the readers glob on (AC-2). Honors $TMPDIR (macOS's
|
|
41
|
+
# per-user temp) with a /tmp fallback, matching the hooks' own _TMPDIR.
|
|
42
|
+
parallel_marker_prefix() {
|
|
43
|
+
local tmp="${TMPDIR:-/tmp}"
|
|
44
|
+
printf '%s/claude-parallel-%s-' "$tmp" "$(parallel_marker_hash)"
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
# parallel_marker_path <group-id> — the full marker path for one group.
|
|
48
|
+
parallel_marker_path() {
|
|
49
|
+
printf '%s%s.marker' "$(parallel_marker_prefix)" "$1"
|
|
50
|
+
}
|
|
@@ -77,22 +77,35 @@ mkdir -p "$_LOG_DIR" 2>/dev/null || _LOG_DIR="${_TMPDIR}"
|
|
|
77
77
|
TIMING_LOG="${_LOG_DIR}/claude-timing.log"
|
|
78
78
|
QUALITY_LOG="${_LOG_DIR}/claude-quality.log"
|
|
79
79
|
TESTS_LOG="${_LOG_DIR}/claude-tests.log"
|
|
80
|
-
|
|
80
|
+
|
|
81
|
+
# Parallel-group marker naming (#881). Source the shared helper next to this
|
|
82
|
+
# hook so the project-scoped prefix stays identical to the writer and pre-tool.sh.
|
|
83
|
+
_MARKER_HELPER="$(dirname "${BASH_SOURCE[0]:-$0}")/parallel-marker.sh"
|
|
84
|
+
if [[ -f "$_MARKER_HELPER" ]]; then
|
|
85
|
+
# shellcheck source=parallel-marker.sh disable=SC1091
|
|
86
|
+
source "$_MARKER_HELPER"
|
|
87
|
+
PARALLEL_MARKER_PREFIX="$(parallel_marker_prefix)"
|
|
88
|
+
else
|
|
89
|
+
PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
|
|
90
|
+
fi
|
|
81
91
|
|
|
82
92
|
# === AGENT ID DETECTION ===
|
|
83
|
-
# For parallel agents, detect group ID from marker files
|
|
84
|
-
#
|
|
93
|
+
# For parallel agents, detect group ID from marker files. The glob is scoped to
|
|
94
|
+
# the current project's marker prefix (#881) so a foreign project's marker never
|
|
95
|
+
# labels this session's timing rows.
|
|
85
96
|
AGENT_ID=""
|
|
86
97
|
IS_PARALLEL_AGENT="false"
|
|
98
|
+
_MARKER_BASE=$(basename "$PARALLEL_MARKER_PREFIX")
|
|
87
99
|
# Find marker files using find (works in both bash and zsh)
|
|
88
100
|
while IFS= read -r marker; do
|
|
89
101
|
if [[ -n "$marker" && -f "$marker" ]]; then
|
|
90
|
-
# Extract group ID
|
|
91
|
-
AGENT_ID=$(basename "$marker"
|
|
102
|
+
# Extract group ID: strip the project-scoped prefix and the suffix.
|
|
103
|
+
AGENT_ID=$(basename "$marker" .marker)
|
|
104
|
+
AGENT_ID=${AGENT_ID#"$_MARKER_BASE"}
|
|
92
105
|
IS_PARALLEL_AGENT="true"
|
|
93
106
|
break
|
|
94
107
|
fi
|
|
95
|
-
done < <(find "${_TMPDIR}" -maxdepth 1 -name "
|
|
108
|
+
done < <(find "${_TMPDIR}" -maxdepth 1 -name "${_MARKER_BASE}*.marker" 2>/dev/null)
|
|
96
109
|
|
|
97
110
|
# === TIMING END ===
|
|
98
111
|
# Include agent ID in log format if available (AC-4)
|
|
@@ -253,7 +266,7 @@ if [[ "${CLAUDE_HOOKS_COVERAGE:-}" == "true" ]]; then
|
|
|
253
266
|
COVERAGE_LOG="${_LOG_DIR}/claude-coverage.log"
|
|
254
267
|
|
|
255
268
|
# Get changed source files (excluding tests)
|
|
256
|
-
changed_files=$(git diff main...HEAD --name-only 2>/dev/null | grep -E '\.(ts|tsx|js|jsx)$' | grep -v -E '\.test\.|\.spec\.|__tests__' || true)
|
|
269
|
+
changed_files=$(git diff origin/main...HEAD --name-only 2>/dev/null | grep -E '\.(ts|tsx|js|jsx)$' | grep -v -E '\.test\.|\.spec\.|__tests__' || true)
|
|
257
270
|
|
|
258
271
|
if [[ -n "$changed_files" ]]; then
|
|
259
272
|
echo "$(date +%H:%M:%S) COVERAGE_ANALYSIS: Checking test coverage for changed files" >> "$QUALITY_LOG"
|