@north-light/crouter 0.3.207 → 0.3.209
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/api/client.d.ts +3 -1
- package/dist/api/client.js +4 -0
- package/dist/api/dto/broker-ops.d.ts +2 -12
- package/dist/api/dto/nodes.d.ts +16 -0
- package/dist/api/routes.d.ts +1 -0
- package/dist/api/routes.js +1 -0
- package/dist/builtin-memory/00-runtime-base.md +3 -2
- package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
- package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
- package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
- package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
- package/dist/builtin-memory/04-base-worker.md +3 -2
- package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
- package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
- package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
- package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
- package/dist/builtin-memory/advisor/council.md +0 -2
- package/dist/builtin-memory/design.md +3 -2
- package/dist/builtin-memory/development.md +3 -2
- package/dist/builtin-memory/insights/capture.md +1 -3
- package/dist/builtin-memory/insights/init.md +1 -3
- package/dist/builtin-memory/insights/listen.md +3 -2
- package/dist/builtin-memory/internal/INDEX.md +5 -4
- package/dist/builtin-memory/internal/agent-shaping.md +5 -4
- package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
- package/dist/builtin-memory/internal/marketplaces.md +3 -2
- package/dist/builtin-memory/internal/memory-loading.md +31 -20
- package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
- package/dist/builtin-memory/internal/plugins.md +7 -6
- package/dist/builtin-memory/internal/storage-tiers.md +3 -2
- package/dist/builtin-memory/plan/roadmap.md +3 -2
- package/dist/builtin-memory/spec/guide.md +0 -2
- package/dist/builtin-memory/spec/requirements.md +0 -2
- package/dist/builtin-memory/spec/roadmap.md +3 -2
- package/dist/builtin-memory/testing.md +1 -3
- package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
- package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
- package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
- package/dist/clients/attach/__tests__/group-activity.test.js +8 -1
- package/dist/clients/attach/render/card-presentation.d.ts +31 -0
- package/dist/clients/attach/render/card-presentation.js +163 -0
- package/dist/clients/attach/render/chat-view.d.ts +0 -2
- package/dist/clients/attach/render/chat-view.js +12 -23
- package/dist/clients/attach/render/context-message.d.ts +5 -9
- package/dist/clients/attach/render/context-message.js +20 -43
- package/dist/clients/attach/render/group-activity.d.ts +28 -4
- package/dist/clients/attach/render/group-activity.js +65 -11
- package/dist/clients/attach/render/group-recap.d.ts +3 -0
- package/dist/clients/attach/render/group-recap.js +17 -0
- package/dist/clients/attach/viewer.js +516 -516
- package/dist/commands/memory/delete.js +3 -18
- package/dist/commands/memory/edit.js +23 -30
- package/dist/commands/memory/history.js +1 -1
- package/dist/commands/memory/lint.d.ts +7 -8
- package/dist/commands/memory/lint.js +178 -132
- package/dist/commands/memory/list.d.ts +0 -1
- package/dist/commands/memory/list.js +2 -12
- package/dist/commands/memory/move.d.ts +1 -0
- package/dist/commands/memory/move.js +195 -0
- package/dist/commands/memory/read.js +135 -141
- package/dist/commands/memory/shared.d.ts +18 -17
- package/dist/commands/memory/shared.js +93 -39
- package/dist/commands/memory/write.js +23 -33
- package/dist/commands/memory.js +5 -4
- package/dist/commands/pkg/browse/catalog.js +2 -4
- package/dist/commands/pkg/browse/doc-view.js +17 -11
- package/dist/commands/pkg/browse/model.d.ts +7 -9
- package/dist/commands/sys/migrate.d.ts +1 -0
- package/dist/commands/sys/migrate.js +106 -0
- package/dist/commands/sys/sync-deps.js +5 -10
- package/dist/commands/sys/sync-project-guidance.js +36 -16
- package/dist/commands/sys/sync-skills.js +8 -4
- package/dist/commands/sys.js +3 -2
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
- package/dist/core/__tests__/helpers/broker-clients.d.ts +3 -1
- package/dist/core/__tests__/helpers/broker-clients.js +7 -2
- package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
- package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
- package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
- package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
- package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
- package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
- package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +9 -0
- package/dist/core/__tests__/serial/broker-snapshot-history.test.js +52 -1
- package/dist/core/__tests__/serial/flagship-lifecycle.test.js +1 -1
- package/dist/core/canvas/db.d.ts +3 -1
- package/dist/core/canvas/db.js +12 -2
- package/dist/core/memory/history.d.ts +4 -1
- package/dist/core/memory/history.js +1 -0
- package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
- package/dist/core/memory/inline-ref-inventory.js +23 -19
- package/dist/core/memory-resolver.d.ts +4 -4
- package/dist/core/memory-resolver.js +16 -43
- package/dist/core/runtime/bearings.d.ts +4 -3
- package/dist/core/runtime/bearings.js +4 -4
- package/dist/core/runtime/broker/fault-retry.js +2 -7
- package/dist/core/runtime/broker/frame-dispatch.js +1 -1
- package/dist/core/runtime/broker/tool-groups.js +7 -6
- package/dist/core/runtime/broker-extension-render.d.ts +3 -2
- package/dist/core/runtime/broker-extension-render.js +3 -3
- package/dist/core/runtime/broker.js +4 -4
- package/dist/core/runtime/kickoff.js +4 -10
- package/dist/core/runtime/memory.js +2 -3
- package/dist/core/runtime/node-read.js +7 -1
- package/dist/core/runtime/stop-guard.js +3 -3
- package/dist/core/substrate/index.d.ts +7 -4
- package/dist/core/substrate/index.js +6 -4
- package/dist/core/substrate/injected-store.d.ts +24 -12
- package/dist/core/substrate/injected-store.js +80 -33
- package/dist/core/substrate/listings.d.ts +21 -0
- package/dist/core/substrate/listings.js +88 -0
- package/dist/core/substrate/on-read-node.d.ts +5 -5
- package/dist/core/substrate/on-read-node.js +4 -5
- package/dist/core/substrate/on-read.d.ts +25 -4
- package/dist/core/substrate/on-read.js +81 -102
- package/dist/core/substrate/render-node.d.ts +5 -2
- package/dist/core/substrate/render-node.js +5 -3
- package/dist/core/substrate/render.d.ts +9 -8
- package/dist/core/substrate/render.js +104 -96
- package/dist/core/substrate/schema.d.ts +34 -18
- package/dist/core/substrate/schema.js +75 -32
- package/dist/core/substrate/surface-match.d.ts +32 -0
- package/dist/core/substrate/surface-match.js +179 -0
- package/dist/daemon/api/handlers/nodes.js +9 -0
- package/dist/daemon/review/comment-notify.js +1 -8
- package/dist/daemon/review/deliver.js +1 -1
- package/dist/daemon/review/finish.js +1 -1
- package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
- package/dist/migrations/001-surfaces-frontmatter.js +276 -0
- package/dist/migrations/convergent.d.ts +31 -0
- package/dist/migrations/convergent.js +71 -0
- package/dist/migrations/registry.d.ts +2 -0
- package/dist/migrations/registry.js +19 -0
- package/dist/migrations/types.d.ts +40 -0
- package/dist/migrations/types.js +11 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
- package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
- package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
- package/dist/pi-extensions/canvas-review-boundary.js +1 -1
- package/dist/shared/__tests__/generated-context-grammar.test.js +19 -22
- package/dist/shared/generated-context.d.ts +0 -18
- package/dist/shared/generated-context.js +16 -120
- package/dist/shared/tool-groups.d.ts +1 -0
- package/dist/shared/tool-groups.js +7 -0
- package/package.json +1 -1
- package/runtime.lock.json +2 -2
- package/dist/core/substrate/ceiling.d.ts +0 -17
- package/dist/core/substrate/ceiling.js +0 -67
|
@@ -3,21 +3,19 @@
|
|
|
3
3
|
// scope modules and build their documented output objects on top of the small
|
|
4
4
|
// helpers here. Nothing in this file forks on kind or re-implements the
|
|
5
5
|
// schema/gate/resolver — it only composes them.
|
|
6
|
-
import { existsSync, realpathSync, statSync } from 'node:fs';
|
|
7
|
-
import { join, resolve as resolvePath } from 'node:path';
|
|
6
|
+
import { existsSync, readdirSync, realpathSync, rmdirSync, statSync } from 'node:fs';
|
|
7
|
+
import { dirname, join, resolve as resolvePath } from 'node:path';
|
|
8
8
|
import { stringify as yamlStringify, parse as yamlParse } from 'yaml';
|
|
9
9
|
import { usage } from '../../core/errors.js';
|
|
10
10
|
import { CRTR_DIR_NAME } from '../../types.js';
|
|
11
11
|
import { scopeMemoryDir, projectScopeRoot, ensureProjectScopeRoot, resetScopeCache, } from '../../core/scope.js';
|
|
12
12
|
import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
|
|
13
13
|
import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
|
|
14
|
+
import { SURFACE_EVENTS, SURFACE_RUNGS } from '../../core/substrate/schema.js';
|
|
14
15
|
// The two memory kinds — knowledge (consult: procedural playbooks + factual
|
|
15
16
|
// references merged) vs preference (behave: standing directives). Used as the
|
|
16
17
|
// `--kind` enum choices everywhere.
|
|
17
18
|
export const MEMORY_KINDS = ['knowledge', 'preference'];
|
|
18
|
-
// Visibility rungs — how much of a document surfaces (none → name → preview →
|
|
19
|
-
// content). Shared by --system-prompt-visibility and --file-read-visibility.
|
|
20
|
-
export const VISIBILITY_RUNGS = ['none', 'name', 'preview', 'content'];
|
|
21
19
|
// Scope choices for filtering / targeting (builtin is read-only, not writable).
|
|
22
20
|
// `node` is the this-node store, writable only inside a running node.
|
|
23
21
|
export const MEMORY_SCOPES = ['user', 'project', 'profile', 'node'];
|
|
@@ -115,6 +113,22 @@ export function resolveWriteTarget(scopeArg, profileArg, dirArg) {
|
|
|
115
113
|
throw usage(`no ${scope} scope available for writing memory documents`);
|
|
116
114
|
return { scope, memoryDir };
|
|
117
115
|
}
|
|
116
|
+
/** Remove a just-removed file's now-empty parent directories up to (never
|
|
117
|
+
* including) its tree root, so removing the last entry under an `area/`
|
|
118
|
+
* prefix does not orphan an empty directory. The root is derived by walking
|
|
119
|
+
* up one dirname per name segment: a doc named `a/b` sits at `<root>/a/b.md`,
|
|
120
|
+
* so its root is two dirnames above — the same arithmetic holds for a
|
|
121
|
+
* `.history/<name>.jsonl` sidecar. Stops at the first non-empty ancestor. */
|
|
122
|
+
export function pruneEmptyParents(filePath, nameSegments) {
|
|
123
|
+
let treeRoot = filePath;
|
|
124
|
+
for (let i = 0; i < nameSegments; i += 1)
|
|
125
|
+
treeRoot = dirname(treeRoot);
|
|
126
|
+
let dir = dirname(filePath);
|
|
127
|
+
while (dir !== treeRoot && dir.startsWith(treeRoot) && existsSync(dir) && readdirSync(dir).length === 0) {
|
|
128
|
+
rmdirSync(dir);
|
|
129
|
+
dir = dirname(dir);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
118
132
|
/** Map a path-derived name (`topic` or `area/topic`) to its file path under a
|
|
119
133
|
* memory dir, guarding against traversal/absolute escapes. */
|
|
120
134
|
export function memoryFilePath(memoryDir, name) {
|
|
@@ -147,29 +161,72 @@ export function coerceGate(raw) {
|
|
|
147
161
|
}
|
|
148
162
|
return result;
|
|
149
163
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
164
|
+
// The frontmatter keys one `surfaces` entry may carry.
|
|
165
|
+
const SURFACE_ENTRY_KEYS = new Set(['on', 'match', 'match-frontmatter', 'at']);
|
|
166
|
+
/** Coerce one `--surface` value into a validated surfaces entry. Strict where
|
|
167
|
+
* the runtime parser is tolerant: a flag that would be silently dropped or
|
|
168
|
+
* trimmed at render time is an authoring mistake and fails HERE. The entry is
|
|
169
|
+
* returned in canonical key order with a single glob kept as a bare string,
|
|
170
|
+
* so the stored YAML stays as compact as the flag that authored it. */
|
|
171
|
+
export function coerceSurface(raw) {
|
|
172
|
+
const parsed = yamlParse(raw);
|
|
173
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
174
|
+
throw usage(`--surface must be a YAML/JSON object entry {on, at, match?, match-frontmatter?}, got ${JSON.stringify(parsed)}. ` +
|
|
175
|
+
`Example: --surface '{on: read, match: "src/**", at: content}'`);
|
|
160
176
|
}
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
177
|
+
const rec = parsed;
|
|
178
|
+
for (const key of Object.keys(rec)) {
|
|
179
|
+
if (!SURFACE_ENTRY_KEYS.has(key)) {
|
|
180
|
+
throw usage(`--surface: unknown key \`${key}\` (an entry carries only on, match, match-frontmatter, at)`);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
const on = rec['on'];
|
|
184
|
+
if (typeof on !== 'string' || !SURFACE_EVENTS.includes(on)) {
|
|
185
|
+
throw usage(`--surface: invalid \`on\`: ${JSON.stringify(on)} (expected ${SURFACE_EVENTS.join('|')})`);
|
|
186
|
+
}
|
|
187
|
+
const at = rec['at'];
|
|
188
|
+
if (typeof at !== 'string' || !SURFACE_RUNGS.includes(at)) {
|
|
189
|
+
throw usage(`--surface: invalid \`at\`: ${JSON.stringify(at)} (expected ${SURFACE_RUNGS.join('|')})`);
|
|
171
190
|
}
|
|
172
|
-
|
|
191
|
+
let match;
|
|
192
|
+
if (rec['match'] !== undefined) {
|
|
193
|
+
if (on === 'boot' || on === 'workspace-open') {
|
|
194
|
+
throw usage(`--surface: \`match\` is meaningless on \`${on}\` — the entry's presence is the match; drop it`);
|
|
195
|
+
}
|
|
196
|
+
if (typeof rec['match'] === 'string') {
|
|
197
|
+
const t = rec['match'].trim();
|
|
198
|
+
if (t === '')
|
|
199
|
+
throw usage('--surface: `match` must be a non-empty glob or a non-empty glob list');
|
|
200
|
+
match = t;
|
|
201
|
+
}
|
|
202
|
+
else if (Array.isArray(rec['match'])) {
|
|
203
|
+
const globs = rec['match'].map((g) => (typeof g === 'string' ? g.trim() : ''));
|
|
204
|
+
if (globs.length === 0 || globs.some((g) => g === '')) {
|
|
205
|
+
throw usage('--surface: `match` must be a non-empty glob or a non-empty glob list');
|
|
206
|
+
}
|
|
207
|
+
match = globs.length === 1 ? globs[0] : globs;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
throw usage(`--surface: invalid \`match\`: ${JSON.stringify(rec['match'])} (expected a glob string or list)`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const matchFrontmatter = rec['match-frontmatter'];
|
|
214
|
+
if (matchFrontmatter !== undefined) {
|
|
215
|
+
if (on !== 'read')
|
|
216
|
+
throw usage('--surface: `match-frontmatter` is a `read`-event predicate only');
|
|
217
|
+
if (matchFrontmatter === null || typeof matchFrontmatter !== 'object' || Array.isArray(matchFrontmatter)) {
|
|
218
|
+
throw usage(`--surface: invalid \`match-frontmatter\`: ${JSON.stringify(matchFrontmatter)} (expected a field→matcher object)`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if ((on === 'read' || on === 'memory-read' || on === 'command') && match === undefined && matchFrontmatter === undefined) {
|
|
222
|
+
throw usage(`--surface: a \`${on}\` entry requires \`match\`${on === 'read' ? ' or `match-frontmatter`' : ''}`);
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
on,
|
|
226
|
+
...(match !== undefined ? { match } : {}),
|
|
227
|
+
...(matchFrontmatter !== undefined ? { 'match-frontmatter': matchFrontmatter } : {}),
|
|
228
|
+
at,
|
|
229
|
+
};
|
|
173
230
|
}
|
|
174
231
|
// Canonical frontmatter field order for a substrate document. Known fields come
|
|
175
232
|
// first in this order; any preserved-on-update extras append after. `origin` is
|
|
@@ -179,11 +236,10 @@ const FRONTMATTER_ORDER = [
|
|
|
179
236
|
'kind',
|
|
180
237
|
'when-and-why-to-read',
|
|
181
238
|
'short-form',
|
|
182
|
-
'
|
|
183
|
-
'
|
|
239
|
+
'unlisted',
|
|
240
|
+
'surfaces',
|
|
184
241
|
'gate',
|
|
185
|
-
'
|
|
186
|
-
'read-when',
|
|
242
|
+
'slash',
|
|
187
243
|
'rationale',
|
|
188
244
|
'last-updated',
|
|
189
245
|
'origin',
|
|
@@ -208,8 +264,8 @@ export function buildOrigin() {
|
|
|
208
264
|
}
|
|
209
265
|
/** Serialize a substrate frontmatter record + body into a complete `.md`
|
|
210
266
|
* document. Frontmatter is emitted as a `---` fenced YAML block (the `yaml`
|
|
211
|
-
* package — the same one the parser uses — so nested gate maps and
|
|
212
|
-
*
|
|
267
|
+
* package — the same one the parser uses — so nested gate maps and surfaces
|
|
268
|
+
* entry lists round-trip), in canonical field order with preserved extras last. */
|
|
213
269
|
export function serializeMemoryDoc(frontmatter, body) {
|
|
214
270
|
const ordered = {};
|
|
215
271
|
for (const key of FRONTMATTER_ORDER) {
|
|
@@ -267,11 +323,9 @@ export const FRONTMATTER_OVERLAY_PARAMS = {
|
|
|
267
323
|
'kind': { kind: 'flag', name: 'kind', type: 'enum', choices: [...MEMORY_KINDS], required: false, constraint: 'Document kind.' },
|
|
268
324
|
'when-and-why-to-read': { kind: 'flag', name: 'when-and-why-to-read', type: 'string', required: false, constraint: 'ONE routing sentence: "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the reader\u2019s payoff \u2014 the consequence they secure for their task by reading \u2014 NEVER the doc summary, its rule, or that rule reworded as an outcome (a benefit-shaped restatement still fails). Rendered verbatim as the preview.' },
|
|
269
325
|
'short-form': { kind: 'flag', name: 'short-form', type: 'string', required: false, constraint: 'Frontmatter short-form \u2014 a very abbreviated version of the content, the hook shown in `crtr memory list`.' },
|
|
270
|
-
'
|
|
271
|
-
'
|
|
326
|
+
'unlisted': { kind: 'flag', name: 'unlisted', type: 'bool', required: false, default: false, constraint: 'Suppress this doc from directory listings. Suppression only — explicit reads, [[links]], and surfaces entries still work.' },
|
|
327
|
+
'surface': { kind: 'flag', name: 'surface', type: 'string', required: false, repeatable: true, constraint: 'One routing entry per occurrence, as a YAML/JSON object `{on, at, match?, match-frontmatter?}`; the flag set replaces the document’s whole surfaces list. `on` is boot|workspace-open|read|memory-read|command; `at` is name|preview|content. `match` holds the event’s globs — required on read/memory-read/command (a read entry may carry `match-frontmatter`, a predicate over the read file’s own frontmatter, instead), meaningless on boot/workspace-open. A `./`-anchored glob is relative: for `read` to the store’s owning repo dir, for `memory-read` to the doc’s own name directory. Constraints within one entry AND together; entries OR together.' },
|
|
272
328
|
'gate': { kind: 'flag', name: 'gate', type: 'string', required: false, constraint: 'Frontmatter gate \u2014 YAML/JSON object predicate over node config using the same field/matcher vocabulary described in the guide.' },
|
|
273
|
-
'applies-to': { kind: 'flag', name: 'applies-to', type: 'string', required: false, repeatable: true, constraint: 'One explicit file-context route per occurrence; the flag set replaces the document\u2019s whole route list. In a project store, `.` fires when cwd/profile mounts that workspace during first-message assembly AND on any file read beneath the store\u2019s owning dir. Any other value is a glob matched after an actual file read against the absolute path, basename, and path relative to the owning project root. There is no positional fallback from where the memory file lives. Required whenever file-read-visibility is not none.' },
|
|
274
|
-
'read-when': { kind: 'flag', name: 'read-when', type: 'string', required: false, constraint: 'Frontmatter read-when \u2014 YAML/JSON object predicate over a read file\u2019s own frontmatter using the same field/matcher vocabulary described in the guide.' },
|
|
275
329
|
'slash': { kind: 'flag', name: 'slash', type: 'bool', required: false, default: false, constraint: 'Presence flags this doc invocable as a pi slash command (`/<name>`, `/` in a nested name rendered as `:`) \u2014 the doc body becomes the command\u2019s injected prompt. Default false: most docs are consulted, not invoked.' },
|
|
276
330
|
};
|
|
277
331
|
/** One overlay flag, with leaf-specific prose appended to the shared
|
|
@@ -288,7 +342,7 @@ export function overlayParam(name, overrides = {}, extraConstraint) {
|
|
|
288
342
|
* `--doc-rationale`, because `--rationale` there means why THIS REVISION is
|
|
289
343
|
* happening. Same field, same prose, two flag names that cannot be confused. */
|
|
290
344
|
export const DOC_RATIONALE_CONSTRAINT = 'Frontmatter rationale \u2014 the observed agent failure that made this doc necessary. Maintainer-facing only: never ships in any delivered surface (boot render, on-read injection, `memory read` content), visible only via `memory read --frontmatter`. Omitting the flag preserves an existing rationale unchanged.';
|
|
291
|
-
export const
|
|
345
|
+
export const GUIDE_SURFACES = 'Every doc appears in its directory’s listing by default (suppress with --unlisted); everything beyond the listing is explicit `surfaces` routing — entries declaring WHEN the doc delivers and how much. Events: `boot` delivers in the boot catalog every agent sees; `workspace-open` delivers in first-message context when cwd/profile mounts the doc’s project store (project stores only); `read` fires when the agent reads a matching file (globs vs the file’s absolute path and basename, `./`-anchored globs vs its path relative to the store’s owning repo dir, `match-frontmatter` predicates over the read file’s own YAML frontmatter); `memory-read` fires when another memory doc is read (globs vs that doc’s canonical name, `./` anchored to this doc’s own name directory); `command` fires when a matching shell command runs (globs vs the whole command string, `*` crossing `/`). `at` sets how much delivers: `name` the bare tag, `preview` the routing line, `content` the whole body. Each entry is paid by every future agent it fires on, so default down: no surfaces at all is correct for reference depth reached through listings and [[links]], and when a doc fits in a single sentence, deliver `content` directly — a `preview` routing line would run longer than the doc itself. Sentence-length `content` docs are correct; never pad a memory to be more verbose than the rule or fact it carries.';
|
|
292
346
|
export const GUIDE_ROUTING_LINE = 'The routing line (--when-and-why-to-read) is the only text an agent reads before deciding to load the doc. The test for its because-clause: if it can be derived by paraphrasing the doc\u2019s advice, it is a restatement, not a payoff \u2014 a real payoff names a consequence in the reader\u2019s world that the document itself never asserts. Bad: "because only genuine first principles belong in taste memory." Bad: "because keeping the test loop fast and free of speculative tests protects the development pace" \u2014 the doc\u2019s rule as an outcome, derivable straight from its advice. Good: "because identifying throughlines in user taste lets future decisions be made faster and with less re-litigation." Someone mid-task who has not read the doc must be able to decide from this line alone whether the read is worth it. If you cannot name the concrete situation that triggers it, you do not yet understand the memory \u2014 ask the user one sharp question instead of improvising.';
|
|
293
|
-
export const GUIDE_PREDICATE_VOCABULARY = 'Gate and
|
|
294
|
-
export const GUIDE_DOC_LINKS = 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes
|
|
347
|
+
export const GUIDE_PREDICATE_VOCABULARY = 'Gate and match-frontmatter share the same predicate language: a field map is AND-ed across fields; dotted fields resolve nested values; field matchers may be scalar, array, or object. Scalar matchers do exact equality, with arrays matching any element. Array matchers do membership or intersection. Object matchers accept `eq`, `ne`, `in`, `nin`, `exists`, `contains`, `containsAll`, `containsAny`, `matches`, `imatches`, `gt`, `gte`, `lt`, and `lte`. Combinators are `all`, `any`, and `not`; sibling field matchers next to them are AND-ed in. An empty condition is inert. An unknown op never matches.';
|
|
348
|
+
export const GUIDE_DOC_LINKS = 'Link related docs with `[[canonical/name]]`. A doc link in any body is a first-class cross-reference to another memory document by its exact canonical name \u2014 the same identifier `crtr memory read` takes. A bare directory name is a valid link too: following it returns that directory\u2019s listing, a browse entrance rather than a doc. Links are pointers, never transclusion: the linked body is loaded only when a reader follows it, so a link costs nothing until needed. `crtr memory lint` fails on a link that resolves to no document or directory, and `crtr memory read` lists a doc\u2019s resolvable links alongside its body. There is no alias or label form.';
|
|
@@ -2,7 +2,7 @@ import { defineLeaf } from '../../core/command.js';
|
|
|
2
2
|
import { usage } from '../../core/errors.js';
|
|
3
3
|
import { writeText, pathExists } from '../../core/fs-utils.js';
|
|
4
4
|
import { appendHistoryRecord, buildHistoryRecord, historyLogPathFor, } from '../../core/memory/history.js';
|
|
5
|
-
import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE,
|
|
5
|
+
import { DOC_RATIONALE_CONSTRAINT, GUIDE_DOC_LINKS, GUIDE_PREDICATE_VOCABULARY, GUIDE_ROUTING_LINE, GUIDE_SURFACES, MEMORY_SCOPES, resolveWriteTarget, memoryFilePath, buildOrigin, coerceGate, coerceSurface, overlayParam, serializeMemoryDoc, } from './shared.js';
|
|
6
6
|
// A topic's memory set stays usable only when authors treat cohesion as a creation
|
|
7
7
|
// gate; otherwise small corrections accumulate as overlapping leaves whose routing
|
|
8
8
|
// lines cannot tell a future reader which one owns the topic.
|
|
@@ -16,15 +16,15 @@ export const writeLeaf = defineLeaf({
|
|
|
16
16
|
guide: 'Every frontmatter flag decides who sees this doc, when, and at what context cost. Each rung up is paid by every future agent at every boot or read, so default each rung down.\n\n' +
|
|
17
17
|
'Pick the kind. knowledge is consulted for facts or procedures; preference directs behavior. The kind choice is about how the doc is used, not about how long it is.\n\n' +
|
|
18
18
|
'Store reusable current truth, not session notes. Useful memories are non-obvious procedures, gotchas, durable preferences, and cross-repo conventions. Do not store chat summaries, implementation history, or facts already recorded in the repo.\n\n' +
|
|
19
|
-
|
|
19
|
+
GUIDE_SURFACES + '\n\n' +
|
|
20
20
|
'Choose the scope. `project` is for facts any agent in one repo needs. `user` is for person-wide facts and preferences that should follow the user everywhere. `profile` is for the profile’s bundle of dirs: cross-repo conventions, how the pieces relate, or the user’s stance toward that body of work. `node` is scratch memory only this running node sees; it rides into this node’s knowledge block and dies with the node. When unsure, choose the narrowest scope that will still reach the next agent who needs it.\n\n' +
|
|
21
|
-
'A
|
|
22
|
-
'Choose the hook — boot vs
|
|
21
|
+
'A workspace front door is an ordinary doc carrying the entry pair {on: workspace-open, at: content} plus {on: read, match: "./**", at: content}: it enters first-message context when cwd/profile mounts its project store and fires on any file read beneath the store’s owning dir. Target the exact project with --dir. Keep only the project constraints, key commands, architecture orientation, and conventions that differ from defaults. `crtr memory lint` enforces exactly one such doc per project store managed by the selected profile.\n\n' +
|
|
22
|
+
'Choose the hook — boot vs read. A boot entry rides the boot catalog every agent sees; a read entry fires only when a matching file is actually read. Put code-specific knowledge in the owning project store, give it the narrowest real file glob, and keep it off boot when the file read is the useful trigger. Knowledge about a person or process usually has no file boundary, so skip read entries and route it through boot instead.\n\n' +
|
|
23
23
|
'Write the routing line (--when-and-why-to-read) first, before storing anything. ' + GUIDE_ROUTING_LINE + '\n\n' +
|
|
24
24
|
GUIDE_PREDICATE_VOCABULARY + '\n\n' +
|
|
25
25
|
GUIDE_DOC_LINKS + '\n\n' +
|
|
26
|
-
'When a doc grows long or information-rich, nest it into a graph instead of letting it become a scroll. The main doc at the topic’s path keeps the high-level, most load-bearing information, most important first; depth splits into reference docs under the topic’s directory (`area/topic/...`), each pointed at with a `[[link]]`. Split by subject: a leaf earns its link by covering a different subject a task might need on its own; a leaf of offloaded “further evidence”, examples, or references is never followed, so supporting material either sits in the main doc next to the point it supports or gets cut. The main doc is the entry point a reader can act from alone; a reference leaf is loaded only when the task needs that depth.
|
|
27
|
-
'A directory
|
|
26
|
+
'When a doc grows long or information-rich, nest it into a graph instead of letting it become a scroll. The main doc at the topic’s path keeps the high-level, most load-bearing information, most important first; depth splits into reference docs under the topic’s directory (`area/topic/...`), each pointed at with a `[[link]]`. Split by subject: a leaf earns its link by covering a different subject a task might need on its own; a leaf of offloaded “further evidence”, examples, or references is never followed, so supporting material either sits in the main doc next to the point it supports or gets cut. The main doc is the entry point a reader can act from alone; a reference leaf is loaded only when the task needs that depth. Give reference leaves no surfaces at all — the directory listing and the link from the main doc are how they are found, so any routing entry just double-charges every boot or read for depth the graph already routes. Keep every doc as short as its job allows; `crtr memory lint` caps body length by delivery rung and its findings carry the split guidance.\n\n' +
|
|
27
|
+
'A directory needs no index doc: reading a directory name returns its listing — each member’s routing line — so never author a doc that merely lists, fronts, or paraphrases its siblings. Write a directory-level doc only for synthesis: an operating guide or the cluster’s mechanics, ordering, conditions, and relationships, content no single member can carry. Guided entrance into a topic is an ordinary member doc, found through the listing like any other.\n\n' +
|
|
28
28
|
'Find before write. Prefer slightly expanding an existing document with `crtr memory edit`, nesting genuinely separate depth under its topic, and updating the existing `when-and-why-to-read` (plus its INDEX router when present) over creating another similar memory. A new document earns its own identity only when it has a distinct read trigger and a coherent body whose merge into the existing document would make it harder to route or use. Group related docs with path names (area/topic). Provenance is stamped here and preserved by every later revision. Run `crtr memory lint` after authoring.\n\n' +
|
|
29
29
|
'--rationale is the gap this doc exists to close — the observed agent failure that prompted it, captured from user signal (a correction, a mistake you watched happen) rather than inferred from the doc’s own content. If the rationale is guessable from reading the doc, it is not the real one — a guessable gap is one agents do not actually fall into. Omit the flag when you have no observed gap to record.\n\n' +
|
|
30
30
|
'Revise an existing doc with `crtr memory edit`.',
|
|
@@ -33,11 +33,9 @@ export const writeLeaf = defineLeaf({
|
|
|
33
33
|
overlayParam('kind', { required: true }),
|
|
34
34
|
overlayParam('when-and-why-to-read', { required: true }),
|
|
35
35
|
overlayParam('short-form'),
|
|
36
|
-
overlayParam('
|
|
37
|
-
overlayParam('
|
|
36
|
+
overlayParam('unlisted'),
|
|
37
|
+
overlayParam('surface'),
|
|
38
38
|
overlayParam('gate'),
|
|
39
|
-
overlayParam('applies-to'),
|
|
40
|
-
overlayParam('read-when'),
|
|
41
39
|
overlayParam('slash'),
|
|
42
40
|
{ kind: 'flag', name: 'rationale', type: 'string', required: false, constraint: DOC_RATIONALE_CONSTRAINT },
|
|
43
41
|
{ kind: 'flag', name: 'scope', type: 'enum', choices: [...MEMORY_SCOPES], required: false, constraint: 'Target scope. Default: project when inside a project, else user. `project` resolves to the NEAREST ancestor `.crouter/` walking up from cwd — in a nested workspace that can be a parent’s store, not the dir you are standing in; pass --dir to pin the exact project directory. `profile` requires a selected profile (CRTR_PROFILE_ID) or an explicit --profile. `node` writes to the this-node store (`nodes/<CRTR_NODE_ID>/context/memory/`) — the nearest scope, seen only by this running node, requires a node context.' },
|
|
@@ -89,32 +87,26 @@ export const writeLeaf = defineLeaf({
|
|
|
89
87
|
};
|
|
90
88
|
setIf('when-and-why-to-read', input['whenAndWhyToRead']);
|
|
91
89
|
setIf('short-form', input['shortForm']);
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
if (input['unlisted'] === true)
|
|
91
|
+
frontmatter['unlisted'] = true;
|
|
92
|
+
if (input['surface'] !== undefined) {
|
|
93
|
+
frontmatter['surfaces'] = input['surface'].map(coerceSurface);
|
|
94
|
+
}
|
|
94
95
|
if (input['gate'] !== undefined)
|
|
95
96
|
frontmatter['gate'] = coerceGate(input['gate']);
|
|
96
|
-
if (input['appliesTo'] !== undefined) {
|
|
97
|
-
frontmatter['applies-to'] = coerceAppliesTo(input['appliesTo']);
|
|
98
|
-
}
|
|
99
|
-
if (input['readWhen'] !== undefined) {
|
|
100
|
-
frontmatter['read-when'] = coerceReadWhen(input['readWhen']);
|
|
101
|
-
}
|
|
102
97
|
if (input['slash'] === true)
|
|
103
98
|
frontmatter['slash'] = true;
|
|
104
99
|
setIf('rationale', input['rationale']);
|
|
105
100
|
// One field consumers read for recency, live from the first byte.
|
|
106
101
|
frontmatter['last-updated'] = origin['created'];
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
throw usage(
|
|
115
|
-
}
|
|
116
|
-
if (scope !== 'project' && routes.some((route) => route.trim() === '.')) {
|
|
117
|
-
throw usage('applies-to "." is a project-store workspace mount route; use a real file glob or choose file-read-visibility none for user, profile, and node memory');
|
|
102
|
+
// A workspace-open entry fires only when cwd/profile mounts the doc's
|
|
103
|
+
// project store, so outside project scope it is a dead route — reject it
|
|
104
|
+
// at authoring time.
|
|
105
|
+
const surfacesVal = frontmatter['surfaces'];
|
|
106
|
+
if (scope !== 'project' &&
|
|
107
|
+
Array.isArray(surfacesVal) &&
|
|
108
|
+
surfacesVal.some((e) => e !== null && typeof e === 'object' && e['on'] === 'workspace-open')) {
|
|
109
|
+
throw usage('a workspace-open surfaces entry is a project-store mount route; user, profile, and node memory cannot carry one — drop the entry or write to a project store');
|
|
118
110
|
}
|
|
119
111
|
const after = serializeMemoryDoc(frontmatter, body);
|
|
120
112
|
writeText(path, after);
|
|
@@ -127,11 +119,9 @@ export const writeLeaf = defineLeaf({
|
|
|
127
119
|
'kind',
|
|
128
120
|
...(input['whenAndWhyToRead'] !== undefined ? ['when-and-why-to-read'] : []),
|
|
129
121
|
...(input['shortForm'] !== undefined ? ['short-form'] : []),
|
|
130
|
-
...(input['
|
|
131
|
-
...(input['
|
|
122
|
+
...(input['unlisted'] === true ? ['unlisted'] : []),
|
|
123
|
+
...(input['surface'] !== undefined ? ['surfaces'] : []),
|
|
132
124
|
...(input['gate'] !== undefined ? ['gate'] : []),
|
|
133
|
-
...(input['appliesTo'] !== undefined ? ['applies-to'] : []),
|
|
134
|
-
...(input['readWhen'] !== undefined ? ['read-when'] : []),
|
|
135
125
|
...(input['slash'] === true ? ['slash'] : []),
|
|
136
126
|
...(input['rationale'] !== undefined ? ['rationale'] : []),
|
|
137
127
|
];
|
package/dist/commands/memory.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
// `crtr memory` subtree — the document substrate (knowledge and preferences)
|
|
2
|
-
// accessed via the CLI. Flat leaves: list, read, find, write, edit,
|
|
3
|
-
// delete, origin, lint.
|
|
2
|
+
// accessed via the CLI. Flat leaves: list, read, find, write, edit, move,
|
|
3
|
+
// history, delete, origin, lint.
|
|
4
4
|
import { defineBranch } from '../core/command.js';
|
|
5
5
|
import { listLeaf } from './memory/list.js';
|
|
6
6
|
import { readLeaf } from './memory/read.js';
|
|
7
7
|
import { findLeaf } from './memory/find.js';
|
|
8
8
|
import { writeLeaf } from './memory/write.js';
|
|
9
9
|
import { editLeaf } from './memory/edit.js';
|
|
10
|
+
import { moveLeaf } from './memory/move.js';
|
|
10
11
|
import { historyLeaf } from './memory/history.js';
|
|
11
12
|
import { deleteLeaf } from './memory/delete.js';
|
|
12
13
|
import { originLeaf } from './memory/origin.js';
|
|
@@ -22,8 +23,8 @@ export function registerMemory() {
|
|
|
22
23
|
help: {
|
|
23
24
|
name: 'memory',
|
|
24
25
|
summary: 'list, read, search, and write memory documents — knowledge and preferences',
|
|
25
|
-
model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Browse the inventory with `list` to see what is stored; address a document directly with `read` once you know its name. One verb per operation: `write` creates, `edit` revises (every revision carries a rationale and is recorded), `delete` removes, and `history` reads how a document reached its current state.
|
|
26
|
+
model: 'Documents have path-derived identities and resolve across layered scopes in precedence order: node > project stack > profile > user > builtin. Browse the inventory with `list` to see what is stored; address a document directly with `read` once you know its name — a directory name is a valid read target answering with its listing. One verb per operation: `write` creates, `edit` revises (every revision carries a rationale and is recorded), `move` relocates or renames a doc and rewrites inbound `[[refs]]` corpus-wide, `delete` removes, and `history` reads how a document reached its current state. Beyond listings, delivery is explicit `surfaces` routing: each entry names the event that fires it (boot, workspace-open, read, memory-read, command) and how much delivers (name, preview, content).',
|
|
26
27
|
},
|
|
27
|
-
children: [listLeaf, readLeaf, findLeaf, writeLeaf, editLeaf, historyLeaf, deleteLeaf, originLeaf, lintLeaf],
|
|
28
|
+
children: [listLeaf, readLeaf, findLeaf, writeLeaf, editLeaf, moveLeaf, historyLeaf, deleteLeaf, originLeaf, lintLeaf],
|
|
28
29
|
});
|
|
29
30
|
}
|
|
@@ -360,12 +360,10 @@ function docDetails(plugin) {
|
|
|
360
360
|
...(parsed.whenAndWhyToRead !== "" ? { routingLine: parsed.whenAndWhyToRead } : {}),
|
|
361
361
|
path: parsed.path,
|
|
362
362
|
...(sizeBytes !== undefined ? { sizeBytes } : {}),
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
surfaces: parsed.surfaces,
|
|
364
|
+
unlisted: parsed.unlisted,
|
|
365
365
|
slash: parsed.slash,
|
|
366
|
-
...(parsed.appliesTo !== undefined ? { appliesTo: parsed.appliesTo } : {}),
|
|
367
366
|
...(parsed.gate !== undefined ? { gate: parsed.gate } : {}),
|
|
368
|
-
...(parsed.readWhen !== undefined ? { readWhenPredicate: parsed.readWhen } : {}),
|
|
369
367
|
...(parsed.rationale !== undefined ? { rationale: parsed.rationale } : {}),
|
|
370
368
|
body: parsed.body,
|
|
371
369
|
}];
|
|
@@ -19,21 +19,27 @@ export function renderDocView(doc, width) {
|
|
|
19
19
|
}
|
|
20
20
|
out.push("", theme.fg("dim", "READ WHEN"));
|
|
21
21
|
out.push(...paragraph(doc.routingLine || "Not declared.", viewWidth));
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
22
|
+
// One row per surfaces entry answers "will an agent ever see this?"; a doc
|
|
23
|
+
// with none is reachable through listings and explicit reads only, and the
|
|
24
|
+
// qualifiers below are shown only when the doc sets one.
|
|
25
|
+
out.push("", theme.fg("dim", "ROUTING"));
|
|
26
|
+
if (doc.surfaces.length === 0) {
|
|
27
|
+
out.push(...paragraph("No surfaces — reachable through directory listings and explicit reads only.", viewWidth));
|
|
28
|
+
}
|
|
29
|
+
for (const entry of doc.surfaces) {
|
|
30
|
+
const parts = [`at ${entry.at}`];
|
|
31
|
+
if (entry.match?.length)
|
|
32
|
+
parts.push(entry.match.join(", "));
|
|
33
|
+
if (entry.matchFrontmatter !== undefined)
|
|
34
|
+
parts.push(JSON.stringify(entry.matchFrontmatter));
|
|
35
|
+
out.push(...field(entry.on, parts.join(" "), viewWidth));
|
|
36
|
+
}
|
|
37
|
+
if (doc.unlisted)
|
|
38
|
+
out.push(...field("unlisted", "suppressed from directory listings", viewWidth));
|
|
29
39
|
if (doc.slash)
|
|
30
40
|
out.push(...field("slash", `/${doc.name.replaceAll("/", ":")}`, viewWidth));
|
|
31
|
-
if (doc.appliesTo?.length)
|
|
32
|
-
out.push(...field("applies to", doc.appliesTo.join(", "), viewWidth));
|
|
33
41
|
if (doc.gate !== undefined)
|
|
34
42
|
out.push(...field("gate", JSON.stringify(doc.gate), viewWidth));
|
|
35
|
-
if (doc.readWhenPredicate !== undefined)
|
|
36
|
-
out.push(...field("read-when predicate", JSON.stringify(doc.readWhenPredicate), viewWidth));
|
|
37
43
|
if (doc.rationale) {
|
|
38
44
|
out.push("", theme.fg("dim", "RATIONALE"));
|
|
39
45
|
out.push(...paragraph(doc.rationale, viewWidth));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { Field, InputParam } from "../../../core/help.js";
|
|
2
|
-
import type {
|
|
2
|
+
import type { SurfaceEntry } from "../../../core/substrate/schema.js";
|
|
3
3
|
import type { OwnerRef, Scope } from "../../../types.js";
|
|
4
4
|
/** The scopes that can hold a marketplace, and that a plugin can be installed
|
|
5
5
|
* into. Narrower than `Scope`: `builtin` names plugins that ship with crtr, and
|
|
@@ -87,18 +87,16 @@ export interface DocDetail {
|
|
|
87
87
|
/** Absolute path to the `.md` on disk. */
|
|
88
88
|
path: string;
|
|
89
89
|
sizeBytes?: number;
|
|
90
|
-
/**
|
|
91
|
-
|
|
92
|
-
|
|
90
|
+
/** Frontmatter `surfaces`: explicit event-routing entries — when the doc
|
|
91
|
+
* delivers, and how much. Empty = reachable through listings and explicit
|
|
92
|
+
* reads only. */
|
|
93
|
+
surfaces: SurfaceEntry[];
|
|
94
|
+
/** Frontmatter `unlisted`: suppressed from directory listings. */
|
|
95
|
+
unlisted: boolean;
|
|
93
96
|
/** Invocable as a pi slash command (`slash: true`). */
|
|
94
97
|
slash: boolean;
|
|
95
|
-
/** Frontmatter `applies-to`: explicit file-context routes. */
|
|
96
|
-
appliesTo?: string[];
|
|
97
98
|
/** Frontmatter `gate`: eligibility predicate over the node's config. */
|
|
98
99
|
gate?: Record<string, unknown>;
|
|
99
|
-
/** Frontmatter `read-when`: predicate over a READ FILE's own frontmatter.
|
|
100
|
-
* Unrelated to `routingLine` despite the neighbouring names. */
|
|
101
|
-
readWhenPredicate?: Record<string, unknown>;
|
|
102
100
|
/** Frontmatter `rationale`: the observed failure the doc exists to close.
|
|
103
101
|
* Maintainer-facing — never delivered to a reading agent. */
|
|
104
102
|
rationale?: string;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const sysMigrateLeaf: import("../../core/command.js").LeafDef;
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// `crtr sys migrate` — the convergent-lane state-migration runner: converge
|
|
2
|
+
// every writable memory store's documents to the current document format.
|
|
3
|
+
// (Journaled canvas-db migrations run in the db's own ordered chain when the
|
|
4
|
+
// daemon opens it; this leaf owns only the on-disk document stores.)
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { defineLeaf } from '../../core/command.js';
|
|
7
|
+
import { pathExists, realpathOrSelf } from '../../core/fs-utils.js';
|
|
8
|
+
import { listInstalledPlugins, listInstalledPluginsInRoot } from '../../core/resolver.js';
|
|
9
|
+
import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from '../../core/scope.js';
|
|
10
|
+
import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
|
|
11
|
+
import { getDefaultProfileId } from '../../core/profiles/default-binding.js';
|
|
12
|
+
import { descendantStoreRoots } from '../../core/nested-stores.js';
|
|
13
|
+
import { memoryDir as nodeMemoryDir } from '../../core/runtime/memory.js';
|
|
14
|
+
import { runConvergentMigrations } from '../../migrations/convergent.js';
|
|
15
|
+
/** Every writable memory store — the lint corpus minus builtin (package-owned,
|
|
16
|
+
* regenerated on every install, so migrating it would fight the package). */
|
|
17
|
+
function migratableStoreDirs() {
|
|
18
|
+
const dirs = new Set();
|
|
19
|
+
const add = (dir) => {
|
|
20
|
+
// realpath so one store reached through two spellings (e.g. a symlinked
|
|
21
|
+
// ancestor) migrates once.
|
|
22
|
+
if (dir !== null && dir !== '' && pathExists(dir))
|
|
23
|
+
dirs.add(realpathOrSelf(dir));
|
|
24
|
+
};
|
|
25
|
+
const projectRoots = projectScopeRoots();
|
|
26
|
+
for (const root of projectRoots) {
|
|
27
|
+
add(join(root, 'memory'));
|
|
28
|
+
for (const plugin of listInstalledPluginsInRoot('project', root)) {
|
|
29
|
+
if (plugin.enabled)
|
|
30
|
+
add(pluginMemoryDir(plugin));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
for (const root of descendantStoreRoots(projectRoots))
|
|
34
|
+
add(join(root, 'memory'));
|
|
35
|
+
add(scopeMemoryDir('user'));
|
|
36
|
+
for (const plugin of listInstalledPlugins('user')) {
|
|
37
|
+
if (plugin.enabled)
|
|
38
|
+
add(pluginMemoryDir(plugin));
|
|
39
|
+
}
|
|
40
|
+
const profileIdOrName = process.env['CRTR_PROFILE_ID'] || getDefaultProfileId(process.cwd());
|
|
41
|
+
if (profileIdOrName) {
|
|
42
|
+
try {
|
|
43
|
+
const { profileId } = loadProfileManifest(profileIdOrName);
|
|
44
|
+
add(profileMemoryDir(profileId));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Unresolvable profile: no profile store to migrate.
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const nodeId = process.env['CRTR_NODE_ID'] ?? '';
|
|
51
|
+
if (nodeId !== '')
|
|
52
|
+
add(nodeMemoryDir(nodeId));
|
|
53
|
+
return [...dirs];
|
|
54
|
+
}
|
|
55
|
+
export const sysMigrateLeaf = defineLeaf({
|
|
56
|
+
name: 'migrate',
|
|
57
|
+
description: 'converge every writable memory store to the current document format',
|
|
58
|
+
whenToUse: 'after a crtr update whose release notes name a document-format migration, or when old-format docs resurface (a restore, a sync, a hand-authored file). Convergent and idempotent: re-running over a current corpus changes nothing, so use --dry-run first only when you want to review the rewrite.',
|
|
59
|
+
help: {
|
|
60
|
+
name: 'sys migrate',
|
|
61
|
+
summary: 'run the registered document-format migrations over every writable memory store',
|
|
62
|
+
params: [
|
|
63
|
+
{ kind: 'flag', name: 'dry-run', type: 'bool', required: false, constraint: 'Plan and report every rewrite without writing any file.' },
|
|
64
|
+
],
|
|
65
|
+
output: [
|
|
66
|
+
{ name: 'stores', type: 'number', required: true, constraint: 'Memory stores scanned.' },
|
|
67
|
+
{ name: 'changed', type: 'number', required: true, constraint: 'Files rewritten (or, under --dry-run, that would be).' },
|
|
68
|
+
{ name: 'applied', type: 'object[]', required: true, constraint: 'One row per migration that changed files in a store: {store, migration, files}. Empty when the corpus is already current.' },
|
|
69
|
+
{ name: 'skipped', type: 'object[]', required: true, constraint: 'Docs left untouched because their frontmatter is not valid YAML: {store, relPath, error}. Fix the doc, then re-run.' },
|
|
70
|
+
{ name: 'dryRun', type: 'boolean', required: true, constraint: 'True when nothing was written (preview only).' },
|
|
71
|
+
],
|
|
72
|
+
outputKind: 'object',
|
|
73
|
+
effects: [
|
|
74
|
+
'Rewrites old-format memory documents in place (atomic per-file writes) across the writable stores: project (plus nested descendant stores and installed plugin stores), user, profile, and this node\'s store. Builtin docs are package-owned and never touched.',
|
|
75
|
+
'Under --dry-run: read-only, writes nothing.',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
run: async (input) => {
|
|
79
|
+
const dryRun = input.dryRun === true;
|
|
80
|
+
const applied = [];
|
|
81
|
+
const skipped = [];
|
|
82
|
+
const stores = migratableStoreDirs();
|
|
83
|
+
for (const store of stores) {
|
|
84
|
+
const result = runConvergentMigrations(store, { dryRun });
|
|
85
|
+
for (const row of result.applied)
|
|
86
|
+
applied.push({ store, migration: row.migration, files: row.files });
|
|
87
|
+
for (const s of result.skipped)
|
|
88
|
+
skipped.push({ store, relPath: s.relPath, error: s.error });
|
|
89
|
+
}
|
|
90
|
+
const changed = applied.reduce((n, row) => n + row.files.length, 0);
|
|
91
|
+
return {
|
|
92
|
+
stores: stores.length,
|
|
93
|
+
changed,
|
|
94
|
+
applied,
|
|
95
|
+
skipped,
|
|
96
|
+
dryRun,
|
|
97
|
+
follow_up: skipped.length > 0
|
|
98
|
+
? 'Some docs have invalid YAML frontmatter and were left untouched — fix each named file, then re-run `crtr sys migrate`.'
|
|
99
|
+
: changed === 0
|
|
100
|
+
? 'Corpus already current — nothing to rewrite.'
|
|
101
|
+
: dryRun
|
|
102
|
+
? 'Preview only — re-run without --dry-run to write these changes.'
|
|
103
|
+
: undefined,
|
|
104
|
+
};
|
|
105
|
+
},
|
|
106
|
+
});
|
|
@@ -186,10 +186,8 @@ const CONTROLLED_FIELDS = [
|
|
|
186
186
|
'kind',
|
|
187
187
|
'when-and-why-to-read',
|
|
188
188
|
'short-form',
|
|
189
|
-
'
|
|
190
|
-
'
|
|
191
|
-
'applies-to',
|
|
192
|
-
'read-when',
|
|
189
|
+
'unlisted',
|
|
190
|
+
'surfaces',
|
|
193
191
|
'gate',
|
|
194
192
|
'rationale',
|
|
195
193
|
];
|
|
@@ -367,8 +365,7 @@ export function runDepsSync(opts) {
|
|
|
367
365
|
'short-form': entry.description !== ''
|
|
368
366
|
? condenseDescription(entry.description)
|
|
369
367
|
: `Vendored agent doc shipped by ${entry.pkg}.`,
|
|
370
|
-
'
|
|
371
|
-
'file-read-visibility': 'none',
|
|
368
|
+
surfaces: [{ on: 'boot', at: 'name' }],
|
|
372
369
|
'source-package': `${entry.pkg}@${entry.version}`,
|
|
373
370
|
...passthroughFm(entry.fm),
|
|
374
371
|
};
|
|
@@ -391,8 +388,7 @@ export function runDepsSync(opts) {
|
|
|
391
388
|
kind: 'knowledge',
|
|
392
389
|
'when-and-why-to-read': `When ${parentName} links to this file, this knowledge should be read because it carries the linked supporting reference.`,
|
|
393
390
|
'short-form': firstHeading(body) ?? `Supporting reference vendored from ${entry.pkg}.`,
|
|
394
|
-
'
|
|
395
|
-
'file-read-visibility': 'none',
|
|
391
|
+
surfaces: [{ on: 'boot', at: 'name' }],
|
|
396
392
|
'source-package': `${entry.pkg}@${entry.version}`,
|
|
397
393
|
...passthroughFm(sourceFm),
|
|
398
394
|
};
|
|
@@ -428,8 +424,7 @@ export function runDepsSync(opts) {
|
|
|
428
424
|
'short-form': emptyCatalog
|
|
429
425
|
? 'No dependency-shipped agent docs are currently vendored.'
|
|
430
426
|
: "Generated catalog of the agent docs shipped inside this project's npm dependencies.",
|
|
431
|
-
'
|
|
432
|
-
'file-read-visibility': 'none',
|
|
427
|
+
surfaces: [{ on: 'boot', at: 'preview' }],
|
|
433
428
|
'generated-by': DEPS_GENERATED_MARKER,
|
|
434
429
|
};
|
|
435
430
|
const indexBody = emptyCatalog
|