sequant 2.2.0 → 2.3.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 (137) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/README.md +73 -0
  4. package/dist/bin/cli.js +94 -9
  5. package/dist/src/commands/doctor.d.ts +25 -0
  6. package/dist/src/commands/doctor.js +36 -1
  7. package/dist/src/commands/locks.d.ts +67 -0
  8. package/dist/src/commands/locks.js +290 -0
  9. package/dist/src/commands/merge.js +11 -0
  10. package/dist/src/commands/prompt.d.ts +39 -0
  11. package/dist/src/commands/prompt.js +179 -0
  12. package/dist/src/commands/run-display.d.ts +11 -2
  13. package/dist/src/commands/run-display.js +62 -28
  14. package/dist/src/commands/run-progress.d.ts +32 -0
  15. package/dist/src/commands/run-progress.js +76 -0
  16. package/dist/src/commands/run.js +80 -18
  17. package/dist/src/commands/stats.d.ts +2 -0
  18. package/dist/src/commands/stats.js +94 -8
  19. package/dist/src/commands/status.js +12 -0
  20. package/dist/src/commands/watch.d.ts +16 -0
  21. package/dist/src/commands/watch.js +147 -0
  22. package/dist/src/lib/ac-linter.d.ts +1 -1
  23. package/dist/src/lib/ac-linter.js +81 -0
  24. package/dist/src/lib/assess-collision-detect.d.ts +91 -0
  25. package/dist/src/lib/assess-collision-detect.js +217 -0
  26. package/dist/src/lib/assess-comment-parser.d.ts +59 -1
  27. package/dist/src/lib/assess-comment-parser.js +124 -2
  28. package/dist/src/lib/cli-ui/format.d.ts +19 -0
  29. package/dist/src/lib/cli-ui/format.js +34 -0
  30. package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
  31. package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
  32. package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
  33. package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
  34. package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
  35. package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
  36. package/dist/src/lib/locks/index.d.ts +7 -0
  37. package/dist/src/lib/locks/index.js +5 -0
  38. package/dist/src/lib/locks/lock-manager.d.ts +168 -0
  39. package/dist/src/lib/locks/lock-manager.js +433 -0
  40. package/dist/src/lib/locks/types.d.ts +59 -0
  41. package/dist/src/lib/locks/types.js +31 -0
  42. package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
  43. package/dist/src/lib/qa/markdown-only-ci.js +74 -0
  44. package/dist/src/lib/relay/activation.d.ts +60 -0
  45. package/dist/src/lib/relay/activation.js +122 -0
  46. package/dist/src/lib/relay/archive.d.ts +34 -0
  47. package/dist/src/lib/relay/archive.js +106 -0
  48. package/dist/src/lib/relay/frame.d.ts +20 -0
  49. package/dist/src/lib/relay/frame.js +76 -0
  50. package/dist/src/lib/relay/index.d.ts +13 -0
  51. package/dist/src/lib/relay/index.js +13 -0
  52. package/dist/src/lib/relay/paths.d.ts +43 -0
  53. package/dist/src/lib/relay/paths.js +59 -0
  54. package/dist/src/lib/relay/pid.d.ts +34 -0
  55. package/dist/src/lib/relay/pid.js +72 -0
  56. package/dist/src/lib/relay/reader.d.ts +35 -0
  57. package/dist/src/lib/relay/reader.js +115 -0
  58. package/dist/src/lib/relay/types.d.ts +68 -0
  59. package/dist/src/lib/relay/types.js +76 -0
  60. package/dist/src/lib/relay/writer.d.ts +48 -0
  61. package/dist/src/lib/relay/writer.js +113 -0
  62. package/dist/src/lib/settings.d.ts +31 -1
  63. package/dist/src/lib/settings.js +18 -3
  64. package/dist/src/lib/version-check.d.ts +60 -5
  65. package/dist/src/lib/version-check.js +97 -9
  66. package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
  67. package/dist/src/lib/workflow/batch-executor.js +248 -175
  68. package/dist/src/lib/workflow/config-resolver.js +4 -0
  69. package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
  70. package/dist/src/lib/workflow/heartbeat.js +194 -0
  71. package/dist/src/lib/workflow/phase-executor.d.ts +62 -8
  72. package/dist/src/lib/workflow/phase-executor.js +157 -16
  73. package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
  74. package/dist/src/lib/workflow/phase-mapper.js +17 -20
  75. package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
  76. package/dist/src/lib/workflow/platforms/github.js +20 -3
  77. package/dist/src/lib/workflow/pr-status.d.ts +18 -2
  78. package/dist/src/lib/workflow/pr-status.js +41 -9
  79. package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
  80. package/dist/src/lib/workflow/qa-stagnation.js +179 -0
  81. package/dist/src/lib/workflow/run-orchestrator.d.ts +39 -0
  82. package/dist/src/lib/workflow/run-orchestrator.js +340 -15
  83. package/dist/src/lib/workflow/run-reflect.js +1 -1
  84. package/dist/src/lib/workflow/run-state.d.ts +71 -0
  85. package/dist/src/lib/workflow/run-state.js +14 -0
  86. package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
  87. package/dist/src/lib/workflow/state-cleanup.js +17 -5
  88. package/dist/src/lib/workflow/state-manager.d.ts +12 -1
  89. package/dist/src/lib/workflow/state-manager.js +37 -0
  90. package/dist/src/lib/workflow/state-schema.d.ts +62 -0
  91. package/dist/src/lib/workflow/state-schema.js +35 -1
  92. package/dist/src/lib/workflow/types.d.ts +74 -1
  93. package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
  94. package/dist/src/lib/workflow/worktree-manager.js +15 -6
  95. package/dist/src/mcp/tools/run.d.ts +44 -0
  96. package/dist/src/mcp/tools/run.js +104 -13
  97. package/dist/src/ui/tui/App.d.ts +14 -0
  98. package/dist/src/ui/tui/App.js +41 -0
  99. package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
  100. package/dist/src/ui/tui/ElapsedTimer.js +31 -0
  101. package/dist/src/ui/tui/Header.d.ts +6 -0
  102. package/dist/src/ui/tui/Header.js +15 -0
  103. package/dist/src/ui/tui/IssueBox.d.ts +16 -0
  104. package/dist/src/ui/tui/IssueBox.js +68 -0
  105. package/dist/src/ui/tui/Spinner.d.ts +9 -0
  106. package/dist/src/ui/tui/Spinner.js +18 -0
  107. package/dist/src/ui/tui/index.d.ts +15 -0
  108. package/dist/src/ui/tui/index.js +29 -0
  109. package/dist/src/ui/tui/theme.d.ts +29 -0
  110. package/dist/src/ui/tui/theme.js +52 -0
  111. package/dist/src/ui/tui/truncate.d.ts +11 -0
  112. package/dist/src/ui/tui/truncate.js +31 -0
  113. package/package.json +10 -3
  114. package/templates/agents/sequant-explorer.md +1 -0
  115. package/templates/agents/sequant-qa-checker.md +2 -1
  116. package/templates/agents/sequant-testgen.md +1 -0
  117. package/templates/hooks/post-tool.sh +11 -0
  118. package/templates/hooks/pre-tool.sh +18 -9
  119. package/templates/hooks/relay-check.sh +107 -0
  120. package/templates/relay/frame.txt +11 -0
  121. package/templates/scripts/cleanup-worktree.sh +25 -3
  122. package/templates/scripts/new-feature.sh +6 -0
  123. package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
  124. package/templates/skills/_shared/references/subagent-types.md +21 -8
  125. package/templates/skills/assess/SKILL.md +103 -49
  126. package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
  127. package/templates/skills/docs/SKILL.md +141 -22
  128. package/templates/skills/exec/SKILL.md +10 -8
  129. package/templates/skills/fullsolve/SKILL.md +79 -5
  130. package/templates/skills/loop/SKILL.md +28 -0
  131. package/templates/skills/merger/SKILL.md +621 -0
  132. package/templates/skills/qa/SKILL.md +727 -8
  133. package/templates/skills/setup/SKILL.md +6 -0
  134. package/templates/skills/spec/SKILL.md +52 -0
  135. package/templates/skills/spec/references/parallel-groups.md +7 -0
  136. package/templates/skills/spec/references/recommended-workflow.md +4 -2
  137. package/templates/skills/testgen/SKILL.md +24 -17
@@ -194,6 +194,84 @@ const DEFAULT_LINT_PATTERNS = [
194
194
  suggestion: "List all items explicitly or define boundaries",
195
195
  },
196
196
  ];
197
+ /**
198
+ * Documentation-noun head words that suggest a doc-only verification bar
199
+ * when they appear in the AC title.
200
+ */
201
+ const DOC_NOUNS = [
202
+ "note",
203
+ "comment",
204
+ "documentation",
205
+ "description",
206
+ "snippet",
207
+ "entry",
208
+ "mention",
209
+ ];
210
+ /**
211
+ * Runtime-imperative phrases that suggest an execution/evidence bar
212
+ * when they appear in the AC body.
213
+ */
214
+ const RUNTIME_IMPERATIVES = [
215
+ "execute",
216
+ "trigger",
217
+ "capture",
218
+ "verify by running",
219
+ "reproduce",
220
+ "confirm at runtime",
221
+ ];
222
+ /**
223
+ * Slash-command runtime pattern: "run /<command>" anywhere in the body.
224
+ */
225
+ const RUN_SLASH_RE = /\brun\s+\/[a-z][a-z0-9_-]*/i;
226
+ /**
227
+ * Detect title/body verification-method tension.
228
+ *
229
+ * Splits the AC description on the first separator (`.`, `\n`, `:`, or `—`).
230
+ * Treats the head as the "title" and the rest as the "body". Warns when the
231
+ * title contains a documentation-noun (suggesting a doc bar) AND the body
232
+ * contains a runtime-imperative (incl. inflections like `triggered`,
233
+ * `captured`) or `run /<command>` (suggesting a runtime bar).
234
+ *
235
+ * Warning-only — same convention as the regex-based DEFAULT_LINT_PATTERNS.
236
+ *
237
+ * @param ac - The acceptance criterion to check
238
+ * @returns A lint issue if tension is detected, otherwise null
239
+ */
240
+ function detectTitleBodyTension(ac) {
241
+ const description = ac.description;
242
+ const splitIdx = description.search(/[.\n:—]/);
243
+ if (splitIdx <= 0)
244
+ return null;
245
+ const title = description.slice(0, splitIdx);
246
+ const body = description.slice(splitIdx + 1);
247
+ if (title.length === 0 || body.length === 0)
248
+ return null;
249
+ const titleLower = title.toLowerCase();
250
+ const bodyLower = body.toLowerCase();
251
+ const docNoun = DOC_NOUNS.find((noun) => new RegExp(`\\b${noun}\\b`, "i").test(titleLower));
252
+ if (!docNoun)
253
+ return null;
254
+ const runtimeImperative = RUNTIME_IMPERATIVES.find((imp) => {
255
+ if (imp.includes(" ")) {
256
+ return new RegExp(`\\b${imp}\\b`, "i").test(bodyLower);
257
+ }
258
+ if (imp.endsWith("e")) {
259
+ const stem = imp.slice(0, -1);
260
+ return new RegExp(`\\b${stem}(?:e|es|ed|ing)\\b`, "i").test(bodyLower);
261
+ }
262
+ return new RegExp(`\\b${imp}(?:s|ed|ing)?\\b`, "i").test(bodyLower);
263
+ });
264
+ const slashRun = RUN_SLASH_RE.test(body);
265
+ if (!runtimeImperative && !slashRun)
266
+ return null;
267
+ const matched = runtimeImperative ?? "run /<command>";
268
+ return {
269
+ type: "title-body-tension",
270
+ matchedPattern: `title="${docNoun}" + body="${matched}"`,
271
+ problem: "Title/body tension: title implies a documentation bar, body specifies a runtime/execution bar",
272
+ suggestion: "Two verification bars detected. Either (a) tighten the title to match the runtime body (e.g., 'Smoke test execution — capture evidence'), or (b) split the runtime requirement into a separate AC.",
273
+ };
274
+ }
197
275
  /**
198
276
  * Lint a single acceptance criterion against all patterns
199
277
  *
@@ -215,6 +293,9 @@ export function lintAcceptanceCriterion(ac, patterns = DEFAULT_LINT_PATTERNS) {
215
293
  });
216
294
  }
217
295
  }
296
+ const tension = detectTitleBodyTension(ac);
297
+ if (tension)
298
+ issues.push(tension);
218
299
  return {
219
300
  ac,
220
301
  issues,
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Predicted file-collision detection between PROCEED issues.
3
+ *
4
+ * Step 5 of `/assess` already inspects active worktrees for in-flight overlap.
5
+ * This module adds a complementary heuristic: read the bodies of unstarted
6
+ * PROCEED issues and predict which pairs will modify the same file once
7
+ * they're both run in parallel worktrees.
8
+ *
9
+ * The detector scans markdown bodies for file-path mentions outside fenced
10
+ * code blocks and HTML comments, then computes pairwise intersections. A
11
+ * small exclusion list filters paths that nearly every PROCEED issue tends
12
+ * to touch (CHANGELOG.md, lockfiles).
13
+ *
14
+ * Tunables — including the exclusion list, path regex, and the
15
+ * slash-command-skill derivation rule — are documented in
16
+ * `references/predicted-collision-detection.md` so they can change without
17
+ * skill-prose edits.
18
+ */
19
+ /**
20
+ * Files that virtually every PROCEED issue mentions. Including them in
21
+ * pairwise intersection would flag every batch as colliding, training
22
+ * users to ignore the warning.
23
+ */
24
+ export declare const EXCLUDED_PATHS: ReadonlySet<string>;
25
+ /**
26
+ * Extract the set of file paths an issue body identifies as
27
+ * targets-of-modification.
28
+ *
29
+ * Strategy:
30
+ * 1. Strip fenced code blocks and HTML comments (AC-5 guard).
31
+ * 2. Pull every backtick-quoted path matching the source-tree regex,
32
+ * normalizing skill-mirror paths to their canonical bare form.
33
+ * 3. If the body mentions "3-dir sync", also pull bare
34
+ * `<name>/SKILL.md` references and `/<skill>` slash-command
35
+ * mentions (where `<skill>` is in KNOWN_SKILL_NAMES). Both already
36
+ * live in the canonical bare form.
37
+ * 4. Remove globally excluded paths.
38
+ *
39
+ * The canonical bare form (`qa/SKILL.md`, not `.claude/skills/qa/SKILL.md`)
40
+ * is what the dashboard's `Order:` annotations render, and it makes
41
+ * mirrored collisions deduplicate without a separate post-processing
42
+ * pass.
43
+ */
44
+ export declare function extractPathsFromIssueBody(body: string): Set<string>;
45
+ /**
46
+ * A predicted file collision: 2+ issues whose bodies both name the same
47
+ * file as a target-of-modification.
48
+ */
49
+ export interface CollisionResult {
50
+ /** Issue numbers involved, in ascending order. */
51
+ issues: number[];
52
+ /** Path of the shared file (POSIX-style). */
53
+ file: string;
54
+ }
55
+ /**
56
+ * Compute file-path overlaps across issue bodies.
57
+ *
58
+ * Each shared file emits one CollisionResult. When N issues all share a
59
+ * file, that's a single result with `issues.length === N` — the caller
60
+ * decides whether to chain-suggest based on count.
61
+ *
62
+ * Sort order: ascending file name, then ascending first-issue number.
63
+ */
64
+ export declare function detectFileCollisions(issuePaths: Map<number, Set<string>>): CollisionResult[];
65
+ /**
66
+ * Rendered collision annotations ready for the dashboard output.
67
+ *
68
+ * - `orderLines` — one `Order:` line per pair (or per group).
69
+ * - `warnings` — `⚠ #N Modifies ... (overlaps #M); land sequentially`,
70
+ * one per affected issue per collision.
71
+ * - `chainSuggestion` — emitted only when ≥3 issues collide on the same
72
+ * file (AC-4); suggest-only, never auto-applied. Annotated with the
73
+ * historical chain-mode success rate at length≥3 (1/6 = 17%, per #604
74
+ * forensics) so users can weigh chain mode against the parallel default.
75
+ */
76
+ export interface CollisionAnnotations {
77
+ orderLines: string[];
78
+ warnings: string[];
79
+ chainSuggestion?: string;
80
+ }
81
+ /**
82
+ * Format collision results as dashboard annotations.
83
+ *
84
+ * - 2-issue collision: `Order: A → B (path)` plus a warning per issue.
85
+ * - 3+-issue collision on the same file: `Order: A → B → C (path)`,
86
+ * warnings per issue, plus a single `Chain:` suggestion.
87
+ *
88
+ * Multiple shared files between the same pair render as multiple
89
+ * Order: lines (one per file). Callers decide whether to truncate.
90
+ */
91
+ export declare function formatCollisionAnnotations(results: CollisionResult[]): CollisionAnnotations;
@@ -0,0 +1,217 @@
1
+ /**
2
+ * Predicted file-collision detection between PROCEED issues.
3
+ *
4
+ * Step 5 of `/assess` already inspects active worktrees for in-flight overlap.
5
+ * This module adds a complementary heuristic: read the bodies of unstarted
6
+ * PROCEED issues and predict which pairs will modify the same file once
7
+ * they're both run in parallel worktrees.
8
+ *
9
+ * The detector scans markdown bodies for file-path mentions outside fenced
10
+ * code blocks and HTML comments, then computes pairwise intersections. A
11
+ * small exclusion list filters paths that nearly every PROCEED issue tends
12
+ * to touch (CHANGELOG.md, lockfiles).
13
+ *
14
+ * Tunables — including the exclusion list, path regex, and the
15
+ * slash-command-skill derivation rule — are documented in
16
+ * `references/predicted-collision-detection.md` so they can change without
17
+ * skill-prose edits.
18
+ */
19
+ /**
20
+ * Files that virtually every PROCEED issue mentions. Including them in
21
+ * pairwise intersection would flag every batch as colliding, training
22
+ * users to ignore the warning.
23
+ */
24
+ export const EXCLUDED_PATHS = new Set([
25
+ "CHANGELOG.md",
26
+ "package-lock.json",
27
+ "yarn.lock",
28
+ "pnpm-lock.yaml",
29
+ ]);
30
+ /**
31
+ * Slash-command names recognized as references to a skill's SKILL.md.
32
+ * Used by the slash-command-skill derivation rule when an issue body
33
+ * also signals 3-dir sync — the skill name maps deterministically to the
34
+ * three mirrored SKILL.md files.
35
+ */
36
+ const KNOWN_SKILL_NAMES = [
37
+ "assess",
38
+ "spec",
39
+ "exec",
40
+ "qa",
41
+ "test",
42
+ "testgen",
43
+ "verify",
44
+ "loop",
45
+ "merger",
46
+ "security-review",
47
+ "fullsolve",
48
+ "docs",
49
+ "release",
50
+ "clean",
51
+ "improve",
52
+ "reflect",
53
+ "setup",
54
+ ];
55
+ /**
56
+ * Regex matching backtick-quoted file paths under the project's tracked
57
+ * directories. The path component must start with a tracked directory
58
+ * root and end with a known source extension.
59
+ */
60
+ const PATH_REGEX = /`((?:\.claude|templates|skills|src|bin|docs)\/[A-Za-z0-9_./@-]+\.(?:md|tsx?|json|sh))`/g;
61
+ /**
62
+ * Bare-filename match for skill files referenced alongside "3-dir sync"
63
+ * language. Captures e.g. `qa/SKILL.md` so we can expand it to the three
64
+ * skill roots.
65
+ */
66
+ const SKILL_FILE_REGEX = /`((?:[a-z][a-z0-9_-]*\/)+SKILL\.md)`/g;
67
+ /**
68
+ * Slash-command mention regex (e.g. `/qa`, `/spec`). Captures the name
69
+ * for cross-reference against KNOWN_SKILL_NAMES. The non-word lookahead
70
+ * keeps `/qa-section` from matching as `/qa`.
71
+ */
72
+ const SLASH_COMMAND_REGEX = /(?<![\w-])\/([a-z][a-z-]*)(?![\w-])/g;
73
+ /**
74
+ * Phrase that signals the cited skill file is mirrored to all three
75
+ * skill-root directories.
76
+ */
77
+ const THREE_DIR_SYNC_PATTERN = /3[- ]dir(?:ectory)?\s+sync|across\s+all\s+three\s+skill\s+directories|across\s+(?:the\s+)?three\s+skill\s+directories/i;
78
+ /**
79
+ * Strip fenced code blocks and HTML comments from a markdown body so the
80
+ * path regex doesn't fire on quoted shell snippets or commented-out drafts.
81
+ *
82
+ * Inline backticks (single ` `) are preserved — the path regex requires a
83
+ * single-backtick wrapper, so this gives us the "paths quoted as code in
84
+ * prose count, paths inside a code block don't" behavior the AC-5 guard
85
+ * specifies.
86
+ */
87
+ function stripCodeBlocksAndComments(body) {
88
+ return body.replace(/```[\s\S]*?```/g, "").replace(/<!--[\s\S]*?-->/g, "");
89
+ }
90
+ /**
91
+ * Collapse a fully-qualified skill-mirror path to its canonical bare form.
92
+ *
93
+ * The repo maintains three byte-identical mirrors of every skill file
94
+ * under `.claude/skills/`, `templates/skills/`, and `skills/`. Treating
95
+ * those mirrors as separate paths in collision detection produces 3× the
96
+ * Order: lines and 6× the warnings for one logical conflict, since both
97
+ * sides of the 3-dir-sync expansion match each mirror.
98
+ *
99
+ * Normalizing to the bare subpath (e.g. `qa/SKILL.md`) makes mirrored
100
+ * collisions deduplicate naturally and matches the issue-body shorthand
101
+ * the dashboard's `Order:` annotation already uses.
102
+ */
103
+ function normalizeSkillMirrorPath(path) {
104
+ const m = path.match(/^(?:\.claude\/skills\/|templates\/skills\/|skills\/)(.+)$/);
105
+ return m ? m[1] : path;
106
+ }
107
+ /**
108
+ * Extract the set of file paths an issue body identifies as
109
+ * targets-of-modification.
110
+ *
111
+ * Strategy:
112
+ * 1. Strip fenced code blocks and HTML comments (AC-5 guard).
113
+ * 2. Pull every backtick-quoted path matching the source-tree regex,
114
+ * normalizing skill-mirror paths to their canonical bare form.
115
+ * 3. If the body mentions "3-dir sync", also pull bare
116
+ * `<name>/SKILL.md` references and `/<skill>` slash-command
117
+ * mentions (where `<skill>` is in KNOWN_SKILL_NAMES). Both already
118
+ * live in the canonical bare form.
119
+ * 4. Remove globally excluded paths.
120
+ *
121
+ * The canonical bare form (`qa/SKILL.md`, not `.claude/skills/qa/SKILL.md`)
122
+ * is what the dashboard's `Order:` annotations render, and it makes
123
+ * mirrored collisions deduplicate without a separate post-processing
124
+ * pass.
125
+ */
126
+ export function extractPathsFromIssueBody(body) {
127
+ const paths = new Set();
128
+ const cleaned = stripCodeBlocksAndComments(body);
129
+ for (const m of cleaned.matchAll(PATH_REGEX)) {
130
+ paths.add(normalizeSkillMirrorPath(m[1]));
131
+ }
132
+ const threeDir = THREE_DIR_SYNC_PATTERN.test(cleaned);
133
+ if (threeDir) {
134
+ for (const m of cleaned.matchAll(SKILL_FILE_REGEX)) {
135
+ paths.add(m[1]);
136
+ }
137
+ for (const m of cleaned.matchAll(SLASH_COMMAND_REGEX)) {
138
+ const name = m[1];
139
+ if (KNOWN_SKILL_NAMES.includes(name)) {
140
+ paths.add(`${name}/SKILL.md`);
141
+ }
142
+ }
143
+ }
144
+ for (const excluded of EXCLUDED_PATHS) {
145
+ paths.delete(excluded);
146
+ }
147
+ return paths;
148
+ }
149
+ /**
150
+ * Compute file-path overlaps across issue bodies.
151
+ *
152
+ * Each shared file emits one CollisionResult. When N issues all share a
153
+ * file, that's a single result with `issues.length === N` — the caller
154
+ * decides whether to chain-suggest based on count.
155
+ *
156
+ * Sort order: ascending file name, then ascending first-issue number.
157
+ */
158
+ export function detectFileCollisions(issuePaths) {
159
+ const fileToIssues = new Map();
160
+ for (const [issueNumber, paths] of issuePaths) {
161
+ for (const file of paths) {
162
+ let bucket = fileToIssues.get(file);
163
+ if (!bucket) {
164
+ bucket = new Set();
165
+ fileToIssues.set(file, bucket);
166
+ }
167
+ bucket.add(issueNumber);
168
+ }
169
+ }
170
+ const results = [];
171
+ for (const [file, issues] of fileToIssues) {
172
+ if (issues.size < 2)
173
+ continue;
174
+ results.push({
175
+ file,
176
+ issues: [...issues].sort((a, b) => a - b),
177
+ });
178
+ }
179
+ results.sort((a, b) => {
180
+ if (a.file !== b.file)
181
+ return a.file < b.file ? -1 : 1;
182
+ return a.issues[0] - b.issues[0];
183
+ });
184
+ return results;
185
+ }
186
+ /**
187
+ * Format collision results as dashboard annotations.
188
+ *
189
+ * - 2-issue collision: `Order: A → B (path)` plus a warning per issue.
190
+ * - 3+-issue collision on the same file: `Order: A → B → C (path)`,
191
+ * warnings per issue, plus a single `Chain:` suggestion.
192
+ *
193
+ * Multiple shared files between the same pair render as multiple
194
+ * Order: lines (one per file). Callers decide whether to truncate.
195
+ */
196
+ export function formatCollisionAnnotations(results) {
197
+ const orderLines = [];
198
+ const warnings = [];
199
+ let chainSuggestion;
200
+ for (const r of results) {
201
+ const arrow = r.issues.join(" → ");
202
+ orderLines.push(`Order: ${arrow} (${r.file})`);
203
+ for (const n of r.issues) {
204
+ const others = r.issues.filter((m) => m !== n);
205
+ const overlapStr = others.map((m) => `#${m}`).join(", ");
206
+ warnings.push(`⚠ #${n} Modifies ${r.file} (overlaps ${overlapStr}); land sequentially`);
207
+ }
208
+ if (r.issues.length >= 3 && !chainSuggestion) {
209
+ const ids = r.issues.join(" ");
210
+ chainSuggestion =
211
+ `Chain: npx sequant run ${ids} --chain --qa-gate -q ` +
212
+ `# alternative — ${r.issues.length} issues modify ${r.file} ` +
213
+ `(chain length≥3 historically 1/6 = 17%; see docs/reference/chain-mode-analysis-2026-05.md)`;
214
+ }
215
+ }
216
+ return { orderLines, warnings, chainSuggestion };
217
+ }
@@ -75,7 +75,12 @@ export interface AssessMarkers {
75
75
  */
76
76
  export type SolveMarkers = AssessMarkers;
77
77
  /**
78
- * Check if a comment is an assess command output (new format)
78
+ * Check if a comment is an assess command output (new format).
79
+ *
80
+ * Matches either prose markers (e.g., "## Assess Analysis") or the durable
81
+ * HTML action marker `<!-- assess:action=... -->` written by every /assess
82
+ * comment regardless of prose format. The HTML marker is the contract;
83
+ * prose can change.
79
84
  */
80
85
  export declare function isAssessComment(body: string): boolean;
81
86
  /**
@@ -87,6 +92,14 @@ export declare function isSolveComment(body: string): boolean;
87
92
  * Find the most recent assess/solve comment from a list of comments
88
93
  */
89
94
  export declare function findAssessComment(comments: IssueComment[]): IssueComment | null;
95
+ /**
96
+ * Find every assess/solve comment in chronological order (oldest first).
97
+ *
98
+ * Preserves the input order of `comments`, which is the order GitHub returns
99
+ * them (chronological). Callers that need the most recent comment should
100
+ * read the last element.
101
+ */
102
+ export declare function findAllAssessComments(comments: IssueComment[]): IssueComment[];
90
103
  /**
91
104
  * Find the most recent solve comment from a list of comments
92
105
  * @deprecated Use findAssessComment instead
@@ -135,3 +148,48 @@ export declare function assessCoversIssue(workflow: AssessWorkflowResult, issueN
135
148
  * @deprecated Use assessCoversIssue instead
136
149
  */
137
150
  export declare function solveCoversIssue(workflow: AssessWorkflowResult, issueNumber: number): boolean;
151
+ /**
152
+ * Build a one-line supersession header for a new assess comment.
153
+ *
154
+ * Format:
155
+ * - 0 priors → null (caller omits the header entirely)
156
+ * - 1 prior → `Supersedes prior assess from <date> (<action>)`
157
+ * - ≥2 priors → `Supersedes N prior assessments (most recent: <date>)`
158
+ *
159
+ * Priors must be passed in chronological order (oldest first), matching
160
+ * the output of findAllAssessComments.
161
+ */
162
+ export declare function buildSupersessionHeader(priors: IssueComment[]): string | null;
163
+ /**
164
+ * Result of churn detection: whether to fire the warning, the count
165
+ * of prior assessments, and the date of the first one.
166
+ */
167
+ export interface ChurnResult {
168
+ isChurn: boolean;
169
+ count: number;
170
+ firstDate?: string;
171
+ }
172
+ /**
173
+ * Detect re-assessment churn — repeated /assess invocations without
174
+ * an intervening exec phase. Fires only when ≥3 priors exist and no
175
+ * exec phase marker appears in any comment dated after the first prior.
176
+ *
177
+ * `allComments` should be the full comment list from the issue
178
+ * (including the prior assess comments themselves) so we can search
179
+ * for SEQUANT_PHASE exec markers in non-assess comments.
180
+ */
181
+ export declare function detectChurn(priors: IssueComment[], allComments: IssueComment[]): ChurnResult;
182
+ /**
183
+ * Decide whether to prompt the user before posting a new assess comment
184
+ * that conflicts with the most recent prior recommendation.
185
+ *
186
+ * Prompts only when:
187
+ * - A prior action exists, AND
188
+ * - The prior action is PROCEED or REWRITE (the "do work" actions
189
+ * where flipping to PARK/CLOSE/etc. needs explicit confirmation), AND
190
+ * - The new action differs from the prior.
191
+ *
192
+ * Skips the prompt for prior CLOSE/PARK/CLARIFY/MERGE — flipping
193
+ * away from those is rarely "wrong" and the prompt would just be noise.
194
+ */
195
+ export declare function shouldPromptOnConflict(priorAction: AssessAction | undefined, newAction: AssessAction | undefined): boolean;
@@ -70,6 +70,11 @@ const HTML_MARKER_PATTERN = /<!--\s*(?:assess|solve):(\w[\w-]*)=([\w,.-]*)\s*-->
70
70
  * Pattern to extract assess-specific HTML markers (takes precedence)
71
71
  */
72
72
  const ASSESS_HTML_MARKER_PATTERN = /<!--\s*assess:(\w[\w-]*)=([\w,.-]*)\s*-->/g;
73
+ /**
74
+ * Non-global pattern for `.test()` checks. Matches any assess/solve action
75
+ * marker — the durable contract written by /assess regardless of prose format.
76
+ */
77
+ const ASSESS_ACTION_MARKER_PATTERN = /<!--\s*(?:assess|solve):action=[\w-]+\s*-->/;
73
78
  /**
74
79
  * Pattern to extract issue numbers from header
75
80
  * Matches: "## Solve Workflow for Issues: 123, 456" or "#123"
@@ -88,10 +93,17 @@ const VALID_ACTIONS = [
88
93
  ];
89
94
  // ─── Detection Functions ────────────────────────────────────────────────────
90
95
  /**
91
- * Check if a comment is an assess command output (new format)
96
+ * Check if a comment is an assess command output (new format).
97
+ *
98
+ * Matches either prose markers (e.g., "## Assess Analysis") or the durable
99
+ * HTML action marker `<!-- assess:action=... -->` written by every /assess
100
+ * comment regardless of prose format. The HTML marker is the contract;
101
+ * prose can change.
92
102
  */
93
103
  export function isAssessComment(body) {
94
- return ALL_MARKERS.some((marker) => body.includes(marker));
104
+ if (ALL_MARKERS.some((marker) => body.includes(marker)))
105
+ return true;
106
+ return ASSESS_ACTION_MARKER_PATTERN.test(body);
95
107
  }
96
108
  /**
97
109
  * Check if a comment is a solve command output (legacy format)
@@ -111,6 +123,16 @@ export function findAssessComment(comments) {
111
123
  }
112
124
  return null;
113
125
  }
126
+ /**
127
+ * Find every assess/solve comment in chronological order (oldest first).
128
+ *
129
+ * Preserves the input order of `comments`, which is the order GitHub returns
130
+ * them (chronological). Callers that need the most recent comment should
131
+ * read the last element.
132
+ */
133
+ export function findAllAssessComments(comments) {
134
+ return comments.filter((c) => isAssessComment(c.body));
135
+ }
114
136
  /**
115
137
  * Find the most recent solve comment from a list of comments
116
138
  * @deprecated Use findAssessComment instead
@@ -342,3 +364,103 @@ export function assessCoversIssue(workflow, issueNumber) {
342
364
  export function solveCoversIssue(workflow, issueNumber) {
343
365
  return assessCoversIssue(workflow, issueNumber);
344
366
  }
367
+ // ─── Prior-Assessment Detection ─────────────────────────────────────────────
368
+ /**
369
+ * Pattern matching exec phase markers in any comment.
370
+ * Used by detectChurn to differentiate "no execution" churn from
371
+ * legitimate re-assessment after exec.
372
+ */
373
+ const EXEC_PHASE_MARKER_PATTERN = /<!--\s*SEQUANT_PHASE:\s*\{[^}]*"phase"\s*:\s*"exec"[^}]*\}\s*-->/;
374
+ /**
375
+ * Format a comment's createdAt timestamp as YYYY-MM-DD.
376
+ * Falls back to the raw value when missing or unparseable so output
377
+ * still says something useful.
378
+ */
379
+ function formatAssessDate(createdAt) {
380
+ if (!createdAt)
381
+ return "unknown date";
382
+ const parsed = new Date(createdAt);
383
+ if (isNaN(parsed.getTime()))
384
+ return createdAt;
385
+ return parsed.toISOString().slice(0, 10);
386
+ }
387
+ /**
388
+ * Build a one-line supersession header for a new assess comment.
389
+ *
390
+ * Format:
391
+ * - 0 priors → null (caller omits the header entirely)
392
+ * - 1 prior → `Supersedes prior assess from <date> (<action>)`
393
+ * - ≥2 priors → `Supersedes N prior assessments (most recent: <date>)`
394
+ *
395
+ * Priors must be passed in chronological order (oldest first), matching
396
+ * the output of findAllAssessComments.
397
+ */
398
+ export function buildSupersessionHeader(priors) {
399
+ if (priors.length === 0)
400
+ return null;
401
+ const mostRecent = priors[priors.length - 1];
402
+ const date = formatAssessDate(mostRecent.createdAt);
403
+ if (priors.length === 1) {
404
+ const workflow = parseAssessWorkflow(mostRecent.body);
405
+ const action = workflow.action ?? "unknown";
406
+ return `Supersedes prior assess from ${date} (${action})`;
407
+ }
408
+ return `Supersedes ${priors.length} prior assessments (most recent: ${date})`;
409
+ }
410
+ /**
411
+ * Detect re-assessment churn — repeated /assess invocations without
412
+ * an intervening exec phase. Fires only when ≥3 priors exist and no
413
+ * exec phase marker appears in any comment dated after the first prior.
414
+ *
415
+ * `allComments` should be the full comment list from the issue
416
+ * (including the prior assess comments themselves) so we can search
417
+ * for SEQUANT_PHASE exec markers in non-assess comments.
418
+ */
419
+ export function detectChurn(priors, allComments) {
420
+ const count = priors.length;
421
+ if (count < 3) {
422
+ return { isChurn: false, count };
423
+ }
424
+ const firstPrior = priors[0];
425
+ const firstDate = formatAssessDate(firstPrior.createdAt);
426
+ const firstTimestamp = firstPrior.createdAt
427
+ ? new Date(firstPrior.createdAt).getTime()
428
+ : NaN;
429
+ const hasExecAfterFirst = allComments.some((c) => {
430
+ if (!EXEC_PHASE_MARKER_PATTERN.test(c.body))
431
+ return false;
432
+ if (!Number.isFinite(firstTimestamp))
433
+ return true;
434
+ if (!c.createdAt)
435
+ return true;
436
+ const ts = new Date(c.createdAt).getTime();
437
+ if (isNaN(ts))
438
+ return true;
439
+ return ts >= firstTimestamp;
440
+ });
441
+ return {
442
+ isChurn: !hasExecAfterFirst,
443
+ count,
444
+ firstDate,
445
+ };
446
+ }
447
+ /**
448
+ * Decide whether to prompt the user before posting a new assess comment
449
+ * that conflicts with the most recent prior recommendation.
450
+ *
451
+ * Prompts only when:
452
+ * - A prior action exists, AND
453
+ * - The prior action is PROCEED or REWRITE (the "do work" actions
454
+ * where flipping to PARK/CLOSE/etc. needs explicit confirmation), AND
455
+ * - The new action differs from the prior.
456
+ *
457
+ * Skips the prompt for prior CLOSE/PARK/CLARIFY/MERGE — flipping
458
+ * away from those is rarely "wrong" and the prompt would just be noise.
459
+ */
460
+ export function shouldPromptOnConflict(priorAction, newAction) {
461
+ if (!priorAction || !newAction)
462
+ return false;
463
+ if (priorAction === newAction)
464
+ return false;
465
+ return priorAction === "PROCEED" || priorAction === "REWRITE";
466
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared formatting helpers used by run-renderer and friends.
3
+ *
4
+ * Lives in cli-ui/ so renderer and heartbeat can share without depending on
5
+ * the legacy phase-spinner module.
6
+ */
7
+ /**
8
+ * Format elapsed time in human-readable form.
9
+ *
10
+ * @example
11
+ * formatElapsedTime(5) // "5s"
12
+ * formatElapsedTime(75) // "1m 15s"
13
+ * formatElapsedTime(3725) // "1h 2m"
14
+ */
15
+ export declare function formatElapsedTime(seconds: number): string;
16
+ /**
17
+ * Format a wall-clock timestamp as `HH:MM:SS`. Used by the non-TTY renderer.
18
+ */
19
+ export declare function formatTimestamp(date: Date): string;
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Shared formatting helpers used by run-renderer and friends.
3
+ *
4
+ * Lives in cli-ui/ so renderer and heartbeat can share without depending on
5
+ * the legacy phase-spinner module.
6
+ */
7
+ /**
8
+ * Format elapsed time in human-readable form.
9
+ *
10
+ * @example
11
+ * formatElapsedTime(5) // "5s"
12
+ * formatElapsedTime(75) // "1m 15s"
13
+ * formatElapsedTime(3725) // "1h 2m"
14
+ */
15
+ export function formatElapsedTime(seconds) {
16
+ const s = Math.max(0, Math.floor(seconds));
17
+ if (s < 60)
18
+ return `${s}s`;
19
+ if (s < 3600) {
20
+ const m = Math.floor(s / 60);
21
+ const r = s % 60;
22
+ return r > 0 ? `${m}m ${r}s` : `${m}m`;
23
+ }
24
+ const h = Math.floor(s / 3600);
25
+ const r = Math.floor((s % 3600) / 60);
26
+ return r > 0 ? `${h}h ${r}m` : `${h}h`;
27
+ }
28
+ /**
29
+ * Format a wall-clock timestamp as `HH:MM:SS`. Used by the non-TTY renderer.
30
+ */
31
+ export function formatTimestamp(date) {
32
+ const pad = (n) => String(n).padStart(2, "0");
33
+ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
34
+ }