sequant 2.1.2 → 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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +73 -0
- package/dist/bin/cli.js +95 -9
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- package/dist/src/commands/init.d.ts +1 -0
- package/dist/src/commands/init.js +118 -0
- package/dist/src/commands/locks.d.ts +67 -0
- package/dist/src/commands/locks.js +290 -0
- package/dist/src/commands/merge.js +11 -0
- package/dist/src/commands/prompt.d.ts +39 -0
- package/dist/src/commands/prompt.js +179 -0
- package/dist/src/commands/run-display.d.ts +26 -0
- package/dist/src/commands/run-display.js +150 -0
- package/dist/src/commands/run-progress.d.ts +32 -0
- package/dist/src/commands/run-progress.js +76 -0
- package/dist/src/commands/run.js +83 -73
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +27 -1
- package/dist/src/commands/watch.d.ts +16 -0
- package/dist/src/commands/watch.js +147 -0
- package/dist/src/lib/ac-linter.d.ts +1 -1
- package/dist/src/lib/ac-linter.js +81 -0
- package/dist/src/lib/assess-collision-detect.d.ts +91 -0
- package/dist/src/lib/assess-collision-detect.js +217 -0
- package/dist/src/lib/assess-comment-parser.d.ts +59 -1
- package/dist/src/lib/assess-comment-parser.js +124 -2
- package/dist/src/lib/cli-ui/format.d.ts +19 -0
- package/dist/src/lib/cli-ui/format.js +34 -0
- package/dist/src/lib/cli-ui/run-renderer-types.d.ts +181 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +239 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1173 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.d.ts +94 -0
- package/dist/src/lib/heuristics/behavior-rule-detector.js +467 -0
- package/dist/src/lib/locks/index.d.ts +7 -0
- package/dist/src/lib/locks/index.js +5 -0
- package/dist/src/lib/locks/lock-manager.d.ts +168 -0
- package/dist/src/lib/locks/lock-manager.js +433 -0
- package/dist/src/lib/locks/types.d.ts +59 -0
- package/dist/src/lib/locks/types.js +31 -0
- package/dist/src/lib/qa/markdown-only-ci.d.ts +46 -0
- package/dist/src/lib/qa/markdown-only-ci.js +74 -0
- package/dist/src/lib/relay/activation.d.ts +60 -0
- package/dist/src/lib/relay/activation.js +122 -0
- package/dist/src/lib/relay/archive.d.ts +34 -0
- package/dist/src/lib/relay/archive.js +106 -0
- package/dist/src/lib/relay/frame.d.ts +20 -0
- package/dist/src/lib/relay/frame.js +76 -0
- package/dist/src/lib/relay/index.d.ts +13 -0
- package/dist/src/lib/relay/index.js +13 -0
- package/dist/src/lib/relay/paths.d.ts +43 -0
- package/dist/src/lib/relay/paths.js +59 -0
- package/dist/src/lib/relay/pid.d.ts +34 -0
- package/dist/src/lib/relay/pid.js +72 -0
- package/dist/src/lib/relay/reader.d.ts +35 -0
- package/dist/src/lib/relay/reader.js +115 -0
- package/dist/src/lib/relay/types.d.ts +68 -0
- package/dist/src/lib/relay/types.js +76 -0
- package/dist/src/lib/relay/writer.d.ts +48 -0
- package/dist/src/lib/relay/writer.js +113 -0
- package/dist/src/lib/settings.d.ts +31 -1
- package/dist/src/lib/settings.js +18 -3
- package/dist/src/lib/skill-version.d.ts +19 -0
- package/dist/src/lib/skill-version.js +68 -0
- package/dist/src/lib/templates.d.ts +1 -0
- package/dist/src/lib/templates.js +1 -1
- package/dist/src/lib/version-check.d.ts +60 -5
- package/dist/src/lib/version-check.js +97 -9
- package/dist/src/lib/workflow/batch-executor.d.ts +20 -1
- package/dist/src/lib/workflow/batch-executor.js +249 -176
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/heartbeat.d.ts +71 -0
- package/dist/src/lib/workflow/heartbeat.js +194 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +88 -3
- package/dist/src/lib/workflow/phase-executor.js +276 -52
- package/dist/src/lib/workflow/phase-mapper.d.ts +3 -2
- package/dist/src/lib/workflow/phase-mapper.js +17 -20
- package/dist/src/lib/workflow/platforms/github.d.ts +1 -1
- package/dist/src/lib/workflow/platforms/github.js +20 -3
- package/dist/src/lib/workflow/pr-status.d.ts +18 -2
- package/dist/src/lib/workflow/pr-status.js +41 -9
- package/dist/src/lib/workflow/qa-stagnation.d.ts +117 -0
- package/dist/src/lib/workflow/qa-stagnation.js +179 -0
- package/dist/src/lib/workflow/run-orchestrator.d.ts +76 -0
- package/dist/src/lib/workflow/run-orchestrator.js +382 -29
- package/dist/src/lib/workflow/run-reflect.js +1 -1
- package/dist/src/lib/workflow/run-state.d.ts +71 -0
- package/dist/src/lib/workflow/run-state.js +14 -0
- package/dist/src/lib/workflow/state-cleanup.d.ts +13 -5
- package/dist/src/lib/workflow/state-cleanup.js +17 -5
- package/dist/src/lib/workflow/state-manager.d.ts +12 -1
- package/dist/src/lib/workflow/state-manager.js +37 -0
- package/dist/src/lib/workflow/state-schema.d.ts +62 -0
- package/dist/src/lib/workflow/state-schema.js +35 -1
- package/dist/src/lib/workflow/types.d.ts +74 -1
- package/dist/src/lib/workflow/worktree-manager.d.ts +12 -4
- package/dist/src/lib/workflow/worktree-manager.js +76 -17
- package/dist/src/mcp/tools/run.d.ts +44 -0
- package/dist/src/mcp/tools/run.js +104 -13
- package/dist/src/ui/tui/App.d.ts +14 -0
- package/dist/src/ui/tui/App.js +41 -0
- package/dist/src/ui/tui/ElapsedTimer.d.ts +10 -0
- package/dist/src/ui/tui/ElapsedTimer.js +31 -0
- package/dist/src/ui/tui/Header.d.ts +6 -0
- package/dist/src/ui/tui/Header.js +15 -0
- package/dist/src/ui/tui/IssueBox.d.ts +16 -0
- package/dist/src/ui/tui/IssueBox.js +68 -0
- package/dist/src/ui/tui/Spinner.d.ts +9 -0
- package/dist/src/ui/tui/Spinner.js +18 -0
- package/dist/src/ui/tui/index.d.ts +15 -0
- package/dist/src/ui/tui/index.js +29 -0
- package/dist/src/ui/tui/theme.d.ts +29 -0
- package/dist/src/ui/tui/theme.js +52 -0
- package/dist/src/ui/tui/truncate.d.ts +11 -0
- package/dist/src/ui/tui/truncate.js +31 -0
- package/package.json +10 -3
- package/templates/agents/sequant-explorer.md +1 -0
- package/templates/agents/sequant-qa-checker.md +2 -1
- package/templates/agents/sequant-testgen.md +1 -0
- package/templates/hooks/post-tool.sh +11 -0
- package/templates/hooks/pre-tool.sh +18 -9
- package/templates/hooks/relay-check.sh +107 -0
- package/templates/relay/frame.txt +11 -0
- package/templates/scripts/cleanup-worktree.sh +25 -3
- package/templates/scripts/new-feature.sh +6 -0
- package/templates/skills/_shared/references/behavior-rule-detection.md +205 -0
- package/templates/skills/_shared/references/subagent-types.md +21 -8
- package/templates/skills/assess/SKILL.md +261 -94
- package/templates/skills/assess/references/predicted-collision-detection.md +109 -0
- package/templates/skills/docs/SKILL.md +141 -22
- package/templates/skills/exec/SKILL.md +10 -49
- package/templates/skills/fullsolve/SKILL.md +80 -32
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +746 -8
- package/templates/skills/qa/scripts/quality-checks.sh +47 -1
- package/templates/skills/setup/SKILL.md +6 -0
- package/templates/skills/spec/SKILL.md +217 -964
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/quality-checklist.md +75 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/test/SKILL.md +0 -27
- package/templates/skills/testgen/SKILL.md +24 -44
|
@@ -10,6 +10,8 @@ import { StateManager } from "../lib/workflow/state-manager.js";
|
|
|
10
10
|
import { rebuildStateFromLogs, cleanupStaleEntries, } from "../lib/workflow/state-utils.js";
|
|
11
11
|
import { reconcileState, getNextActionHint, formatRelativeTime, } from "../lib/workflow/reconcile.js";
|
|
12
12
|
import { getSettingsWithWarnings } from "../lib/settings.js";
|
|
13
|
+
import { getSkillVersions } from "../lib/skill-version.js";
|
|
14
|
+
import { LockManager, formatLockedMessage } from "../lib/locks/index.js";
|
|
13
15
|
/**
|
|
14
16
|
* Run reconciliation and display warnings.
|
|
15
17
|
* Returns the reconcile result for use in display.
|
|
@@ -181,11 +183,16 @@ function displayIssueSummary(issues) {
|
|
|
181
183
|
const hintDisplay = hint
|
|
182
184
|
? chalk.gray(`→ ${hint.length > 30 ? hint.substring(0, 27) + "..." : hint}`)
|
|
183
185
|
: "";
|
|
186
|
+
// Relay column (#383). Shows ✓ when active, "-" otherwise.
|
|
187
|
+
const relayDisplay = issue.relay?.enabled
|
|
188
|
+
? chalk.green("on")
|
|
189
|
+
: chalk.gray("-");
|
|
184
190
|
rows.push([
|
|
185
191
|
`#${issue.number}`,
|
|
186
192
|
title,
|
|
187
193
|
colorStatus(issue.status, issue.resolvedAt),
|
|
188
194
|
issue.currentPhase || "-",
|
|
195
|
+
relayDisplay,
|
|
189
196
|
hintDisplay,
|
|
190
197
|
]);
|
|
191
198
|
}
|
|
@@ -198,6 +205,7 @@ function displayIssueSummary(issues) {
|
|
|
198
205
|
{ header: "Title", width: 32 },
|
|
199
206
|
{ header: "Status", width: 20 },
|
|
200
207
|
{ header: "Phase", width: 10 },
|
|
208
|
+
{ header: "Relay", width: 5 },
|
|
201
209
|
{ header: "Next", width: 34 },
|
|
202
210
|
],
|
|
203
211
|
}));
|
|
@@ -263,13 +271,26 @@ export async function statusCommand(options = {}) {
|
|
|
263
271
|
console.log(chalk.yellow(` ${w.message}`));
|
|
264
272
|
}
|
|
265
273
|
}
|
|
266
|
-
// Count skills
|
|
274
|
+
// Count skills and check versions
|
|
267
275
|
const skillsDir = ".claude/skills";
|
|
268
276
|
if (await fileExists(skillsDir)) {
|
|
269
277
|
try {
|
|
270
278
|
const skills = await readdir(skillsDir);
|
|
271
279
|
const skillCount = skills.filter((s) => !s.startsWith(".")).length;
|
|
272
280
|
console.log(chalk.gray(`Skills: ${skillCount}`));
|
|
281
|
+
// Show skill version info
|
|
282
|
+
const { getTemplatesDir } = await import("../lib/templates.js");
|
|
283
|
+
const { join } = await import("path");
|
|
284
|
+
const templateSkillsDir = join(getTemplatesDir(), "skills");
|
|
285
|
+
const skillVersions = await getSkillVersions(templateSkillsDir);
|
|
286
|
+
const outdated = skillVersions.filter((s) => s.updateAvailable);
|
|
287
|
+
if (outdated.length > 0) {
|
|
288
|
+
console.log(chalk.yellow(`\n Skill updates available (${outdated.length}):`));
|
|
289
|
+
for (const s of outdated) {
|
|
290
|
+
console.log(chalk.yellow(` ${s.name}: v${s.installedVersion} → v${s.templateVersion}`));
|
|
291
|
+
}
|
|
292
|
+
console.log(chalk.gray(" Run `sequant init --upgrade-skills` to update."));
|
|
293
|
+
}
|
|
273
294
|
}
|
|
274
295
|
catch {
|
|
275
296
|
// Ignore errors
|
|
@@ -342,6 +363,11 @@ async function displayIssueState(options) {
|
|
|
342
363
|
else if (issueState) {
|
|
343
364
|
console.log(chalk.bold(`\nIssue #${options.issue} State\n`));
|
|
344
365
|
console.log(formatIssueState(issueState));
|
|
366
|
+
const lockManager = new LockManager();
|
|
367
|
+
const holder = lockManager.check(options.issue);
|
|
368
|
+
if (holder) {
|
|
369
|
+
console.log(chalk.yellow(` ! ${formatLockedMessage(options.issue, holder)}`));
|
|
370
|
+
}
|
|
345
371
|
const hint = getNextActionHint(issueState);
|
|
346
372
|
if (hint) {
|
|
347
373
|
console.log(chalk.cyan(`\n Next: ${hint}`));
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant watch <issue>` — tail the relay outbox for replies from a running
|
|
3
|
+
* headless session (#383). Uses `fs.watch()` when available, falls back to
|
|
4
|
+
* polling on platforms where `fs.watch` is unreliable (NFS, some WSL setups).
|
|
5
|
+
*/
|
|
6
|
+
export interface WatchCommandOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
/** Poll interval (ms); used when fs.watch is unavailable. Default: 200. */
|
|
9
|
+
pollIntervalMs?: number;
|
|
10
|
+
/** Abort signal for clean shutdown (tests). */
|
|
11
|
+
signal?: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
export declare function watchCommand(argsAndOptions: {
|
|
14
|
+
args: string[];
|
|
15
|
+
options: WatchCommandOptions;
|
|
16
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `sequant watch <issue>` — tail the relay outbox for replies from a running
|
|
3
|
+
* headless session (#383). Uses `fs.watch()` when available, falls back to
|
|
4
|
+
* polling on platforms where `fs.watch` is unreliable (NFS, some WSL setups).
|
|
5
|
+
*/
|
|
6
|
+
import { existsSync, statSync, createReadStream, watch } from "fs";
|
|
7
|
+
import chalk from "chalk";
|
|
8
|
+
import { outboxPathFor } from "../lib/relay/paths.js";
|
|
9
|
+
import { StateManager } from "../lib/workflow/state-manager.js";
|
|
10
|
+
import { RelayResponseSchema } from "../lib/relay/types.js";
|
|
11
|
+
function formatTimestamp(iso) {
|
|
12
|
+
try {
|
|
13
|
+
const date = new Date(iso);
|
|
14
|
+
const hh = String(date.getHours()).padStart(2, "0");
|
|
15
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
16
|
+
const ss = String(date.getSeconds()).padStart(2, "0");
|
|
17
|
+
return `${hh}:${mm}:${ss}`;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return iso;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function formatLine(reply, json) {
|
|
24
|
+
if (json)
|
|
25
|
+
return JSON.stringify(reply);
|
|
26
|
+
return chalk.gray(`[${formatTimestamp(reply.timestamp)}] `) + reply.message;
|
|
27
|
+
}
|
|
28
|
+
function readNewLines(path, state) {
|
|
29
|
+
return new Promise((resolve, reject) => {
|
|
30
|
+
if (!existsSync(path)) {
|
|
31
|
+
resolve([]);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
const stat = statSync(path);
|
|
35
|
+
if (stat.size <= state.offset) {
|
|
36
|
+
// File was truncated/rotated — reset offset.
|
|
37
|
+
if (stat.size < state.offset)
|
|
38
|
+
state.offset = 0;
|
|
39
|
+
resolve([]);
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const stream = createReadStream(path, {
|
|
43
|
+
start: state.offset,
|
|
44
|
+
end: stat.size - 1,
|
|
45
|
+
encoding: "utf-8",
|
|
46
|
+
});
|
|
47
|
+
let buf = state.partial;
|
|
48
|
+
stream.on("data", (chunk) => {
|
|
49
|
+
buf += chunk;
|
|
50
|
+
});
|
|
51
|
+
stream.on("error", (err) => reject(err));
|
|
52
|
+
stream.on("end", () => {
|
|
53
|
+
state.offset = stat.size;
|
|
54
|
+
const lines = buf.split("\n");
|
|
55
|
+
state.partial = lines.pop() ?? "";
|
|
56
|
+
const replies = [];
|
|
57
|
+
for (const line of lines) {
|
|
58
|
+
if (line.trim() === "")
|
|
59
|
+
continue;
|
|
60
|
+
try {
|
|
61
|
+
const parsed = RelayResponseSchema.safeParse(JSON.parse(line));
|
|
62
|
+
if (parsed.success)
|
|
63
|
+
replies.push(parsed.data);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
/* skip malformed */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
resolve(replies);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
export async function watchCommand(argsAndOptions) {
|
|
74
|
+
const { args, options } = argsAndOptions;
|
|
75
|
+
const issueArg = args[0];
|
|
76
|
+
if (!issueArg) {
|
|
77
|
+
throw new Error("Usage: sequant watch <issue>");
|
|
78
|
+
}
|
|
79
|
+
const issueNumber = Number.parseInt(issueArg, 10);
|
|
80
|
+
if (!Number.isInteger(issueNumber) || issueNumber <= 0) {
|
|
81
|
+
throw new Error(`Invalid issue number: '${issueArg}'`);
|
|
82
|
+
}
|
|
83
|
+
const stateManager = new StateManager();
|
|
84
|
+
const issueState = await stateManager.getIssueState(issueNumber);
|
|
85
|
+
const outboxPath = outboxPathFor(issueNumber, {
|
|
86
|
+
worktreePath: issueState?.worktree,
|
|
87
|
+
});
|
|
88
|
+
const pollIntervalMs = options.pollIntervalMs ?? 200;
|
|
89
|
+
const tail = { offset: 0, partial: "" };
|
|
90
|
+
// Seed tail offset at current EOF so we only show NEW replies.
|
|
91
|
+
if (existsSync(outboxPath)) {
|
|
92
|
+
tail.offset = statSync(outboxPath).size;
|
|
93
|
+
}
|
|
94
|
+
if (!options.json) {
|
|
95
|
+
console.log(chalk.gray(`Watching #${issueNumber} outbox — Ctrl+C to stop`));
|
|
96
|
+
}
|
|
97
|
+
let stopped = false;
|
|
98
|
+
const stop = () => {
|
|
99
|
+
stopped = true;
|
|
100
|
+
};
|
|
101
|
+
options.signal?.addEventListener("abort", stop);
|
|
102
|
+
process.on("SIGINT", () => {
|
|
103
|
+
stop();
|
|
104
|
+
if (!options.json)
|
|
105
|
+
console.log(chalk.gray("\nStopped watching."));
|
|
106
|
+
process.exit(0);
|
|
107
|
+
});
|
|
108
|
+
let useWatcher = false;
|
|
109
|
+
let watcher = null;
|
|
110
|
+
try {
|
|
111
|
+
if (existsSync(outboxPath)) {
|
|
112
|
+
watcher = watch(outboxPath, { persistent: true }, () => {
|
|
113
|
+
// fs.watch fires on any modification; we still poll readNewLines.
|
|
114
|
+
});
|
|
115
|
+
useWatcher = true;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch {
|
|
119
|
+
useWatcher = false;
|
|
120
|
+
}
|
|
121
|
+
const emit = (replies) => {
|
|
122
|
+
for (const r of replies) {
|
|
123
|
+
console.log(formatLine(r, options.json === true));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
// Polling loop — also used as a heartbeat when fs.watch is active so we
|
|
127
|
+
// don't miss events on filesystems where watch is unreliable.
|
|
128
|
+
while (!stopped) {
|
|
129
|
+
try {
|
|
130
|
+
const replies = await readNewLines(outboxPath, tail);
|
|
131
|
+
emit(replies);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
/* transient — try again next tick */
|
|
135
|
+
}
|
|
136
|
+
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
137
|
+
}
|
|
138
|
+
if (watcher) {
|
|
139
|
+
try {
|
|
140
|
+
watcher.close();
|
|
141
|
+
}
|
|
142
|
+
catch {
|
|
143
|
+
/* swallow */
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
void useWatcher; // currently unused beyond best-effort init
|
|
147
|
+
}
|
|
@@ -19,7 +19,7 @@ import type { AcceptanceCriterion } from "./workflow/state-schema.js";
|
|
|
19
19
|
/**
|
|
20
20
|
* Types of issues that can be flagged in AC
|
|
21
21
|
*/
|
|
22
|
-
export type ACLintIssueType = "vague" | "unmeasurable" | "incomplete" | "open_ended";
|
|
22
|
+
export type ACLintIssueType = "vague" | "unmeasurable" | "incomplete" | "open_ended" | "title-body-tension";
|
|
23
23
|
/**
|
|
24
24
|
* A lint issue found in an acceptance criterion
|
|
25
25
|
*/
|
|
@@ -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
|
+
}
|