sequant 2.2.0 → 2.4.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 +81 -5
- package/dist/bin/cli.js +140 -13
- package/dist/src/commands/abort.d.ts +36 -0
- package/dist/src/commands/abort.js +138 -0
- package/dist/src/commands/doctor.d.ts +25 -0
- package/dist/src/commands/doctor.js +36 -1
- 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 +46 -0
- package/dist/src/commands/prompt.js +273 -0
- package/dist/src/commands/run-display.d.ts +11 -2
- package/dist/src/commands/run-display.js +62 -28
- package/dist/src/commands/run-progress.d.ts +42 -0
- package/dist/src/commands/run-progress.js +93 -0
- package/dist/src/commands/run.js +90 -18
- package/dist/src/commands/stats.d.ts +2 -0
- package/dist/src/commands/stats.js +94 -8
- package/dist/src/commands/status.js +12 -0
- package/dist/src/commands/watch.d.ts +18 -0
- package/dist/src/commands/watch.js +211 -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 +220 -0
- package/dist/src/lib/cli-ui/run-renderer-types.js +7 -0
- package/dist/src/lib/cli-ui/run-renderer.d.ts +265 -0
- package/dist/src/lib/cli-ui/run-renderer.js +1390 -0
- package/dist/src/lib/cli-ui/scrollback-harness.d.ts +112 -0
- package/dist/src/lib/cli-ui/scrollback-harness.js +294 -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/merge-check/types.js +1 -1
- 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 +112 -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 +70 -0
- package/dist/src/lib/relay/types.js +85 -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/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 +274 -185
- package/dist/src/lib/workflow/config-resolver.js +4 -0
- package/dist/src/lib/workflow/drivers/agent-driver.d.ts +48 -1
- package/dist/src/lib/workflow/drivers/aider.d.ts +7 -1
- package/dist/src/lib/workflow/drivers/aider.js +9 -0
- package/dist/src/lib/workflow/drivers/claude-code.d.ts +17 -1
- package/dist/src/lib/workflow/drivers/claude-code.js +51 -2
- package/dist/src/lib/workflow/drivers/index.d.ts +1 -1
- package/dist/src/lib/workflow/event-emitter.d.ts +157 -0
- package/dist/src/lib/workflow/event-emitter.js +102 -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/notice.d.ts +32 -0
- package/dist/src/lib/workflow/notice.js +38 -0
- package/dist/src/lib/workflow/phase-executor.d.ts +58 -16
- package/dist/src/lib/workflow/phase-executor.js +244 -130
- package/dist/src/lib/workflow/phase-mapper.d.ts +27 -13
- package/dist/src/lib/workflow/phase-mapper.js +70 -51
- package/dist/src/lib/workflow/phase-registry.d.ts +127 -0
- package/dist/src/lib/workflow/phase-registry.js +233 -0
- 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-log-schema.d.ts +5 -55
- package/dist/src/lib/workflow/run-orchestrator.d.ts +70 -1
- package/dist/src/lib/workflow/run-orchestrator.js +464 -25
- 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 +31 -2
- package/dist/src/lib/workflow/state-manager.js +64 -1
- package/dist/src/lib/workflow/state-schema.d.ts +82 -35
- package/dist/src/lib/workflow/state-schema.js +63 -4
- package/dist/src/lib/workflow/types.d.ts +139 -16
- package/dist/src/lib/workflow/types.js +18 -13
- package/dist/src/lib/workflow/worktree-manager.d.ts +8 -1
- package/dist/src/lib/workflow/worktree-manager.js +15 -6
- 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 +14 -6
- 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 +92 -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 +122 -68
- 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 -8
- package/templates/skills/fullsolve/SKILL.md +79 -5
- package/templates/skills/loop/SKILL.md +28 -0
- package/templates/skills/merger/SKILL.md +621 -0
- package/templates/skills/qa/SKILL.md +727 -8
- package/templates/skills/setup/SKILL.md +12 -6
- package/templates/skills/spec/SKILL.md +52 -0
- package/templates/skills/spec/references/parallel-groups.md +7 -0
- package/templates/skills/spec/references/recommended-workflow.md +4 -2
- package/templates/skills/testgen/SKILL.md +24 -17
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual-terminal harness for renderer regression tests (#647).
|
|
3
|
+
*
|
|
4
|
+
* The test stub embedded in TTYRenderer (see {@link
|
|
5
|
+
* ./run-renderer.ts#TTYTestStub}) mocks `log-update` itself — it cannot reveal
|
|
6
|
+
* whether the real `log-update` actually erases prior frames once the terminal
|
|
7
|
+
* scrolls. That gap is what allowed #624's fix to ship green while the
|
|
8
|
+
* underlying duplicate-header bug remained.
|
|
9
|
+
*
|
|
10
|
+
* This harness models a real terminal:
|
|
11
|
+
* - bounded visible viewport (rows × cols)
|
|
12
|
+
* - unbounded scrollback that captures every line that scrolls off the top
|
|
13
|
+
* - the ANSI escape vocabulary that `log-update@7` + `ansi-escapes`
|
|
14
|
+
* actually emit (cursor up/down/forward/back, eraseLine variants,
|
|
15
|
+
* SGR colour stripping, private mode set/reset, save/restore)
|
|
16
|
+
*
|
|
17
|
+
* With it, a test can wire the production renderer through a real
|
|
18
|
+
* `createLogUpdate` instance, replay an event sequence, and assert on
|
|
19
|
+
* `(visible + scrollback)` to catch any duplicate-header rendering — the
|
|
20
|
+
* exact regression #647 was opened for.
|
|
21
|
+
*/
|
|
22
|
+
import { createLogUpdate } from "log-update";
|
|
23
|
+
export interface VirtualTerminalOptions {
|
|
24
|
+
rows: number;
|
|
25
|
+
cols: number;
|
|
26
|
+
/** Newline mode. POSIX shells default to ONLCR which translates `\n` to
|
|
27
|
+
* `\r\n`, so most apps see "move down + col 0". Default true. */
|
|
28
|
+
onlcr?: boolean;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Minimal vt100 model: visible grid + scrollback + cursor. Strips SGR colour
|
|
32
|
+
* codes (they're styling, not content) and ignores private-mode toggles
|
|
33
|
+
* (cursor hide/show). Implements the cursor and erase escapes that
|
|
34
|
+
* `log-update@7` actually emits.
|
|
35
|
+
*/
|
|
36
|
+
export declare class VirtualTerminal {
|
|
37
|
+
readonly rows: number;
|
|
38
|
+
readonly cols: number;
|
|
39
|
+
private readonly onlcr;
|
|
40
|
+
/** visible[row][col] = char (always single-codepoint slot). */
|
|
41
|
+
visible: string[][];
|
|
42
|
+
/** Scrollback grows oldest-first as rows shift off the top. */
|
|
43
|
+
scrollback: string[];
|
|
44
|
+
cursorRow: number;
|
|
45
|
+
cursorCol: number;
|
|
46
|
+
constructor(opts: VirtualTerminalOptions);
|
|
47
|
+
write(text: string): void;
|
|
48
|
+
/** Visible viewport as a list of trimmed-right rows. */
|
|
49
|
+
getVisibleLines(): string[];
|
|
50
|
+
/** Single multi-line string of (scrollback + visible). */
|
|
51
|
+
getAllText(): string;
|
|
52
|
+
/** Match count of the regex against (scrollback + visible). */
|
|
53
|
+
countOccurrences(pattern: RegExp): number;
|
|
54
|
+
private putChar;
|
|
55
|
+
private linefeed;
|
|
56
|
+
/** Returns the index AFTER the consumed escape sequence. */
|
|
57
|
+
private handleEscape;
|
|
58
|
+
private handleCSI;
|
|
59
|
+
private executeCSI;
|
|
60
|
+
private eraseLine;
|
|
61
|
+
private eraseFromCursorToEndOfLine;
|
|
62
|
+
private eraseFromStartOfLineToCursor;
|
|
63
|
+
private eraseFromCursorToEndOfScreen;
|
|
64
|
+
private eraseFromStartOfScreenToCursor;
|
|
65
|
+
private eraseScreen;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Bundle a VirtualTerminal with a real `log-update` instance writing into it
|
|
69
|
+
* and a matching `stdoutWrite` for renderer event-line writes. Both paths hit
|
|
70
|
+
* the same VT, mirroring real-terminal interleaving.
|
|
71
|
+
*
|
|
72
|
+
* Production runs frequently hit a width/height mismatch between what
|
|
73
|
+
* `log-update` reads from `process.stdout` and what the real terminal actually
|
|
74
|
+
* uses (e.g. `process.stdout.columns` is undefined under `npx` so log-update
|
|
75
|
+
* falls back to 80 while the terminal is 200 cols). Those mismatches cause
|
|
76
|
+
* `previousLineCount` to under- or over-count the rows log-update actually
|
|
77
|
+
* wrote, breaking `eraseLines` and leaving stale rows in scrollback. The
|
|
78
|
+
* `streamColumns` / `streamRows` overrides let tests reproduce this without
|
|
79
|
+
* needing a real PTY.
|
|
80
|
+
*/
|
|
81
|
+
export interface TerminalHarness {
|
|
82
|
+
vt: VirtualTerminal;
|
|
83
|
+
logUpdate: ReturnType<typeof createLogUpdate>;
|
|
84
|
+
stdoutWrite: (s: string) => void;
|
|
85
|
+
/**
|
|
86
|
+
* Out-of-band write that lands in the same VT as `logUpdate` and
|
|
87
|
+
* `stdoutWrite` — mirrors how a real pty merges stderr writes with stdout
|
|
88
|
+
* when both descriptors point at the same terminal. log-update has no
|
|
89
|
+
* knowledge of these writes, so they advance the cursor in ways
|
|
90
|
+
* `previousLineCount` cannot account for. Use this to reproduce the
|
|
91
|
+
* Mechanism #2-class bug (out-of-band writes break log-update's cursor
|
|
92
|
+
* model) that #647 AC-1 capture diagnosed.
|
|
93
|
+
*/
|
|
94
|
+
stderrWrite: (s: string) => void;
|
|
95
|
+
}
|
|
96
|
+
export interface HarnessOptions extends VirtualTerminalOptions {
|
|
97
|
+
/**
|
|
98
|
+
* Width log-update is told about via `stream.columns`. Defaults to
|
|
99
|
+
* `opts.cols` (matched terminal). Override to simulate a mismatch where
|
|
100
|
+
* log-update wraps at one width but the real terminal wraps at another.
|
|
101
|
+
*/
|
|
102
|
+
streamColumns?: number;
|
|
103
|
+
/**
|
|
104
|
+
* Height log-update is told about via `stream.rows`. Defaults to
|
|
105
|
+
* `opts.rows`. Override to simulate `process.stdout.rows = undefined`
|
|
106
|
+
* (the `npx` symptom): pass `undefined` explicitly via the harness's stream
|
|
107
|
+
* by setting this to a non-positive number — log-update then falls through
|
|
108
|
+
* to its internal `defaultHeight ?? 24`.
|
|
109
|
+
*/
|
|
110
|
+
streamRows?: number;
|
|
111
|
+
}
|
|
112
|
+
export declare function createTerminalHarness(opts: HarnessOptions): TerminalHarness;
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Virtual-terminal harness for renderer regression tests (#647).
|
|
3
|
+
*
|
|
4
|
+
* The test stub embedded in TTYRenderer (see {@link
|
|
5
|
+
* ./run-renderer.ts#TTYTestStub}) mocks `log-update` itself — it cannot reveal
|
|
6
|
+
* whether the real `log-update` actually erases prior frames once the terminal
|
|
7
|
+
* scrolls. That gap is what allowed #624's fix to ship green while the
|
|
8
|
+
* underlying duplicate-header bug remained.
|
|
9
|
+
*
|
|
10
|
+
* This harness models a real terminal:
|
|
11
|
+
* - bounded visible viewport (rows × cols)
|
|
12
|
+
* - unbounded scrollback that captures every line that scrolls off the top
|
|
13
|
+
* - the ANSI escape vocabulary that `log-update@7` + `ansi-escapes`
|
|
14
|
+
* actually emit (cursor up/down/forward/back, eraseLine variants,
|
|
15
|
+
* SGR colour stripping, private mode set/reset, save/restore)
|
|
16
|
+
*
|
|
17
|
+
* With it, a test can wire the production renderer through a real
|
|
18
|
+
* `createLogUpdate` instance, replay an event sequence, and assert on
|
|
19
|
+
* `(visible + scrollback)` to catch any duplicate-header rendering — the
|
|
20
|
+
* exact regression #647 was opened for.
|
|
21
|
+
*/
|
|
22
|
+
import { createLogUpdate } from "log-update";
|
|
23
|
+
const ESC = "";
|
|
24
|
+
/**
|
|
25
|
+
* Minimal vt100 model: visible grid + scrollback + cursor. Strips SGR colour
|
|
26
|
+
* codes (they're styling, not content) and ignores private-mode toggles
|
|
27
|
+
* (cursor hide/show). Implements the cursor and erase escapes that
|
|
28
|
+
* `log-update@7` actually emits.
|
|
29
|
+
*/
|
|
30
|
+
export class VirtualTerminal {
|
|
31
|
+
rows;
|
|
32
|
+
cols;
|
|
33
|
+
onlcr;
|
|
34
|
+
/** visible[row][col] = char (always single-codepoint slot). */
|
|
35
|
+
visible;
|
|
36
|
+
/** Scrollback grows oldest-first as rows shift off the top. */
|
|
37
|
+
scrollback = [];
|
|
38
|
+
cursorRow = 0;
|
|
39
|
+
cursorCol = 0;
|
|
40
|
+
constructor(opts) {
|
|
41
|
+
this.rows = opts.rows;
|
|
42
|
+
this.cols = opts.cols;
|
|
43
|
+
this.onlcr = opts.onlcr ?? true;
|
|
44
|
+
this.visible = Array.from({ length: this.rows }, () => Array(this.cols).fill(" "));
|
|
45
|
+
}
|
|
46
|
+
// ---------------------------------------------------------------- input
|
|
47
|
+
write(text) {
|
|
48
|
+
let i = 0;
|
|
49
|
+
while (i < text.length) {
|
|
50
|
+
const ch = text[i];
|
|
51
|
+
if (ch === ESC) {
|
|
52
|
+
i = this.handleEscape(text, i);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (ch === "\n") {
|
|
56
|
+
this.linefeed();
|
|
57
|
+
i++;
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
if (ch === "\r") {
|
|
61
|
+
this.cursorCol = 0;
|
|
62
|
+
i++;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (ch === "\b") {
|
|
66
|
+
if (this.cursorCol > 0)
|
|
67
|
+
this.cursorCol--;
|
|
68
|
+
i++;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
this.putChar(ch);
|
|
72
|
+
i++;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// ---------------------------------------------------------------- output
|
|
76
|
+
/** Visible viewport as a list of trimmed-right rows. */
|
|
77
|
+
getVisibleLines() {
|
|
78
|
+
return this.visible.map((row) => row.join("").replace(/\s+$/, ""));
|
|
79
|
+
}
|
|
80
|
+
/** Single multi-line string of (scrollback + visible). */
|
|
81
|
+
getAllText() {
|
|
82
|
+
const visibleText = this.getVisibleLines().join("\n");
|
|
83
|
+
if (this.scrollback.length === 0)
|
|
84
|
+
return visibleText;
|
|
85
|
+
return this.scrollback.join("\n") + "\n" + visibleText;
|
|
86
|
+
}
|
|
87
|
+
/** Match count of the regex against (scrollback + visible). */
|
|
88
|
+
countOccurrences(pattern) {
|
|
89
|
+
const text = this.getAllText();
|
|
90
|
+
const flags = pattern.flags.includes("g")
|
|
91
|
+
? pattern.flags
|
|
92
|
+
: pattern.flags + "g";
|
|
93
|
+
const globalPattern = new RegExp(pattern.source, flags);
|
|
94
|
+
const matches = text.match(globalPattern);
|
|
95
|
+
return matches?.length ?? 0;
|
|
96
|
+
}
|
|
97
|
+
// ------------------------------------------------------- internal: text
|
|
98
|
+
putChar(ch) {
|
|
99
|
+
if (this.cursorCol >= this.cols) {
|
|
100
|
+
// Auto-wrap into the next row. Most terminals do this; log-update wraps
|
|
101
|
+
// upstream so this rarely triggers in practice.
|
|
102
|
+
this.cursorCol = 0;
|
|
103
|
+
this.linefeed();
|
|
104
|
+
}
|
|
105
|
+
this.visible[this.cursorRow][this.cursorCol] = ch;
|
|
106
|
+
this.cursorCol++;
|
|
107
|
+
}
|
|
108
|
+
linefeed() {
|
|
109
|
+
if (this.cursorRow + 1 < this.rows) {
|
|
110
|
+
this.cursorRow++;
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
// Bottom of viewport: scroll the top row into scrollback.
|
|
114
|
+
const top = this.visible.shift();
|
|
115
|
+
this.scrollback.push(top.join("").replace(/\s+$/, ""));
|
|
116
|
+
this.visible.push(Array(this.cols).fill(" "));
|
|
117
|
+
// Cursor stays clamped at last visible row.
|
|
118
|
+
}
|
|
119
|
+
if (this.onlcr)
|
|
120
|
+
this.cursorCol = 0;
|
|
121
|
+
}
|
|
122
|
+
// --------------------------------------------------- internal: escapes
|
|
123
|
+
/** Returns the index AFTER the consumed escape sequence. */
|
|
124
|
+
handleEscape(text, start) {
|
|
125
|
+
// Bare ESC at end → consume.
|
|
126
|
+
if (start + 1 >= text.length)
|
|
127
|
+
return text.length;
|
|
128
|
+
const next = text[start + 1];
|
|
129
|
+
// CSI: ESC [ ... <final>
|
|
130
|
+
if (next === "[") {
|
|
131
|
+
return this.handleCSI(text, start + 2);
|
|
132
|
+
}
|
|
133
|
+
// OSC: ESC ] ... BEL or ESC \
|
|
134
|
+
if (next === "]") {
|
|
135
|
+
let i = start + 2;
|
|
136
|
+
while (i < text.length) {
|
|
137
|
+
if (text[i] === "")
|
|
138
|
+
return i + 1;
|
|
139
|
+
if (text[i] === ESC && text[i + 1] === "\\")
|
|
140
|
+
return i + 2;
|
|
141
|
+
i++;
|
|
142
|
+
}
|
|
143
|
+
return text.length;
|
|
144
|
+
}
|
|
145
|
+
// 2-byte non-CSI escapes: ESC 7 / ESC 8 (save/restore — cursor only,
|
|
146
|
+
// safe to ignore for our uses).
|
|
147
|
+
return start + 2;
|
|
148
|
+
}
|
|
149
|
+
handleCSI(text, start) {
|
|
150
|
+
let i = start;
|
|
151
|
+
let isPrivate = false;
|
|
152
|
+
if (text[i] === "?" || text[i] === ">" || text[i] === "<") {
|
|
153
|
+
isPrivate = true;
|
|
154
|
+
i++;
|
|
155
|
+
}
|
|
156
|
+
let params = "";
|
|
157
|
+
while (i < text.length && /[0-9;]/.test(text[i])) {
|
|
158
|
+
params += text[i];
|
|
159
|
+
i++;
|
|
160
|
+
}
|
|
161
|
+
if (i >= text.length)
|
|
162
|
+
return text.length;
|
|
163
|
+
const final = text[i];
|
|
164
|
+
i++;
|
|
165
|
+
this.executeCSI(params, final, isPrivate);
|
|
166
|
+
return i;
|
|
167
|
+
}
|
|
168
|
+
executeCSI(params, final, isPrivate) {
|
|
169
|
+
const parts = params.length === 0 ? [] : params.split(";").map((p) => parseInt(p, 10));
|
|
170
|
+
const n = (idx, def) => {
|
|
171
|
+
const v = parts[idx];
|
|
172
|
+
return v === undefined || isNaN(v) ? def : v;
|
|
173
|
+
};
|
|
174
|
+
// Private modes (e.g. ?25l/?25h cursor hide/show) — ignore.
|
|
175
|
+
if (isPrivate)
|
|
176
|
+
return;
|
|
177
|
+
switch (final) {
|
|
178
|
+
case "A": // cursor up
|
|
179
|
+
this.cursorRow = Math.max(0, this.cursorRow - n(0, 1));
|
|
180
|
+
return;
|
|
181
|
+
case "B": // cursor down (no scroll)
|
|
182
|
+
this.cursorRow = Math.min(this.rows - 1, this.cursorRow + n(0, 1));
|
|
183
|
+
return;
|
|
184
|
+
case "C": // cursor forward
|
|
185
|
+
this.cursorCol = Math.min(this.cols - 1, this.cursorCol + n(0, 1));
|
|
186
|
+
return;
|
|
187
|
+
case "D": // cursor back
|
|
188
|
+
this.cursorCol = Math.max(0, this.cursorCol - n(0, 1));
|
|
189
|
+
return;
|
|
190
|
+
case "E": // cursor next line
|
|
191
|
+
this.cursorRow = Math.min(this.rows - 1, this.cursorRow + n(0, 1));
|
|
192
|
+
this.cursorCol = 0;
|
|
193
|
+
return;
|
|
194
|
+
case "F": // cursor prev line
|
|
195
|
+
this.cursorRow = Math.max(0, this.cursorRow - n(0, 1));
|
|
196
|
+
this.cursorCol = 0;
|
|
197
|
+
return;
|
|
198
|
+
case "G": // cursor absolute column (1-based)
|
|
199
|
+
this.cursorCol = Math.min(this.cols - 1, Math.max(0, n(0, 1) - 1));
|
|
200
|
+
return;
|
|
201
|
+
case "H": // cursor position (1-based row;col)
|
|
202
|
+
case "f":
|
|
203
|
+
this.cursorRow = Math.min(this.rows - 1, Math.max(0, n(0, 1) - 1));
|
|
204
|
+
this.cursorCol = Math.min(this.cols - 1, Math.max(0, n(1, 1) - 1));
|
|
205
|
+
return;
|
|
206
|
+
case "J": {
|
|
207
|
+
// erase in display
|
|
208
|
+
const mode = n(0, 0);
|
|
209
|
+
if (mode === 0)
|
|
210
|
+
this.eraseFromCursorToEndOfScreen();
|
|
211
|
+
else if (mode === 1)
|
|
212
|
+
this.eraseFromStartOfScreenToCursor();
|
|
213
|
+
else if (mode === 2 || mode === 3)
|
|
214
|
+
this.eraseScreen();
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
case "K": {
|
|
218
|
+
// erase in line
|
|
219
|
+
const mode = n(0, 0);
|
|
220
|
+
if (mode === 0)
|
|
221
|
+
this.eraseFromCursorToEndOfLine();
|
|
222
|
+
else if (mode === 1)
|
|
223
|
+
this.eraseFromStartOfLineToCursor();
|
|
224
|
+
else if (mode === 2)
|
|
225
|
+
this.eraseLine();
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
case "S": // scroll up
|
|
229
|
+
case "T": // scroll down
|
|
230
|
+
case "m": // SGR colour — ignore (we don't model styling)
|
|
231
|
+
case "s": // save cursor
|
|
232
|
+
case "u": // restore cursor
|
|
233
|
+
case "n": // device status report — ignore
|
|
234
|
+
case "h": // set mode — ignore
|
|
235
|
+
case "l": // reset mode — ignore
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
eraseLine() {
|
|
240
|
+
for (let c = 0; c < this.cols; c++)
|
|
241
|
+
this.visible[this.cursorRow][c] = " ";
|
|
242
|
+
}
|
|
243
|
+
eraseFromCursorToEndOfLine() {
|
|
244
|
+
for (let c = this.cursorCol; c < this.cols; c++)
|
|
245
|
+
this.visible[this.cursorRow][c] = " ";
|
|
246
|
+
}
|
|
247
|
+
eraseFromStartOfLineToCursor() {
|
|
248
|
+
for (let c = 0; c <= this.cursorCol; c++)
|
|
249
|
+
this.visible[this.cursorRow][c] = " ";
|
|
250
|
+
}
|
|
251
|
+
eraseFromCursorToEndOfScreen() {
|
|
252
|
+
this.eraseFromCursorToEndOfLine();
|
|
253
|
+
for (let r = this.cursorRow + 1; r < this.rows; r++) {
|
|
254
|
+
for (let c = 0; c < this.cols; c++)
|
|
255
|
+
this.visible[r][c] = " ";
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
eraseFromStartOfScreenToCursor() {
|
|
259
|
+
for (let r = 0; r < this.cursorRow; r++) {
|
|
260
|
+
for (let c = 0; c < this.cols; c++)
|
|
261
|
+
this.visible[r][c] = " ";
|
|
262
|
+
}
|
|
263
|
+
this.eraseFromStartOfLineToCursor();
|
|
264
|
+
}
|
|
265
|
+
eraseScreen() {
|
|
266
|
+
for (let r = 0; r < this.rows; r++) {
|
|
267
|
+
for (let c = 0; c < this.cols; c++)
|
|
268
|
+
this.visible[r][c] = " ";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
export function createTerminalHarness(opts) {
|
|
273
|
+
const vt = new VirtualTerminal(opts);
|
|
274
|
+
const stream = {
|
|
275
|
+
write: (chunk) => {
|
|
276
|
+
vt.write(chunk);
|
|
277
|
+
return true;
|
|
278
|
+
},
|
|
279
|
+
columns: opts.streamColumns ?? opts.cols,
|
|
280
|
+
rows: opts.streamRows ?? opts.rows,
|
|
281
|
+
isTTY: true,
|
|
282
|
+
};
|
|
283
|
+
// log-update reads `stream.columns` / `stream.rows` defensively; the cast is
|
|
284
|
+
// safe because we exercise only those fields plus `write`.
|
|
285
|
+
const lu = createLogUpdate(stream, {
|
|
286
|
+
showCursor: true,
|
|
287
|
+
});
|
|
288
|
+
return {
|
|
289
|
+
vt,
|
|
290
|
+
logUpdate: lu,
|
|
291
|
+
stdoutWrite: (s) => vt.write(s),
|
|
292
|
+
stderrWrite: (s) => vt.write(s),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Behavior-Rule Detector (issue #552)
|
|
3
|
+
*
|
|
4
|
+
* Shared heuristic for `/spec` (proactive) and `/qa` (reactive) phases that
|
|
5
|
+
* detects when an AC describes a *behavior rule* (e.g. "default becomes X",
|
|
6
|
+
* "always include Y", "never skip Z") and, when triggered, surfaces all
|
|
7
|
+
* touchpoints in the codebase that likely implement the rule.
|
|
8
|
+
*
|
|
9
|
+
* Behavior rules are routinely duplicated across a skill prompt
|
|
10
|
+
* (LLM-interpreted) AND the runtime TypeScript that backs it. Without this
|
|
11
|
+
* detector, edits land at one site and the other goes stale — see issue #533
|
|
12
|
+
* (motivating miss; documented in `references/behavior-rule-detection.md`).
|
|
13
|
+
*
|
|
14
|
+
* Three exported functions:
|
|
15
|
+
* - `detectBehaviorRule` — cheap keyword check; gates the more expensive greps
|
|
16
|
+
* - `findTouchpoints` — used by `/spec` to enumerate likely implementations
|
|
17
|
+
* - `findSurvivingInverseSymbols` — used by `/qa` to flag OLD-rule survivors
|
|
18
|
+
* inside the diff blast radius
|
|
19
|
+
*
|
|
20
|
+
* The keyword set is the source of truth in this file (per the /spec Open
|
|
21
|
+
* Question on keyword location). The reference doc cites it.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* ```typescript
|
|
25
|
+
* import { detectBehaviorRule, findTouchpoints } from "./behavior-rule-detector.ts";
|
|
26
|
+
*
|
|
27
|
+
* const detection = detectBehaviorRule(ac);
|
|
28
|
+
* if (detection.triggered) {
|
|
29
|
+
* const hits = findTouchpoints(ac, process.cwd());
|
|
30
|
+
* for (const h of hits) console.log(`${h.path}:${h.line} ${h.snippet}`);
|
|
31
|
+
* }
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import type { AcceptanceCriterion } from "../workflow/state-schema.js";
|
|
35
|
+
/**
|
|
36
|
+
* Behavior keywords whose presence (≥2 distinct, OR matching the explicit
|
|
37
|
+
* pattern below) signals an AC describes a rule rather than a localized fix.
|
|
38
|
+
* Tunable here; cited from `references/behavior-rule-detection.md`.
|
|
39
|
+
*/
|
|
40
|
+
export declare const BEHAVIOR_KEYWORDS: readonly ["default", "always", "never", "rule", "behavior", "skip"];
|
|
41
|
+
export type BehaviorKeyword = (typeof BEHAVIOR_KEYWORDS)[number];
|
|
42
|
+
/** A single touchpoint hit (file location matching a behavior-rule symbol). */
|
|
43
|
+
export interface TouchpointHit {
|
|
44
|
+
path: string;
|
|
45
|
+
line: number;
|
|
46
|
+
snippet: string;
|
|
47
|
+
}
|
|
48
|
+
export interface BehaviorRuleDetection {
|
|
49
|
+
triggered: boolean;
|
|
50
|
+
keywords: BehaviorKeyword[];
|
|
51
|
+
matchedPattern?: string;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Detect whether an AC describes a behavior rule.
|
|
55
|
+
*
|
|
56
|
+
* Trigger conditions:
|
|
57
|
+
* 1. ≥2 distinct {@link BEHAVIOR_KEYWORDS} present in the AC description
|
|
58
|
+
* (case-insensitive, word-boundary match), OR
|
|
59
|
+
* 2. Description matches one of the {@link EXPLICIT_PATTERNS}
|
|
60
|
+
* (e.g. "always X unless Y").
|
|
61
|
+
*
|
|
62
|
+
* Returns `triggered: false` for empty or undefined descriptions, single
|
|
63
|
+
* keyword matches without an explicit pattern, and file-specific ACs
|
|
64
|
+
* ("Update line 42 of foo.ts").
|
|
65
|
+
*/
|
|
66
|
+
export declare function detectBehaviorRule(ac: AcceptanceCriterion): BehaviorRuleDetection;
|
|
67
|
+
/**
|
|
68
|
+
* Find touchpoints in the codebase that likely implement the behavior rule
|
|
69
|
+
* described by `ac`. Returns `[]` when {@link detectBehaviorRule} does not
|
|
70
|
+
* trigger (cheap short-circuit per the /spec performance budget).
|
|
71
|
+
*
|
|
72
|
+
* Heuristic:
|
|
73
|
+
* - Extract identifier-like symbols from the AC (backticked strings, file
|
|
74
|
+
* paths with extensions, ALL_CAPS / camelCase / kebab-case identifiers).
|
|
75
|
+
* - Walk {@link TOUCHPOINT_ROOTS}; for each line in matching files, mark a
|
|
76
|
+
* hit if the line contains any extracted symbol OR ≥2 distinct AC
|
|
77
|
+
* behavior keywords.
|
|
78
|
+
* - Hits are deduplicated by `path:line` and capped (per-file: 3, total: 200)
|
|
79
|
+
* to keep `/spec` output readable (callers can re-run with a tighter scope
|
|
80
|
+
* if needed).
|
|
81
|
+
*/
|
|
82
|
+
export declare function findTouchpoints(ac: AcceptanceCriterion, repoRoot: string): TouchpointHit[];
|
|
83
|
+
/**
|
|
84
|
+
* Find OLD-rule survivors inside the diff blast radius. Used by `/qa` to flag
|
|
85
|
+
* an AC `NOT_MET` when the inverse of the asserted rule still has live code.
|
|
86
|
+
*
|
|
87
|
+
* Differs from {@link findTouchpoints}:
|
|
88
|
+
* - Scope is `diffPaths` (caller is responsible for pre-expanding to 1-hop
|
|
89
|
+
* importers when desired — this avoids embedding a TS-only importer scanner
|
|
90
|
+
* here and keeps the function language-agnostic).
|
|
91
|
+
* - Search terms are *inverse* keywords derived from the AC's keywords (and
|
|
92
|
+
* inverse English phrasing as a fallback when no symbols match).
|
|
93
|
+
*/
|
|
94
|
+
export declare function findSurvivingInverseSymbols(ac: AcceptanceCriterion, repoRoot: string, diffPaths: string[]): TouchpointHit[];
|