@crouton-kit/crouter 0.3.17 → 0.3.18
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/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/human/prompts.js +3 -9
- package/dist/commands/human/shared.d.ts +26 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/node.js +53 -4
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +34 -16
- package/dist/core/personas/loader.js +102 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +16 -14
- package/dist/core/runtime/placement.d.ts +10 -0
- package/dist/core/runtime/placement.js +37 -1
- package/dist/core/spawn.d.ts +20 -1
- package/dist/core/spawn.js +52 -5
- package/dist/pi-extensions/canvas-nav.js +77 -30
- package/package.json +1 -1
package/dist/core/spawn.js
CHANGED
|
@@ -13,15 +13,60 @@ export function isInTmux() {
|
|
|
13
13
|
export function shellQuote(s) {
|
|
14
14
|
return `'${s.replace(/'/g, "'\\''")}'`;
|
|
15
15
|
}
|
|
16
|
-
/** Count panes in
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
16
|
+
/** Count panes in a tmux window (0 outside tmux / on error). With `targetPane`,
|
|
17
|
+
* counts the window THAT pane lives in (the placement decision must reflect the
|
|
18
|
+
* window the new pane will actually open into, not the caller's backstage one);
|
|
19
|
+
* without it, the caller's current window. */
|
|
20
|
+
export function countPanesInWindow(targetPane) {
|
|
21
|
+
const args = targetPane !== undefined && targetPane !== ''
|
|
22
|
+
? ['list-panes', '-t', targetPane, '-F', '#{pane_id}']
|
|
23
|
+
: ['list-panes', '-F', '#{pane_id}'];
|
|
24
|
+
const result = spawnSync('tmux', args, { encoding: 'utf8' });
|
|
21
25
|
if (result.status !== 0)
|
|
22
26
|
return 0;
|
|
23
27
|
return result.stdout.split('\n').filter((line) => line.trim() !== '').length;
|
|
24
28
|
}
|
|
29
|
+
/** Back-compat alias: panes in the caller's current window. */
|
|
30
|
+
export function countPanesInCurrentWindow() {
|
|
31
|
+
return countPanesInWindow();
|
|
32
|
+
}
|
|
33
|
+
/** Does this tmux pane id still exist? `display-message` EXITS 0 with EMPTY
|
|
34
|
+
* output on an unresolvable pane, so test for non-empty stdout, not just `.ok`.
|
|
35
|
+
* False outside tmux / on error. */
|
|
36
|
+
export function paneAlive(pane) {
|
|
37
|
+
if (!isInTmux() || !/^%\d+$/.test(pane))
|
|
38
|
+
return false;
|
|
39
|
+
const r = spawnSync('tmux', ['display-message', '-p', '-t', pane, '#{pane_id}'], {
|
|
40
|
+
encoding: 'utf8',
|
|
41
|
+
});
|
|
42
|
+
return r.status === 0 && r.stdout.trim() !== '';
|
|
43
|
+
}
|
|
44
|
+
/** The active pane of the user's attached tmux client — where they are looking
|
|
45
|
+
* right now. `list-clients` first attached client, then its current pane. Used
|
|
46
|
+
* to surface a human prompt in the user's view when nothing in the asking
|
|
47
|
+
* node's graph is focused. null outside tmux / no client / on error. */
|
|
48
|
+
export function attachedClientPane() {
|
|
49
|
+
if (!isInTmux())
|
|
50
|
+
return null;
|
|
51
|
+
const clients = spawnSync('tmux', ['list-clients', '-F', '#{client_name}'], {
|
|
52
|
+
encoding: 'utf8',
|
|
53
|
+
});
|
|
54
|
+
if (clients.status !== 0)
|
|
55
|
+
return null;
|
|
56
|
+
const name = clients.stdout
|
|
57
|
+
.split('\n')
|
|
58
|
+
.map((l) => l.trim())
|
|
59
|
+
.find((l) => l !== '');
|
|
60
|
+
if (name === undefined)
|
|
61
|
+
return null;
|
|
62
|
+
const pane = spawnSync('tmux', ['display-message', '-p', '-c', name, '#{pane_id}'], {
|
|
63
|
+
encoding: 'utf8',
|
|
64
|
+
});
|
|
65
|
+
if (pane.status !== 0)
|
|
66
|
+
return null;
|
|
67
|
+
const id = pane.stdout.trim();
|
|
68
|
+
return id !== '' ? id : null;
|
|
69
|
+
}
|
|
25
70
|
/**
|
|
26
71
|
* Schedule a kill-pane on the *current* tmux pane after `delaySeconds`, detached
|
|
27
72
|
* so the caller can return normally before the pane dies. No-op outside tmux,
|
|
@@ -53,6 +98,8 @@ export function spawnAndDetach(opts) {
|
|
|
53
98
|
const splitArgs = [];
|
|
54
99
|
if (opts.placement === 'new-window') {
|
|
55
100
|
splitArgs.push('new-window');
|
|
101
|
+
if (opts.detached === true)
|
|
102
|
+
splitArgs.push('-d'); // don't switch the client to it
|
|
56
103
|
if (opts.targetPane !== undefined && opts.targetPane !== '') {
|
|
57
104
|
// -a = insert after target window; -t <pane> resolves to that pane's window.
|
|
58
105
|
splitArgs.push('-a', '-t', opts.targetPane);
|
|
@@ -18,9 +18,15 @@
|
|
|
18
18
|
// alt+c is a tmux display-menu (not a pi key), so prefix chords (m/e/1-9/custom)
|
|
19
19
|
// are tmux menu items that route through `crtr canvas chord`.
|
|
20
20
|
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
21
|
+
// Selection / liveness signals:
|
|
22
|
+
// CURSOR (selected) = reverse-video bar (ESC[7m), full width — an attribute,
|
|
23
|
+
// not a colour, so it reads under NO_COLOR. Plus a ▸ caret.
|
|
24
|
+
// ACTIVE (running) = a coloured background bar (status 'active'); the dot
|
|
25
|
+
// glyph still carries the signal where colour is stripped.
|
|
26
|
+
// SELF = bold name — a quiet "you are here" marker.
|
|
27
|
+
//
|
|
28
|
+
// Folding is auto by default: a branch stays COLLAPSED unless its subtree holds
|
|
29
|
+
// a running ('active') agent or self. h/l override that per-node and persist.
|
|
24
30
|
//
|
|
25
31
|
// ⚑K pending-asks is PER-NODE, inline on each waiting node's own row (manager,
|
|
26
32
|
// reports, tree rows; self shows a trailing ⚑ line in BASE). ⤳M direct-children
|
|
@@ -45,10 +51,13 @@ let liveTimer;
|
|
|
45
51
|
* exactly one key tap exists (mirrors the liveTimer double-guard). */
|
|
46
52
|
let liveUnsub;
|
|
47
53
|
let view = 'base';
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
|
|
54
|
+
/** Manual fold OVERRIDES in GRAPH, keyed by id (so a topology change can't
|
|
55
|
+
* corrupt them; stale ids are ignored). They override the default policy —
|
|
56
|
+
* collapsed UNLESS the subtree holds a running ('active') agent or self (see
|
|
57
|
+
* computeDefaultExpanded). `h` collapses → userCollapsed; `l` expands →
|
|
58
|
+
* userExpanded. Both survive renders AND BASE↔GRAPH toggles. */
|
|
59
|
+
const userCollapsed = new Set();
|
|
60
|
+
const userExpanded = new Set();
|
|
52
61
|
/** GRAPH cursor (a node id, not an index — indices shift as topology changes). */
|
|
53
62
|
let cursorId;
|
|
54
63
|
/** GRAPH viewport scroll offset (row index of the top visible row). */
|
|
@@ -69,16 +78,19 @@ const PI_MAX_WIDGET_LINES = 10;
|
|
|
69
78
|
const VIEWPORT_FALLBACK_ROWS = 30;
|
|
70
79
|
// ---------------------------------------------------------------------------
|
|
71
80
|
// ANSI styling. pi renders embedded escapes in widget lines and measures width
|
|
72
|
-
// ANSI-aware, so raw escapes are safe and need no pi-tui dependency.
|
|
73
|
-
// uses theme-agnostic
|
|
74
|
-
//
|
|
75
|
-
//
|
|
81
|
+
// ANSI-aware, so raw escapes are safe and need no pi-tui dependency. The cursor
|
|
82
|
+
// (selected row) uses a theme-agnostic ATTRIBUTE (reverse), so it reads under
|
|
83
|
+
// NO_COLOR; the active-row tint is a background COLOUR, but the differing dot
|
|
84
|
+
// glyph (●/○/✓/✗) keeps the running signal even where colour is stripped.
|
|
76
85
|
// ---------------------------------------------------------------------------
|
|
77
86
|
const ESC = '\x1b[';
|
|
78
87
|
const RESET = `${ESC}0m`;
|
|
79
88
|
const BOLD = `${ESC}1m`;
|
|
80
89
|
const DIM = `${ESC}2m`;
|
|
81
90
|
const REVERSE = `${ESC}7m`;
|
|
91
|
+
/** Dark-green background bar marking a running ('active') node — distinct from
|
|
92
|
+
* the cursor's reverse-video bar; chosen so default-fg text stays readable. */
|
|
93
|
+
const BG_ACTIVE = `${ESC}48;5;22m`;
|
|
82
94
|
const GREEN = `${ESC}32m`;
|
|
83
95
|
const RED = `${ESC}31m`;
|
|
84
96
|
const YELLOW = `${ESC}33m`;
|
|
@@ -137,15 +149,16 @@ function truncate(s, max = fillWidth()) {
|
|
|
137
149
|
function fillWidth() {
|
|
138
150
|
return Math.max(20, Math.min((process.stdout.columns ?? 80) - 2, 180));
|
|
139
151
|
}
|
|
140
|
-
/** Wrap `content` in a full-width
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* closes with a real
|
|
144
|
-
|
|
152
|
+
/** Wrap `content` in a full-width background bar opened by `open` (REVERSE for
|
|
153
|
+
* the cursor, BG_ACTIVE for a running node). `open` is re-asserted after every
|
|
154
|
+
* embedded RESET so a coloured cell (the status dot) can't punch a hole in the
|
|
155
|
+
* bar; the visible width is padded out to `width`; the line closes with a real
|
|
156
|
+
* RESET so the style never bleeds into the editor below. */
|
|
157
|
+
function fillBar(content, width, open) {
|
|
145
158
|
const clipped = truncate(content, width);
|
|
146
|
-
const reasserted = clipped.replace(/\x1b\[0m/g, `${RESET}${
|
|
159
|
+
const reasserted = clipped.replace(/\x1b\[0m/g, `${RESET}${open}`);
|
|
147
160
|
const pad = Math.max(0, width - visibleWidth(clipped));
|
|
148
|
-
return `${
|
|
161
|
+
return `${open}${reasserted}${' '.repeat(pad)}${RESET}`;
|
|
149
162
|
}
|
|
150
163
|
function readTelemetry(nodeId) {
|
|
151
164
|
try {
|
|
@@ -294,21 +307,48 @@ function statusRank(id) {
|
|
|
294
307
|
function sortedChildIds(id) {
|
|
295
308
|
return convoChildIds(id).sort((a, b) => statusRank(a) - statusRank(b));
|
|
296
309
|
}
|
|
310
|
+
/** Default fold policy: which nodes auto-EXPAND. A node expands only when one
|
|
311
|
+
* of its child subtrees holds a running ('active') agent or self — so the path
|
|
312
|
+
* to any live agent (and to you) is revealed while quiescent branches stay
|
|
313
|
+
* folded. One bottom-up O(N) pass from the ancestry root; cycle-guarded. */
|
|
314
|
+
function computeDefaultExpanded(root, self) {
|
|
315
|
+
const expand = new Set();
|
|
316
|
+
const seen = new Set();
|
|
317
|
+
// Returns whether subtree(id), INCLUDING id, holds an active node or self.
|
|
318
|
+
const visit = (id) => {
|
|
319
|
+
if (seen.has(id))
|
|
320
|
+
return id === self || getNode(id)?.status === 'active';
|
|
321
|
+
seen.add(id);
|
|
322
|
+
let childRevealing = false;
|
|
323
|
+
for (const c of convoChildIds(id))
|
|
324
|
+
if (visit(c))
|
|
325
|
+
childRevealing = true;
|
|
326
|
+
if (childRevealing)
|
|
327
|
+
expand.add(id); // a descendant is worth revealing → unfold id
|
|
328
|
+
return childRevealing || id === self || getNode(id)?.status === 'active';
|
|
329
|
+
};
|
|
330
|
+
visit(root);
|
|
331
|
+
return expand;
|
|
332
|
+
}
|
|
297
333
|
function buildGraphModel(self) {
|
|
298
334
|
const rootId = climbRoot(self);
|
|
335
|
+
const defaultExpanded = computeDefaultExpanded(rootId, self);
|
|
336
|
+
// userExpanded / userCollapsed override the auto policy; absent → policy decides.
|
|
337
|
+
const isFolded = (id) => userExpanded.has(id) ? false : userCollapsed.has(id) ? true : !defaultExpanded.has(id);
|
|
299
338
|
const rows = [];
|
|
300
339
|
const visited = new Set();
|
|
301
340
|
const walk = (id, prefix, isRoot, isLast) => {
|
|
302
341
|
if (visited.has(id)) {
|
|
303
342
|
const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
|
|
304
|
-
rows.push({ id, hasKids: false, isSelf: id === self, branch: prefix + connector, cycle: true });
|
|
343
|
+
rows.push({ id, hasKids: false, isSelf: id === self, branch: prefix + connector, cycle: true, collapsed: false });
|
|
305
344
|
return;
|
|
306
345
|
}
|
|
307
346
|
visited.add(id);
|
|
308
347
|
const kids = sortedChildIds(id);
|
|
348
|
+
const folded = isFolded(id);
|
|
309
349
|
const connector = isRoot ? '' : isLast ? '└─ ' : '├─ ';
|
|
310
|
-
rows.push({ id, hasKids: kids.length > 0, isSelf: id === self, branch: prefix + connector, cycle: false });
|
|
311
|
-
if (
|
|
350
|
+
rows.push({ id, hasKids: kids.length > 0, isSelf: id === self, branch: prefix + connector, cycle: false, collapsed: folded });
|
|
351
|
+
if (folded)
|
|
312
352
|
return; // folded — don't descend
|
|
313
353
|
const childPrefix = isRoot ? '' : prefix + (isLast ? ' ' : '│ ');
|
|
314
354
|
for (let i = 0; i < kids.length; i++)
|
|
@@ -317,22 +357,27 @@ function buildGraphModel(self) {
|
|
|
317
357
|
walk(rootId, '', true, true);
|
|
318
358
|
return rows;
|
|
319
359
|
}
|
|
320
|
-
/** Render one GRAPH row.
|
|
360
|
+
/** Render one GRAPH row. CURSOR (selected) → reverse-video bar; an ACTIVE
|
|
361
|
+
* (running) node → a coloured background bar; SELF → bold name. The cursor
|
|
362
|
+
* outranks the active tint when both land on the same row. */
|
|
321
363
|
function renderGraphRow(r, isCursor) {
|
|
364
|
+
const wrap = (line, active) => isCursor ? fillBar(line, fillWidth(), REVERSE)
|
|
365
|
+
: active ? fillBar(line, fillWidth(), BG_ACTIVE)
|
|
366
|
+
: truncate(line);
|
|
322
367
|
if (r.cycle) {
|
|
323
368
|
const line = `${r.branch} ${DIM}↺ ${shortId(r.id)}${RESET}`;
|
|
324
|
-
return
|
|
369
|
+
return wrap(line, false);
|
|
325
370
|
}
|
|
326
371
|
const node = getNode(r.id);
|
|
327
372
|
const dot = coloredGlyph(node);
|
|
328
373
|
const rawName = node !== null ? fullName(node) : shortId(r.id);
|
|
329
|
-
const name =
|
|
374
|
+
const name = r.isSelf ? `${BOLD}${rawName}${RESET}` : rawName;
|
|
330
375
|
const kind = `${DIM}${node?.kind ?? ''}${RESET}`;
|
|
331
376
|
const tokens = `${DIM}${tokensCell(r.id)}${RESET}`;
|
|
332
377
|
const caret = isCursor ? `${BOLD}▸${RESET} ` : ' ';
|
|
333
|
-
const fold = r.hasKids &&
|
|
378
|
+
const fold = r.hasKids && r.collapsed ? ` ${DIM}[+${childCount(r.id)}]${RESET}` : '';
|
|
334
379
|
const line = `${r.branch}${caret}${dot} ${name} ${kind} ${tokens}${childBadge(node)}${fold}${askBadge(r.id)}`;
|
|
335
|
-
return
|
|
380
|
+
return wrap(line, node?.status === 'active');
|
|
336
381
|
}
|
|
337
382
|
/** Total lines the GRAPH widget may emit. pi hard-caps extension widgets at
|
|
338
383
|
* MAX_WIDGET_LINES — anything past that pi truncates itself, eating our own
|
|
@@ -677,8 +722,9 @@ export function registerCanvasNav(pi) {
|
|
|
677
722
|
return { consume: true };
|
|
678
723
|
}
|
|
679
724
|
if (isPlain(data, 'h')) {
|
|
680
|
-
if (cur !== undefined && cur.hasKids && !
|
|
681
|
-
|
|
725
|
+
if (cur !== undefined && cur.hasKids && !cur.collapsed) {
|
|
726
|
+
userCollapsed.add(cur.id);
|
|
727
|
+
userExpanded.delete(cur.id);
|
|
682
728
|
}
|
|
683
729
|
else {
|
|
684
730
|
const p = managerOf(cursorId ?? nodeId);
|
|
@@ -689,8 +735,9 @@ export function registerCanvasNav(pi) {
|
|
|
689
735
|
return { consume: true };
|
|
690
736
|
}
|
|
691
737
|
if (isPlain(data, 'l')) {
|
|
692
|
-
if (cur !== undefined && collapsed
|
|
693
|
-
|
|
738
|
+
if (cur !== undefined && cur.collapsed && cur.hasKids) {
|
|
739
|
+
userExpanded.add(cur.id);
|
|
740
|
+
userCollapsed.delete(cur.id);
|
|
694
741
|
}
|
|
695
742
|
else if (cur !== undefined && cur.hasKids) {
|
|
696
743
|
const c = sortedChildIds(cur.id)[0];
|