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.
Files changed (188) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +14 -3
  4. package/dist/bin/cli.js +97 -17
  5. package/dist/marketplace/external_plugins/sequant/.claude-plugin/plugin.json +1 -1
  6. package/dist/marketplace/external_plugins/sequant/.mcp.json +1 -1
  7. package/dist/marketplace/external_plugins/sequant/README.md +56 -2
  8. package/dist/marketplace/external_plugins/sequant/hooks/parallel-marker.sh +50 -0
  9. package/dist/marketplace/external_plugins/sequant/hooks/post-tool.sh +20 -7
  10. package/dist/marketplace/external_plugins/sequant/hooks/pre-tool.sh +75 -10
  11. package/dist/marketplace/external_plugins/sequant/skills/_shared/references/trust-model.md +18 -0
  12. package/dist/marketplace/external_plugins/sequant/skills/assess/SKILL.md +215 -390
  13. package/dist/marketplace/external_plugins/sequant/skills/assess/references/predicted-collision-detection.md +1 -1
  14. package/dist/marketplace/external_plugins/sequant/skills/docs/SKILL.md +4 -4
  15. package/dist/marketplace/external_plugins/sequant/skills/exec/SKILL.md +26 -6
  16. package/dist/marketplace/external_plugins/sequant/skills/fullsolve/SKILL.md +3 -1
  17. package/dist/marketplace/external_plugins/sequant/skills/loop/SKILL.md +38 -2
  18. package/dist/marketplace/external_plugins/sequant/skills/merger/SKILL.md +2 -2
  19. package/dist/marketplace/external_plugins/sequant/skills/qa/SKILL.md +337 -57
  20. package/dist/marketplace/external_plugins/sequant/skills/qa/references/anti-pattern-detection.md +6 -6
  21. package/dist/marketplace/external_plugins/sequant/skills/qa/references/call-site-review.md +2 -2
  22. package/dist/marketplace/external_plugins/sequant/skills/qa/references/code-review-checklist.md +2 -2
  23. package/dist/marketplace/external_plugins/sequant/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  24. package/dist/marketplace/external_plugins/sequant/skills/qa/references/quality-gates.md +21 -7
  25. package/dist/marketplace/external_plugins/sequant/skills/qa/references/testing-requirements.md +1 -1
  26. package/dist/marketplace/external_plugins/sequant/skills/qa/scripts/quality-checks.sh +11 -11
  27. package/dist/marketplace/external_plugins/sequant/skills/release/SKILL.md +1 -1
  28. package/dist/marketplace/external_plugins/sequant/skills/setup/SKILL.md +5 -5
  29. package/dist/marketplace/external_plugins/sequant/skills/spec/SKILL.md +2 -0
  30. package/dist/marketplace/external_plugins/sequant/skills/test/SKILL.md +1 -1
  31. package/dist/src/commands/assess-render.d.ts +23 -0
  32. package/dist/src/commands/assess-render.js +60 -0
  33. package/dist/src/commands/doctor.js +8 -13
  34. package/dist/src/commands/init.js +13 -1
  35. package/dist/src/commands/locks.js +2 -0
  36. package/dist/src/commands/logs.js +14 -5
  37. package/dist/src/commands/merge.js +154 -2
  38. package/dist/src/commands/ready-tui-adapter.js +6 -1
  39. package/dist/src/commands/ready.d.ts +25 -3
  40. package/dist/src/commands/ready.js +39 -13
  41. package/dist/src/commands/resume.d.ts +113 -0
  42. package/dist/src/commands/resume.js +193 -0
  43. package/dist/src/commands/run-display.js +37 -4
  44. package/dist/src/commands/run-flags.d.ts +42 -1
  45. package/dist/src/commands/run-flags.js +53 -1
  46. package/dist/src/commands/run-progress.js +26 -1
  47. package/dist/src/commands/run.js +11 -10
  48. package/dist/src/commands/state.js +7 -0
  49. package/dist/src/commands/status.d.ts +9 -0
  50. package/dist/src/commands/status.js +24 -1
  51. package/dist/src/commands/sync.js +90 -19
  52. package/dist/src/commands/update.js +28 -5
  53. package/dist/src/lib/ac-parser.d.ts +14 -0
  54. package/dist/src/lib/ac-parser.js +99 -5
  55. package/dist/src/lib/assess/index.d.ts +10 -0
  56. package/dist/src/lib/assess/index.js +9 -0
  57. package/dist/src/lib/assess/renderer.d.ts +64 -0
  58. package/dist/src/lib/assess/renderer.js +481 -0
  59. package/dist/src/lib/assess/types.d.ts +224 -0
  60. package/dist/src/lib/assess/types.js +241 -0
  61. package/dist/src/lib/ci/outputs.d.ts +15 -2
  62. package/dist/src/lib/ci/outputs.js +17 -5
  63. package/dist/src/lib/cli-flags.d.ts +33 -0
  64. package/dist/src/lib/cli-flags.js +46 -0
  65. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +21 -7
  66. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  67. package/dist/src/lib/cli-ui/run-renderer.js +62 -0
  68. package/dist/src/lib/errors.d.ts +78 -6
  69. package/dist/src/lib/errors.js +133 -15
  70. package/dist/src/lib/locks/index.d.ts +3 -3
  71. package/dist/src/lib/locks/index.js +2 -2
  72. package/dist/src/lib/locks/lock-manager.d.ts +20 -2
  73. package/dist/src/lib/locks/lock-manager.js +47 -3
  74. package/dist/src/lib/locks/types.d.ts +34 -3
  75. package/dist/src/lib/locks/types.js +16 -0
  76. package/dist/src/lib/mcp-config.d.ts +45 -0
  77. package/dist/src/lib/mcp-config.js +77 -1
  78. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  79. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  80. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  81. package/dist/src/lib/merge-check/command-result.js +56 -0
  82. package/dist/src/lib/merge-check/index.d.ts +4 -0
  83. package/dist/src/lib/merge-check/index.js +1 -1
  84. package/dist/src/lib/merge-check/report.js +5 -1
  85. package/dist/src/lib/merge-check/types.d.ts +9 -0
  86. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  87. package/dist/src/lib/merge-check/watch.js +215 -0
  88. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  89. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  90. package/dist/src/lib/settings.d.ts +14 -0
  91. package/dist/src/lib/settings.js +61 -8
  92. package/dist/src/lib/shutdown.d.ts +42 -2
  93. package/dist/src/lib/shutdown.js +75 -5
  94. package/dist/src/lib/skills-check.d.ts +26 -0
  95. package/dist/src/lib/skills-check.js +31 -0
  96. package/dist/src/lib/stacks.d.ts +110 -1
  97. package/dist/src/lib/stacks.js +244 -5
  98. package/dist/src/lib/templates.d.ts +74 -0
  99. package/dist/src/lib/templates.js +171 -16
  100. package/dist/src/lib/test-tautology-detector.d.ts +9 -2
  101. package/dist/src/lib/test-tautology-detector.js +153 -12
  102. package/dist/src/lib/version-check.d.ts +31 -0
  103. package/dist/src/lib/version-check.js +45 -2
  104. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  105. package/dist/src/lib/workflow/batch-executor.js +477 -44
  106. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  107. package/dist/src/lib/workflow/chain-resume.js +24 -20
  108. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  109. package/dist/src/lib/workflow/completed-status.js +57 -0
  110. package/dist/src/lib/workflow/config-resolver.d.ts +26 -0
  111. package/dist/src/lib/workflow/config-resolver.js +54 -2
  112. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +7 -0
  113. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  114. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  115. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  116. package/dist/src/lib/workflow/drivers/claude-code.js +20 -2
  117. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  118. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  119. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  120. package/dist/src/lib/workflow/heartbeat.js +90 -0
  121. package/dist/src/lib/workflow/log-writer.d.ts +44 -1
  122. package/dist/src/lib/workflow/log-writer.js +75 -6
  123. package/dist/src/lib/workflow/metrics-schema.d.ts +3 -3
  124. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  125. package/dist/src/lib/workflow/phase-executor.js +457 -52
  126. package/dist/src/lib/workflow/phase-registry.js +2 -1
  127. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  128. package/dist/src/lib/workflow/platforms/github.js +152 -0
  129. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  130. package/dist/src/lib/workflow/qa-cache.js +22 -9
  131. package/dist/src/lib/workflow/ready-gate.d.ts +17 -4
  132. package/dist/src/lib/workflow/ready-gate.js +25 -9
  133. package/dist/src/lib/workflow/run-log-schema.d.ts +14 -0
  134. package/dist/src/lib/workflow/run-log-schema.js +47 -1
  135. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  136. package/dist/src/lib/workflow/run-orchestrator.js +141 -20
  137. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  138. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  139. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  140. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  141. package/dist/src/lib/workflow/state-manager.js +83 -0
  142. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  143. package/dist/src/lib/workflow/state-schema.js +47 -0
  144. package/dist/src/lib/workflow/types.d.ts +126 -4
  145. package/dist/src/lib/workflow/types.js +1 -0
  146. package/dist/src/lib/workflow/worktree-manager.d.ts +28 -2
  147. package/dist/src/lib/workflow/worktree-manager.js +102 -23
  148. package/dist/src/mcp/tools/run.d.ts +9 -1
  149. package/dist/src/mcp/tools/run.js +20 -1
  150. package/dist/src/mcp/tools/status.js +6 -0
  151. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  152. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  153. package/dist/src/ui/tui/IssueBox.js +10 -2
  154. package/dist/src/ui/tui/row-cap.js +2 -1
  155. package/dist/src/ui/tui/theme.d.ts +7 -0
  156. package/dist/src/ui/tui/theme.js +9 -0
  157. package/package.json +8 -7
  158. package/templates/hooks/parallel-marker.sh +50 -0
  159. package/templates/hooks/post-tool.sh +20 -7
  160. package/templates/hooks/pre-tool.sh +75 -10
  161. package/templates/scripts/cleanup-worktree.sh +182 -19
  162. package/templates/scripts/new-feature.sh +264 -16
  163. package/templates/skills/_shared/references/trust-model.md +18 -0
  164. package/templates/skills/assess/SKILL.md +215 -390
  165. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  166. package/templates/skills/docs/SKILL.md +4 -4
  167. package/templates/skills/exec/SKILL.md +26 -6
  168. package/templates/skills/fullsolve/SKILL.md +3 -1
  169. package/templates/skills/loop/SKILL.md +38 -2
  170. package/templates/skills/merger/SKILL.md +2 -2
  171. package/templates/skills/qa/SKILL.md +337 -57
  172. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  173. package/templates/skills/qa/references/call-site-review.md +2 -2
  174. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  175. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  176. package/templates/skills/qa/references/quality-gates.md +21 -7
  177. package/templates/skills/qa/references/testing-requirements.md +1 -1
  178. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  179. package/templates/skills/release/SKILL.md +1 -1
  180. package/templates/skills/setup/SKILL.md +5 -5
  181. package/templates/skills/spec/SKILL.md +2 -0
  182. package/templates/skills/test/SKILL.md +1 -1
  183. package/dist/src/lib/phase-spinner.d.ts +0 -146
  184. package/dist/src/lib/phase-spinner.js +0 -255
  185. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  186. package/dist/src/lib/workflow/pr-operations.js +0 -326
  187. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  188. 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 { PM_CONFIG } from "../stacks.js";
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 (configurable threshold)
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 result = spawnSync("git", ["-C", worktreePath, "diff", "--name-only", "main...HEAD"], { stdio: "pipe" });
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 result = spawnSync("git", ["-C", worktreePath, "diff", "--stat", "main...HEAD"], { stdio: "pipe" });
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
- if (verbose) {
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
- const pm = packageManager || "npm";
746
- const pmConfig = PM_CONFIG[pm];
747
- const [cmd, ...args] = pmConfig.installSilent.split(" ");
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
- const note = qaVerdictNote(opts?.qaVerdict);
905
- if (note) {
906
- bodyLines.push(note, ``);
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
- * Failures are warnings — they don't fail the run.
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. Owns its own interval so tick-driven re-renders
4
- * are scoped to this leaf component and do not propagate to `IssueBox`.
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. Owns its own interval so tick-driven re-renders
6
- * are scoped to this leaf component and do not propagate to `IssueBox`.
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 secs = Math.max(0, Math.floor((now - startedAt.getTime()) / 1000));
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 })] })] }), _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) })) })] }));
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 === "queued" || issue.status === "running";
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.
@@ -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.9.0",
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
- "import": "./dist/index.js",
14
- "types": "./dist/index.d.ts"
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.12.0"
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
- PARALLEL_MARKER_PREFIX="${_TMPDIR}/claude-parallel-"
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
- # Format: ${_TMPDIR}/claude-parallel-<group-id>.marker
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 from marker filename
91
- AGENT_ID=$(basename "$marker" | sed 's/claude-parallel-//' | sed 's/\.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 "claude-parallel-*.marker" 2>/dev/null)
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"