@crouton-kit/crouter 0.3.20 → 0.3.22

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 (144) 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/canvas-history/read.d.ts +1 -0
  12. package/dist/commands/canvas-history/read.js +61 -0
  13. package/dist/commands/canvas-history/search.d.ts +1 -0
  14. package/dist/commands/canvas-history/search.js +270 -0
  15. package/dist/commands/canvas-history/show.d.ts +1 -0
  16. package/dist/commands/canvas-history/show.js +79 -0
  17. package/dist/commands/canvas-history.d.ts +2 -0
  18. package/dist/commands/canvas-history.js +33 -0
  19. package/dist/commands/canvas.js +3 -2
  20. package/dist/commands/human/queue.js +2 -2
  21. package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
  22. package/dist/commands/memory/find.js +14 -52
  23. package/dist/commands/memory/lint.d.ts +7 -0
  24. package/dist/commands/memory/lint.js +29 -35
  25. package/dist/commands/memory/list.js +8 -18
  26. package/dist/commands/memory/read.js +4 -37
  27. package/dist/commands/memory/shared.js +1 -2
  28. package/dist/commands/memory/write.js +6 -9
  29. package/dist/commands/node.js +47 -25
  30. package/dist/commands/pkg/bridge.d.ts +1 -0
  31. package/dist/commands/pkg/bridge.js +137 -0
  32. package/dist/commands/pkg/plugin-inspect.js +7 -6
  33. package/dist/commands/pkg.js +2 -1
  34. package/dist/commands/push.js +10 -15
  35. package/dist/commands/skill/author.js +35 -44
  36. package/dist/commands/skill/shared.d.ts +1 -5
  37. package/dist/commands/skill/shared.js +9 -63
  38. package/dist/commands/skill.js +5 -6
  39. package/dist/commands/sys/doctor.js +6 -144
  40. package/dist/commands/tmux-spread.js +16 -1
  41. package/dist/commands/view-cycle.js +9 -5
  42. package/dist/commands/view-run.js +3 -2
  43. package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
  44. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
  45. package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
  46. package/dist/core/__tests__/child-death-wake.test.js +133 -185
  47. package/dist/core/__tests__/context-intro.test.js +23 -27
  48. package/dist/core/__tests__/daemon-liveness.test.js +28 -258
  49. package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
  50. package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
  51. package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
  52. package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
  53. package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
  54. package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
  55. package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
  56. package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +4 -3
  57. package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
  58. package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
  59. package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +7 -7
  60. package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
  61. package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
  62. package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
  63. package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
  64. package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
  65. package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
  66. package/dist/core/__tests__/full/spike-harness.test.d.ts +1 -0
  67. package/dist/core/__tests__/full/spike-harness.test.js +117 -0
  68. package/dist/core/__tests__/grace-clock.test.js +72 -75
  69. package/dist/core/__tests__/helpers/harness.d.ts +35 -1
  70. package/dist/core/__tests__/helpers/harness.js +70 -12
  71. package/dist/core/__tests__/live-mutation-verbs.test.js +100 -105
  72. package/dist/core/__tests__/live-mutation.test.js +111 -134
  73. package/dist/core/__tests__/memory.test.js +4 -4
  74. package/dist/core/__tests__/revive.test.js +96 -86
  75. package/dist/core/__tests__/subscription-delivery.test.js +116 -138
  76. package/dist/core/__tests__/wake-origin.test.js +54 -50
  77. package/dist/core/bootstrap.js +18 -14
  78. package/dist/core/bridge-map.d.ts +19 -0
  79. package/dist/core/bridge-map.js +73 -0
  80. package/dist/core/canvas/history.d.ts +91 -0
  81. package/dist/core/canvas/history.js +389 -0
  82. package/dist/core/canvas/index.d.ts +1 -0
  83. package/dist/core/canvas/index.js +1 -0
  84. package/dist/core/config.js +3 -22
  85. package/dist/core/help.d.ts +2 -2
  86. package/dist/core/help.js +1 -1
  87. package/dist/core/memory-resolver.d.ts +9 -2
  88. package/dist/core/memory-resolver.js +47 -7
  89. package/dist/core/render.d.ts +4 -3
  90. package/dist/core/render.js +38 -41
  91. package/dist/core/resolver.d.ts +1 -30
  92. package/dist/core/resolver.js +6 -546
  93. package/dist/core/runtime/bearings.d.ts +2 -2
  94. package/dist/core/runtime/bearings.js +2 -2
  95. package/dist/core/runtime/persona.js +5 -7
  96. package/dist/core/scope.d.ts +7 -5
  97. package/dist/core/scope.js +16 -17
  98. package/dist/core/substrate/ceiling.d.ts +28 -0
  99. package/dist/core/substrate/ceiling.js +87 -0
  100. package/dist/core/substrate/index.d.ts +2 -0
  101. package/dist/core/substrate/index.js +1 -0
  102. package/dist/core/substrate/on-read.js +18 -2
  103. package/dist/core/substrate/render.d.ts +3 -5
  104. package/dist/core/substrate/render.js +68 -59
  105. package/dist/core/substrate/schema.d.ts +18 -13
  106. package/dist/core/substrate/schema.js +12 -11
  107. package/dist/pi-extensions/canvas-context-intro.js +5 -3
  108. package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
  109. package/dist/types.d.ts +3 -18
  110. package/dist/types.js +0 -8
  111. package/package.json +4 -3
  112. package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
  113. package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
  114. package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
  115. package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
  116. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
  117. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
  118. package/dist/builtin-skills/skills/design/SKILL.md +0 -51
  119. package/dist/builtin-skills/skills/development/SKILL.md +0 -109
  120. package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
  121. package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
  122. package/dist/commands/skill/state.d.ts +0 -3
  123. package/dist/commands/skill/state.js +0 -71
  124. package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
  125. package/dist/core/__tests__/broker-dialogs.test.js +0 -126
  126. package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
  127. package/dist/core/__tests__/resolver.test.js +0 -181
  128. package/dist/core/__tests__/spike-harness.test.js +0 -242
  129. /package/dist/{core/__tests__/broker-attach-limits.test.d.ts → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
  130. /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
  131. /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
  132. /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
  133. /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
  134. /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
  135. /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
  136. /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
  137. /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
  138. /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
  139. /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
  140. /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
  141. /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
  142. /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
  143. /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
  144. /package/dist/core/__tests__/{spike-harness.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
@@ -164,7 +164,14 @@ const nodeNew = defineLeaf({
164
164
  : childFollowUp(parent ?? process.env['CRTR_NODE_ID']),
165
165
  };
166
166
  },
167
- render: (r) => `<spawned name="${r['name']}" id="${r['node_id']}" status="${r['status']}">\n${r['follow_up']}\n</spawned>`,
167
+ render: (r) => {
168
+ const win = r['window'] ? ` in window ${r['window']}` : '';
169
+ const lines = [`Spawned "${r['name']}" (${r['node_id']}) — ${r['status']}${win}.`];
170
+ if (r['session'])
171
+ lines.push(`- session: ${r['session']}`);
172
+ lines.push('', String(r['follow_up']));
173
+ return lines.join('\n');
174
+ },
168
175
  });
169
176
  // ---------------------------------------------------------------------------
170
177
  // node list — the active canvas (or a status slice)
@@ -352,9 +359,16 @@ const nodeRecycle = defineLeaf({
352
359
  const res = await recycleNode(id, pane);
353
360
  return { recycled: res.recycled, node_id: id, finalized: res.finalized, delivered: res.delivered.length, new_root: res.newRoot ?? undefined };
354
361
  },
355
- render: (r) => r['recycled'] === true
356
- ? `<recycled id="${r['node_id']}" finalized="${r['finalized']}" delivered="${r['delivered']}" new_root="${r['new_root'] ?? ''}"/>`
357
- : `<recycle-failed id="${r['node_id'] ?? ''}">not in tmux, or no agent in this pane</recycle-failed>`,
362
+ render: (r) => {
363
+ if (r['recycled'] !== true)
364
+ return `Recycle failed for ${r['node_id'] ?? '?'}not in tmux, or no agent in this pane.`;
365
+ const lines = [`Recycled the pane — finished ${r['node_id']}.`, `- finalized: ${r['finalized']}`];
366
+ if (r['finalized'] === true)
367
+ lines.push(`- delivered: ${r['delivered']}`);
368
+ if (r['new_root'])
369
+ lines.push(`- new root: ${r['new_root']}`);
370
+ return lines.join('\n');
371
+ },
358
372
  });
359
373
  // ---------------------------------------------------------------------------
360
374
  // node close — close a node + cascade-cancel its exclusive subtree (Alt+C → x)
@@ -398,9 +412,12 @@ const nodeClose = defineLeaf({
398
412
  const res = closeNode(id);
399
413
  return { closed: true, node_id: res.root, count: res.closed.length, closed_ids: res.closed, spared: res.spared };
400
414
  },
401
- render: (r) => r['closed'] === true
402
- ? `<closed id="${r['node_id']}" count="${r['count']}" spared="${r['spared']?.length ?? 0}"/>`
403
- : `<close-failed/>`,
415
+ render: (r) => {
416
+ if (r['closed'] !== true)
417
+ return 'Close failed — no node found in this pane to close.';
418
+ const spared = r['spared']?.length ?? 0;
419
+ return `Closed ${r['node_id']} and its exclusive subtree — ${r['count']} node(s) closed${spared > 0 ? `, ${spared} spared (still managed from outside the subtree)` : ''}.`;
420
+ },
404
421
  });
405
422
  // ---------------------------------------------------------------------------
406
423
  // node cycle — DFS-walk the canvas one window at a time (Alt+] / Alt+[)
@@ -486,8 +503,8 @@ const nodeCycle = defineLeaf({
486
503
  return { focused: res.focused, node_id: targetId, name: target.name, from: fromId };
487
504
  },
488
505
  render: (r) => r['focused'] === true
489
- ? `<cycled to="${r['node_id']}" name="${r['name'] ?? ''}" from="${r['from'] ?? ''}"/>`
490
- : `<cycle-noop>no other live node to focus</cycle-noop>`,
506
+ ? `Cycled to "${r['name'] ?? ''}" (${r['node_id']}) from ${r['from'] ?? '?'}.`
507
+ : 'No other live node to focus.',
491
508
  });
492
509
  // ---------------------------------------------------------------------------
493
510
  // node msg — direct-address any node at a wake tier (wakes a dormant target)
@@ -582,7 +599,7 @@ const nodeSubscribe = defineLeaf({
582
599
  subscribe(subscriber, publisher, !passive);
583
600
  return { subscribed: true, subscriber, publisher, mode: passive ? 'passive' : 'active' };
584
601
  },
585
- render: (r) => `<subscribed subscriber="${r['subscriber']}" publisher="${r['publisher']}" mode="${r['mode']}"/>`,
602
+ render: (r) => `Subscribed ${r['subscriber']} ${r['publisher']} (${r['mode']}).`,
586
603
  });
587
604
  const nodeUnsubscribe = defineLeaf({
588
605
  name: 'unsubscribe',
@@ -609,7 +626,7 @@ const nodeUnsubscribe = defineLeaf({
609
626
  unsubscribe(subscriber, publisher);
610
627
  return { unsubscribed: true, subscriber, publisher };
611
628
  },
612
- render: (r) => `<unsubscribed subscriber="${r['subscriber']}" publisher="${r['publisher']}"/>`,
629
+ render: (r) => `Unsubscribed ${r['subscriber']} from ${r['publisher']}.`,
613
630
  });
614
631
  // ---------------------------------------------------------------------------
615
632
  // node promote — become an orchestrator (the worker→orchestrator polymorph)
@@ -728,7 +745,7 @@ const nodeDemote = defineLeaf({
728
745
  const res = setLifecycle(id, 'terminal', { pane, detach: input['detach'] === true });
729
746
  return { node_id: res.node_id, lifecycle: res.lifecycle, detached: res.detached };
730
747
  },
731
- render: (r) => `<demoted node="${r['node_id']}" lifecycle="${r['lifecycle']}"${r['detached'] === true ? ' detached="true"' : ''}/>`,
748
+ render: (r) => `Demoted ${r['node_id']} lifecycle now ${r['lifecycle']} (in place)${r['detached'] === true ? ', relocated to the background crtr session' : ''}.`,
732
749
  });
733
750
  // ---------------------------------------------------------------------------
734
751
  // node lifecycle — flip the lifecycle axis (terminal ↔ resident), independent
@@ -765,7 +782,7 @@ const nodeLifecycle = defineLeaf({
765
782
  const res = setLifecycle(id, value, { pane, detach: input['detach'] === true });
766
783
  return { node_id: res.node_id, lifecycle: res.lifecycle, detached: res.detached };
767
784
  },
768
- render: (r) => `<lifecycle node="${r['node_id']}" set="${r['lifecycle']}"${r['detached'] === true ? ' detached="true"' : ''}/>`,
785
+ render: (r) => `Set ${r['node_id']} lifecycle → ${r['lifecycle']}${r['detached'] === true ? ', relocated to the background crtr session' : ''}.`,
769
786
  });
770
787
  // ---------------------------------------------------------------------------
771
788
  // node yield — refresh: discard context, revive fresh against the roadmap
@@ -908,10 +925,6 @@ function etaHint(fireAt, now) {
908
925
  return ` (~${hrs}h)`;
909
926
  return ` (~${Math.round(ms / 86_400_000)}d)`;
910
927
  }
911
- /** Escape a value for a rendered XML attribute (list rows carry free-text notes). */
912
- function xmlAttr(s) {
913
- return s.replace(/&/g, '&amp;').replace(/"/g, '&quot;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
914
- }
915
928
  // node wake at ---------------------------------------------------------------
916
929
  const nodeWakeAt = defineLeaf({
917
930
  name: 'at',
@@ -1011,7 +1024,10 @@ const nodeWakeAt = defineLeaf({
1011
1024
  : `Bare self-alarm armed. You wake in a fresh window at ${fireAt}${eta}. End your turn now to go dormant; do not push final. The wake re-reads your roadmap.`;
1012
1025
  return { id, kind, fires_at: fireAt, recur: cadenceDisplay(recur), target, guidance };
1013
1026
  },
1014
- render: (r) => `<wake-armed id="${r['id']}" kind="${r['kind']}" fires-at="${r['fires_at']}" recur="${r['recur']}" target="${r['target']}">\n${r['guidance']}\n</wake-armed>`,
1027
+ render: (r) => {
1028
+ const recur = r['recur'] !== 'none' ? `, recurs ${r['recur']}` : '';
1029
+ return `Armed ${r['kind']} wake ${r['id']} — fires ${r['fires_at']}${recur}, target ${r['target']}.\n\n${r['guidance']}`;
1030
+ },
1015
1031
  });
1016
1032
  // node wake until ------------------------------------------------------------
1017
1033
  const nodeWakeUntil = defineLeaf({
@@ -1063,7 +1079,7 @@ const nodeWakeUntil = defineLeaf({
1063
1079
  const guidance = `Deadline armed. You wake on the first inbox message, or at ${w.fireAt}${eta} if none arrives. Now delegate / end your turn to go dormant — whichever fires first cancels the other.`;
1064
1080
  return { id, fires_at: w.fireAt, target: 'self', guidance };
1065
1081
  },
1066
- render: (r) => `<deadline-armed id="${r['id']}" fires-at="${r['fires_at']}" target="${r['target']}">\n${r['guidance']}\n</deadline-armed>`,
1082
+ render: (r) => `Armed deadline ${r['id']} fires ${r['fires_at']}, target ${r['target']}.\n\n${r['guidance']}`,
1067
1083
  });
1068
1084
  // node wake spawn -----------------------------------------------------------
1069
1085
  const nodeWakeSpawn = defineLeaf({
@@ -1164,7 +1180,10 @@ const nodeWakeSpawn = defineLeaf({
1164
1180
  : `Deferred spawn armed: a fresh ${kind} node is born at ${fireAt}${eta}. No node exists yet — inspect/cancel via \`crtr node wake list\` / \`crtr node wake cancel ${id}\`. Pick up other work or end your turn.`;
1165
1181
  return { id, kind, fires_at: fireAt, recur: cadenceDisplay(recur), guidance };
1166
1182
  },
1167
- render: (r) => `<spawn-deferred id="${r['id']}" kind="${r['kind']}" fires-at="${r['fires_at']}" recur="${r['recur']}">\n${r['guidance']}\n</spawn-deferred>`,
1183
+ render: (r) => {
1184
+ const recur = r['recur'] !== 'none' ? `, recurs ${r['recur']}` : '';
1185
+ return `Armed deferred spawn ${r['id']} — a ${r['kind']} node, fires ${r['fires_at']}${recur}.\n\n${r['guidance']}`;
1186
+ },
1168
1187
  });
1169
1188
  // node wake list -------------------------------------------------------------
1170
1189
  const nodeWakeList = defineLeaf({
@@ -1246,10 +1265,13 @@ const nodeWakeList = defineLeaf({
1246
1265
  },
1247
1266
  render: (r) => {
1248
1267
  const wakes = r['wakes'];
1249
- const rows = wakes.map((w) => ` <wake id="${w['id']}" kind="${w['kind']}" next="${w['next']}" recur="${xmlAttr(String(w['recur']))}" target="${xmlAttr(String(w['target']))}" owner="${xmlAttr(String(w['owner']))}" note="${xmlAttr(String(w['note']))}"/>`);
1250
- const body = rows.length > 0 ? '\n' + rows.join('\n') + '\n' : '\n';
1251
- return (`<wakes scope="${xmlAttr(String(r['scope']))}" count="${wakes.length}">${body}</wakes>\n` +
1252
- '<follow_up>Cancel one with `crtr node wake cancel <id>`. Fired one-shots are gone; a recurrence shows its NEXT fire, not past slots.</follow_up>');
1268
+ const follow = 'Cancel one with `crtr node wake cancel <id>`. Fired one-shots are gone; a recurrence shows its NEXT fire, not past slots.';
1269
+ if (wakes.length === 0)
1270
+ return `No pending wakes in scope ${r['scope']}.\n\n${follow}`;
1271
+ const cell = (v) => String(v).replace(/\|/g, '\\|').replace(/\n/g, ' ');
1272
+ const head = '| id | kind | next | recur | target | owner | note |\n| --- | --- | --- | --- | --- | --- | --- |';
1273
+ const rows = wakes.map((w) => `| ${cell(w['id'])} | ${cell(w['kind'])} | ${cell(w['next'])} | ${cell(w['recur'])} | ${cell(w['target'])} | ${cell(w['owner'])} | ${cell(w['note'])} |`);
1274
+ return `${wakes.length} pending wake(s) in scope ${r['scope']}.\n\n${head}\n${rows.join('\n')}\n\n${follow}`;
1253
1275
  },
1254
1276
  });
1255
1277
  // node wake cancel -----------------------------------------------------------
@@ -1276,7 +1298,7 @@ const nodeWakeCancel = defineLeaf({
1276
1298
  cancelWake(id);
1277
1299
  return { id, was_pending: wasPending };
1278
1300
  },
1279
- render: (r) => `<wake-canceled id="${r['id']}" was-pending="${r['was_pending']}"/>`,
1301
+ render: (r) => `Canceled wake ${r['id']}${r['was_pending'] === true ? '' : ' (already gone — no-op)'}.`,
1280
1302
  });
1281
1303
  // node wake (branch) ---------------------------------------------------------
1282
1304
  const nodeWake = defineBranch({
@@ -0,0 +1 @@
1
+ export declare const bridgeBranch: import("../../core/command.js").BranchDef;
@@ -0,0 +1,137 @@
1
+ // `crtr pkg bridge` — generate crouter substrate docs from a Claude skills root
2
+ // and file-level reconcile the bridge plugin (design §2). The mapping lives in
3
+ // core/bridge-map.ts (one testable source of truth); the extension shells to
4
+ // this command rather than duplicating the transform.
5
+ import { join, basename, dirname, isAbsolute } from 'node:path';
6
+ import { existsSync, readdirSync } from 'node:fs';
7
+ import { defineBranch, defineLeaf } from '../../core/command.js';
8
+ import { usage } from '../../core/errors.js';
9
+ import { userScopeRoot } from '../../core/scope.js';
10
+ import { ensureDir, writeJson, writeText, readText, removePath, walkFiles, } from '../../core/fs-utils.js';
11
+ import { parseFrontmatter, parseFrontmatterGeneric } from '../../core/frontmatter.js';
12
+ import { buildBridgeDoc } from '../../core/bridge-map.js';
13
+ import { PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE } from '../../types.js';
14
+ /** Enumerate `<source>/**\/SKILL.md`, stopping recursion at the first SKILL.md
15
+ * on any branch — a dir that holds SKILL.md is a skill; its subdirs are assets,
16
+ * not nested skills. */
17
+ function enumerateSkillFiles(root) {
18
+ const out = [];
19
+ if (!existsSync(root))
20
+ return out;
21
+ const stack = [root];
22
+ while (stack.length) {
23
+ const dir = stack.pop();
24
+ let entries;
25
+ try {
26
+ entries = readdirSync(dir, { withFileTypes: true });
27
+ }
28
+ catch {
29
+ continue;
30
+ }
31
+ const hasSkill = entries.some((e) => e.isFile() && e.name === 'SKILL.md');
32
+ if (hasSkill) {
33
+ out.push(join(dir, 'SKILL.md'));
34
+ continue; // stop descent — deeper dirs are assets
35
+ }
36
+ for (const e of entries)
37
+ if (e.isDirectory())
38
+ stack.push(join(dir, e.name));
39
+ }
40
+ return out;
41
+ }
42
+ const bridgeSync = defineLeaf({
43
+ name: 'sync',
44
+ description: 'generate substrate docs from a Claude skills root',
45
+ whenToUse: 'mirroring a Claude Code skills directory into a crtr bridge plugin: maps each SKILL.md to a substrate memory doc and file-level reconciles the plugin (regenerate, prune stale, preserve hand-authored leaves + INDEX)',
46
+ help: {
47
+ name: 'pkg bridge sync',
48
+ summary: 'mirror a Claude skills root into a crtr bridge plugin (idempotent reconcile)',
49
+ guide: 'Each <source>/skills/<skillname>/SKILL.md becomes ~/.crouter/plugins/<name>/memory/<skillname>.md, stamped generator:claude-bridge. Stamped leaves whose source skill is gone are pruned; hand-authored leaves and any INDEX.md are never touched.',
50
+ params: [
51
+ { kind: 'flag', name: 'source', type: 'path', required: true, constraint: 'Absolute path to a Claude skills root (a dir whose <skillname>/SKILL.md leaves are bridged; recursion stops at the first SKILL.md).' },
52
+ { kind: 'flag', name: 'name', type: 'string', required: true, constraint: 'Bridge plugin name. Dest plugin dir is ~/.crouter/plugins/<name>/.' },
53
+ { kind: 'flag', name: 'source-desc', type: 'string', required: false, constraint: 'Optional plugin description written into the generated plugin.json.' },
54
+ ],
55
+ output: [
56
+ { name: 'plugin', type: 'string', required: true, constraint: 'Bridge plugin name.' },
57
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the bridge plugin directory.' },
58
+ { name: 'written', type: 'number', required: true, constraint: 'Count of substrate leaves generated from source SKILL.md files.' },
59
+ { name: 'pruned', type: 'number', required: true, constraint: 'Count of stale generator:claude-bridge leaves deleted (source skill gone).' },
60
+ { name: 'preserved', type: 'number', required: true, constraint: 'Count of untouched protected leaves (INDEX.md or unstamped hand-authored docs).' },
61
+ ],
62
+ outputKind: 'object',
63
+ effects: ['Writes ~/.crouter/plugins/<name>/.crouter-plugin/plugin.json and memory/*.md. Deletes stale generator:claude-bridge leaves. Never writes or deletes INDEX.md or unstamped docs.'],
64
+ },
65
+ run: async (input) => {
66
+ const source = input['source'];
67
+ const name = input['name'];
68
+ const sourceDesc = input['sourceDesc'];
69
+ if (!isAbsolute(source)) {
70
+ throw usage(`--source must be an absolute path: ${source}`);
71
+ }
72
+ if (!existsSync(source)) {
73
+ throw usage(`--source does not exist: ${source}`);
74
+ }
75
+ const destRoot = join(userScopeRoot(), 'plugins', name);
76
+ const memoryDir = join(destRoot, 'memory');
77
+ // 1. Ensure the bridge plugin manifest.
78
+ const manifestDir = join(destRoot, PLUGIN_MANIFEST_DIR);
79
+ ensureDir(manifestDir);
80
+ const manifest = {
81
+ name,
82
+ version: '0.0.0',
83
+ ...(sourceDesc !== undefined ? { description: sourceDesc } : {}),
84
+ source,
85
+ generator: 'claude-bridge',
86
+ };
87
+ writeJson(join(manifestDir, PLUGIN_MANIFEST_FILE), manifest);
88
+ // 2 + 3. Regenerate one stamped leaf per source skill.
89
+ ensureDir(memoryDir);
90
+ const writtenPaths = new Set();
91
+ for (const file of enumerateSkillFiles(source)) {
92
+ const skillName = basename(dirname(file));
93
+ const parsed = parseFrontmatter(readText(file));
94
+ const doc = buildBridgeDoc({
95
+ name: parsed.data?.name ?? skillName,
96
+ description: parsed.data?.description ?? '',
97
+ body: parsed.body,
98
+ });
99
+ const dest = join(memoryDir, `${skillName}.md`);
100
+ writeText(dest, doc);
101
+ writtenPaths.add(dest);
102
+ }
103
+ const written = writtenPaths.size;
104
+ // 4. Reconcile: prune stale stamped leaves; never touch INDEX.md or
105
+ // unstamped (hand-authored) leaves.
106
+ let pruned = 0;
107
+ let preserved = 0;
108
+ for (const f of walkFiles(memoryDir, (n) => n.endsWith('.md'))) {
109
+ if (writtenPaths.has(f))
110
+ continue; // just regenerated → counted in `written`
111
+ if (basename(f) === 'INDEX.md') {
112
+ preserved++;
113
+ continue;
114
+ }
115
+ const generator = parseFrontmatterGeneric(readText(f)).data?.['generator'];
116
+ if (generator === 'claude-bridge') {
117
+ removePath(f);
118
+ pruned++;
119
+ }
120
+ else {
121
+ preserved++;
122
+ }
123
+ }
124
+ return { plugin: name, path: destRoot, written, pruned, preserved };
125
+ },
126
+ });
127
+ export const bridgeBranch = defineBranch({
128
+ name: 'bridge',
129
+ description: 'mirror Claude skills into crtr bridge plugins',
130
+ whenToUse: 'generating a crtr bridge plugin from a Claude Code skills root — the substrate-doc mapping and file-level reconcile invoked by the pi bridge extension',
131
+ help: {
132
+ name: 'pkg bridge',
133
+ summary: 'mirror Claude skills roots into crtr bridge plugins',
134
+ model: 'A bridge plugin is a generator:claude-bridge plugin whose memory/ docs are generated from a Claude skills root. Hand-authored leaves and INDEX ceilings are preserved across syncs.',
135
+ },
136
+ children: [bridgeSync],
137
+ });
@@ -1,7 +1,8 @@
1
1
  import { defineBranch, defineLeaf } from '../../core/command.js';
2
2
  import { notFound } from '../../core/errors.js';
3
3
  import { paginate } from '../../core/pagination.js';
4
- import { listInstalledPlugins, findPluginByName, listSkillsInPlugin, } from '../../core/resolver.js';
4
+ import { listInstalledPlugins, findPluginByName, } from '../../core/resolver.js';
5
+ import { listAllMemoryDocs } from '../../core/memory-resolver.js';
5
6
  import { resolveScopeArg, projectScopeRoot } from '../../core/scope.js';
6
7
  // ---------------------------------------------------------------------------
7
8
  // plugin.inspect.list
@@ -78,8 +79,8 @@ const pluginList = defineLeaf({
78
79
  // ---------------------------------------------------------------------------
79
80
  const pluginShow = defineLeaf({
80
81
  name: 'show',
81
- description: 'read plugin manifest and skill inventory',
82
- whenToUse: 'reading one installed plugin in detail to decide before you enable, disable, or remove it — returns the full manifest, the skills it provides, its scope, and whether it is currently enabled. Use `pkg plugin inspect list` instead to enumerate plugins rather than drill into one',
82
+ description: 'read plugin manifest and substrate inventory',
83
+ whenToUse: 'reading one installed plugin in detail to decide before you enable, disable, or remove it — returns the full manifest, the substrate docs it provides (its `<pluginName>/` memory subtree), its scope, and whether it is currently enabled. Use `pkg plugin inspect list` instead to enumerate plugins rather than drill into one',
83
84
  help: {
84
85
  name: 'pkg plugin inspect show',
85
86
  summary: 'read plugin manifest and metadata by name',
@@ -93,7 +94,7 @@ const pluginShow = defineLeaf({
93
94
  { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the plugin directory.' },
94
95
  { name: 'enabled', type: 'boolean', required: true, constraint: 'Whether the plugin is active.' },
95
96
  { name: 'manifest', type: 'object', required: true, constraint: 'Full plugin.json contents.' },
96
- { name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path, enabled}. Skills provided by the plugin.' },
97
+ { name: 'skills', type: 'object[]', required: true, constraint: 'Each: {name, path}. Substrate docs provided by the plugin (its `<pluginName>/` memory subtree).' },
97
98
  ],
98
99
  outputKind: 'object',
99
100
  effects: ['None. Read-only.'],
@@ -117,7 +118,8 @@ const pluginShow = defineLeaf({
117
118
  if (found === null) {
118
119
  throw notFound(`plugin not found: ${name}`);
119
120
  }
120
- const skills = listSkillsInPlugin(found);
121
+ const prefix = `${found.name}/`;
122
+ const skills = listAllMemoryDocs().filter((d) => d.name.startsWith(prefix));
121
123
  return {
122
124
  name: found.name,
123
125
  scope: found.scope,
@@ -127,7 +129,6 @@ const pluginShow = defineLeaf({
127
129
  skills: skills.map((s) => ({
128
130
  name: s.name,
129
131
  path: s.path,
130
- enabled: s.enabled,
131
132
  })),
132
133
  };
133
134
  },
@@ -4,6 +4,7 @@
4
4
  import { defineBranch } from '../core/command.js';
5
5
  import { pluginBranch } from './pkg/plugin.js';
6
6
  import { marketBranch } from './pkg/market.js';
7
+ import { bridgeBranch } from './pkg/bridge.js';
7
8
  export function registerPkg() {
8
9
  return defineBranch({
9
10
  name: 'pkg',
@@ -16,6 +17,6 @@ export function registerPkg() {
16
17
  name: 'pkg',
17
18
  summary: 'manage plugins and plugin marketplaces',
18
19
  },
19
- children: [pluginBranch, marketBranch],
20
+ children: [pluginBranch, marketBranch, bridgeBranch],
20
21
  });
21
22
  }
@@ -112,7 +112,7 @@ function makeTierLeaf(tier) {
112
112
  : tier === 'urgent'
113
113
  ? `Urgent report fanned to ${n} subscriber(s) — they are force-woken.`
114
114
  : `Progress report fanned to ${n} subscriber(s).`;
115
- return `<pushed tier="${tier}" status="${r['status']}" delivered="${n}">\n${line}\nreport: ${r['report_path']}\n</pushed>`;
115
+ return `${line}\n\n- report: ${r['report_path']}`;
116
116
  },
117
117
  });
118
118
  }
@@ -161,16 +161,16 @@ const feedReadLeaf = defineLeaf({
161
161
  const n = Array.isArray(r['entries']) ? r['entries'].length : 0;
162
162
  const rawDigest = typeof r['digest'] === 'string' ? r['digest'] : '';
163
163
  if (n > 0 && rawDigest.trim() !== '') {
164
- return `<feed node="${r['node_id']}" unread="${n}">\n${rawDigest}\n</feed>`;
164
+ return `${n} unread pointer${n === 1 ? '' : 's'} drained from ${r['node_id']}'s inbox.\n\n${rawDigest.trim()}`;
165
165
  }
166
166
  // Empty drain has two distinct causes — say which, honestly. An inbox that
167
167
  // already holds entries was drained when the watcher woke you (the wake
168
168
  // message WAS that digest); a never-used inbox simply has nothing yet.
169
169
  const total = typeof r['inbox_total'] === 'number' ? r['inbox_total'] : 0;
170
- const digest = total > 0
171
- ? 'Nothing unread — but your inbox is not empty. The watcher drains your inbox to wake you, so the entries you already saw in your wake message are the same ones you would read here; that is why this is empty. Re-read the whole history (full message bodies included) with `crtr feed read --all`, or dereference the refs from the wake digest you already have.'
172
- : 'Inbox empty — nothing has arrived from your subscriptions yet. Expected while workers run: a worker that has not pushed yet leaves no pointer, and it will wake you the moment it does. The wake is automatic — just continue your own work or end your turn. (Reach for `crtr feed peek` only if you suspect a worker died, not to confirm a live one.)';
173
- return `<feed node="${r['node_id']}" unread="${n}">\n${digest}\n</feed>`;
170
+ const explanation = total > 0
171
+ ? 'Your inbox is not empty, though. The watcher drains your inbox to wake you, so the entries you already saw in your wake message are the same ones you would read here; that is why this is empty. Re-read the whole history (full message bodies included) with `crtr feed read --all`, or dereference the refs from the wake digest you already have.'
172
+ : 'Your inbox is empty — nothing has arrived from your subscriptions yet. Expected while workers run: a worker that has not pushed yet leaves no pointer, and it will wake you the moment it does. The wake is automatic — just continue your own work or end your turn. (Reach for `crtr feed peek` only if you suspect a worker died, not to confirm a live one.)';
173
+ return `No unread pointers for ${r['node_id']} (unread=0).\n\n${explanation}`;
174
174
  },
175
175
  });
176
176
  // ---------------------------------------------------------------------------
@@ -249,28 +249,23 @@ const feedPeekLeaf = defineLeaf({
249
249
  const tail = unread > 0
250
250
  ? `${unread} unread report${unread === 1 ? '' : 's'} sit in your inbox — run \`crtr feed read\` to absorb ${unread === 1 ? 'it' : 'them'}.`
251
251
  : 'If you spawned workers, they have finished and detached, or you never subscribed. Nothing will wake you.';
252
- return `<peek node="${id}" subscriptions="0" unread="${unread}" verdict="empty">\nNo nodes below you. ${tail}\n</peek>`;
252
+ return `No nodes below you (0 subscriptions, ${unread} unread). ${tail}`;
253
253
  }
254
254
  const working = kids.filter((k) => k.status === 'active' || k.status === 'idle');
255
255
  const liveWaking = working.filter((k) => k.active); // active sub to a live node = it will wake me
256
256
  const done = kids.filter((k) => k.status === 'done' || k.status === 'canceled');
257
257
  const dead = kids.filter((k) => k.status === 'dead');
258
- let verdict;
259
258
  let line;
260
259
  if (dead.length > 0) {
261
- verdict = 'attention';
262
260
  line = `\u26a0 ${dead.length} below you ${dead.length === 1 ? 'is' : 'are'} dead and will NOT wake you. Inspect with \`crtr node inspect show <id>\`, then re-delegate or proceed without ${dead.length === 1 ? 'it' : 'them'}.`;
263
261
  }
264
262
  else if (liveWaking.length > 0) {
265
- verdict = 'working';
266
263
  line = `Safe to yield \u2014 ${liveWaking.length} worker${liveWaking.length === 1 ? '' : 's'} running async will wake you on the next push. Nothing to do now; end your turn and chill.`;
267
264
  }
268
265
  else if (unread > 0) {
269
- verdict = 'ready';
270
266
  line = `Nothing still running, but ${unread} unread report${unread === 1 ? '' : 's'} \u2014 run \`crtr feed read\` to absorb ${unread === 1 ? 'it' : 'them'}, then continue or finish.`;
271
267
  }
272
268
  else {
273
- verdict = 'idle';
274
269
  line = 'Everything below you has finished and been drained \u2014 nothing is running. Continue your own work, or `crtr push final` to finish.';
275
270
  }
276
271
  const rows = kids.map((k) => {
@@ -279,10 +274,10 @@ const feedPeekLeaf = defineLeaf({
279
274
  ? `pushed ${fmtAge(k.last_push.ts)} [${k.last_push.kind}]${k.last_push.ref !== null ? ` ref:${k.last_push.ref}` : ''}`
280
275
  : 'no push yet';
281
276
  const glyph = STATUS_GLYPH[k.status] ?? '?';
282
- return ` ${glyph} ${k.node_id} ${k.kind} ${k.name}${sub} \u00b7 ${k.status} \u00b7 spawned ${fmtAge(k.spawned)} \u00b7 cyc ${k.cycles} \u00b7 ${push}`;
277
+ return `- ${glyph} ${k.name}${sub} (${k.node_id}) \u00b7 ${k.kind} \u00b7 ${k.status} \u00b7 spawned ${fmtAge(k.spawned)} \u00b7 cyc ${k.cycles} \u00b7 ${push}`;
283
278
  }).join('\n');
284
- const attrs = `node="${id}" subscriptions="${kids.length}" working="${working.length}" done="${done.length}" dead="${dead.length}" unread="${unread}" verdict="${verdict}"`;
285
- return `<peek ${attrs}>\n${line}\n\n${rows}\n</peek>`;
279
+ const summary = `${kids.length} node${kids.length === 1 ? '' : 's'} below you — ${working.length} working, ${done.length} done, ${dead.length} dead, ${unread} unread:`;
280
+ return `${line}\n\n${summary}\n${rows}`;
286
281
  },
287
282
  });
288
283
  // ---------------------------------------------------------------------------
@@ -1,14 +1,14 @@
1
- import { join } from 'node:path';
1
+ import { dirname } from 'node:path';
2
2
  import { writeFileSync } from 'node:fs';
3
3
  import { defineLeaf, defineBranch } from '../../core/command.js';
4
4
  import { usage, general, notFound } from '../../core/errors.js';
5
- import { SCOPE_SKILL_PLUGIN, SKILL_ENTRY_FILE, SKILLS_DIR, SKILL_TYPES, isSkillType, } from '../../types.js';
5
+ import { SKILL_TYPES, isSkillType } from '../../types.js';
6
6
  import { parseSkillQualifier, findPluginByName, } from '../../core/resolver.js';
7
- import { resolveScopeArg, requireScopeRoot, scopeSkillsDir, } from '../../core/scope.js';
8
- import { serializeFrontmatter } from '../../core/frontmatter.js';
7
+ import { resolveScopeArg, requireScopeRoot, scopeMemoryDir, pluginMemoryDir, } from '../../core/scope.js';
9
8
  import { ensureScopeInitialized } from '../../core/config.js';
10
9
  import { ensureDir, pathExists } from '../../core/fs-utils.js';
11
10
  import { skillCreatePrompt, skillTemplatePrompt } from '../../prompts/skill.js';
11
+ import { memoryFilePath, serializeMemoryDoc } from '../memory/shared.js';
12
12
  import { VALID_TYPES, resolveWriteScope } from './shared.js';
13
13
  export const authorGuide = defineLeaf({
14
14
  name: 'guide',
@@ -42,25 +42,25 @@ export const authorGuide = defineLeaf({
42
42
  });
43
43
  export const authorScaffold = defineLeaf({
44
44
  name: 'scaffold',
45
- description: 'create an empty SKILL.md stub',
46
- whenToUse: 'creating the empty SKILL.md stub at a `<plugin>/<skill>` qualifier before you fill in content — it writes the frontmatter and file, then points you at the authoring guide.',
45
+ description: 'create an empty skill substrate doc stub',
46
+ whenToUse: 'creating the empty `kind: skill` substrate doc at a `<plugin>/<skill>` qualifier (or a bare `<skill>` name for a scope-owned doc) before you fill in content — it writes the frontmatter and file, then points you at the authoring guide.',
47
47
  help: {
48
48
  name: 'skill author scaffold',
49
- summary: 'create an empty SKILL.md stub at the given qualifier',
49
+ summary: 'create an empty kind:skill substrate doc stub at the given qualifier',
50
50
  params: [
51
- { kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier in <plugin>/<skill> form.' },
52
- { kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform.' },
53
- { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Short description written to frontmatter.' },
51
+ { kind: 'positional', name: 'qualifier', required: true, constraint: 'Skill identifier: <plugin>/<skill> to scaffold into a plugin\'s memory/, or a bare <skill> for the scope-owned memory/.' },
52
+ { kind: 'flag', name: 'type', type: 'enum', choices: [...VALID_TYPES], required: false, constraint: 'One of: playbook, primer, reference, runbook, freeform. Drives the follow-up authoring guide only — not stored on the substrate doc.' },
53
+ { kind: 'flag', name: 'description', type: 'string', required: false, constraint: 'Read-routing line written to the doc\'s when-and-why-to-read frontmatter.' },
54
54
  { kind: 'flag', name: 'scope', type: 'enum', choices: ['user', 'project'], required: false, constraint: 'Default: project if available, else user.' },
55
55
  ],
56
56
  output: [
57
- { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded SKILL.md.' },
57
+ { name: 'path', type: 'string', required: true, constraint: 'Absolute path to the scaffolded substrate doc.' },
58
58
  { name: 'follow_up', type: 'string', required: true, constraint: 'Recommended next call to load the authoring guide.' },
59
59
  ],
60
60
  outputKind: 'object',
61
61
  effects: [
62
- 'Creates the skill directory and SKILL.md stub at the resolved location.',
63
- 'Writes frontmatter with name, description (if provided), and type (if provided).',
62
+ 'Creates a `kind: skill` substrate doc stub under the resolved memory/ dir (scope-owned, or the named plugin\'s).',
63
+ 'Writes substrate frontmatter: kind:skill, when-and-why-to-read (from --description if given), system-prompt-visibility:name, file-read-visibility:none.',
64
64
  ],
65
65
  },
66
66
  run: async (input) => {
@@ -82,31 +82,20 @@ export const authorScaffold = defineLeaf({
82
82
  throw usage(`unknown skill type: ${typeStr} / valid: ${SKILL_TYPES.join(' | ')}`);
83
83
  }
84
84
  const skillType = typeStr !== undefined && isSkillType(typeStr) ? typeStr : undefined;
85
- let skillFile;
86
- // Scope-direct: no plugin qualifier, or explicit `_/` sentinel (internal only)
87
- if (pluginName === undefined || pluginName === SCOPE_SKILL_PLUGIN) {
85
+ // Resolve the memory/ dir to scaffold into: the named plugin's, or the
86
+ // scope-owned one. One substrate, one author path.
87
+ let memoryDir;
88
+ if (pluginName === undefined) {
88
89
  const scope = resolveWriteScope(scopeStr);
89
90
  const scopeRootPath = requireScopeRoot(scope);
90
91
  ensureScopeInitialized(scope, scopeRootPath);
91
- const skillsRoot = scopeSkillsDir(scope);
92
- if (!skillsRoot) {
93
- throw general(`no skills dir for scope ${scope}`);
92
+ const dir = scopeMemoryDir(scope);
93
+ if (!dir) {
94
+ throw general(`no memory dir for scope ${scope}`);
94
95
  }
95
- const skillDir = join(skillsRoot, ...skillName.split('/'));
96
- skillFile = join(skillDir, SKILL_ENTRY_FILE);
97
- if (pathExists(skillFile)) {
98
- throw general(`skill already exists: ${skillFile}`);
99
- }
100
- ensureDir(skillDir);
101
- const fm = serializeFrontmatter({
102
- name: skillName,
103
- description,
104
- type: skillType,
105
- });
106
- writeFileSync(skillFile, fm, 'utf8');
96
+ memoryDir = dir;
107
97
  }
108
98
  else {
109
- // Plugin-scoped scaffold
110
99
  const scopeForLookup = scopeStr !== undefined
111
100
  ? (() => {
112
101
  const r = resolveScopeArg(scopeStr);
@@ -119,19 +108,21 @@ export const authorScaffold = defineLeaf({
119
108
  if (!plugin) {
120
109
  throw notFound(`plugin not found: ${pluginName}`);
121
110
  }
122
- const skillDir = join(plugin.root, SKILLS_DIR, ...skillName.split('/'));
123
- skillFile = join(skillDir, SKILL_ENTRY_FILE);
124
- if (pathExists(skillFile)) {
125
- throw general(`skill already exists: ${skillFile}`);
126
- }
127
- ensureDir(skillDir);
128
- const fm = serializeFrontmatter({
129
- name: skillName,
130
- description,
131
- type: skillType,
132
- });
133
- writeFileSync(skillFile, fm, 'utf8');
111
+ memoryDir = pluginMemoryDir(plugin);
112
+ }
113
+ const skillFile = memoryFilePath(memoryDir, skillName);
114
+ if (pathExists(skillFile)) {
115
+ throw general(`skill already exists: ${skillFile}`);
134
116
  }
117
+ ensureDir(dirname(skillFile));
118
+ // Catalog-default substrate frontmatter (design §1.5): a name-rung,
119
+ // file-read-none skill doc. --description becomes the read-routing line.
120
+ const frontmatter = { kind: 'skill' };
121
+ if (description !== undefined)
122
+ frontmatter['when-and-why-to-read'] = description;
123
+ frontmatter['system-prompt-visibility'] = 'name';
124
+ frontmatter['file-read-visibility'] = 'none';
125
+ writeFileSync(skillFile, serializeMemoryDoc(frontmatter, ''), 'utf8');
135
126
  const typeHint = skillType !== undefined ? `--type ${skillType} ` : '';
136
127
  const follow_up = `crtr skill author guide ${typeHint}--topic "${skillName}"`;
137
128
  return { path: skillFile, follow_up };
@@ -1,8 +1,4 @@
1
- import type { Skill, Scope } from '../../types.js';
2
- export declare function formatNeighborQualifier(s: Skill): string;
3
- export declare function formatNeighborKeywords(s: Skill): string;
4
- export declare function buildNeighborsSection(skill: Skill): string | null;
5
- export declare function appendNeighbors(skill: Skill, body: string): string;
1
+ import type { Scope } from '../../types.js';
6
2
  export declare function resolveWriteScope(scopeStr: string | undefined): Scope;
7
3
  export declare const VALID_TYPES: readonly ["playbook", "primer", "reference", "runbook", "freeform"];
8
4
  export type CatalogSource = {