@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.
- package/dist/builtin-memory/crouter-development/marketplaces.md +2 -4
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
- package/dist/builtin-memory/crouter-development/personas.md +2 -6
- package/dist/builtin-memory/crouter-development/plugins.md +2 -4
- package/dist/builtin-memory/design.md +2 -4
- package/dist/builtin-memory/development.md +3 -3
- package/dist/builtin-memory/planning.md +3 -4
- package/dist/builtin-memory/spec.md +3 -10
- package/dist/builtin-personas/orchestration-kernel.md +1 -1
- package/dist/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +61 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +270 -0
- package/dist/commands/canvas-history/show.d.ts +1 -0
- package/dist/commands/canvas-history/show.js +79 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +33 -0
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/human/queue.js +2 -2
- package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
- package/dist/commands/memory/find.js +14 -52
- package/dist/commands/memory/lint.d.ts +7 -0
- package/dist/commands/memory/lint.js +29 -35
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/memory/shared.js +1 -2
- package/dist/commands/memory/write.js +6 -9
- package/dist/commands/node.js +47 -25
- package/dist/commands/pkg/bridge.d.ts +1 -0
- package/dist/commands/pkg/bridge.js +137 -0
- package/dist/commands/pkg/plugin-inspect.js +7 -6
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/push.js +10 -15
- package/dist/commands/skill/author.js +35 -44
- package/dist/commands/skill/shared.d.ts +1 -5
- package/dist/commands/skill/shared.js +9 -63
- package/dist/commands/skill.js +5 -6
- package/dist/commands/sys/doctor.js +6 -144
- package/dist/commands/tmux-spread.js +16 -1
- package/dist/commands/view-cycle.js +9 -5
- package/dist/commands/view-run.js +3 -2
- package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
- package/dist/core/__tests__/child-death-wake.test.js +133 -185
- package/dist/core/__tests__/context-intro.test.js +23 -27
- package/dist/core/__tests__/daemon-liveness.test.js +28 -258
- package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
- package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
- package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
- package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
- package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
- package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +4 -3
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
- package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +7 -7
- package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
- package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
- package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
- package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
- package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
- package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/full/spike-harness.test.d.ts +1 -0
- package/dist/core/__tests__/full/spike-harness.test.js +117 -0
- package/dist/core/__tests__/grace-clock.test.js +72 -75
- package/dist/core/__tests__/helpers/harness.d.ts +35 -1
- package/dist/core/__tests__/helpers/harness.js +70 -12
- package/dist/core/__tests__/live-mutation-verbs.test.js +100 -105
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/memory.test.js +4 -4
- package/dist/core/__tests__/revive.test.js +96 -86
- package/dist/core/__tests__/subscription-delivery.test.js +116 -138
- package/dist/core/__tests__/wake-origin.test.js +54 -50
- package/dist/core/bootstrap.js +18 -14
- package/dist/core/bridge-map.d.ts +19 -0
- package/dist/core/bridge-map.js +73 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +389 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/config.js +3 -22
- package/dist/core/help.d.ts +2 -2
- package/dist/core/help.js +1 -1
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/render.d.ts +4 -3
- package/dist/core/render.js +38 -41
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +2 -2
- package/dist/core/runtime/persona.js +5 -7
- package/dist/core/scope.d.ts +7 -5
- package/dist/core/scope.js +16 -17
- package/dist/core/substrate/ceiling.d.ts +28 -0
- package/dist/core/substrate/ceiling.js +87 -0
- package/dist/core/substrate/index.d.ts +2 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/core/substrate/on-read.js +18 -2
- package/dist/core/substrate/render.d.ts +3 -5
- package/dist/core/substrate/render.js +68 -59
- package/dist/core/substrate/schema.d.ts +18 -13
- package/dist/core/substrate/schema.js +12 -11
- package/dist/pi-extensions/canvas-context-intro.js +5 -3
- package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
- package/dist/types.d.ts +3 -18
- package/dist/types.js +0 -8
- package/package.json +4 -3
- package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
- package/dist/builtin-skills/skills/design/SKILL.md +0 -51
- package/dist/builtin-skills/skills/development/SKILL.md +0 -109
- package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
- package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
- package/dist/commands/skill/state.d.ts +0 -3
- package/dist/commands/skill/state.js +0 -71
- package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
- package/dist/core/__tests__/broker-dialogs.test.js +0 -126
- package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
- package/dist/core/__tests__/resolver.test.js +0 -181
- package/dist/core/__tests__/spike-harness.test.js +0 -242
- /package/dist/{core/__tests__/broker-attach-limits.test.d.ts → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /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
|
+
}
|
package/dist/core/config.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { join } from 'node:path';
|
|
2
|
-
import { CONFIG_FILE, STATE_FILE,
|
|
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
|
-
|
|
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,
|
|
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);
|
package/dist/core/help.d.ts
CHANGED
|
@@ -94,7 +94,7 @@ export interface RootEntry {
|
|
|
94
94
|
useWhen: string;
|
|
95
95
|
/** Optional bounded block this subtree contributes to its <name> block at
|
|
96
96
|
* root. Returns a complete self-named state element (build it with
|
|
97
|
-
* stateBlock), e.g. `<
|
|
97
|
+
* stateBlock), e.g. `<kinds count="7">…</kinds>`. Aggregate, never an
|
|
98
98
|
* unbounded enumeration on a cold path. Soft-fails to omission on
|
|
99
99
|
* null/throw. */
|
|
100
100
|
dynamicState?: () => string | null;
|
|
@@ -141,7 +141,7 @@ export interface BranchHelp {
|
|
|
141
141
|
* child's purpose lives in its own listing row). */
|
|
142
142
|
model?: string;
|
|
143
143
|
/** Bounded runtime aggregate as a complete self-named state element (build
|
|
144
|
-
* it with stateBlock), e.g. `<
|
|
144
|
+
* it with stateBlock), e.g. `<kinds count="7">…</kinds>`. Renderer
|
|
145
145
|
* soft-fails to omission if this returns null or throws. */
|
|
146
146
|
dynamicState?: () => string | null;
|
|
147
147
|
/** Parent-level listing assembled by defineBranch from the actual child defs.
|
package/dist/core/help.js
CHANGED
|
@@ -105,7 +105,7 @@ export function renderRoot(h) {
|
|
|
105
105
|
for (const l of rootSubcommandLines(c))
|
|
106
106
|
lines.push(l);
|
|
107
107
|
// dynamicState returns a complete self-named element (e.g.
|
|
108
|
-
// <
|
|
108
|
+
// <kinds count="7">…</kinds>) — emit it as-is, nested in the command.
|
|
109
109
|
const state = evalDynamic(c.dynamicState);
|
|
110
110
|
if (state !== null)
|
|
111
111
|
lines.push(state);
|
|
@@ -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)
|
|
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)
|
|
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(
|
|
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 (
|
|
104
|
+
if (pathExists(path)) {
|
|
105
|
+
matches.push(loadMemoryDoc(name, s, path));
|
|
69
106
|
continue;
|
|
70
|
-
|
|
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
|
}
|
package/dist/core/render.d.ts
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { LeafHelp } from './help.js';
|
|
2
2
|
import type { ErrorPayload } from './io.js';
|
|
3
|
-
/** Schema-driven fallback: turn a result object into agent-ready
|
|
3
|
+
/** Schema-driven fallback: turn a result object into agent-ready plain markdown.
|
|
4
4
|
* Field order follows the leaf's declared output schema; any extra keys append
|
|
5
|
-
* after. Scalars
|
|
6
|
-
* become
|
|
5
|
+
* after. Scalars (and nested objects) list as `- name: value` bullets, prose
|
|
6
|
+
* fields render as paragraphs, and arrays become a count lead-in plus a table
|
|
7
|
+
* or bullets. No root tag — the result is read as a continuation of the prompt. */
|
|
7
8
|
export declare function renderResult(result: Record<string, unknown>, help: LeafHelp): string;
|
|
8
9
|
/** Render a structured failure as an instruction-shaped block: what broke, what
|
|
9
10
|
* was received, and the recovery road sign — the same recovery info the JSON
|