@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.
Files changed (43) hide show
  1. package/dist/builtin-memory/crouter-development/marketplaces.md +2 -4
  2. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
  3. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
  4. package/dist/builtin-memory/crouter-development/personas.md +2 -6
  5. package/dist/builtin-memory/crouter-development/plugins.md +2 -4
  6. package/dist/builtin-memory/design.md +2 -4
  7. package/dist/builtin-memory/development.md +3 -3
  8. package/dist/builtin-memory/planning.md +3 -4
  9. package/dist/builtin-memory/spec.md +3 -10
  10. package/dist/builtin-personas/orchestration-kernel.md +1 -1
  11. package/dist/commands/human/queue.js +2 -2
  12. package/dist/commands/memory/__tests__/lint-schema.test.d.ts +1 -0
  13. package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
  14. package/dist/commands/memory/find.js +10 -15
  15. package/dist/commands/memory/lint.d.ts +7 -0
  16. package/dist/commands/memory/lint.js +10 -1
  17. package/dist/commands/memory/shared.js +1 -2
  18. package/dist/commands/memory/write.js +6 -9
  19. package/dist/commands/node.js +47 -25
  20. package/dist/commands/push.js +10 -15
  21. package/dist/commands/tmux-spread.js +16 -1
  22. package/dist/commands/view-cycle.js +9 -5
  23. package/dist/commands/view-run.js +3 -2
  24. package/dist/core/__tests__/cascade-close.test.js +3 -2
  25. package/dist/core/__tests__/context-intro.test.js +23 -27
  26. package/dist/core/__tests__/detach-focus.test.js +2 -2
  27. package/dist/core/__tests__/live-mutation-verbs.test.js +5 -5
  28. package/dist/core/__tests__/memory.test.js +4 -4
  29. package/dist/core/__tests__/subscription-delivery.test.js +3 -3
  30. package/dist/core/bootstrap.js +18 -14
  31. package/dist/core/help.d.ts +2 -2
  32. package/dist/core/help.js +1 -1
  33. package/dist/core/render.d.ts +4 -3
  34. package/dist/core/render.js +38 -41
  35. package/dist/core/resolver.js +1 -1
  36. package/dist/core/runtime/bearings.d.ts +2 -2
  37. package/dist/core/runtime/bearings.js +2 -2
  38. package/dist/core/substrate/render.d.ts +1 -2
  39. package/dist/core/substrate/render.js +9 -12
  40. package/dist/core/substrate/schema.d.ts +18 -13
  41. package/dist/core/substrate/schema.js +12 -11
  42. package/dist/pi-extensions/canvas-context-intro.js +5 -3
  43. 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) => `<spread node window="${r['window'] ?? ''}" joined="${r['children_joined'].length}" overflow="${r['overflow'].length}" focused="${r['focused']}"/>`,
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) => r['cycled'] === true
121
- ? `<view-cycled to="${r['view']}" from="${r['from'] ?? ''}"/>`
122
- : r['from'] !== undefined
123
- ? `<view-cycle-noop>no other view to switch to</view-cycle-noop>`
124
- : `<view-cycle-noop>this pane is not hosting a view</view-cycle-noop>`,
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 `<view-opened mode="window" view="${result['view']}" window="${result['window']}" pane="${result['pane']}"/>`;
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 `<view-opened mode="split" view="${result['view']}" pane="${result['pane']}"/>`;
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 4 closed, 0 spared.
107
- assert.match(closeRes.stdout, new RegExp(`<closed id="${B}" count="4" spared="0"\\s*/>`), `close report names B as root, count=4, spared=0\n${closeRes.stdout}`);
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) in
4
- // place of the removed `<memory>` block no `label · dir` stanzas, no
5
- // `(empty)` markers, no MEMORY.md index inlining. Orchestrators add the
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 (no <memory>, no label·dir stanzas), NO across-cycles framing', () => {
40
- // Bug-regression: review finding M1 — buildContextBearings was changed from
41
- // buildMemoryBlock (<memory> + label·dir per-store stanzas) to
42
- // renderReferencesBlock (## References + ### <name> sub-sections). This test
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: when testing\nwhy: regression fixture\nsystem-prompt-visibility: preview\n---\nTest body.\n');
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
- // New contract: NO <memory> block replaced by ## References.
49
+ // Reference content renders ONLY as ## References + ### sub-sections.
53
50
  assert.doesNotMatch(block, /<memory>/, 'no <memory> block');
54
- // New contract: NO label·dir stanza headers (the removed per-store format).
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
- // New contract: NO (empty) placeholder (the removed empty-store marker).
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; the MEMORY.md index never renders', () => {
66
- // Bug-regression: review finding M1 — buildContextBearings replaced the old
67
- // <memory> pointer-line block with ## References. Node-local substrate docs
68
- // render as ### sub-sections at their rung; the old index file never surfaces.
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: when the build flakes\nwhy: first run fails\nsystem-prompt-visibility: preview\n---\nFirst run always fails; rerun once.\n');
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, /when the build flakes, read this reference\. first run fails\./, 'preview rung renders the routing line');
85
- // The index file itself never renders: no header comment, no pointer line, no path.
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 three-store `label · dir`
94
- // stanzas and (empty) markers are gone, and node-local docs are NOT filtered
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 index paths.
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, /detached="true"/, `the agent was detached\n${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, /detached="true"/, `the agent was detached\n${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, /<demoted /, `demote rendered\n${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 `<recycled ... finalized=".." new_root=".."/>` (not JSON).
87
- assert.match(res.stdout, /<recycled /, `recycle recycled the pane\n${res.stdout}`);
88
- const newRoot = /new_root="([^"]+)"/.exec(res.stdout)?.[1];
89
- const finalized = /finalized="true"/.test(res.stdout);
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, not MEMORY.md
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
- // The old MEMORY.md pointer-line flow must NOT be present.
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 old <memory> block reference must NOT be present in guidance.
59
- assert.ok(!guidance.includes('<memory>'), 'guidance does not point at the removed <memory> block');
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, /mode="passive"/, 'P→T wired PASSIVE');
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, /mode="active"/, 'P→K wired ACTIVE');
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, /mode="passive"/, 'X→K wired PASSIVE-only (its ONLY subscription)');
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;
@@ -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: 'Capture, list, search, and load skills via the crtr CLI. Skills 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 skill. Triggers: "save", "remember", "build context for", "what skills do we have", "skill for X".'
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 — skill router
39
+ # /crtr-skills — crtr memory router
40
40
 
41
- Skills = durable agent memory. Written for **future LLM sessions**, not the
42
- user. \`crtr skill\` is the index discoverable via list/search/grep.
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 skill create $ARGUMENTS\` pick template (primer/playbook/freeform)
48
- \`crtr skill template <type> $ARGUMENTS\` for the full workflow. Follow it
49
- directly.
50
- - **Find** ("what do we have on X"): \`crtr skill search "$ARGUMENTS"\` →
51
- \`crtr skill show <name>\` on the best hit.
52
- - **Load by name**: \`crtr skill show <name>\`.
53
- - **List all**: \`crtr skill list\`.
54
- - **Anything else**: \`crtr skill\` (no args) prints the full workflow guide.
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
- - Don't load \`create\` and \`template\` outputs in the same turn (progressive
62
- disclosure). \`create\` decides type; \`template\` returns the workflow.
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
  }
@@ -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. `<skills count="42">…</skills>`. Aggregate, never an
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. `<skills count="42">…</skills>`. Renderer
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
- // <skills count="42">…</skills>) — emit it as-is, nested in the command.
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);
@@ -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 XML+markdown.
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 become attributes or a bullet list, prose and collections
6
- * become their own fenced blocks. */
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
@@ -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 — light XML fences around markdown, prose where prose belongs. The
4
- // raw JSON object is still available behind the `--json` global for tooling.
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`). Everything else falls through to
8
- // the schema-driven generic renderer here, so every command obeys the paradigm
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 fenced block, never as a tag attribute.
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
- /** Short, space-free scalars ride as attributes on the open tag (ids, statuses,
41
- * counts); everything else that isn't prose lists as a markdown bullet. */
42
- function isAttr(v) {
43
- if (typeof v === 'number' || typeof v === 'boolean')
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 `<${name} count="0"></${name}>`;
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) => scalarStr(r[c])).join(' | ')} |`).join('\n');
65
- return `<${name} count="${arr.length}">\n${head}\n${sep}\n${body}\n</${name}>`;
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 `<${name} count="${arr.length}">\n${items}\n</${name}>`;
69
+ return `${lead}\n${items}`;
69
70
  }
70
- /** Schema-driven fallback: turn a result object into agent-ready XML+markdown.
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 become attributes or a bullet list, prose and collections
73
- * become their own fenced blocks. */
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 blocks = [];
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
- blocks.push(renderArray(name, v));
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
- blocks.push(`<${name}>\n${v}\n</${name}>`);
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
- if (blocks.length > 0)
110
- parts.push(blocks.join('\n\n'));
111
- const inner = parts.join('\n\n');
112
- return inner !== '' ? `${open}\n${inner}\n</${tag}>` : `${open}</${tag}>`;
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
@@ -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 skill find list` or `crtr skill find search <query>` to discover skills');
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 (substrate reference docs +
60
- * node-local docs) replaces the old `<memory>` block. */
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 (substrate reference docs +
149
- * node-local docs) replaces the old `<memory>` block. */
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. Replaces the old `<memory>` block. Returns '' when
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),