@crouton-kit/crouter 0.3.16 → 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/orchestration-kernel.md +6 -6
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +5 -1
- 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} +5 -1
- 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/canvas-browse.d.ts +2 -0
- package/dist/commands/canvas-browse.js +45 -0
- package/dist/commands/canvas-prune.js +11 -2
- package/dist/commands/canvas.js +3 -2
- 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 +66 -4
- package/dist/commands/skill/author.js +2 -2
- package/dist/core/__tests__/cascade-close.test.js +199 -0
- package/dist/core/__tests__/daemon-boot.test.js +7 -0
- package/dist/core/__tests__/daemon-liveness.test.js +59 -4
- package/dist/core/__tests__/dead-pane-regression.test.js +151 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.d.ts +2 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +301 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +273 -0
- package/dist/core/__tests__/grace-clock.test.d.ts +1 -0
- package/dist/core/__tests__/grace-clock.test.js +115 -0
- package/dist/core/__tests__/helpers/harness.d.ts +78 -0
- package/dist/core/__tests__/helpers/harness.js +406 -0
- 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__/lifecycle.test.js +6 -13
- package/dist/core/__tests__/live-mutation.test.d.ts +1 -0
- package/dist/core/__tests__/live-mutation.test.js +341 -0
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +53 -15
- package/dist/core/__tests__/relaunch.test.js +12 -12
- package/dist/core/__tests__/reset.test.js +11 -6
- package/dist/core/__tests__/spike-harness.test.d.ts +1 -0
- package/dist/core/__tests__/spike-harness.test.js +241 -0
- package/dist/core/__tests__/subscription-delivery.test.d.ts +1 -0
- package/dist/core/__tests__/subscription-delivery.test.js +233 -0
- package/dist/core/canvas/browse/__tests__/model.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/model.test.js +142 -0
- package/dist/core/canvas/browse/__tests__/render.test.d.ts +1 -0
- package/dist/core/canvas/browse/__tests__/render.test.js +102 -0
- package/dist/core/canvas/browse/app.d.ts +4 -0
- package/dist/core/canvas/browse/app.js +349 -0
- package/dist/core/canvas/browse/model.d.ts +97 -0
- package/dist/core/canvas/browse/model.js +258 -0
- package/dist/core/canvas/browse/render.d.ts +41 -0
- package/dist/core/canvas/browse/render.js +387 -0
- package/dist/core/canvas/browse/terminal.d.ts +23 -0
- package/dist/core/canvas/browse/terminal.js +100 -0
- package/dist/core/canvas/canvas.d.ts +9 -2
- package/dist/core/canvas/canvas.js +41 -3
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/render.d.ts +10 -0
- package/dist/core/canvas/render.js +25 -1
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/feed/inbox.d.ts +0 -3
- package/dist/core/feed/inbox.js +1 -5
- 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/busy.d.ts +8 -0
- package/dist/core/runtime/busy.js +46 -0
- package/dist/core/runtime/lifecycle.d.ts +1 -1
- package/dist/core/runtime/lifecycle.js +12 -4
- package/dist/core/runtime/naming.d.ts +3 -3
- package/dist/core/runtime/naming.js +6 -6
- package/dist/core/runtime/placement.d.ts +32 -5
- package/dist/core/runtime/placement.js +81 -14
- package/dist/core/runtime/reset.d.ts +11 -8
- package/dist/core/runtime/reset.js +23 -18
- package/dist/core/spawn.d.ts +20 -1
- package/dist/core/spawn.js +52 -5
- package/dist/daemon/crtrd.js +43 -21
- package/dist/pi-extensions/canvas-nav.js +106 -55
- package/dist/pi-extensions/canvas-resume.d.ts +0 -1
- package/dist/pi-extensions/canvas-resume.js +35 -126
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +16 -0
- package/dist/prompts/skill.js +6 -1
- package/package.json +1 -1
- package/dist/commands/__tests__/skill.test.js +0 -290
- package/dist/core/__tests__/pkg.test.js +0 -218
- package/dist/core/__tests__/sys.test.js +0 -208
- /package/dist/{commands/__tests__/skill.test.d.ts → core/__tests__/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{pkg.test.d.ts → dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{sys.test.d.ts → flagship-lifecycle.test.d.ts} +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface Key {
|
|
2
|
+
upArrow: boolean;
|
|
3
|
+
downArrow: boolean;
|
|
4
|
+
leftArrow: boolean;
|
|
5
|
+
rightArrow: boolean;
|
|
6
|
+
return: boolean;
|
|
7
|
+
escape: boolean;
|
|
8
|
+
ctrl: boolean;
|
|
9
|
+
meta: boolean;
|
|
10
|
+
tab: boolean;
|
|
11
|
+
shiftTab: boolean;
|
|
12
|
+
backspace: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseKeypress(data: Buffer): {
|
|
15
|
+
input: string;
|
|
16
|
+
key: Key;
|
|
17
|
+
};
|
|
18
|
+
export declare function setupTerminal(): void;
|
|
19
|
+
export declare function restoreTerminal(): void;
|
|
20
|
+
export declare function getTerminalSize(): {
|
|
21
|
+
cols: number;
|
|
22
|
+
rows: number;
|
|
23
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// terminal.ts — raw-mode helpers for the `crtr canvas browse` TUI.
|
|
2
|
+
//
|
|
3
|
+
// Hand-rolled (no deps), mirroring humanloop's src/tui/terminal.ts. Extends its
|
|
4
|
+
// key parsing beyond up/down/return/escape/tab/backspace/ctrl + printable input:
|
|
5
|
+
// adds leftArrow/rightArrow (cursor keys) and shiftTab (`\x1b[Z`) so the browser
|
|
6
|
+
// can drive tree expand/collapse and tab cycling.
|
|
7
|
+
function emptyKey() {
|
|
8
|
+
return {
|
|
9
|
+
upArrow: false,
|
|
10
|
+
downArrow: false,
|
|
11
|
+
leftArrow: false,
|
|
12
|
+
rightArrow: false,
|
|
13
|
+
return: false,
|
|
14
|
+
escape: false,
|
|
15
|
+
ctrl: false,
|
|
16
|
+
meta: false,
|
|
17
|
+
tab: false,
|
|
18
|
+
shiftTab: false,
|
|
19
|
+
backspace: false,
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function parseKeypress(data) {
|
|
23
|
+
const str = data.toString('utf8');
|
|
24
|
+
const key = emptyKey();
|
|
25
|
+
if (str === '\x1b[A') {
|
|
26
|
+
key.upArrow = true;
|
|
27
|
+
return { input: '', key };
|
|
28
|
+
}
|
|
29
|
+
if (str === '\x1b[B') {
|
|
30
|
+
key.downArrow = true;
|
|
31
|
+
return { input: '', key };
|
|
32
|
+
}
|
|
33
|
+
if (str === '\x1b[C') {
|
|
34
|
+
key.rightArrow = true;
|
|
35
|
+
return { input: '', key };
|
|
36
|
+
}
|
|
37
|
+
if (str === '\x1b[D') {
|
|
38
|
+
key.leftArrow = true;
|
|
39
|
+
return { input: '', key };
|
|
40
|
+
}
|
|
41
|
+
if (str === '\x1b[Z') {
|
|
42
|
+
key.shiftTab = true;
|
|
43
|
+
return { input: '', key };
|
|
44
|
+
}
|
|
45
|
+
if (str === '\r' || str === '\n') {
|
|
46
|
+
key.return = true;
|
|
47
|
+
return { input: '', key };
|
|
48
|
+
}
|
|
49
|
+
// Alt+Backspace: terminals send ESC followed by DEL/BS. Must precede the
|
|
50
|
+
// bare-ESC check so the two-byte sequence isn't swallowed as plain escape.
|
|
51
|
+
if (str === '\x1b\x7f' || str === '\x1b\b') {
|
|
52
|
+
key.meta = true;
|
|
53
|
+
key.backspace = true;
|
|
54
|
+
return { input: '', key };
|
|
55
|
+
}
|
|
56
|
+
if (str === '\x1b') {
|
|
57
|
+
key.escape = true;
|
|
58
|
+
return { input: '', key };
|
|
59
|
+
}
|
|
60
|
+
if (str === '\t') {
|
|
61
|
+
key.tab = true;
|
|
62
|
+
return { input: '', key };
|
|
63
|
+
}
|
|
64
|
+
if (str === '\x7f' || str === '\b') {
|
|
65
|
+
key.backspace = true;
|
|
66
|
+
return { input: '', key };
|
|
67
|
+
}
|
|
68
|
+
if (str.length === 1 && str.charCodeAt(0) < 32) {
|
|
69
|
+
key.ctrl = true;
|
|
70
|
+
const ch = String.fromCharCode(str.charCodeAt(0) + 64).toLowerCase();
|
|
71
|
+
return { input: ch, key };
|
|
72
|
+
}
|
|
73
|
+
// Multi-byte chunks (paste, multi-byte UTF-8, unknown escape sequences) are
|
|
74
|
+
// returned as-is in `input`; the input-mode handler sanitises them before
|
|
75
|
+
// appending to its buffer. Top-level handlers ignore strings of length > 1.
|
|
76
|
+
return { input: str, key };
|
|
77
|
+
}
|
|
78
|
+
export function setupTerminal() {
|
|
79
|
+
if (!process.stdin.isTTY) {
|
|
80
|
+
throw new Error('crtr canvas browse requires an interactive terminal (TTY)');
|
|
81
|
+
}
|
|
82
|
+
process.stdin.setRawMode(true);
|
|
83
|
+
process.stdin.resume();
|
|
84
|
+
process.stdin.setEncoding('utf8');
|
|
85
|
+
process.stdout.write('\x1b[?25l'); // hide cursor
|
|
86
|
+
process.stdout.write('\x1b[?1049h'); // alt screen
|
|
87
|
+
process.stdout.write('\x1b[2J\x1b[H'); // clear
|
|
88
|
+
}
|
|
89
|
+
export function restoreTerminal() {
|
|
90
|
+
process.stdout.write('\x1b[?25h'); // show cursor
|
|
91
|
+
process.stdout.write('\x1b[?1049l'); // restore screen
|
|
92
|
+
process.stdin.setRawMode(false);
|
|
93
|
+
process.stdin.pause();
|
|
94
|
+
}
|
|
95
|
+
export function getTerminalSize() {
|
|
96
|
+
return {
|
|
97
|
+
cols: process.stdout.columns || 80,
|
|
98
|
+
rows: process.stdout.rows || 24,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
@@ -86,8 +86,14 @@ export interface PruneResult {
|
|
|
86
86
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
87
87
|
* node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
|
|
88
88
|
* each pruned node's edges automatically; the on-disk `nodes/<id>/` dir is
|
|
89
|
-
* removed too.
|
|
90
|
-
*
|
|
89
|
+
* removed too.
|
|
90
|
+
*
|
|
91
|
+
* With `includeStale`, ALSO prunes nominally-live (active | idle) nodes past the
|
|
92
|
+
* TTL whose process is provably gone — `pi_pid` is NULL or no longer alive. This
|
|
93
|
+
* reaps stale roots (a bare `crtr` whose pi died without the row transitioning),
|
|
94
|
+
* which the daemon's supervision never reconciled. A genuinely-running node keeps
|
|
95
|
+
* a live `pi_pid`, so it is protected, as is the CALLER ($CRTR_NODE_ID). Without
|
|
96
|
+
* the flag, active | idle are NEVER touched (the daemon's domain).
|
|
91
97
|
*
|
|
92
98
|
* The row deletes run in ONE transaction (so the sweep is all-or-nothing); the
|
|
93
99
|
* dir removals follow after COMMIT — the fs isn't transactional, and by then the
|
|
@@ -96,4 +102,5 @@ export interface PruneResult {
|
|
|
96
102
|
export declare function pruneNodes(opts: {
|
|
97
103
|
ttlDays: number;
|
|
98
104
|
dryRun?: boolean;
|
|
105
|
+
includeStale?: boolean;
|
|
99
106
|
}): PruneResult;
|
|
@@ -352,12 +352,29 @@ export function rebuildIndex() {
|
|
|
352
352
|
recordSpawn(meta.node_id, prov);
|
|
353
353
|
}
|
|
354
354
|
}
|
|
355
|
+
/** Is `pid` a live process? `kill(pid, 0)` sends no signal — it only probes
|
|
356
|
+
* existence/permission. ESRCH ⇒ gone; EPERM ⇒ alive but not ours (still alive). */
|
|
357
|
+
function pidAlive(pid) {
|
|
358
|
+
try {
|
|
359
|
+
process.kill(pid, 0);
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
catch (e) {
|
|
363
|
+
return e.code === 'EPERM';
|
|
364
|
+
}
|
|
365
|
+
}
|
|
355
366
|
/** Retention sweep: remove TERMINAL nodes (status dead | done | canceled) whose
|
|
356
367
|
* `created` is older than `ttlDays`, bounding the otherwise-unbounded growth of
|
|
357
368
|
* node rows + dirs. The edges→nodes FK (`ON DELETE CASCADE`, migration v4) GCs
|
|
358
369
|
* each pruned node's edges automatically; the on-disk `nodes/<id>/` dir is
|
|
359
|
-
* removed too.
|
|
360
|
-
*
|
|
370
|
+
* removed too.
|
|
371
|
+
*
|
|
372
|
+
* With `includeStale`, ALSO prunes nominally-live (active | idle) nodes past the
|
|
373
|
+
* TTL whose process is provably gone — `pi_pid` is NULL or no longer alive. This
|
|
374
|
+
* reaps stale roots (a bare `crtr` whose pi died without the row transitioning),
|
|
375
|
+
* which the daemon's supervision never reconciled. A genuinely-running node keeps
|
|
376
|
+
* a live `pi_pid`, so it is protected, as is the CALLER ($CRTR_NODE_ID). Without
|
|
377
|
+
* the flag, active | idle are NEVER touched (the daemon's domain).
|
|
361
378
|
*
|
|
362
379
|
* The row deletes run in ONE transaction (so the sweep is all-or-nothing); the
|
|
363
380
|
* dir removals follow after COMMIT — the fs isn't transactional, and by then the
|
|
@@ -365,9 +382,11 @@ export function rebuildIndex() {
|
|
|
365
382
|
* reports the candidate set and deletes NOTHING. */
|
|
366
383
|
export function pruneNodes(opts) {
|
|
367
384
|
const dryRun = opts.dryRun ?? false;
|
|
385
|
+
const includeStale = opts.includeStale ?? false;
|
|
368
386
|
const cutoff = new Date(Date.now() - opts.ttlDays * 86_400_000).toISOString();
|
|
387
|
+
const selfId = process.env['CRTR_NODE_ID'] ?? '';
|
|
369
388
|
const db = openDb();
|
|
370
|
-
const
|
|
389
|
+
const terminal = db
|
|
371
390
|
.prepare(`SELECT node_id, status, created FROM nodes
|
|
372
391
|
WHERE status IN ('dead', 'done', 'canceled') AND created < ?
|
|
373
392
|
ORDER BY created`)
|
|
@@ -376,6 +395,25 @@ export function pruneNodes(opts) {
|
|
|
376
395
|
status: r['status'],
|
|
377
396
|
created: r['created'],
|
|
378
397
|
}));
|
|
398
|
+
// Stale non-terminal sweep (opt-in): active | idle past the TTL whose process
|
|
399
|
+
// is provably gone (pi_pid NULL or not alive). Never the caller itself.
|
|
400
|
+
const stale = !includeStale ? [] : db
|
|
401
|
+
.prepare(`SELECT node_id, status, created, pi_pid FROM nodes
|
|
402
|
+
WHERE status IN ('active', 'idle') AND created < ?
|
|
403
|
+
ORDER BY created`)
|
|
404
|
+
.all(cutoff)
|
|
405
|
+
.filter((r) => {
|
|
406
|
+
if (r['node_id'] === selfId)
|
|
407
|
+
return false;
|
|
408
|
+
const pid = r['pi_pid'];
|
|
409
|
+
return pid === null || !pidAlive(pid);
|
|
410
|
+
})
|
|
411
|
+
.map((r) => ({
|
|
412
|
+
node_id: r['node_id'],
|
|
413
|
+
status: r['status'],
|
|
414
|
+
created: r['created'],
|
|
415
|
+
}));
|
|
416
|
+
const candidates = [...terminal, ...stale];
|
|
379
417
|
if (dryRun || candidates.length === 0)
|
|
380
418
|
return { pruned: candidates, dryRun };
|
|
381
419
|
// One transactioned sweep — delete the rows; the FK cascades their edges.
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
/** Root of the global canvas home (`~/.
|
|
1
|
+
/** Root of the global canvas home (`~/.crouter/canvas` unless `CRTR_HOME` is set).
|
|
2
|
+
* Nested under the `.crouter` scope root so the whole runtime lives in one
|
|
3
|
+
* visible top-level dir; `canvas/` keeps node-graph runtime state separate from
|
|
4
|
+
* durable user content (skills/plugins/marketplaces/config) at the scope root. */
|
|
2
5
|
export declare function crtrHome(): string;
|
|
3
6
|
export declare function canvasDbPath(): string;
|
|
4
7
|
export declare function nodesRoot(): string;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
// The `~/.
|
|
1
|
+
// The `~/.crouter/canvas/` layout. One global, cwd-agnostic home for the whole canvas.
|
|
2
2
|
//
|
|
3
|
-
// ~/.
|
|
3
|
+
// ~/.crouter/canvas/
|
|
4
4
|
// canvas.db sqlite (WAL) — topology only (nodes + edges)
|
|
5
5
|
// nodes/<node_id>/
|
|
6
6
|
// meta.json source of truth for the node's row
|
|
@@ -15,10 +15,16 @@
|
|
|
15
15
|
import { homedir } from 'node:os';
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { mkdirSync } from 'node:fs';
|
|
18
|
-
|
|
18
|
+
import { CRTR_DIR_NAME } from '../../types.js';
|
|
19
|
+
/** Root of the global canvas home (`~/.crouter/canvas` unless `CRTR_HOME` is set).
|
|
20
|
+
* Nested under the `.crouter` scope root so the whole runtime lives in one
|
|
21
|
+
* visible top-level dir; `canvas/` keeps node-graph runtime state separate from
|
|
22
|
+
* durable user content (skills/plugins/marketplaces/config) at the scope root. */
|
|
19
23
|
export function crtrHome() {
|
|
20
24
|
const override = process.env['CRTR_HOME'];
|
|
21
|
-
return override !== undefined && override !== ''
|
|
25
|
+
return override !== undefined && override !== ''
|
|
26
|
+
? override
|
|
27
|
+
: join(homedir(), CRTR_DIR_NAME, 'canvas');
|
|
22
28
|
}
|
|
23
29
|
export function canvasDbPath() {
|
|
24
30
|
return join(crtrHome(), 'canvas.db');
|
|
@@ -23,6 +23,16 @@ export interface DashboardRow {
|
|
|
23
23
|
mode: string;
|
|
24
24
|
ctx_tokens: number;
|
|
25
25
|
asks: number;
|
|
26
|
+
/** The dir the node is pinned to (its cwd). Drives the browser's cwd-scope
|
|
27
|
+
* filter + the All-dirs basename cue. */
|
|
28
|
+
cwd: string;
|
|
29
|
+
/** ISO 8601 birth timestamp — drives the recency sort + the relative-age cue. */
|
|
30
|
+
created: string;
|
|
31
|
+
/** The node's spawn prompt (context/initial-prompt.md), trimmed + capped. Only
|
|
32
|
+
* populated by dashboardRowsAll (the browser snapshot) — the dashboard leaf
|
|
33
|
+
* leaves it undefined to avoid a file read per node. Indexed by super-search
|
|
34
|
+
* and shown in the preview panel. */
|
|
35
|
+
goal?: string;
|
|
26
36
|
}
|
|
27
37
|
/** One row per node visible in the sub-DAG of `rootId` (including root). */
|
|
28
38
|
export declare function dashboardRows(rootId: string): DashboardRow[];
|
|
@@ -16,7 +16,7 @@ import { existsSync, readFileSync } from 'node:fs';
|
|
|
16
16
|
import { join } from 'node:path';
|
|
17
17
|
import { getNode, listNodes, subscriptionsOf, view } from './canvas.js';
|
|
18
18
|
import { fullName } from './labels.js';
|
|
19
|
-
import { jobDir } from './paths.js';
|
|
19
|
+
import { jobDir, contextDir } from './paths.js';
|
|
20
20
|
import { countAsks } from './attention.js';
|
|
21
21
|
// ---------------------------------------------------------------------------
|
|
22
22
|
// Glyphs
|
|
@@ -162,6 +162,25 @@ export function renderForest() {
|
|
|
162
162
|
}
|
|
163
163
|
return parts.join('\n\n');
|
|
164
164
|
}
|
|
165
|
+
/** The spawn prompt, read straight off disk (canvas-home state) and capped so a
|
|
166
|
+
* giant initial-prompt.md can't bloat the snapshot. Mirrors how telemetry is
|
|
167
|
+
* read here directly rather than via the runtime layer (which would invert the
|
|
168
|
+
* canvas→runtime dependency). Never throws. */
|
|
169
|
+
const GOAL_CAP = 4096;
|
|
170
|
+
function readGoalText(nodeId) {
|
|
171
|
+
try {
|
|
172
|
+
const p = join(contextDir(nodeId), 'initial-prompt.md');
|
|
173
|
+
if (!existsSync(p))
|
|
174
|
+
return undefined;
|
|
175
|
+
const body = readFileSync(p, 'utf8').trim();
|
|
176
|
+
if (body === '')
|
|
177
|
+
return undefined;
|
|
178
|
+
return body.length > GOAL_CAP ? body.slice(0, GOAL_CAP) : body;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
165
184
|
/** One row per node visible in the sub-DAG of `rootId` (including root). */
|
|
166
185
|
export function dashboardRows(rootId) {
|
|
167
186
|
const ids = [rootId, ...view(rootId)];
|
|
@@ -178,6 +197,8 @@ export function dashboardRows(rootId) {
|
|
|
178
197
|
mode: node.mode,
|
|
179
198
|
ctx_tokens: tel.tokens_in ?? 0,
|
|
180
199
|
asks: countAsks(id),
|
|
200
|
+
cwd: node.cwd,
|
|
201
|
+
created: node.created,
|
|
181
202
|
}];
|
|
182
203
|
});
|
|
183
204
|
}
|
|
@@ -196,6 +217,9 @@ export function dashboardRowsAll() {
|
|
|
196
217
|
mode: row.mode,
|
|
197
218
|
ctx_tokens: tel.tokens_in ?? 0,
|
|
198
219
|
asks: countAsks(row.node_id),
|
|
220
|
+
cwd: row.cwd,
|
|
221
|
+
created: row.created,
|
|
222
|
+
goal: readGoalText(row.node_id),
|
|
199
223
|
}];
|
|
200
224
|
});
|
|
201
225
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// The canvas vocabulary — the node + edge model the whole runtime hangs on.
|
|
2
2
|
//
|
|
3
|
-
// One global canvas (`~/.
|
|
4
|
-
// each node's flesh lives on disk under `~/.
|
|
3
|
+
// One global canvas (`~/.crouter/canvas/canvas.db`) holds the topology (nodes +
|
|
4
|
+
// edges); each node's flesh lives on disk under `~/.crouter/canvas/nodes/<id>/`. A node's
|
|
5
5
|
// `meta.json` is the source of truth for its own row; the db is a queryable
|
|
6
6
|
// index over those metas, plus the authoritative store for the mutable
|
|
7
7
|
// `subscribes_to` edges (which no single meta owns).
|
|
@@ -45,8 +45,5 @@ export declare function writeCursor(nodeId: string, iso: string): void;
|
|
|
45
45
|
* [<kind>] ← ref-less msg: full body inlined
|
|
46
46
|
* <body line>
|
|
47
47
|
* …
|
|
48
|
-
*
|
|
49
|
-
* A header line announces the total count and instructs the receiver to
|
|
50
|
-
* dereference only what matters.
|
|
51
48
|
*/
|
|
52
49
|
export declare function coalesce(entries: InboxEntry[]): string;
|
package/dist/core/feed/inbox.js
CHANGED
|
@@ -139,14 +139,10 @@ function renderEntry(e) {
|
|
|
139
139
|
* [<kind>] ← ref-less msg: full body inlined
|
|
140
140
|
* <body line>
|
|
141
141
|
* …
|
|
142
|
-
*
|
|
143
|
-
* A header line announces the total count and instructs the receiver to
|
|
144
|
-
* dereference only what matters.
|
|
145
142
|
*/
|
|
146
143
|
export function coalesce(entries) {
|
|
147
144
|
if (entries.length === 0)
|
|
148
145
|
return '(inbox empty)';
|
|
149
|
-
const header = `${entries.length} update${entries.length === 1 ? '' : 's'} since last read — dereference what matters.\n`;
|
|
150
146
|
// Group by `from` (null → 'system').
|
|
151
147
|
const groups = new Map();
|
|
152
148
|
for (const e of entries) {
|
|
@@ -160,5 +156,5 @@ export function coalesce(entries) {
|
|
|
160
156
|
const lines = items.map(renderEntry);
|
|
161
157
|
sections.push(`From ${sender} — ${items.length} update${items.length === 1 ? '' : 's'}:\n${lines.join('\n')}`);
|
|
162
158
|
}
|
|
163
|
-
return
|
|
159
|
+
return sections.join('\n\n');
|
|
164
160
|
}
|
package/dist/core/help.d.ts
CHANGED
|
@@ -162,6 +162,12 @@ export interface LeafHelp {
|
|
|
162
162
|
/** Every persistent change the command makes to the world. For read-only
|
|
163
163
|
* leaves use exactly: ["None. Read-only."] */
|
|
164
164
|
effects: string[];
|
|
165
|
+
/** Bounded runtime aggregate as a complete self-named state element (build it
|
|
166
|
+
* with stateBlock), e.g. `<kinds count="7">…</kinds>`. Lazily evaluated at
|
|
167
|
+
* render time so it reflects the caller's cwd/project scope; appended after
|
|
168
|
+
* the schema. Renderer soft-fails to omission if it returns null or throws.
|
|
169
|
+
* Mirrors BranchHelp.dynamicState. */
|
|
170
|
+
dynamicState?: () => string | null;
|
|
165
171
|
}
|
|
166
172
|
/** Build a self-named runtime-state element: `<tag attr="v">body</tag>`. The
|
|
167
173
|
* subtree that owns the state authors it through this, so the tag name and any
|
package/dist/core/help.js
CHANGED
|
@@ -238,5 +238,12 @@ export function renderLeafArgv(h) {
|
|
|
238
238
|
for (const e of h.effects) {
|
|
239
239
|
lines.push(` ${e}`);
|
|
240
240
|
}
|
|
241
|
+
// Optional bounded runtime-state block (e.g. the live <kinds> list), appended
|
|
242
|
+
// after the schema. Soft-fails to omission on null/throw, mirroring renderBranch.
|
|
243
|
+
const state = evalDynamic(h.dynamicState);
|
|
244
|
+
if (state !== null) {
|
|
245
|
+
lines.push('');
|
|
246
|
+
lines.push(state);
|
|
247
|
+
}
|
|
241
248
|
return lines.join('\n');
|
|
242
249
|
}
|
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Persona composer public surface.
|
|
3
3
|
*
|
|
4
4
|
* Re-exports:
|
|
5
|
-
* - loadPersona / loadKernel / availableKinds
|
|
5
|
+
* - loadPersona / loadKernel / availableKinds / kindWhenToUse / subPersonasFor
|
|
6
|
+
* (loader — raw file access)
|
|
6
7
|
* - resolve (high-level composer)
|
|
7
8
|
* - ResolvedPersona (return type of resolve)
|
|
8
9
|
*/
|
|
9
|
-
export { loadPersona, loadKernel, availableKinds, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
|
-
export type { LoadedPersona } from './loader.js';
|
|
10
|
+
export { loadPersona, loadKernel, availableKinds, kindWhenToUse, subPersonasFor, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
11
|
+
export type { LoadedPersona, SubPersona } from './loader.js';
|
|
11
12
|
export { resolve } from './resolve.js';
|
|
12
13
|
export type { ResolvedPersona } from './resolve.js';
|
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Persona composer public surface.
|
|
3
3
|
*
|
|
4
4
|
* Re-exports:
|
|
5
|
-
* - loadPersona / loadKernel / availableKinds
|
|
5
|
+
* - loadPersona / loadKernel / availableKinds / kindWhenToUse / subPersonasFor
|
|
6
|
+
* (loader — raw file access)
|
|
6
7
|
* - resolve (high-level composer)
|
|
7
8
|
* - ResolvedPersona (return type of resolve)
|
|
8
9
|
*/
|
|
9
|
-
export { loadPersona, loadKernel, availableKinds, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
|
+
export { loadPersona, loadKernel, availableKinds, kindWhenToUse, subPersonasFor, loadLifecycleFragment, loadSpineFragment } from './loader.js';
|
|
10
11
|
export { resolve } from './resolve.js';
|
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
* Resolution order (highest → lowest precedence): project > user > builtin.
|
|
6
6
|
*
|
|
7
7
|
* Layout on disk:
|
|
8
|
-
* <root>/personas/<kind>/
|
|
8
|
+
* <root>/personas/<kind>/PERSONA.md
|
|
9
9
|
* <root>/personas/<kind>/orchestrator.md
|
|
10
10
|
* <root>/personas/orchestration-kernel.md
|
|
11
|
+
* <root>/personas/<kind>/<...>/PERSONA.md (nested sub-personas)
|
|
11
12
|
*
|
|
12
13
|
* The builtin root is src/builtin-personas (or dist/builtin-personas in the
|
|
13
14
|
* compiled build), resolved relative to this module — mirrors the pattern used
|
|
@@ -55,29 +56,46 @@ export declare function loadLifecycleFragment(lifecycle: 'terminal' | 'resident'
|
|
|
55
56
|
*/
|
|
56
57
|
export declare function loadSpineFragment(hasManager: boolean): string;
|
|
57
58
|
/**
|
|
58
|
-
* Enumerate the kinds with at least one persona file (
|
|
59
|
+
* Enumerate the kinds with at least one persona file (PERSONA.md or
|
|
59
60
|
* orchestrator.md) across all scope roots (project/user/builtin). Used to
|
|
60
|
-
* validate a requested `--kind` and to list the valid choices.
|
|
61
|
+
* validate a requested `--kind` and to list the valid choices. Only the
|
|
62
|
+
* IMMEDIATE children of each root count — nested sub-personas never pollute
|
|
63
|
+
* the global kind list (see subPersonasFor).
|
|
61
64
|
*/
|
|
62
65
|
export declare function availableKinds(): string[];
|
|
63
|
-
|
|
66
|
+
/**
|
|
67
|
+
* The one-line "when to use this node type" gloss for `kind`, read from its
|
|
68
|
+
* `<kind>/PERSONA.md` `whenToUse` frontmatter (resolved project > user >
|
|
69
|
+
* builtin). Returns '' when the kind has no PERSONA.md or no `whenToUse`.
|
|
70
|
+
* Drives the dynamic kind list in `node new -h` / `node promote -h`.
|
|
71
|
+
*/
|
|
72
|
+
export declare function kindWhenToUse(kind: string): string;
|
|
73
|
+
export interface SubPersona {
|
|
64
74
|
/** Full kind string to spawn, e.g. 'plan/reviewers/security'. */
|
|
65
75
|
kind: string;
|
|
66
76
|
/** Leaf name, e.g. 'security'. */
|
|
67
77
|
name: string;
|
|
68
|
-
/** One-line "
|
|
69
|
-
|
|
78
|
+
/** One-line "when to use", from the sub-persona PERSONA.md `whenToUse` frontmatter (or ''). */
|
|
79
|
+
whenToUse: string;
|
|
70
80
|
}
|
|
71
81
|
/**
|
|
72
|
-
* Enumerate the
|
|
73
|
-
* personas
|
|
74
|
-
*
|
|
82
|
+
* Enumerate the sub-personas AVAILABLE TO `kind` — the nested specialist
|
|
83
|
+
* personas (e.g. `plan/reviewers/security`) a `kind` node may spawn, surfaced
|
|
84
|
+
* in its composed prompt (resolve.ts) and nowhere else.
|
|
85
|
+
*
|
|
86
|
+
* A sub-persona is any descendant dir (ANY depth) under a top-level kind dir
|
|
87
|
+
* that holds a PERSONA.md, EXCLUDING the top-level PERSONA.md itself. Its
|
|
88
|
+
* availability is its `availableTo` frontmatter: an explicit list of kind
|
|
89
|
+
* strings, or the wildcard `"*"`/`"all"` (visible to every kind); absent, it
|
|
90
|
+
* defaults to its own top-level ancestor kind. So the five `plan/reviewers/*`
|
|
91
|
+
* (no `availableTo`) are visible only to `plan`, while a sub-persona under
|
|
92
|
+
* `developer/` can declare `availableTo: [plan]` to surface in plan's menu —
|
|
93
|
+
* which is why ALL top-level kinds' descendants are scanned, not just `<kind>/`.
|
|
75
94
|
*
|
|
76
|
-
* Sub-
|
|
77
|
-
* immediate children of each
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
* `<kind>/reviewers/<name>/base.md` — no code change.
|
|
95
|
+
* Sub-personas are intentionally NOT global kinds: `availableKinds()` scans only
|
|
96
|
+
* the immediate children of each root, so a nested sub-persona never leaks into
|
|
97
|
+
* the global list; it is reachable only by its full kind string. Precedence is
|
|
98
|
+
* project > user > builtin keyed on the FULL kind string — the highest root that
|
|
99
|
+
* defines a given kind string wins (and owns its `availableTo`).
|
|
82
100
|
*/
|
|
83
|
-
export declare function
|
|
101
|
+
export declare function subPersonasFor(kind: string): SubPersona[];
|