@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
@@ -0,0 +1,389 @@
1
+ // canvas history — the scan-first engine over the per-cwd episodic record.
2
+ //
3
+ // The canvas accumulates a high-signal corpus per cwd: every node that ran
4
+ // there plus the artifacts it left — its `reports/` (append-only push history:
5
+ // final/update/urgent outcome summaries) and `context/` docs (specs, designs,
6
+ // roadmaps, findings). This module is the ONE place that scans that corpus; it
7
+ // lives in the canvas data-access layer, so it resolves every node + path
8
+ // through canvas.ts + paths.ts and never hand-joins. No persistent index —
9
+ // node facets (cwd/kind/status) are filtered cheaply against the db FIRST, then
10
+ // only the surviving nodes' artifact files are read. FTS5/bm25 is the future
11
+ // upgrade; the surface above is backend-independent.
12
+ import { statSync, readdirSync } from 'node:fs';
13
+ import { join, relative, sep } from 'node:path';
14
+ import { getNode, listNodes, view } from './canvas.js';
15
+ import { contextDir, reportsDir, nodeDir } from './paths.js';
16
+ import { readTextIfExists, walkFiles, pathExists } from '../fs-utils.js';
17
+ import { parseFrontmatterGeneric } from '../frontmatter.js';
18
+ /** The cwd the caller means by default: the calling node's cwd (resolved from
19
+ * CRTR_NODE_ID via the canvas data layer), falling back to the process cwd
20
+ * when not run as a node. */
21
+ export function callerCwd() {
22
+ const id = process.env['CRTR_NODE_ID'];
23
+ if (id !== undefined && id !== '') {
24
+ const node = getNode(id);
25
+ if (node !== null)
26
+ return node.cwd;
27
+ }
28
+ return process.cwd();
29
+ }
30
+ /** Compact report stamp `20260607T075536` → `2026-06-07T07:55:36`. */
31
+ function compactToIso(stamp) {
32
+ const m = stamp.match(/^(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})$/);
33
+ if (m === null)
34
+ return stamp;
35
+ return `${m[1]}-${m[2]}-${m[3]}T${m[4]}:${m[5]}:${m[6]}`;
36
+ }
37
+ function toMs(ts) {
38
+ const n = Date.parse(ts);
39
+ return Number.isNaN(n) ? 0 : n;
40
+ }
41
+ /** First markdown heading (`# …`) or first non-empty line, trimmed. */
42
+ function firstHeading(body) {
43
+ const lines = body.split('\n');
44
+ for (const l of lines) {
45
+ const t = l.trim();
46
+ if (t.startsWith('#'))
47
+ return t.replace(/^#+\s*/, '').trim();
48
+ }
49
+ for (const l of lines) {
50
+ const t = l.trim();
51
+ if (t !== '')
52
+ return t.length > 120 ? t.slice(0, 120) : t;
53
+ }
54
+ return '';
55
+ }
56
+ /** A memoized body loader for a file artifact — reads + strips frontmatter once. */
57
+ function fileBodyLoader(absPath) {
58
+ let cached = null;
59
+ const load = () => {
60
+ if (cached !== null)
61
+ return cached;
62
+ const raw = readTextIfExists(absPath);
63
+ cached = raw === null ? '' : parseFrontmatterGeneric(raw).body;
64
+ return cached;
65
+ };
66
+ // Read the head eagerly for ts/title derivation (the file is small markdown).
67
+ const raw = readTextIfExists(absPath);
68
+ const head = raw === null ? '' : raw;
69
+ if (raw !== null)
70
+ cached = parseFrontmatterGeneric(raw).body;
71
+ return { load, head };
72
+ }
73
+ function reportArtifact(row, desc, filename) {
74
+ const abs = join(reportsDir(row.node_id), filename);
75
+ const m = filename.match(/^([0-9]{8}T[0-9]{6})-(\w+)\.md$/);
76
+ const { load, head } = fileBodyLoader(abs);
77
+ const fm = parseFrontmatterGeneric(head).data;
78
+ const reportKind = (typeof fm?.['kind'] === 'string' ? fm['kind'] : undefined) ?? (m ? m[2] : 'report');
79
+ const ts = (typeof fm?.['ts'] === 'string' ? fm['ts'] : undefined) ??
80
+ (m ? compactToIso(m[1]) : new Date(statSync(abs).mtimeMs).toISOString());
81
+ const body = load();
82
+ return {
83
+ ref: `${row.node_id}:reports/${filename}`,
84
+ nodeId: row.node_id,
85
+ relpath: `reports/${filename}`,
86
+ source: 'report',
87
+ reportKind,
88
+ ts,
89
+ tsMs: toMs(ts),
90
+ title: firstHeading(body),
91
+ nodeName: row.name,
92
+ nodeKind: row.kind,
93
+ nodeStatus: row.status,
94
+ nodeCwd: row.cwd,
95
+ nodeDesc: desc,
96
+ path: abs,
97
+ loadBody: load,
98
+ };
99
+ }
100
+ function docArtifact(row, desc, abs) {
101
+ const rel = relative(nodeDir(row.node_id), abs).split(sep).join('/');
102
+ const isRoadmap = rel === 'context/roadmap.md';
103
+ const { load } = fileBodyLoader(abs);
104
+ let ts;
105
+ try {
106
+ ts = new Date(statSync(abs).mtimeMs).toISOString();
107
+ }
108
+ catch {
109
+ ts = row.created;
110
+ }
111
+ const body = load();
112
+ return {
113
+ ref: `${row.node_id}:${rel}`,
114
+ nodeId: row.node_id,
115
+ relpath: rel,
116
+ source: isRoadmap ? 'roadmap' : 'doc',
117
+ ts,
118
+ tsMs: toMs(ts),
119
+ title: firstHeading(body) || rel.replace(/^context\//, ''),
120
+ nodeName: row.name,
121
+ nodeKind: row.kind,
122
+ nodeStatus: row.status,
123
+ nodeCwd: row.cwd,
124
+ nodeDesc: desc,
125
+ path: abs,
126
+ loadBody: load,
127
+ };
128
+ }
129
+ function metaArtifact(row, desc) {
130
+ const body = `${row.name}\n${desc}\n${row.kind}`;
131
+ return {
132
+ ref: `${row.node_id}:meta`,
133
+ nodeId: row.node_id,
134
+ relpath: 'meta',
135
+ source: 'meta',
136
+ ts: row.created,
137
+ tsMs: toMs(row.created),
138
+ title: row.name,
139
+ nodeName: row.name,
140
+ nodeKind: row.kind,
141
+ nodeStatus: row.status,
142
+ nodeCwd: row.cwd,
143
+ nodeDesc: desc,
144
+ path: null,
145
+ loadBody: () => body,
146
+ };
147
+ }
148
+ /** List the markdown reports a node left, newest filename first. */
149
+ function listReportFiles(nodeId) {
150
+ const dir = reportsDir(nodeId);
151
+ if (!pathExists(dir))
152
+ return [];
153
+ try {
154
+ return readdirSync(dir).filter((f) => f.endsWith('.md'));
155
+ }
156
+ catch {
157
+ return [];
158
+ }
159
+ }
160
+ /** Enumerate every context markdown doc a node left (recursive). */
161
+ function listDocFiles(nodeId) {
162
+ const dir = contextDir(nodeId);
163
+ if (!pathExists(dir))
164
+ return [];
165
+ try {
166
+ return walkFiles(dir, (name) => name.endsWith('.md'));
167
+ }
168
+ catch {
169
+ return [];
170
+ }
171
+ }
172
+ /** The node desc lives in meta.json, not the row — read it once per survivor. */
173
+ function nodeDescOf(nodeId) {
174
+ const n = getNode(nodeId);
175
+ return n?.description ?? '';
176
+ }
177
+ /** Build the searchable artifact set for a scope + corpus filter. Node facets
178
+ * are applied against the db rows first (cheap); only surviving nodes' files
179
+ * are read. */
180
+ export function buildCorpus(scope, corpus) {
181
+ const rows = scopedRows(scope);
182
+ // Node-facet narrowing (cheap, on the row).
183
+ const kinds = corpus.kinds;
184
+ const statuses = corpus.statuses;
185
+ const survivors = rows.filter((r) => {
186
+ if (kinds !== undefined && kinds.length > 0 && !kinds.includes(r.kind))
187
+ return false;
188
+ if (statuses !== undefined && statuses.length > 0 && !statuses.includes(r.status))
189
+ return false;
190
+ return true;
191
+ });
192
+ // --report-kind implies type=report.
193
+ const wantReportKind = corpus.reportKind;
194
+ const types = corpus.types !== undefined && corpus.types.length > 0
195
+ ? corpus.types
196
+ : wantReportKind !== undefined
197
+ ? ['report']
198
+ : ['report', 'doc', 'roadmap', 'meta'];
199
+ const want = new Set(types);
200
+ const out = [];
201
+ for (const row of survivors) {
202
+ const desc = want.has('meta') || want.has('report') || want.has('doc') || want.has('roadmap')
203
+ ? nodeDescOf(row.node_id)
204
+ : '';
205
+ if (want.has('meta'))
206
+ out.push(metaArtifact(row, desc));
207
+ if (want.has('report')) {
208
+ for (const f of listReportFiles(row.node_id)) {
209
+ const a = reportArtifact(row, desc, f);
210
+ if (a === null)
211
+ continue;
212
+ if (wantReportKind !== undefined && a.reportKind !== wantReportKind)
213
+ continue;
214
+ out.push(a);
215
+ }
216
+ }
217
+ if (want.has('doc') || want.has('roadmap')) {
218
+ for (const abs of listDocFiles(row.node_id)) {
219
+ const a = docArtifact(row, desc, abs);
220
+ if (a.source === 'roadmap' && !want.has('roadmap'))
221
+ continue;
222
+ if (a.source === 'doc' && !want.has('doc'))
223
+ continue;
224
+ out.push(a);
225
+ }
226
+ }
227
+ }
228
+ // Time window on artifact ts.
229
+ return out.filter((a) => {
230
+ if (corpus.sinceMs !== undefined && a.tsMs < corpus.sinceMs)
231
+ return false;
232
+ if (corpus.untilMs !== undefined && a.tsMs > corpus.untilMs)
233
+ return false;
234
+ return true;
235
+ });
236
+ }
237
+ /** Resolve the scope to the node rows in play. */
238
+ function scopedRows(scope) {
239
+ const all = listNodes();
240
+ const byId = new Map(all.map((r) => [r.node_id, r]));
241
+ if (scope.nodes !== undefined && scope.nodes.length > 0) {
242
+ return scope.nodes.map((id) => byId.get(id)).filter((r) => r !== undefined);
243
+ }
244
+ if (scope.under !== undefined && scope.under !== '') {
245
+ const ids = [scope.under, ...view(scope.under)];
246
+ return ids.map((id) => byId.get(id)).filter((r) => r !== undefined);
247
+ }
248
+ if (scope.allCwds === true)
249
+ return all;
250
+ const cwd = scope.cwd !== undefined && scope.cwd !== '' ? scope.cwd : callerCwd();
251
+ return all.filter((r) => r.cwd === cwd);
252
+ }
253
+ /** Resolve a `<node-id>:<relpath>` handle to its content. Returns null when the
254
+ * node is unknown or the artifact does not exist. Throws on path traversal. */
255
+ export function resolveRef(ref) {
256
+ const idx = ref.indexOf(':');
257
+ if (idx === -1)
258
+ return null;
259
+ const nodeId = ref.slice(0, idx);
260
+ const relpath = ref.slice(idx + 1);
261
+ const node = getNode(nodeId);
262
+ if (node === null)
263
+ return null;
264
+ if (relpath === 'meta') {
265
+ const body = `# ${node.name}\n\n- kind: ${node.kind}\n- status: ${node.status}\n- cwd: ${node.cwd}\n- created: ${node.created}\n\n${node.description ?? ''}`;
266
+ return {
267
+ nodeId,
268
+ relpath,
269
+ source: 'meta',
270
+ ts: node.created,
271
+ nodeName: node.name,
272
+ body,
273
+ raw: null,
274
+ path: null,
275
+ };
276
+ }
277
+ if (relpath.includes('..') || relpath.startsWith('/')) {
278
+ throw new Error(`illegal relpath in ref: ${relpath}`);
279
+ }
280
+ const abs = join(nodeDir(nodeId), relpath);
281
+ const raw = readTextIfExists(abs);
282
+ if (raw === null)
283
+ return null;
284
+ const { body, data: fm } = parseFrontmatterGeneric(raw);
285
+ const isReport = relpath.startsWith('reports/');
286
+ const isRoadmap = relpath === 'context/roadmap.md';
287
+ let ts;
288
+ if (typeof fm?.['ts'] === 'string')
289
+ ts = fm['ts'];
290
+ else {
291
+ try {
292
+ ts = new Date(statSync(abs).mtimeMs).toISOString();
293
+ }
294
+ catch {
295
+ ts = node.created;
296
+ }
297
+ }
298
+ return {
299
+ nodeId,
300
+ relpath,
301
+ source: isReport ? 'report' : isRoadmap ? 'roadmap' : 'doc',
302
+ reportKind: isReport && typeof fm?.['kind'] === 'string' ? fm['kind'] : undefined,
303
+ ts,
304
+ nodeName: node.name,
305
+ body,
306
+ raw,
307
+ path: abs,
308
+ };
309
+ }
310
+ /** Enumerate one node's artifacts (for `history show`). */
311
+ export function nodeArtifacts(nodeId, types) {
312
+ const node = getNode(nodeId);
313
+ if (node === null)
314
+ return [];
315
+ const row = {
316
+ node_id: node.node_id,
317
+ name: node.name,
318
+ kind: node.kind,
319
+ mode: node.mode,
320
+ lifecycle: node.lifecycle,
321
+ status: node.status,
322
+ cwd: node.cwd,
323
+ host_kind: node.host_kind ?? null,
324
+ parent: node.parent ?? null,
325
+ created: node.created,
326
+ intent: node.intent ?? null,
327
+ pi_pid: node.pi_pid ?? null,
328
+ window: node.window ?? null,
329
+ tmux_session: node.tmux_session ?? null,
330
+ pane: node.pane ?? null,
331
+ };
332
+ const want = new Set(types !== undefined && types.length > 0 ? types : ['report', 'doc', 'roadmap']);
333
+ const desc = node.description ?? '';
334
+ const out = [];
335
+ if (want.has('report')) {
336
+ for (const f of listReportFiles(nodeId)) {
337
+ const a = reportArtifact(row, desc, f);
338
+ if (a !== null)
339
+ out.push(a);
340
+ }
341
+ }
342
+ if (want.has('doc') || want.has('roadmap')) {
343
+ for (const abs of listDocFiles(nodeId)) {
344
+ const a = docArtifact(row, desc, abs);
345
+ if (a.source === 'roadmap' && !want.has('roadmap'))
346
+ continue;
347
+ if (a.source === 'doc' && !want.has('doc'))
348
+ continue;
349
+ out.push(a);
350
+ }
351
+ }
352
+ return out;
353
+ }
354
+ /** Bounded per-cwd aggregate for the branch `-h` `<corpus>` block. Counts files
355
+ * on disk without reading bodies. */
356
+ export function corpusStats(cwd) {
357
+ const rows = listNodes().filter((r) => r.cwd === cwd);
358
+ let reports = 0;
359
+ let docs = 0;
360
+ let min = Infinity;
361
+ let max = -Infinity;
362
+ for (const row of rows) {
363
+ const cMs = toMs(row.created);
364
+ if (cMs > 0) {
365
+ if (cMs < min)
366
+ min = cMs;
367
+ if (cMs > max)
368
+ max = cMs;
369
+ }
370
+ for (const f of listReportFiles(row.node_id)) {
371
+ reports++;
372
+ const m = f.match(/^([0-9]{8}T[0-9]{6})-/);
373
+ if (m !== null) {
374
+ const t = toMs(compactToIso(m[1]));
375
+ if (t > 0) {
376
+ if (t < min)
377
+ min = t;
378
+ if (t > max)
379
+ max = t;
380
+ }
381
+ }
382
+ }
383
+ docs += listDocFiles(row.node_id).length;
384
+ }
385
+ const span = min !== Infinity && max !== -Infinity
386
+ ? { from: new Date(min).toISOString().slice(0, 10), to: new Date(max).toISOString().slice(0, 10) }
387
+ : null;
388
+ return { cwd, nodes: rows.length, reports, docs, span };
389
+ }
@@ -2,6 +2,7 @@ export * from './types.js';
2
2
  export * from './labels.js';
3
3
  export * from './paths.js';
4
4
  export * from './canvas.js';
5
+ export * from './history.js';
5
6
  export * from './focuses.js';
6
7
  export * from './wakeups.js';
7
8
  export * from './telemetry.js';
@@ -4,6 +4,7 @@ export * from './types.js';
4
4
  export * from './labels.js';
5
5
  export * from './paths.js';
6
6
  export * from './canvas.js';
7
+ export * from './history.js';
7
8
  export * from './focuses.js';
8
9
  export * from './wakeups.js';
9
10
  export * from './telemetry.js';
@@ -1,8 +1,7 @@
1
1
  import { join } from 'node:path';
2
- import { CONFIG_FILE, STATE_FILE, SCHEMA_VERSION, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig } from '../types.js';
2
+ import { CONFIG_FILE, STATE_FILE, defaultScopeConfig, defaultScopeState, defaultCanvasNavConfig } from '../types.js';
3
3
  import { readJsonIfExists, writeJson, ensureDir } from './fs-utils.js';
4
4
  import { scopeRoot, requireScopeRoot } from './scope.js';
5
- import { diag } from './io.js';
6
5
  function configPathFor(root) {
7
6
  return join(root, CONFIG_FILE);
8
7
  }
@@ -24,24 +23,7 @@ export function readConfig(scope) {
24
23
  const existing = readJsonIfExists(configPathFor(root));
25
24
  if (!existing)
26
25
  return defaultScopeConfig();
27
- const cfg = mergeConfig(existing);
28
- if ((existing.schema_version ?? 0) < SCHEMA_VERSION) {
29
- migrateSkillConfigKeys(cfg, scope, root);
30
- }
31
- return cfg;
32
- }
33
- function migrateSkillConfigKeys(cfg, scope, root) {
34
- const colonKeys = Object.keys(cfg.skills).filter((k) => k.includes(':'));
35
- if (colonKeys.length > 0) {
36
- for (const key of colonKeys) {
37
- const newKey = key.replace(/:/g, '/');
38
- cfg.skills[newKey] = cfg.skills[key];
39
- delete cfg.skills[key];
40
- }
41
- diag(`crtr: migrated ${colonKeys.length} skill config keys to slash form in ${scope}`);
42
- }
43
- cfg.schema_version = SCHEMA_VERSION;
44
- writeJson(configPathFor(root), cfg);
26
+ return mergeConfig(existing);
45
27
  }
46
28
  export function readState(scope) {
47
29
  const root = scopeRoot(scope);
@@ -121,7 +103,6 @@ function mergeConfig(partial) {
121
103
  const schema_version = partial.schema_version === undefined ? defaults.schema_version : partial.schema_version;
122
104
  const marketplaces = partial.marketplaces === undefined ? {} : partial.marketplaces;
123
105
  const plugins = partial.plugins === undefined ? {} : partial.plugins;
124
- const skills = partial.skills === undefined ? {} : partial.skills;
125
106
  const au = partial.auto_update;
126
107
  const rawInterval = au && typeof au.interval_hours === 'number' ? au.interval_hours : undefined;
127
108
  const interval_hours = rawInterval !== undefined && Number.isFinite(rawInterval) && rawInterval >= 0
@@ -140,7 +121,7 @@ function mergeConfig(partial) {
140
121
  // The merge drops unknown keys, so `headless` must be carried explicitly or it
141
122
  // is lost on every read-modify-write of config.json.
142
123
  const headless = typeof partial.headless === 'boolean' ? partial.headless : defaults.headless;
143
- return { schema_version, marketplaces, plugins, skills, auto_update, max_panes_per_window, canvasNav, headless };
124
+ return { schema_version, marketplaces, plugins, auto_update, max_panes_per_window, canvasNav, headless };
144
125
  }
145
126
  export function updateConfig(scope, mutate) {
146
127
  const cfg = readConfig(scope);
@@ -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,4 +1,4 @@
1
- import type { Scope } from '../types.js';
1
+ import type { InstalledPlugin, Scope } from '../types.js';
2
2
  /**
3
3
  * Thin memory-document resolver for the document substrate. Mirrors the SHAPE
4
4
  * of the skill resolver (qualifier parse → scope precedence → direct path
@@ -31,8 +31,15 @@ export declare function memoryDocId(doc: MemoryDoc): string;
31
31
  /** All memory docs in one scope's memory/ dir, scanned recursively for *.md
32
32
  * (topical subdirs supported), sorted by path-derived name. */
33
33
  export declare function listMemoryDocs(scope: Scope): MemoryDoc[];
34
+ /** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
35
+ * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
36
+ * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
37
+ * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
38
+ export declare function listPluginMemoryDocs(plugin: InstalledPlugin, scope: Scope): MemoryDoc[];
34
39
  /** All memory docs across the resolved scopes, in precedence order
35
- * (project, then user, then builtin), each scope's docs name-sorted within. */
40
+ * (project, then user, then builtin). Within each scope, native docs are
41
+ * emitted FIRST, then that scope's enabled-plugin docs — so native wins on the
42
+ * caller's first-wins (scope,name) dedup. Each group is name-sorted within. */
36
43
  export declare function listAllMemoryDocs(scope?: Scope): MemoryDoc[];
37
44
  /**
38
45
  * Resolve a path-derived name to a single memory document.
@@ -1,10 +1,10 @@
1
1
  import { join, relative, sep } from 'node:path';
2
2
  import { pathExists, readText, walkFiles } from './fs-utils.js';
3
3
  import { parseFrontmatterGeneric } from './frontmatter.js';
4
- import { parseSkillQualifier } from './resolver.js';
4
+ import { listInstalledPlugins, parseSkillQualifier } from './resolver.js';
5
5
  import { ambiguous, notFound, usage } from './errors.js';
6
6
  import { warn } from './output.js';
7
- import { projectScopeRoot, scopeMemoryDir } from './scope.js';
7
+ import { pluginMemoryDir, projectScopeRoot, scopeMemoryDir } from './scope.js';
8
8
  /** Canonical, unambiguous identifier for a memory document: `<scope>/<name>`. */
9
9
  export function memoryDocId(doc) {
10
10
  return `${doc.scope}/${doc.name}`;
@@ -50,14 +50,50 @@ export function listMemoryDocs(scope) {
50
50
  }
51
51
  return docs.sort((a, b) => a.name.localeCompare(b.name));
52
52
  }
53
+ /** All of one plugin's substrate docs, mounted under the virtual `<pluginName>/`
54
+ * namespace. Walks `pluginMemoryDir(plugin)` recursively for *.md, deriving each
55
+ * doc's name exactly as `listMemoryDocs` does (path-relative, no extension,
56
+ * slash-separated) then prefixing the plugin name. Builtin has no plugins. */
57
+ export function listPluginMemoryDocs(plugin, scope) {
58
+ const dir = pluginMemoryDir(plugin);
59
+ if (!pathExists(dir))
60
+ return [];
61
+ const docs = [];
62
+ for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
63
+ const derived = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
64
+ if (!derived)
65
+ continue;
66
+ const name = `${plugin.name}/${derived}`;
67
+ try {
68
+ docs.push(loadMemoryDoc(name, scope, file));
69
+ }
70
+ catch (e) {
71
+ const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
72
+ warn(`invalid frontmatter in ${file}: ${msg}`);
73
+ }
74
+ }
75
+ return docs.sort((a, b) => a.name.localeCompare(b.name));
76
+ }
53
77
  /** All memory docs across the resolved scopes, in precedence order
54
- * (project, then user, then builtin), each scope's docs name-sorted within. */
78
+ * (project, then user, then builtin). Within each scope, native docs are
79
+ * emitted FIRST, then that scope's enabled-plugin docs — so native wins on the
80
+ * caller's first-wins (scope,name) dedup. Each group is name-sorted within. */
55
81
  export function listAllMemoryDocs(scope) {
56
- return scopesInPrecedence(scope).flatMap(listMemoryDocs);
82
+ return scopesInPrecedence(scope).flatMap((s) => [
83
+ ...listMemoryDocs(s),
84
+ ...listInstalledPlugins(s)
85
+ .filter((p) => p.enabled)
86
+ .flatMap((p) => listPluginMemoryDocs(p, s)),
87
+ ]);
57
88
  }
58
89
  /** Direct full-path lookup of memory/<name>.md across scopes, precedence-first.
59
90
  * Returns every scope's hit in precedence order; the resolver takes the first
60
- * (highest-precedence) one — a fully-specified name is never ambiguous. */
91
+ * (highest-precedence) one — a fully-specified name is never ambiguous.
92
+ *
93
+ * A directory INDEX is the cleaner contract: when `<name>.md` is absent but
94
+ * `<name>/INDEX.md` exists, the bare dir name (`taste`) resolves to the dir's
95
+ * INDEX doc — carrying the dir name as its identity. (`taste/INDEX` still
96
+ * resolves directly as the file path.) */
61
97
  function findMemoryMatches(name, scope) {
62
98
  const matches = [];
63
99
  for (const s of scopesInPrecedence(scope)) {
@@ -65,9 +101,13 @@ function findMemoryMatches(name, scope) {
65
101
  if (!dir)
66
102
  continue;
67
103
  const path = join(dir, ...name.split('/')) + '.md';
68
- if (!pathExists(path))
104
+ if (pathExists(path)) {
105
+ matches.push(loadMemoryDoc(name, s, path));
69
106
  continue;
70
- matches.push(loadMemoryDoc(name, s, path));
107
+ }
108
+ const indexPath = join(dir, ...name.split('/'), 'INDEX.md');
109
+ if (pathExists(indexPath))
110
+ matches.push(loadMemoryDoc(name, s, indexPath));
71
111
  }
72
112
  return matches;
73
113
  }
@@ -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