sequant 2.9.0 → 2.11.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 (209) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +20 -5
  4. package/dist/bin/cli.js +143 -18
  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.d.ts +20 -1
  36. package/dist/src/commands/locks.js +208 -4
  37. package/dist/src/commands/logs.js +14 -5
  38. package/dist/src/commands/merge.js +154 -2
  39. package/dist/src/commands/ready-tui-adapter.js +6 -1
  40. package/dist/src/commands/ready.d.ts +31 -3
  41. package/dist/src/commands/ready.js +53 -13
  42. package/dist/src/commands/resume.d.ts +113 -0
  43. package/dist/src/commands/resume.js +193 -0
  44. package/dist/src/commands/run-display.js +38 -4
  45. package/dist/src/commands/run-flags.d.ts +42 -1
  46. package/dist/src/commands/run-flags.js +53 -1
  47. package/dist/src/commands/run-progress.js +26 -1
  48. package/dist/src/commands/run.js +11 -10
  49. package/dist/src/commands/state.js +7 -0
  50. package/dist/src/commands/status.d.ts +9 -0
  51. package/dist/src/commands/status.js +24 -1
  52. package/dist/src/commands/sync.js +90 -19
  53. package/dist/src/commands/update.js +28 -5
  54. package/dist/src/commands/worktree.d.ts +31 -0
  55. package/dist/src/commands/worktree.js +95 -0
  56. package/dist/src/lib/ac-parser.d.ts +14 -0
  57. package/dist/src/lib/ac-parser.js +99 -5
  58. package/dist/src/lib/assess/index.d.ts +10 -0
  59. package/dist/src/lib/assess/index.js +9 -0
  60. package/dist/src/lib/assess/renderer.d.ts +64 -0
  61. package/dist/src/lib/assess/renderer.js +481 -0
  62. package/dist/src/lib/assess/types.d.ts +224 -0
  63. package/dist/src/lib/assess/types.js +241 -0
  64. package/dist/src/lib/ci/outputs.d.ts +15 -2
  65. package/dist/src/lib/ci/outputs.js +17 -5
  66. package/dist/src/lib/cli-flags.d.ts +56 -0
  67. package/dist/src/lib/cli-flags.js +89 -0
  68. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +23 -7
  69. package/dist/src/lib/cli-ui/run-renderer.d.ts +10 -0
  70. package/dist/src/lib/cli-ui/run-renderer.js +69 -1
  71. package/dist/src/lib/errors.d.ts +78 -6
  72. package/dist/src/lib/errors.js +133 -15
  73. package/dist/src/lib/locks/checkout-lock.d.ts +193 -0
  74. package/dist/src/lib/locks/checkout-lock.js +389 -0
  75. package/dist/src/lib/locks/index.d.ts +7 -4
  76. package/dist/src/lib/locks/index.js +5 -3
  77. package/dist/src/lib/locks/lock-manager.d.ts +101 -3
  78. package/dist/src/lib/locks/lock-manager.js +277 -8
  79. package/dist/src/lib/locks/types.d.ts +106 -3
  80. package/dist/src/lib/locks/types.js +44 -0
  81. package/dist/src/lib/mcp-config.d.ts +45 -0
  82. package/dist/src/lib/mcp-config.js +77 -1
  83. package/dist/src/lib/merge-check/combined-branch-test.d.ts +21 -2
  84. package/dist/src/lib/merge-check/combined-branch-test.js +233 -110
  85. package/dist/src/lib/merge-check/command-result.d.ts +36 -0
  86. package/dist/src/lib/merge-check/command-result.js +56 -0
  87. package/dist/src/lib/merge-check/index.d.ts +4 -0
  88. package/dist/src/lib/merge-check/index.js +1 -1
  89. package/dist/src/lib/merge-check/report.js +5 -1
  90. package/dist/src/lib/merge-check/types.d.ts +9 -0
  91. package/dist/src/lib/merge-check/watch.d.ts +121 -0
  92. package/dist/src/lib/merge-check/watch.js +215 -0
  93. package/dist/src/lib/qa/infra-blocked-ci.d.ts +90 -0
  94. package/dist/src/lib/qa/infra-blocked-ci.js +80 -0
  95. package/dist/src/lib/settings.d.ts +87 -0
  96. package/dist/src/lib/settings.js +106 -8
  97. package/dist/src/lib/shutdown.d.ts +42 -2
  98. package/dist/src/lib/shutdown.js +75 -5
  99. package/dist/src/lib/skills-check.d.ts +26 -0
  100. package/dist/src/lib/skills-check.js +31 -0
  101. package/dist/src/lib/stacks.d.ts +110 -1
  102. package/dist/src/lib/stacks.js +244 -5
  103. package/dist/src/lib/templates.d.ts +74 -0
  104. package/dist/src/lib/templates.js +171 -16
  105. package/dist/src/lib/test-tautology-detector.d.ts +10 -2
  106. package/dist/src/lib/test-tautology-detector.js +213 -12
  107. package/dist/src/lib/version-check.d.ts +31 -0
  108. package/dist/src/lib/version-check.js +45 -2
  109. package/dist/src/lib/workflow/batch-executor.d.ts +125 -6
  110. package/dist/src/lib/workflow/batch-executor.js +552 -60
  111. package/dist/src/lib/workflow/chain-resume.d.ts +13 -11
  112. package/dist/src/lib/workflow/chain-resume.js +24 -20
  113. package/dist/src/lib/workflow/completed-status.d.ts +52 -0
  114. package/dist/src/lib/workflow/completed-status.js +57 -0
  115. package/dist/src/lib/workflow/config-resolver.d.ts +51 -0
  116. package/dist/src/lib/workflow/config-resolver.js +143 -2
  117. package/dist/src/lib/workflow/drivers/agent-driver.d.ts +22 -0
  118. package/dist/src/lib/workflow/drivers/aider.d.ts +2 -0
  119. package/dist/src/lib/workflow/drivers/aider.js +2 -0
  120. package/dist/src/lib/workflow/drivers/claude-code.d.ts +14 -0
  121. package/dist/src/lib/workflow/drivers/claude-code.js +25 -2
  122. package/dist/src/lib/workflow/effort-escalation.d.ts +73 -0
  123. package/dist/src/lib/workflow/effort-escalation.js +82 -0
  124. package/dist/src/lib/workflow/error-classifier.d.ts +4 -1
  125. package/dist/src/lib/workflow/error-classifier.js +4 -0
  126. package/dist/src/lib/workflow/git-diff-utils.d.ts +35 -3
  127. package/dist/src/lib/workflow/git-diff-utils.js +77 -4
  128. package/dist/src/lib/workflow/heartbeat.d.ts +38 -0
  129. package/dist/src/lib/workflow/heartbeat.js +90 -0
  130. package/dist/src/lib/workflow/log-writer.d.ts +54 -2
  131. package/dist/src/lib/workflow/log-writer.js +95 -6
  132. package/dist/src/lib/workflow/metrics-schema.d.ts +52 -9
  133. package/dist/src/lib/workflow/metrics-schema.js +33 -0
  134. package/dist/src/lib/workflow/metrics-writer.d.ts +11 -0
  135. package/dist/src/lib/workflow/phase-detection.d.ts +12 -0
  136. package/dist/src/lib/workflow/phase-detection.js +5 -1
  137. package/dist/src/lib/workflow/phase-executor.d.ts +203 -14
  138. package/dist/src/lib/workflow/phase-executor.js +467 -52
  139. package/dist/src/lib/workflow/phase-registry.js +2 -1
  140. package/dist/src/lib/workflow/platforms/github.d.ts +64 -0
  141. package/dist/src/lib/workflow/platforms/github.js +152 -0
  142. package/dist/src/lib/workflow/qa-cache.d.ts +6 -1
  143. package/dist/src/lib/workflow/qa-cache.js +22 -9
  144. package/dist/src/lib/workflow/ready-gate.d.ts +45 -4
  145. package/dist/src/lib/workflow/ready-gate.js +49 -12
  146. package/dist/src/lib/workflow/run-log-schema.d.ts +69 -0
  147. package/dist/src/lib/workflow/run-log-schema.js +78 -2
  148. package/dist/src/lib/workflow/run-orchestrator.d.ts +19 -0
  149. package/dist/src/lib/workflow/run-orchestrator.js +168 -20
  150. package/dist/src/lib/workflow/skills-preflight.d.ts +89 -0
  151. package/dist/src/lib/workflow/skills-preflight.js +112 -0
  152. package/dist/src/lib/workflow/spec-recommendation.d.ts +71 -0
  153. package/dist/src/lib/workflow/spec-recommendation.js +142 -0
  154. package/dist/src/lib/workflow/state-cleanup.js +9 -2
  155. package/dist/src/lib/workflow/state-manager.d.ts +33 -0
  156. package/dist/src/lib/workflow/state-manager.js +83 -0
  157. package/dist/src/lib/workflow/state-schema.d.ts +26 -0
  158. package/dist/src/lib/workflow/state-schema.js +47 -0
  159. package/dist/src/lib/workflow/types.d.ts +190 -4
  160. package/dist/src/lib/workflow/types.js +1 -0
  161. package/dist/src/lib/workflow/worktree-manager.d.ts +36 -3
  162. package/dist/src/lib/workflow/worktree-manager.js +111 -24
  163. package/dist/src/lib/workflow/worktree-resolver.d.ts +73 -0
  164. package/dist/src/lib/workflow/worktree-resolver.js +126 -0
  165. package/dist/src/mcp/tools/run.d.ts +9 -1
  166. package/dist/src/mcp/tools/run.js +20 -1
  167. package/dist/src/mcp/tools/status.js +6 -0
  168. package/dist/src/ui/tui/ElapsedTimer.d.ts +12 -3
  169. package/dist/src/ui/tui/ElapsedTimer.js +12 -10
  170. package/dist/src/ui/tui/IssueBox.js +10 -2
  171. package/dist/src/ui/tui/row-cap.js +2 -1
  172. package/dist/src/ui/tui/theme.d.ts +7 -0
  173. package/dist/src/ui/tui/theme.js +9 -0
  174. package/package.json +10 -8
  175. package/templates/hooks/parallel-marker.sh +50 -0
  176. package/templates/hooks/post-tool.sh +20 -7
  177. package/templates/hooks/pre-tool.sh +303 -10
  178. package/templates/scripts/cleanup-worktree.sh +217 -33
  179. package/templates/scripts/new-feature.sh +289 -35
  180. package/templates/skills/_shared/references/subagent-types.md +7 -18
  181. package/templates/skills/_shared/references/trust-model.md +18 -0
  182. package/templates/skills/assess/SKILL.md +220 -391
  183. package/templates/skills/assess/references/predicted-collision-detection.md +1 -1
  184. package/templates/skills/docs/SKILL.md +4 -4
  185. package/templates/skills/exec/SKILL.md +87 -13
  186. package/templates/skills/fullsolve/SKILL.md +130 -22
  187. package/templates/skills/loop/SKILL.md +94 -13
  188. package/templates/skills/merger/SKILL.md +100 -12
  189. package/templates/skills/qa/SKILL.md +396 -63
  190. package/templates/skills/qa/references/anti-pattern-detection.md +6 -6
  191. package/templates/skills/qa/references/call-site-review.md +2 -2
  192. package/templates/skills/qa/references/code-review-checklist.md +2 -2
  193. package/templates/skills/qa/references/fixtures/injection-issue-body.md +36 -0
  194. package/templates/skills/qa/references/quality-gates.md +21 -7
  195. package/templates/skills/qa/references/testing-requirements.md +1 -1
  196. package/templates/skills/qa/scripts/quality-checks.sh +11 -11
  197. package/templates/skills/release/SKILL.md +80 -1
  198. package/templates/skills/setup/SKILL.md +5 -5
  199. package/templates/skills/spec/SKILL.md +33 -15
  200. package/templates/skills/spec/references/recommended-workflow.md +14 -1
  201. package/templates/skills/test/SKILL.md +1 -1
  202. package/templates/skills/testgen/SKILL.md +23 -6
  203. package/dist/src/lib/phase-spinner.d.ts +0 -146
  204. package/dist/src/lib/phase-spinner.js +0 -255
  205. package/dist/src/lib/workflow/pr-operations.d.ts +0 -86
  206. package/dist/src/lib/workflow/pr-operations.js +0 -326
  207. package/dist/src/lib/workflow/run-summary.d.ts +0 -36
  208. package/dist/src/lib/workflow/run-summary.js +0 -142
  209. package/templates/agents/sequant-explorer.md +0 -24
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Repo-scoped worktree resolution and verification (#899).
3
+ *
4
+ * Skill-driven `/fullsolve` used to hand `/exec` an unexpanded glob
5
+ * (`../worktrees/feature/<issue>-` followed by a star and a slash) and `/exec`
6
+ * was instructed to trust it without checking. Two things went wrong:
7
+ *
8
+ * 1. When nothing had created the worktree, the `cd` failed and the agent
9
+ * silently kept working in the main checkout.
10
+ * 2. `../worktrees/` is one flat namespace shared by every repo under the same
11
+ * parent directory, and issue numbers are per-repo — so where the glob *did*
12
+ * match, it could match a sibling project's worktree.
13
+ *
14
+ * Both are fixed the same way: never resolve worktrees through the filesystem.
15
+ * `git worktree list` reports only the worktrees of the repository containing
16
+ * `cwd`, so scoping is structural rather than a filter that can be forgotten,
17
+ * and it reports the *branch*, which is the real identity — a worktree
18
+ * directory slug can drift from its branch after a rename.
19
+ */
20
+ import { existsSync, realpathSync, statSync } from "fs";
21
+ import path from "path";
22
+ import { listWorktrees } from "./worktree-manager.js";
23
+ /**
24
+ * Find the worktree of the *current* repository that holds issue `issue`.
25
+ *
26
+ * Selection keys on the branch git reports, never on the directory name, so a
27
+ * worktree whose slug has drifted from its branch still resolves — and a
28
+ * directory that merely looks like a match (a sibling repo's worktree, or a
29
+ * stray directory) never does.
30
+ *
31
+ * @param issue - Issue number to look up.
32
+ * @param cwd - Directory inside the repository to search. Defaults to process cwd.
33
+ */
34
+ export function resolveIssueWorktree(issue, cwd) {
35
+ const matches = listWorktrees(cwd)
36
+ .filter((w) => w.issue === issue)
37
+ .map(({ path: p, branch }) => ({ path: p, branch }));
38
+ if (matches.length === 0) {
39
+ return {
40
+ ok: false,
41
+ error: "WORKTREE_NOT_FOUND",
42
+ message: `WORKTREE_NOT_FOUND: no worktree of this repository has a branch for issue #${issue}. ` +
43
+ `Create one with \`./scripts/new-feature.sh ${issue}\`.`,
44
+ candidates: [],
45
+ };
46
+ }
47
+ if (matches.length > 1) {
48
+ const list = matches.map((m) => `${m.branch} -> ${m.path}`).join("; ");
49
+ return {
50
+ ok: false,
51
+ error: "WORKTREE_AMBIGUOUS",
52
+ message: `WORKTREE_AMBIGUOUS: ${matches.length} worktrees claim issue #${issue} (${list}). ` +
53
+ `Remove the stale one, or set SEQUANT_WORKTREE explicitly.`,
54
+ candidates: matches,
55
+ };
56
+ }
57
+ return { ok: true, ...matches[0] };
58
+ }
59
+ /**
60
+ * Canonicalize a path for comparison, tolerating paths that do not exist.
61
+ *
62
+ * `realpathSync` throws on a missing path, so fall back to resolving the
63
+ * deepest existing ancestor and re-appending the remainder — enough to make
64
+ * `/tmp/...` and `/private/tmp/...` compare equal on macOS.
65
+ */
66
+ function canonicalize(target) {
67
+ const absolute = path.resolve(target);
68
+ try {
69
+ return realpathSync(absolute);
70
+ }
71
+ catch {
72
+ const parent = path.dirname(absolute);
73
+ if (parent === absolute)
74
+ return absolute;
75
+ return path.join(canonicalize(parent), path.basename(absolute));
76
+ }
77
+ }
78
+ /**
79
+ * Verify that a caller-supplied worktree path is safe to work in.
80
+ *
81
+ * This is the guard `/exec` runs before trusting `SEQUANT_WORKTREE`. It fails
82
+ * closed on every uncertain case — a bad path must halt the run, never degrade
83
+ * into "keep going in whatever directory we happen to be in".
84
+ *
85
+ * @param worktreePath - The path to check (typically `$SEQUANT_WORKTREE`).
86
+ * @param options.issue - When set, the branch must belong to this issue.
87
+ * @param options.cwd - Directory inside the repository to check against.
88
+ */
89
+ export function verifyWorktreePath(worktreePath, options = {}) {
90
+ const raw = worktreePath.trim();
91
+ if (raw.length === 0 || raw.includes("*")) {
92
+ return {
93
+ ok: false,
94
+ error: "SEQUANT_WORKTREE_NOT_FOUND",
95
+ message: `SEQUANT_WORKTREE_NOT_FOUND: ${raw.length === 0 ? "empty path" : `unexpanded glob "${raw}"`}. Expected a resolved absolute directory.`,
96
+ };
97
+ }
98
+ const absolute = path.resolve(raw);
99
+ if (!existsSync(absolute) || !statSync(absolute).isDirectory()) {
100
+ return {
101
+ ok: false,
102
+ error: "SEQUANT_WORKTREE_NOT_FOUND",
103
+ message: `SEQUANT_WORKTREE_NOT_FOUND: "${raw}" is not an existing directory.`,
104
+ };
105
+ }
106
+ const canonical = canonicalize(absolute);
107
+ const known = listWorktrees(options.cwd);
108
+ const match = known.find((w) => canonicalize(w.path) === canonical);
109
+ if (!match) {
110
+ return {
111
+ ok: false,
112
+ error: "SEQUANT_WORKTREE_FOREIGN",
113
+ message: `SEQUANT_WORKTREE_FOREIGN: "${raw}" is not a worktree of this repository. ` +
114
+ `It belongs to another project or is stale; \`../worktrees/\` is shared across repos.`,
115
+ };
116
+ }
117
+ if (options.issue !== undefined && match.issue !== options.issue) {
118
+ return {
119
+ ok: false,
120
+ error: "SEQUANT_WORKTREE_ISSUE_MISMATCH",
121
+ message: `SEQUANT_WORKTREE_ISSUE_MISMATCH: "${raw}" has branch "${match.branch}", ` +
122
+ `which is not issue #${options.issue}.`,
123
+ };
124
+ }
125
+ return { ok: true, path: match.path, branch: match.branch };
126
+ }
@@ -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.11.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,14 +21,16 @@
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
- "lint": "eslint src/ bin/ --max-warnings 0",
27
+ "lint": "eslint src/ bin/ scripts/ --max-warnings 0",
28
28
  "sync:skills": "npx tsx scripts/check-skill-sync.ts --fix",
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
+ "typecheck:scripts": "tsc --noEmit -p tsconfig.scripts.json",
31
32
  "lint:skill-calls": "npx tsx scripts/lint-skill-calls.ts",
33
+ "lint:skill-gates": "npx tsx scripts/lint-skill-gates.ts",
32
34
  "lint:skill-sync": "npx tsx scripts/check-skill-sync.ts",
33
35
  "prepare:marketplace": "npx tsx scripts/prepare-marketplace.ts",
34
36
  "validate:marketplace": "npx tsx scripts/prepare-marketplace.ts --validate-only",
@@ -78,7 +80,7 @@
78
80
  },
79
81
  "homepage": "https://sequant.io",
80
82
  "engines": {
81
- "node": ">=22.12.0"
83
+ "node": ">=22.13.0"
82
84
  },
83
85
  "peerDependencies": {
84
86
  "@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"