@crouton-kit/crouter 0.3.20 → 0.3.21
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-memory/crouter-development/marketplaces.md +2 -4
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
- package/dist/builtin-memory/crouter-development/personas.md +2 -6
- package/dist/builtin-memory/crouter-development/plugins.md +2 -4
- package/dist/builtin-memory/design.md +2 -4
- package/dist/builtin-memory/development.md +3 -3
- package/dist/builtin-memory/planning.md +3 -4
- package/dist/builtin-memory/spec.md +3 -10
- package/dist/builtin-personas/orchestration-kernel.md +1 -1
- package/dist/commands/human/queue.js +2 -2
- package/dist/commands/memory/__tests__/lint-schema.test.d.ts +1 -0
- package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
- package/dist/commands/memory/find.js +10 -15
- package/dist/commands/memory/lint.d.ts +7 -0
- package/dist/commands/memory/lint.js +10 -1
- package/dist/commands/memory/shared.js +1 -2
- package/dist/commands/memory/write.js +6 -9
- package/dist/commands/node.js +47 -25
- package/dist/commands/push.js +10 -15
- package/dist/commands/tmux-spread.js +16 -1
- package/dist/commands/view-cycle.js +9 -5
- package/dist/commands/view-run.js +3 -2
- package/dist/core/__tests__/cascade-close.test.js +3 -2
- package/dist/core/__tests__/context-intro.test.js +23 -27
- package/dist/core/__tests__/detach-focus.test.js +2 -2
- package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
- package/dist/core/__tests__/memory.test.js +4 -4
- package/dist/core/__tests__/subscription-delivery.test.js +3 -3
- package/dist/core/bootstrap.js +18 -14
- package/dist/core/help.d.ts +2 -2
- package/dist/core/help.js +1 -1
- package/dist/core/render.d.ts +4 -3
- package/dist/core/render.js +38 -41
- package/dist/core/resolver.js +1 -1
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +2 -2
- package/dist/core/substrate/render.d.ts +1 -2
- package/dist/core/substrate/render.js +9 -12
- package/dist/core/substrate/schema.d.ts +18 -13
- package/dist/core/substrate/schema.js +12 -11
- package/dist/pi-extensions/canvas-context-intro.js +5 -3
- package/package.json +1 -1
|
@@ -125,5 +125,20 @@ export const tmuxSpreadLeaf = defineLeaf({
|
|
|
125
125
|
focused: spread.focused,
|
|
126
126
|
};
|
|
127
127
|
},
|
|
128
|
-
render: (r) =>
|
|
128
|
+
render: (r) => {
|
|
129
|
+
const joined = r['children_joined'] ?? [];
|
|
130
|
+
const overflow = r['overflow'] ?? [];
|
|
131
|
+
const where = r['window'] !== undefined && r['window'] !== ''
|
|
132
|
+
? ` into window ${r['window']}${r['session'] !== undefined ? ` (session ${r['session']})` : ''}`
|
|
133
|
+
: '';
|
|
134
|
+
const lead = `Spread node${where} — ${joined.length} child pane(s) joined, ${overflow.length} overflow, ${r['focused'] === true ? 'window focused' : 'window not focused'}.`;
|
|
135
|
+
const blocks = [lead];
|
|
136
|
+
if (joined.length > 0) {
|
|
137
|
+
blocks.push(['Children joined:', ...joined.map((c) => `- ${c}`)].join('\n'));
|
|
138
|
+
}
|
|
139
|
+
if (overflow.length > 0) {
|
|
140
|
+
blocks.push(['Overflow (max_panes_per_window reached):', ...overflow.map((c) => `- ${c}`)].join('\n'));
|
|
141
|
+
}
|
|
142
|
+
return blocks.join('\n\n');
|
|
143
|
+
},
|
|
129
144
|
});
|
|
@@ -117,9 +117,13 @@ export const viewCycleLeaf = defineLeaf({
|
|
|
117
117
|
catch { /* best-effort */ }
|
|
118
118
|
return { cycled: true, view: targetId, from: current };
|
|
119
119
|
},
|
|
120
|
-
render: (r) =>
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
120
|
+
render: (r) => {
|
|
121
|
+
if (r['cycled'] === true) {
|
|
122
|
+
return `Cycled the monitor to view "${r['view']}" (from "${r['from']}").`;
|
|
123
|
+
}
|
|
124
|
+
if (r['from'] !== undefined) {
|
|
125
|
+
return `No other view to switch to — the pane is still on "${r['from']}".`;
|
|
126
|
+
}
|
|
127
|
+
return 'Nothing to cycle — this pane is not hosting a view.';
|
|
128
|
+
},
|
|
125
129
|
});
|
|
@@ -181,11 +181,12 @@ export const viewRunLeaf = defineLeaf({
|
|
|
181
181
|
render: (result) => {
|
|
182
182
|
const hosted = result['hosted'];
|
|
183
183
|
if (hosted === 'window') {
|
|
184
|
-
return
|
|
184
|
+
return `Opened view "${result['view']}" as a window monitor — window ${result['window']}, pane ${result['pane']}.`;
|
|
185
185
|
}
|
|
186
186
|
if (hosted === 'split') {
|
|
187
|
-
return
|
|
187
|
+
return `Opened view "${result['view']}" as a split monitor — pane ${result['pane']}.`;
|
|
188
188
|
}
|
|
189
|
+
// In-pane host: the terminal was held until quit; nothing to report.
|
|
189
190
|
return '';
|
|
190
191
|
},
|
|
191
192
|
});
|
|
@@ -103,8 +103,9 @@ test('cascade close: middle-node close reaps EXACTLY its subtree (canceled), anc
|
|
|
103
103
|
// ===================================================================
|
|
104
104
|
const closeRes = h.cli(A, ['node', 'close', '--node', B]);
|
|
105
105
|
assert.equal(closeRes.code, 0, `node close exit 0\n${closeRes.stderr}`);
|
|
106
|
-
// The CLI's own rendered report: cascade root B, exactly
|
|
107
|
-
|
|
106
|
+
// The CLI's own rendered report (plain markdown): cascade root B, exactly
|
|
107
|
+
// 4 closed, 0 spared (no spared clause is emitted when none were spared).
|
|
108
|
+
assert.match(closeRes.stdout, new RegExp(`^Closed ${B} and its exclusive subtree — 4 node\\(s\\) closed\\.`), `close report names B as root, count=4, spared=0\n${closeRes.stdout}`);
|
|
108
109
|
// ===================================================================
|
|
109
110
|
// ASSERT 1 — the A5 DELIVERABLE: the EXACT terminal status of every
|
|
110
111
|
// cascade-reaped node. CURRENT behavior: status='canceled', intent=null,
|
|
@@ -1,10 +1,8 @@
|
|
|
1
1
|
// Tests for the <crtr-context> bearings preamble:
|
|
2
2
|
// 1. Worker and orchestrator bearings carry the `## References` block
|
|
3
|
-
// (substrate reference docs + node-local docs as `###` sub-sections)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// across-cycles framing; promotion delivers that same orchestrator
|
|
7
|
-
// context-dir framing to a node that spawned base.
|
|
3
|
+
// (substrate reference docs + node-local docs as `###` sub-sections).
|
|
4
|
+
// Orchestrators add the across-cycles framing; promotion delivers that
|
|
5
|
+
// same orchestrator context-dir framing to a node that spawned base.
|
|
8
6
|
// 2. canvas-context-intro injects the block as its own session message at
|
|
9
7
|
// session_start (before the first prompt), idempotent across resumes.
|
|
10
8
|
//
|
|
@@ -36,53 +34,51 @@ after(() => {
|
|
|
36
34
|
delete process.env['CRTR_HOME'];
|
|
37
35
|
delete process.env['CRTR_NODE_ID'];
|
|
38
36
|
});
|
|
39
|
-
test('worker bearings: base framing + ## References block
|
|
40
|
-
// Bug-regression: review finding M1 — buildContextBearings
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
// locks in the new contract.
|
|
37
|
+
test('worker bearings: base framing + ## References block, NO across-cycles framing', () => {
|
|
38
|
+
// Bug-regression: review finding M1 — buildContextBearings renders the
|
|
39
|
+
// ## References block (renderReferencesBlock): ## References + ### <name>
|
|
40
|
+
// sub-sections. This test locks in that contract.
|
|
44
41
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
45
42
|
// Seed a node-local substrate doc so the ## References block is non-empty.
|
|
46
43
|
const dir = memoryDir(meta.node_id);
|
|
47
44
|
mkdirSync(dir, { recursive: true });
|
|
48
|
-
writeFileSync(join(dir, 'test-ref.md'), '---\nkind: reference\nwhen:
|
|
45
|
+
writeFileSync(join(dir, 'test-ref.md'), '---\nkind: reference\nwhen-and-why-to-read: When testing, this reference should be read because it is a regression fixture\nsystem-prompt-visibility: preview\n---\nTest body.\n');
|
|
49
46
|
const block = buildContextIntro(meta.node_id);
|
|
50
47
|
assert.match(block, new RegExp(`<crtr-context dir="${contextDir(meta.node_id)}">`));
|
|
51
48
|
assert.match(block, /shared document store, not a task tracker/, 'base = shared docs, not tasks');
|
|
52
|
-
//
|
|
49
|
+
// Reference content renders ONLY as ## References + ### sub-sections.
|
|
53
50
|
assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
|
|
54
|
-
//
|
|
51
|
+
// Per-store stanza headers (label · dir) never appear.
|
|
55
52
|
assert.doesNotMatch(block, /user-global · /, 'no user-global label·dir stanza');
|
|
56
53
|
assert.doesNotMatch(block, /node-local · /, 'no node-local label·dir stanza');
|
|
57
|
-
//
|
|
54
|
+
// No (empty) placeholder marker.
|
|
58
55
|
assert.doesNotMatch(block, /\(empty\)/, 'no (empty) placeholder');
|
|
59
|
-
// Reference content renders as ## References + ### sub-sections — no other shape.
|
|
60
56
|
assert.match(block, /## References\n\n###/, '## References followed by ### sub-sections');
|
|
61
57
|
// A terminal worker must NOT carry the orchestrator across-cycles framing.
|
|
62
58
|
assert.doesNotMatch(block, /refresh cycles/, 'no across-cycles framing for a terminal worker');
|
|
63
59
|
assert.match(block, /<\/crtr-context>/);
|
|
64
60
|
});
|
|
65
|
-
test('orchestrator bearings: across-cycles framing + node-local substrate docs ride into ## References;
|
|
66
|
-
// Bug-regression: review finding M1 — buildContextBearings
|
|
67
|
-
//
|
|
68
|
-
//
|
|
61
|
+
test('orchestrator bearings: across-cycles framing + node-local substrate docs ride into ## References; a non-substrate .md file is never inlined', () => {
|
|
62
|
+
// Bug-regression: review finding M1 — buildContextBearings renders ## References.
|
|
63
|
+
// Node-local substrate docs render as ### sub-sections at their rung; a
|
|
64
|
+
// non-substrate .md file (no frontmatter `kind`) never surfaces.
|
|
69
65
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
70
66
|
promote(meta.node_id); // flip to orchestrator mode — the across-cycles gate
|
|
71
67
|
const dir = memoryDir(meta.node_id);
|
|
72
68
|
mkdirSync(dir, { recursive: true });
|
|
73
|
-
// Write a MEMORY.md file to verify it is never surfaced in the block.
|
|
69
|
+
// Write a non-substrate MEMORY.md file to verify it is never surfaced in the block.
|
|
74
70
|
const legacyIndexPath = join(dir, 'MEMORY.md');
|
|
75
71
|
writeFileSync(legacyIndexPath, '# memory index — one pointer line per memory; how-to in "Your long-term memory".\n\n- [Flaky build](flaky-build.md) — first run fails\n');
|
|
76
72
|
// A node-local substrate doc DOES ride into ## References at its rung.
|
|
77
|
-
writeFileSync(join(dir, 'flaky-build.md'), '---\nkind: reference\nwhen:
|
|
73
|
+
writeFileSync(join(dir, 'flaky-build.md'), '---\nkind: reference\nwhen-and-why-to-read: When the build flakes, this reference should be read because the first run fails\nsystem-prompt-visibility: preview\n---\nFirst run always fails; rerun once.\n');
|
|
78
74
|
const block = buildContextIntro(meta.node_id);
|
|
79
75
|
assert.match(block, /shared document store, not a task tracker/, 'still carries the base framing');
|
|
80
76
|
assert.match(block, /refresh cycles/, 'orchestrator gets the across-cycles framing');
|
|
81
77
|
assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
|
|
82
78
|
assert.match(block, /## References\n/, 'references block present');
|
|
83
79
|
assert.match(block, /### flaky-build\n/, 'node-local doc gets its ### sub-section');
|
|
84
|
-
assert.match(block, /
|
|
85
|
-
// The
|
|
80
|
+
assert.match(block, /When the build flakes, this reference should be read because the first run fails\./, 'preview rung renders the routing line');
|
|
81
|
+
// The non-substrate file never renders: no header line, no pointer line, no path.
|
|
86
82
|
assert.ok(!block.includes('# memory index'), 'the index header comment is NOT inlined');
|
|
87
83
|
assert.ok(!block.includes('- [Flaky build](flaky-build.md)'), 'index pointer lines are NOT inlined');
|
|
88
84
|
assert.ok(!block.includes(legacyIndexPath), 'no absolute index (MEMORY.md) path');
|
|
@@ -90,9 +86,9 @@ test('orchestrator bearings: across-cycles framing + node-local substrate docs r
|
|
|
90
86
|
assert.match(block, /<\/crtr-context>/);
|
|
91
87
|
});
|
|
92
88
|
test('orchestrator bearings: no per-store stanzas or (empty) markers; a rung-none node-local doc still surfaces as a ### title stub', () => {
|
|
93
|
-
// Bug-regression: review findings M1 + M6 — the
|
|
94
|
-
// stanzas
|
|
95
|
-
// on rung: a migrated node-local reference defaults
|
|
89
|
+
// Bug-regression: review findings M1 + M6 — the ## References block carries no
|
|
90
|
+
// per-store `label · dir` stanzas or (empty) markers, and node-local docs are
|
|
91
|
+
// NOT filtered on rung: a migrated node-local reference defaults
|
|
96
92
|
// system-prompt-visibility: none and must still ride into ## References as
|
|
97
93
|
// its bare title (never its body).
|
|
98
94
|
const meta = spawnNode({ kind: 'general', cwd: '/tmp/work', parent: null });
|
|
@@ -101,7 +97,7 @@ test('orchestrator bearings: no per-store stanzas or (empty) markers; a rung-non
|
|
|
101
97
|
mkdirSync(dir, { recursive: true });
|
|
102
98
|
writeFileSync(join(dir, 'rung-none-fact.md'), '---\nkind: reference\n---\nbody that must not render at the none rung\n');
|
|
103
99
|
const block = buildContextIntro(meta.node_id);
|
|
104
|
-
// No per-store stanza headers, no (empty) markers, no
|
|
100
|
+
// No per-store stanza headers, no (empty) markers, no MEMORY.md paths.
|
|
105
101
|
assert.ok(!block.includes(`node-local · ${memoryDir(meta.node_id)}`), 'no node-local stanza header');
|
|
106
102
|
assert.ok(!block.includes('user-global · '), 'no user-global stanza header');
|
|
107
103
|
assert.ok(!block.includes('project · '), 'no project stanza header');
|
|
@@ -109,7 +109,7 @@ test('node demote --detach on a FOCUSED node: flips terminal, relocates the pane
|
|
|
109
109
|
// breaks the pane back into h.session.
|
|
110
110
|
const res = h.cli(B, ['node', 'demote', '--node', B, '--pane', bPane, '--detach']);
|
|
111
111
|
assert.equal(res.code, 0, `node demote --detach exit 0\n${res.stderr}`);
|
|
112
|
-
assert.match(res.stdout, /
|
|
112
|
+
assert.match(res.stdout, /relocated to the background/, `the agent was detached\n${res.stdout}`);
|
|
113
113
|
closeDb();
|
|
114
114
|
const b = getNode(B);
|
|
115
115
|
// (a) lifecycle flipped to terminal IN PLACE.
|
|
@@ -180,7 +180,7 @@ test('node demote --detach on a NON-GENERATING focused node: RELEASES to dormant
|
|
|
180
180
|
// --- Drive the REAL verb on the non-generating focused node.
|
|
181
181
|
const res = h.cli(B, ['node', 'demote', '--node', B, '--pane', bPane, '--detach']);
|
|
182
182
|
assert.equal(res.code, 0, `node demote --detach exit 0\n${res.stderr}`);
|
|
183
|
-
assert.match(res.stdout, /
|
|
183
|
+
assert.match(res.stdout, /relocated to the background/, `the agent was detached\n${res.stdout}`);
|
|
184
184
|
closeDb();
|
|
185
185
|
const b = getNode(B);
|
|
186
186
|
// (b) RELEASED to dormant — idle + intent='idle-release' (revivable via inbox
|
|
@@ -66,7 +66,7 @@ test('node demote flips lifecycle→terminal IN PLACE; node recycle is FINISH+RE
|
|
|
66
66
|
// untouched, NOT finalized.
|
|
67
67
|
const dem = h.cli(B, ['node', 'demote', '--node', B]);
|
|
68
68
|
assert.equal(dem.code, 0, `node demote exit 0\n${dem.stderr}`);
|
|
69
|
-
assert.match(dem.stdout,
|
|
69
|
+
assert.match(dem.stdout, /^Demoted /, `demote rendered\n${dem.stdout}`);
|
|
70
70
|
{
|
|
71
71
|
const b = h.node(B);
|
|
72
72
|
assert.equal(b.lifecycle, 'terminal', 'demote flips lifecycle→terminal IN PLACE');
|
|
@@ -83,10 +83,10 @@ test('node demote flips lifecycle→terminal IN PLACE; node recycle is FINISH+RE
|
|
|
83
83
|
// RECYCLE via the real verb (TMUX_PANE is scrubbed from child env → pass --pane).
|
|
84
84
|
const res = h.cli(B, ['node', 'recycle', '--node', B, '--pane', pane]);
|
|
85
85
|
assert.equal(res.code, 0, `recycle exit 0\n${res.stderr}`);
|
|
86
|
-
// The leaf renders
|
|
87
|
-
assert.match(res.stdout,
|
|
88
|
-
const newRoot =
|
|
89
|
-
const finalized =
|
|
86
|
+
// The leaf renders plain markdown: a lead sentence + `- finalized:` / `- new root:` bullets.
|
|
87
|
+
assert.match(res.stdout, /^Recycled the pane /, `recycle recycled the pane\n${res.stdout}`);
|
|
88
|
+
const newRoot = /- new root: (\S+)/.exec(res.stdout)?.[1];
|
|
89
|
+
const finalized = /- finalized: true/.test(res.stdout);
|
|
90
90
|
// The recycled node is FINISHED, not mode-flipped.
|
|
91
91
|
{
|
|
92
92
|
const b = h.node(B);
|
|
@@ -29,7 +29,7 @@ after(() => {
|
|
|
29
29
|
delete process.env['CRTR_HOME'];
|
|
30
30
|
});
|
|
31
31
|
// ---------------------------------------------------------------------------
|
|
32
|
-
// Kernel + guidance: substrate flow
|
|
32
|
+
// Kernel + guidance: the document substrate flow
|
|
33
33
|
// ---------------------------------------------------------------------------
|
|
34
34
|
test('the orchestration kernel names the three stores and the substrate commands', () => {
|
|
35
35
|
const kernel = loadKernel();
|
|
@@ -41,7 +41,7 @@ test('the orchestration kernel names the three stores and the substrate commands
|
|
|
41
41
|
assert.ok(kernel.includes('crtr memory list'), 'kernel mentions crtr memory list');
|
|
42
42
|
assert.ok(kernel.includes('crtr memory find'), 'kernel mentions crtr memory find');
|
|
43
43
|
assert.ok(kernel.includes('crtr memory read'), 'kernel mentions crtr memory read');
|
|
44
|
-
//
|
|
44
|
+
// It teaches the substrate flow only — no MEMORY.md pointer-line index.
|
|
45
45
|
assert.ok(!kernel.includes('MEMORY.md'), 'kernel does not mention MEMORY.md');
|
|
46
46
|
assert.ok(!kernel.includes('pointer line'), 'kernel does not teach the pointer-line flow');
|
|
47
47
|
});
|
|
@@ -55,6 +55,6 @@ test('promotion guidance references the three stores; no <memory> block', () =>
|
|
|
55
55
|
for (const store of ['user-global', 'project', 'node-local']) {
|
|
56
56
|
assert.ok(guidance.includes(store), `guidance names the ${store} store`);
|
|
57
57
|
}
|
|
58
|
-
// The
|
|
59
|
-
assert.ok(!guidance.includes('<memory>'), 'guidance does not point at
|
|
58
|
+
// The guidance points at the substrate flow, not a <memory> block.
|
|
59
|
+
assert.ok(!guidance.includes('<memory>'), 'guidance does not point at a <memory> block');
|
|
60
60
|
});
|
|
@@ -79,10 +79,10 @@ test('multi-level subscription delivery: active subscriber WOKEN vs passive subs
|
|
|
79
79
|
{
|
|
80
80
|
const passiveRes = h.cli(P, ['node', 'subscribe', T, '--passive']);
|
|
81
81
|
assert.equal(passiveRes.code, 0, `P→T passive subscribe exit 0\n${passiveRes.stderr}`);
|
|
82
|
-
assert.match(passiveRes.stdout,
|
|
82
|
+
assert.match(passiveRes.stdout, /\(passive\)/, 'P→T wired PASSIVE');
|
|
83
83
|
const activeRes = h.cli(P, ['node', 'subscribe', K]);
|
|
84
84
|
assert.equal(activeRes.code, 0, `P→K active subscribe exit 0\n${activeRes.stderr}`);
|
|
85
|
-
assert.match(activeRes.stdout,
|
|
85
|
+
assert.match(activeRes.stdout, /\(active\)/, 'P→K wired ACTIVE');
|
|
86
86
|
}
|
|
87
87
|
// T now has BOTH an active (B) and a passive (P) subscriber — the crux.
|
|
88
88
|
assert.deepEqual(edgeSet(h.subscribers(T)), new Set([`${B}:active`, `${P}:passive`]), 'T has B(active) + P(passive) as subscribers — the same-target split');
|
|
@@ -103,7 +103,7 @@ test('multi-level subscription delivery: active subscriber WOKEN vs passive subs
|
|
|
103
103
|
{
|
|
104
104
|
const res = h.cli(X, ['node', 'subscribe', K, '--passive']);
|
|
105
105
|
assert.equal(res.code, 0, `X→K passive subscribe exit 0\n${res.stderr}`);
|
|
106
|
-
assert.match(res.stdout,
|
|
106
|
+
assert.match(res.stdout, /\(passive\)/, 'X→K wired PASSIVE-only (its ONLY subscription)');
|
|
107
107
|
}
|
|
108
108
|
assert.deepEqual(edgeSet(h.subscriptions(X)), new Set([`${K}:passive`]), 'X holds exactly one tie: K(passive) — no active live subscription');
|
|
109
109
|
const injBeforeStall = h.injected(X).length;
|
package/dist/core/bootstrap.js
CHANGED
|
@@ -30,36 +30,40 @@ const BOOT_SKILL_MARKER_PREFIX = '<!-- crtr-boot-skill v';
|
|
|
30
30
|
function bootSkillBody() {
|
|
31
31
|
return `---
|
|
32
32
|
name: crtr-skills
|
|
33
|
-
description: '
|
|
33
|
+
description: 'Author, list, search, and load crtr memory documents (skills, references, preferences) via the crtr CLI. These are durable agent memory — markdown future LLM sessions load on demand. Use when the user wants to remember/save knowledge, build a context primer, or recall a previously saved doc. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".'
|
|
34
34
|
argument-hint: [topic or verb]
|
|
35
35
|
---
|
|
36
36
|
|
|
37
37
|
${BOOT_SKILL_MARKER}
|
|
38
38
|
|
|
39
|
-
# /crtr-skills —
|
|
39
|
+
# /crtr-skills — crtr memory router
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
crtr memory documents = durable agent memory (kind: skill, reference, or
|
|
42
|
+
preference). Written for **future LLM sessions**, not the user. \`crtr memory\`
|
|
43
|
+
is the surface — author, discover, and load them.
|
|
43
44
|
|
|
44
45
|
## Route by intent
|
|
45
46
|
|
|
46
47
|
- **Capture** ("save", "remember", "build context for", "make a skill"):
|
|
47
|
-
\`crtr
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
- **
|
|
54
|
-
|
|
48
|
+
\`crtr memory write <name> --kind <skill|reference|preference>\` with the
|
|
49
|
+
body piped on stdin — e.g.
|
|
50
|
+
\`crtr memory write $ARGUMENTS --kind skill <<'EOF' … EOF\`. Run
|
|
51
|
+
\`crtr memory write -h\` for the full frontmatter schema
|
|
52
|
+
(--when-and-why-to-read, --short-form, --scope, visibility rungs), then
|
|
53
|
+
\`crtr memory lint\` to validate what you wrote.
|
|
54
|
+
- **Find** ("what do we have on X"): \`crtr memory find "$ARGUMENTS"\` →
|
|
55
|
+
\`crtr memory read <name>\` on the best hit (add --body/--grep to search
|
|
56
|
+
bodies).
|
|
57
|
+
- **Load by name**: \`crtr memory read <name>\`.
|
|
58
|
+
- **List all**: \`crtr memory list\`.
|
|
55
59
|
|
|
56
60
|
If \`$ARGUMENTS\` is empty, ask the user what they want before running.
|
|
57
61
|
|
|
58
62
|
## Rules
|
|
59
63
|
|
|
60
64
|
- CLI stdout is the prompt — act on it, don't paraphrase to the user.
|
|
61
|
-
-
|
|
62
|
-
|
|
65
|
+
- Append \`-h\` at any leaf (\`crtr memory write -h\`) for its full schema
|
|
66
|
+
before authoring.
|
|
63
67
|
- If \`crtr\` is not on PATH, tell the user and stop.
|
|
64
68
|
`;
|
|
65
69
|
}
|
package/dist/core/help.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface RootEntry {
|
|
|
94
94
|
useWhen: string;
|
|
95
95
|
/** Optional bounded block this subtree contributes to its <name> block at
|
|
96
96
|
* root. Returns a complete self-named state element (build it with
|
|
97
|
-
* stateBlock), e.g. `<
|
|
97
|
+
* stateBlock), e.g. `<kinds count="7">…</kinds>`. Aggregate, never an
|
|
98
98
|
* unbounded enumeration on a cold path. Soft-fails to omission on
|
|
99
99
|
* null/throw. */
|
|
100
100
|
dynamicState?: () => string | null;
|
|
@@ -141,7 +141,7 @@ export interface BranchHelp {
|
|
|
141
141
|
* child's purpose lives in its own listing row). */
|
|
142
142
|
model?: string;
|
|
143
143
|
/** Bounded runtime aggregate as a complete self-named state element (build
|
|
144
|
-
* it with stateBlock), e.g. `<
|
|
144
|
+
* it with stateBlock), e.g. `<kinds count="7">…</kinds>`. Renderer
|
|
145
145
|
* soft-fails to omission if this returns null or throws. */
|
|
146
146
|
dynamicState?: () => string | null;
|
|
147
147
|
/** Parent-level listing assembled by defineBranch from the actual child defs.
|
package/dist/core/help.js
CHANGED
|
@@ -105,7 +105,7 @@ export function renderRoot(h) {
|
|
|
105
105
|
for (const l of rootSubcommandLines(c))
|
|
106
106
|
lines.push(l);
|
|
107
107
|
// dynamicState returns a complete self-named element (e.g.
|
|
108
|
-
// <
|
|
108
|
+
// <kinds count="7">…</kinds>) — emit it as-is, nested in the command.
|
|
109
109
|
const state = evalDynamic(c.dynamicState);
|
|
110
110
|
if (state !== null)
|
|
111
111
|
lines.push(state);
|
package/dist/core/render.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { LeafHelp } from './help.js';
|
|
2
2
|
import type { ErrorPayload } from './io.js';
|
|
3
|
-
/** Schema-driven fallback: turn a result object into agent-ready
|
|
3
|
+
/** Schema-driven fallback: turn a result object into agent-ready plain markdown.
|
|
4
4
|
* Field order follows the leaf's declared output schema; any extra keys append
|
|
5
|
-
* after. Scalars
|
|
6
|
-
* become
|
|
5
|
+
* after. Scalars (and nested objects) list as `- name: value` bullets, prose
|
|
6
|
+
* fields render as paragraphs, and arrays become a count lead-in plus a table
|
|
7
|
+
* or bullets. No root tag — the result is read as a continuation of the prompt. */
|
|
7
8
|
export declare function renderResult(result: Record<string, unknown>, help: LeafHelp): string;
|
|
8
9
|
/** Render a structured failure as an instruction-shaped block: what broke, what
|
|
9
10
|
* was received, and the recovery road sign — the same recovery info the JSON
|
package/dist/core/render.js
CHANGED
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
// Default stdout rendering: the result a leaf returns is written FOR the model
|
|
2
2
|
// to act on, not as data to parse. Output is a continuation of the agent's
|
|
3
|
-
// prompt —
|
|
4
|
-
//
|
|
3
|
+
// prompt — PLAIN MARKDOWN, no XML wrapper. Scalars list as `- name: value`
|
|
4
|
+
// bullets in output-schema order, prose fields render as paragraphs, and arrays
|
|
5
|
+
// become a count lead-in plus a markdown table (objects) or bullets (scalars).
|
|
6
|
+
// The raw JSON object is still available behind the `--json` global for tooling.
|
|
5
7
|
//
|
|
6
8
|
// A leaf may hand `defineLeaf` a bespoke `render(result)` for instruction-shaped
|
|
7
|
-
// output (see `node new`, `push`, `feed read`).
|
|
8
|
-
// the schema-driven generic renderer here, so
|
|
9
|
-
// without a hand-written renderer.
|
|
9
|
+
// output that leads with the outcome (see `node new`, `push`, `feed read`).
|
|
10
|
+
// Everything else falls through to the schema-driven generic renderer here, so
|
|
11
|
+
// every command obeys the markdown paradigm without a hand-written renderer.
|
|
12
|
+
// `renderError` is the one sanctioned exception: a failure is a different domain
|
|
13
|
+
// from a result, so it keeps its `<error>` block.
|
|
10
14
|
// Field names whose value is prose the agent reads, not a scalar it keys on —
|
|
11
|
-
// these always render as their own
|
|
15
|
+
// these always render as their own paragraph, never as a `- name: value` bullet.
|
|
12
16
|
const PROSE_FIELDS = new Set([
|
|
13
17
|
'follow_up', 'digest', 'message', 'next', 'result', 'content', 'body', 'guide',
|
|
14
18
|
'summary', 'note', 'reason', 'detail', 'details', 'hint', 'instruction',
|
|
@@ -37,20 +41,17 @@ function isProse(name, v) {
|
|
|
37
41
|
return true;
|
|
38
42
|
return v.includes('\n') || v.length > 80;
|
|
39
43
|
}
|
|
40
|
-
/**
|
|
41
|
-
*
|
|
42
|
-
function
|
|
43
|
-
|
|
44
|
-
return true;
|
|
45
|
-
return typeof v === 'string' && v.length <= 40 && !v.includes(' ') && !v.includes('\n');
|
|
46
|
-
}
|
|
47
|
-
/** Root element name = the command path with spaces hyphenated. */
|
|
48
|
-
function rootTag(help) {
|
|
49
|
-
return help.name.trim().split(/\s+/).join('-').replace(/[^a-zA-Z0-9_-]/g, '') || 'result';
|
|
44
|
+
/** Keep a value safe inside a markdown table cell: pipes would split columns,
|
|
45
|
+
* newlines would break the row. */
|
|
46
|
+
function cellEsc(v) {
|
|
47
|
+
return scalarStr(v).replace(/\|/g, '\\|').replace(/\n/g, ' ');
|
|
50
48
|
}
|
|
49
|
+
/** An array renders as a count lead-in followed by the collection: a markdown
|
|
50
|
+
* table when every element is an object, plain bullets otherwise. */
|
|
51
51
|
function renderArray(name, arr) {
|
|
52
52
|
if (arr.length === 0)
|
|
53
|
-
return
|
|
53
|
+
return `0 ${name}.`;
|
|
54
|
+
const lead = `${arr.length} ${name}:`;
|
|
54
55
|
const allObjects = arr.every((x) => x !== null && typeof x === 'object' && !Array.isArray(x));
|
|
55
56
|
if (allObjects) {
|
|
56
57
|
const rows = arr;
|
|
@@ -61,55 +62,51 @@ function renderArray(name, arr) {
|
|
|
61
62
|
cols.push(k);
|
|
62
63
|
const head = `| ${cols.join(' | ')} |`;
|
|
63
64
|
const sep = `| ${cols.map(() => '---').join(' | ')} |`;
|
|
64
|
-
const body = rows.map((r) => `| ${cols.map((c) =>
|
|
65
|
-
return
|
|
65
|
+
const body = rows.map((r) => `| ${cols.map((c) => cellEsc(r[c])).join(' | ')} |`).join('\n');
|
|
66
|
+
return `${lead}\n\n${head}\n${sep}\n${body}`;
|
|
66
67
|
}
|
|
67
68
|
const items = arr.map((x) => `- ${scalarStr(x)}`).join('\n');
|
|
68
|
-
return
|
|
69
|
+
return `${lead}\n${items}`;
|
|
69
70
|
}
|
|
70
|
-
/** Schema-driven fallback: turn a result object into agent-ready
|
|
71
|
+
/** Schema-driven fallback: turn a result object into agent-ready plain markdown.
|
|
71
72
|
* Field order follows the leaf's declared output schema; any extra keys append
|
|
72
|
-
* after. Scalars
|
|
73
|
-
* become
|
|
73
|
+
* after. Scalars (and nested objects) list as `- name: value` bullets, prose
|
|
74
|
+
* fields render as paragraphs, and arrays become a count lead-in plus a table
|
|
75
|
+
* or bullets. No root tag — the result is read as a continuation of the prompt. */
|
|
74
76
|
export function renderResult(result, help) {
|
|
75
77
|
const order = help.output.map((f) => f.name);
|
|
76
78
|
for (const k of Object.keys(result))
|
|
77
79
|
if (!order.includes(k))
|
|
78
80
|
order.push(k);
|
|
79
|
-
const attrs = [];
|
|
80
81
|
const bullets = [];
|
|
81
|
-
const
|
|
82
|
+
const proseFields = [];
|
|
83
|
+
const arrays = [];
|
|
82
84
|
for (const name of order) {
|
|
83
85
|
const v = result[name];
|
|
84
86
|
if (v === undefined || v === null)
|
|
85
87
|
continue;
|
|
86
88
|
if (Array.isArray(v)) {
|
|
87
|
-
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
if (typeof v === 'object') {
|
|
91
|
-
blocks.push(`<${name}>\n${JSON.stringify(v, null, 2)}\n</${name}>`);
|
|
89
|
+
arrays.push(renderArray(name, v));
|
|
92
90
|
continue;
|
|
93
91
|
}
|
|
94
92
|
if (isProse(name, v)) {
|
|
95
|
-
|
|
96
|
-
continue;
|
|
97
|
-
}
|
|
98
|
-
if (isAttr(v)) {
|
|
99
|
-
attrs.push(`${name}="${attrEsc(v)}"`);
|
|
93
|
+
proseFields.push({ name, text: v });
|
|
100
94
|
continue;
|
|
101
95
|
}
|
|
96
|
+
// Scalars and nested objects alike list as a bullet (objects via compact JSON).
|
|
102
97
|
bullets.push(`- ${name}: ${scalarStr(v)}`);
|
|
103
98
|
}
|
|
104
|
-
const tag = rootTag(help);
|
|
105
|
-
const open = attrs.length > 0 ? `<${tag} ${attrs.join(' ')}>` : `<${tag}>`;
|
|
106
99
|
const parts = [];
|
|
107
100
|
if (bullets.length > 0)
|
|
108
101
|
parts.push(bullets.join('\n'));
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const
|
|
112
|
-
|
|
102
|
+
// Label prose only when more than one block would otherwise blur together.
|
|
103
|
+
const labelProse = proseFields.length > 1;
|
|
104
|
+
for (const { name, text } of proseFields) {
|
|
105
|
+
parts.push(labelProse ? `**${name}:** ${text}` : text);
|
|
106
|
+
}
|
|
107
|
+
for (const block of arrays)
|
|
108
|
+
parts.push(block);
|
|
109
|
+
return parts.join('\n\n');
|
|
113
110
|
}
|
|
114
111
|
/** Render a structured failure as an instruction-shaped block: what broke, what
|
|
115
112
|
* was received, and the recovery road sign — the same recovery info the JSON
|
package/dist/core/resolver.js
CHANGED
|
@@ -403,7 +403,7 @@ function formatNotFoundMessage(rawName, skillName, pluginQualifier) {
|
|
|
403
403
|
lines.push(` did you mean: ${formatted.join(', ')}`);
|
|
404
404
|
}
|
|
405
405
|
else {
|
|
406
|
-
lines.push(' run `crtr
|
|
406
|
+
lines.push(' run `crtr memory list` or `crtr memory find "<query>"` to discover skills');
|
|
407
407
|
}
|
|
408
408
|
return lines.join('\n');
|
|
409
409
|
}
|
|
@@ -56,6 +56,6 @@ export declare function buildWakeBearings(origin: WakeOrigin): string;
|
|
|
56
56
|
* any copied-in persona) followed by the `<crtr-context>` bearings block. Base
|
|
57
57
|
* framing rides for EVERY node; the across-cycles context-dir note is added
|
|
58
58
|
* ONLY for an orchestrator (by mode) — the one node whose dir a future cycle
|
|
59
|
-
* resumes from. The `## References` block
|
|
60
|
-
* node-local docs
|
|
59
|
+
* resumes from. The `## References` block carries the substrate reference docs
|
|
60
|
+
* + node-local docs. */
|
|
61
61
|
export declare function buildContextBearings(nodeId: string): string;
|
|
@@ -145,8 +145,8 @@ export function buildWakeBearings(origin) {
|
|
|
145
145
|
* any copied-in persona) followed by the `<crtr-context>` bearings block. Base
|
|
146
146
|
* framing rides for EVERY node; the across-cycles context-dir note is added
|
|
147
147
|
* ONLY for an orchestrator (by mode) — the one node whose dir a future cycle
|
|
148
|
-
* resumes from. The `## References` block
|
|
149
|
-
* node-local docs
|
|
148
|
+
* resumes from. The `## References` block carries the substrate reference docs
|
|
149
|
+
* + node-local docs. */
|
|
150
150
|
export function buildContextBearings(nodeId) {
|
|
151
151
|
const identity = buildIdentityAssertion(nodeId);
|
|
152
152
|
const dir = contextDir(nodeId);
|
|
@@ -14,8 +14,7 @@ export declare function renderPreferencesSection(nodeId: string): string;
|
|
|
14
14
|
* drops it when ''). Holds every eligible `kind: reference` resolver doc at its
|
|
15
15
|
* `system-prompt-visibility` (reference boot default is `none`, so only
|
|
16
16
|
* author-promoted references show) PLUS the node-local memory docs (any kind),
|
|
17
|
-
* each a `###` sub-section.
|
|
18
|
-
* nothing is eligible.
|
|
17
|
+
* each a `###` sub-section. Returns '' when nothing is eligible.
|
|
19
18
|
*
|
|
20
19
|
* DEFENSIVE: each doc is rendered in its own try/catch so a single malformed
|
|
21
20
|
* doc drops only itself (with a loud stderr warning naming the offending path),
|