@north-light/crouter 0.3.208 → 0.3.210
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/viewer.js +366 -366
- 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__/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/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-extension-render.d.ts +3 -2
- package/dist/core/runtime/broker-extension-render.js +3 -3
- package/dist/core/runtime/memory.js +2 -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/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/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/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
|
@@ -13,27 +13,29 @@ import { listInstalledPlugins, listInstalledPluginsInRoot } from '../../core/res
|
|
|
13
13
|
import { pluginMemoryDir, projectScopeRoots, scopeMemoryDir } from '../../core/scope.js';
|
|
14
14
|
import { loadProfileManifest, profileMemoryDir } from '../../core/profiles/manifest.js';
|
|
15
15
|
import { getDefaultProfileId } from '../../core/profiles/default-binding.js';
|
|
16
|
-
import { isDocKind, normalizeDocName, resolveDocName,
|
|
17
|
-
import { displayName } from '../../core/substrate/ceiling.js';
|
|
16
|
+
import { isDocKind, normalizeDocName, parseSubstrateFrontmatter, resolveDocName, SURFACE_EVENTS, SURFACE_RUNGS, } from '../../core/substrate/schema.js';
|
|
18
17
|
import { docLinkNames } from '../../core/memory/doc-link-grammar.js';
|
|
19
18
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
20
19
|
import { descendantStoreRoots } from '../../core/nested-stores.js';
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
/** The four retired visibility-axis fields. The cut is hard: an old-format doc
|
|
21
|
+
* fails loudly and names the migration, never gets a dual-parse fallback. */
|
|
22
|
+
const RETIRED_FIELDS = ['system-prompt-visibility', 'file-read-visibility', 'applies-to', 'read-when'];
|
|
23
|
+
/** The frontmatter keys one `surfaces` entry may carry. */
|
|
24
|
+
const SURFACE_ENTRY_KEYS = ['on', 'match', 'match-frontmatter', 'at'];
|
|
23
25
|
/** Rung-scaled body-length caps, measured in WORDS (frontmatter excluded).
|
|
24
26
|
* Words, not lines: house style writes each paragraph as ONE logical line
|
|
25
27
|
* and lets the editor soft-wrap, so a line count measures wrapping style
|
|
26
28
|
* rather than context cost — words track what the reader actually pays.
|
|
27
29
|
*
|
|
28
|
-
* `
|
|
29
|
-
* system prompt
|
|
30
|
-
* the whole body
|
|
31
|
-
* cap at 1000 words. `
|
|
30
|
+
* A `boot` entry at `content` inlines the whole body into every agent's
|
|
31
|
+
* system prompt; a `content` entry on any other event (workspace-open, read,
|
|
32
|
+
* memory-read, command) surfaces the whole body whenever the entry fires —
|
|
33
|
+
* both cap at 1000 words. A `boot` entry at `preview` routes a deliberate
|
|
32
34
|
* reader into the whole body, so its cap is the longest doc worth reading
|
|
33
|
-
* end-to-end (calibrated to the humanizer doc, 2936 words). `name
|
|
34
|
-
*
|
|
35
|
-
* [[link]]), so its length is the reader's choice.
|
|
36
|
-
* applicable cap wins.
|
|
35
|
+
* end-to-end (calibrated to the humanizer doc, 2936 words). `name` entries
|
|
36
|
+
* and surface-less docs never cap: such a doc is only reached deliberately
|
|
37
|
+
* (a listing, browse, or a [[link]]), so its length is the reader's choice.
|
|
38
|
+
* The strictest applicable cap wins.
|
|
37
39
|
*
|
|
38
40
|
* Persona layers — canonical names under `kinds/`, the docs the prompt
|
|
39
41
|
* render composes into an agent's persona — are structurally exempt: an
|
|
@@ -42,9 +44,9 @@ const RUNG_FIELDS = ['system-prompt-visibility', 'file-read-visibility'];
|
|
|
42
44
|
* deliberate and per-doc: `lint-ignore: length` in the frontmatter,
|
|
43
45
|
* surfaced ONLY by the finding itself — never advertised in authoring
|
|
44
46
|
* help. */
|
|
45
|
-
const
|
|
46
|
-
const
|
|
47
|
-
const
|
|
47
|
+
const BOOT_CONTENT_MAX_WORDS = 1000;
|
|
48
|
+
const EVENT_CONTENT_MAX_WORDS = 1000;
|
|
49
|
+
const BOOT_PREVIEW_MAX_WORDS = 3000;
|
|
48
50
|
/** A routing line costs more than it saves for a short rule or fact. */
|
|
49
51
|
const PREVIEW_TO_CONTENT_MAX_WORDS = 30;
|
|
50
52
|
/** Rules a doc may suppress via frontmatter `lint-ignore`. */
|
|
@@ -57,24 +59,28 @@ function countBodyWords(body) {
|
|
|
57
59
|
const trimmed = body.trim();
|
|
58
60
|
return trimmed === '' ? 0 : trimmed.split(/\s+/).length;
|
|
59
61
|
}
|
|
60
|
-
/** The
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
|
|
64
|
-
|
|
62
|
+
/** The doc's surfaces entries as the runtime will read them. Used by the cap
|
|
63
|
+
* checks AFTER the strict schema check has run — for a doc that passes it,
|
|
64
|
+
* the tolerant parse and the strict contract agree. */
|
|
65
|
+
function parsedSurfaces(fm) {
|
|
66
|
+
return parseSubstrateFrontmatter(fm)?.surfaces ?? [];
|
|
67
|
+
}
|
|
68
|
+
/** Warn when a body short enough to inline still pays a routing line. */
|
|
65
69
|
export function lintShortPreviewBody(fm, body) {
|
|
66
70
|
const words = countBodyWords(body);
|
|
67
71
|
if (words >= PREVIEW_TO_CONTENT_MAX_WORDS)
|
|
68
72
|
return null;
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
fm['file-read-visibility'] === 'preview' ? 'file-read-visibility' : undefined,
|
|
72
|
-
].filter((axis) => axis !== undefined);
|
|
73
|
-
if (previewAxes.length === 0)
|
|
73
|
+
const previewEvents = [...new Set(parsedSurfaces(fm).filter((e) => e.at === 'preview').map((e) => e.on))];
|
|
74
|
+
if (previewEvents.length === 0)
|
|
74
75
|
return null;
|
|
75
|
-
const
|
|
76
|
-
return `body is ${words} words but ${
|
|
76
|
+
const noun = previewEvents.length === 1 ? 'entry delivers' : 'entries deliver';
|
|
77
|
+
return `body is ${words} words but the ${previewEvents.join('/')} ${noun} \`preview\`; use \`at: content\` so agents receive the whole rule without a separate memory read`;
|
|
77
78
|
}
|
|
79
|
+
/** The length rule. Caps are computed from the parsed entries — an invalid
|
|
80
|
+
* entry already fails the schema check, so it simply matches no cap here.
|
|
81
|
+
* `docName` is the doc's canonical resolver identity (explicit frontmatter
|
|
82
|
+
* `name`, else the normalized path-derived name) — persona layers under
|
|
83
|
+
* `kinds/` are recognized by it and never capped. */
|
|
78
84
|
export function lintBodyLength(fm, body, docName) {
|
|
79
85
|
if (ignoresRule(fm, 'length'))
|
|
80
86
|
return null;
|
|
@@ -83,28 +89,78 @@ export function lintBodyLength(fm, body, docName) {
|
|
|
83
89
|
// construction — persona length is persona design, never an authoring smell.
|
|
84
90
|
if (docName === 'kinds' || docName.startsWith('kinds/'))
|
|
85
91
|
return null;
|
|
86
|
-
const
|
|
87
|
-
const file = fm['file-read-visibility'];
|
|
92
|
+
const entries = parsedSurfaces(fm);
|
|
88
93
|
const words = countBodyWords(body);
|
|
89
|
-
const remedy = 'Keep the load-bearing core here and split the depth into [[linked]] reference docs
|
|
90
|
-
if (
|
|
91
|
-
return `body is ${words} words but
|
|
94
|
+
const remedy = 'Keep the load-bearing core here and split the depth into [[linked]] reference docs carrying no surfaces at all (the listing and the link are how they are found, so they cost nothing until followed). Split by subject: each leaf covers a different subject a task might need on its own; never split off “further evidence”, examples, or references — a references leaf is never followed, so supporting material stays next to the point it supports or gets cut. Keep it whole — `lint-ignore: length` in the frontmatter — only when every reader who surfaces this doc genuinely benefits from reading 100% of it, or it is one indivisible body of knowledge; then splitting just adds hops.';
|
|
95
|
+
if (entries.some((e) => e.on === 'boot' && e.at === 'content') && words > BOOT_CONTENT_MAX_WORDS) {
|
|
96
|
+
return `body is ${words} words but a boot entry at \`content\` inlines every word into every agent's system prompt — capped at ${BOOT_CONTENT_MAX_WORDS} words (boot preview gets ${BOOT_PREVIEW_MAX_WORDS}; name entries are never capped). ${remedy}`;
|
|
92
97
|
}
|
|
93
|
-
|
|
94
|
-
|
|
98
|
+
const contentEvents = [...new Set(entries.filter((e) => e.on !== 'boot' && e.at === 'content').map((e) => e.on))];
|
|
99
|
+
if (contentEvents.length > 0 && words > EVENT_CONTENT_MAX_WORDS) {
|
|
100
|
+
return `body is ${words} words, over the ${EVENT_CONTENT_MAX_WORDS}-word cap for a \`content\` entry on ${contentEvents.join('/')} (the whole body delivers every time the entry fires; name entries are never capped). ${remedy}`;
|
|
95
101
|
}
|
|
96
|
-
if (
|
|
97
|
-
return `body is ${words} words, over the ${
|
|
102
|
+
if (entries.some((e) => e.on === 'boot' && e.at === 'preview') && words > BOOT_PREVIEW_MAX_WORDS) {
|
|
103
|
+
return `body is ${words} words, over the ${BOOT_PREVIEW_MAX_WORDS}-word cap for boot preview (the routing line invites every reader into the whole body, so the cap is the longest doc worth reading end-to-end; boot content is capped at ${BOOT_CONTENT_MAX_WORDS} words; name entries are never capped). ${remedy}`;
|
|
104
|
+
}
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
/** Strict validation of a raw `surfaces` value — the mirror of `coerceSurface`
|
|
108
|
+
* in shared.ts: everything the runtime parser would silently drop or trim is
|
|
109
|
+
* an error here. Returns the first problem found, or null. */
|
|
110
|
+
function lintSurfacesField(v) {
|
|
111
|
+
if (v === undefined)
|
|
112
|
+
return null;
|
|
113
|
+
if (!Array.isArray(v))
|
|
114
|
+
return `invalid surfaces: ${JSON.stringify(v)} (expected a list of {on, at, match?, match-frontmatter?} entries)`;
|
|
115
|
+
for (const raw of v) {
|
|
116
|
+
if (raw === null || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
117
|
+
return `invalid surfaces entry: ${JSON.stringify(raw)} (expected an {on, at, match?, match-frontmatter?} object)`;
|
|
118
|
+
}
|
|
119
|
+
const rec = raw;
|
|
120
|
+
for (const key of Object.keys(rec)) {
|
|
121
|
+
if (!SURFACE_ENTRY_KEYS.includes(key)) {
|
|
122
|
+
return `invalid surfaces entry: unknown key \`${key}\` (an entry carries only on, match, match-frontmatter, at)`;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const on = rec['on'];
|
|
126
|
+
if (typeof on !== 'string' || !SURFACE_EVENTS.includes(on)) {
|
|
127
|
+
return `invalid surfaces entry \`on\`: ${JSON.stringify(on)} (expected ${SURFACE_EVENTS.join('|')})`;
|
|
128
|
+
}
|
|
129
|
+
const at = rec['at'];
|
|
130
|
+
if (typeof at !== 'string' || !SURFACE_RUNGS.includes(at)) {
|
|
131
|
+
return `invalid surfaces entry \`at\`: ${JSON.stringify(at)} (expected ${SURFACE_RUNGS.join('|')})`;
|
|
132
|
+
}
|
|
133
|
+
let hasMatch = false;
|
|
134
|
+
if (rec['match'] !== undefined) {
|
|
135
|
+
if (on === 'boot' || on === 'workspace-open') {
|
|
136
|
+
return `invalid surfaces entry: \`match\` is meaningless on \`${on}\` — the entry's presence is the match; drop it`;
|
|
137
|
+
}
|
|
138
|
+
const m = rec['match'];
|
|
139
|
+
const globs = typeof m === 'string' ? [m] : Array.isArray(m) && m.every((g) => typeof g === 'string') ? m : null;
|
|
140
|
+
if (globs === null || globs.length === 0 || globs.some((g) => g.trim() === '')) {
|
|
141
|
+
return `invalid surfaces entry \`match\`: ${JSON.stringify(m)} (expected a non-empty glob or non-empty glob list)`;
|
|
142
|
+
}
|
|
143
|
+
hasMatch = true;
|
|
144
|
+
}
|
|
145
|
+
const mf = rec['match-frontmatter'];
|
|
146
|
+
if (mf !== undefined) {
|
|
147
|
+
if (on !== 'read')
|
|
148
|
+
return 'invalid surfaces entry: `match-frontmatter` is a `read`-event predicate only';
|
|
149
|
+
if (mf === null || typeof mf !== 'object' || Array.isArray(mf)) {
|
|
150
|
+
return `invalid surfaces entry \`match-frontmatter\`: ${JSON.stringify(mf)} (expected a field→matcher object)`;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if ((on === 'read' || on === 'memory-read' || on === 'command') && !hasMatch && mf === undefined) {
|
|
154
|
+
return `invalid surfaces entry: a \`${on}\` entry requires \`match\`${on === 'read' ? ' or `match-frontmatter`' : ''}`;
|
|
155
|
+
}
|
|
98
156
|
}
|
|
99
157
|
return null;
|
|
100
158
|
}
|
|
101
159
|
/** Schema checks for a doc living in a substrate memory dir: a memory store
|
|
102
160
|
* holds ONLY substrate docs, so a missing/invalid `kind` is an authoring
|
|
103
|
-
* error here (elsewhere it just means "not a substrate doc").
|
|
104
|
-
*
|
|
105
|
-
*
|
|
106
|
-
* silently falls back to the neutral floor / inert gates, which is exactly the
|
|
107
|
-
* silent tolerance this lint exists to catch at authoring time. */
|
|
161
|
+
* error here (elsewhere it just means "not a substrate doc"). Fields are
|
|
162
|
+
* checked RAW — the runtime parser silently drops invalid entries, which is
|
|
163
|
+
* exactly the silent tolerance this lint exists to catch at authoring time. */
|
|
108
164
|
export function lintSubstrateSchema(fm) {
|
|
109
165
|
if (fm === null)
|
|
110
166
|
return 'missing frontmatter: a memory store doc requires `kind: knowledge|preference`';
|
|
@@ -120,52 +176,22 @@ export function lintSubstrateSchema(fm) {
|
|
|
120
176
|
if (typeof fm['when-and-why-to-read'] !== 'string' || fm['when-and-why-to-read'].trim() === '') {
|
|
121
177
|
return 'missing `when-and-why-to-read`: one routing line — "When <circumstance>, this <kind> should be read because <broader downstream payoff>." WHY is the benefit unlocked by reading, not the document summary, rule, or obedience rationale.';
|
|
122
178
|
}
|
|
123
|
-
for (const field of
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
return `missing ${field}: choose a rung explicitly (${RUNGS.join('|')}) — there is no kind default`;
|
|
127
|
-
}
|
|
128
|
-
if (typeof v !== 'string' || !VALID_RUNGS.includes(v)) {
|
|
129
|
-
return `invalid ${field}: ${JSON.stringify(v)} (expected ${RUNGS.join('|')})`;
|
|
179
|
+
for (const field of RETIRED_FIELDS) {
|
|
180
|
+
if (field in fm) {
|
|
181
|
+
return `retired \`${field}\` key: the visibility axes were replaced by \`surfaces\` event routing — run \`crtr sys migrate\` to convert old-format docs, or author \`surfaces\` entries by hand (\`crtr memory write -h\`)`;
|
|
130
182
|
}
|
|
131
183
|
}
|
|
184
|
+
const unlisted = fm['unlisted'];
|
|
185
|
+
if (unlisted !== undefined && typeof unlisted !== 'boolean') {
|
|
186
|
+
return `invalid unlisted: ${JSON.stringify(unlisted)} (expected a boolean)`;
|
|
187
|
+
}
|
|
188
|
+
const surfacesError = lintSurfacesField(fm['surfaces']);
|
|
189
|
+
if (surfacesError !== null)
|
|
190
|
+
return surfacesError;
|
|
132
191
|
const gate = fm.gate;
|
|
133
192
|
if (gate !== undefined && (gate === null || typeof gate !== 'object' || Array.isArray(gate))) {
|
|
134
193
|
return `invalid gate: ${JSON.stringify(gate)} (expected a field→matcher object)`;
|
|
135
194
|
}
|
|
136
|
-
const appliesTo = fm['applies-to'];
|
|
137
|
-
let appliesToGlobs;
|
|
138
|
-
if (appliesTo !== undefined) {
|
|
139
|
-
if (typeof appliesTo === 'string') {
|
|
140
|
-
appliesToGlobs = [appliesTo];
|
|
141
|
-
}
|
|
142
|
-
else if (Array.isArray(appliesTo) && appliesTo.every((g) => typeof g === 'string')) {
|
|
143
|
-
appliesToGlobs = appliesTo;
|
|
144
|
-
}
|
|
145
|
-
else {
|
|
146
|
-
return `invalid applies-to: ${JSON.stringify(appliesTo)} (expected one non-empty file glob or a non-empty list of file globs)`;
|
|
147
|
-
}
|
|
148
|
-
if (appliesToGlobs.length === 0 || appliesToGlobs.some((glob) => glob.trim() === '')) {
|
|
149
|
-
return `invalid applies-to: ${JSON.stringify(appliesTo)} (expected one non-empty file glob or a non-empty list of file globs)`;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
// read-when (Stream A on-read frontmatter trigger): same well-formed-object
|
|
153
|
-
// contract as gate — a non-object is inert (never fires), so catch it here.
|
|
154
|
-
const readWhen = fm['read-when'];
|
|
155
|
-
if (readWhen !== undefined && (readWhen === null || typeof readWhen !== 'object' || Array.isArray(readWhen))) {
|
|
156
|
-
return `invalid read-when: ${JSON.stringify(readWhen)} (expected a field→matcher object)`;
|
|
157
|
-
}
|
|
158
|
-
// Every authored file-read surface needs an explicit event boundary. Runtime
|
|
159
|
-
// parsing stays tolerant so one bad doc cannot break an agent's context.
|
|
160
|
-
if (fm['file-read-visibility'] !== 'none' && appliesToGlobs === undefined) {
|
|
161
|
-
return `missing applies-to: file-read-visibility is \`${fm['file-read-visibility']}\`, so this memory must declare when it surfaces. Use \`applies-to: "."\` for context loaded when cwd or a selected profile mounts this project store and on any file read beneath the store's owning dir, use a non-empty file glob (or YAML list) for context loaded only after a matching file is read, or set \`file-read-visibility: none\` when this memory has no file-context route. File globs match the read file's absolute path, basename, or path relative to the memory's owning project root. \`read-when\` matches file metadata but does not replace the required workspace/file boundary. Run \`crtr memory write -h\` before changing routing frontmatter.`;
|
|
162
|
-
}
|
|
163
|
-
// A dead on-read trigger: an explicit applies-to/read-when with nothing to
|
|
164
|
-
// surface (file-read-visibility none) can never fire — flag it loudly rather
|
|
165
|
-
// than store a silent no-op.
|
|
166
|
-
if (fm['file-read-visibility'] === 'none' && (appliesTo !== undefined || readWhen !== undefined)) {
|
|
167
|
-
return 'dead on-read trigger: applies-to/read-when is set but file-read-visibility is `none` — raise the rung or drop the trigger';
|
|
168
|
-
}
|
|
169
195
|
const slash = fm.slash;
|
|
170
196
|
if (slash !== undefined && typeof slash !== 'boolean') {
|
|
171
197
|
return `invalid slash: ${JSON.stringify(slash)} (expected a boolean)`;
|
|
@@ -214,27 +240,64 @@ function lintFile(file, substrateStore, findings, warnings, corpusNames, fallbac
|
|
|
214
240
|
const shortPreviewWarning = lintShortPreviewBody(fm, body);
|
|
215
241
|
if (shortPreviewWarning !== null)
|
|
216
242
|
warnings.push(`${file}: ${shortPreviewWarning}`);
|
|
243
|
+
// An over-broad memory-read glob fires on EVERY memory read — almost
|
|
244
|
+
// always an authoring accident, so flag it without failing the corpus.
|
|
245
|
+
for (const entry of parsedSurfaces(fm)) {
|
|
246
|
+
if (entry.on !== 'memory-read')
|
|
247
|
+
continue;
|
|
248
|
+
for (const glob of entry.match ?? []) {
|
|
249
|
+
if (glob === '**' || glob === '*') {
|
|
250
|
+
warnings.push(`${file}: memory-read glob \`${glob}\` fires on every memory read — scope it to a name subtree`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
217
254
|
}
|
|
218
255
|
for (const name of docLinkNames(body)) {
|
|
219
256
|
if (!corpusNames.has(name)) {
|
|
220
257
|
findings.push({
|
|
221
258
|
path: file,
|
|
222
|
-
error: `dangling doc link [[${name}]]: no memory document has that exact canonical name — retarget the link (\`crtr memory find ${name.split('/').pop()}\`) or drop it`,
|
|
259
|
+
error: `dangling doc link [[${name}]]: no memory document or directory has that exact canonical name — retarget the link (\`crtr memory find ${name.split('/').pop()}\`) or drop it`,
|
|
223
260
|
});
|
|
224
261
|
}
|
|
225
262
|
}
|
|
226
263
|
}
|
|
264
|
+
/** The files in a project store carrying a workspace front-door entry
|
|
265
|
+
* ({on: workspace-open, at: content}). Raw fm scan; a missing store or an
|
|
266
|
+
* unparseable doc contributes zero (the YAML failure is its own finding). */
|
|
267
|
+
function frontDoorDocs(storeDir) {
|
|
268
|
+
const hits = [];
|
|
269
|
+
if (!pathExists(storeDir))
|
|
270
|
+
return hits;
|
|
271
|
+
for (const file of walkFiles(storeDir, (n) => n.endsWith('.md'), (d) => d.startsWith('.'))) {
|
|
272
|
+
if (basename(file) === 'MEMORY.md')
|
|
273
|
+
continue;
|
|
274
|
+
let fm;
|
|
275
|
+
try {
|
|
276
|
+
fm = parseFrontmatterGeneric(readText(file)).data;
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (fm === null)
|
|
282
|
+
continue;
|
|
283
|
+
const entries = parsedSurfaces(fm);
|
|
284
|
+
if (entries.some((e) => e.on === 'workspace-open' && e.at === 'content'))
|
|
285
|
+
hits.push(file);
|
|
286
|
+
}
|
|
287
|
+
return hits;
|
|
288
|
+
}
|
|
289
|
+
const FRONT_DOOR_REMEDY = 'author the project\'s operating guide as an ordinary doc with `surfaces: [{on: workspace-open, at: content}, {on: read, match: "./**", at: content}]`; run `crtr memory write -h` first';
|
|
227
290
|
export const lintLeaf = defineLeaf({
|
|
228
291
|
name: 'lint',
|
|
229
292
|
description: 'validate frontmatter and body length across the whole bounded document corpus',
|
|
230
|
-
whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (the substrate memory stores) and fail loudly on any invalid YAML, substrate schema violation, body longer than its
|
|
293
|
+
whenToUse: 'you authored or migrated documents and want the authoring-time gate: strict-parse every doc in the bounded corpus (the substrate memory stores) and fail loudly on any invalid YAML, substrate schema violation (including retired visibility fields and malformed `surfaces` entries), body longer than its delivery entries earn, or dangling `[[canonical/name]]` doc link; also validates that every project managed by the selected profile has exactly one workspace front door and warns when an unprofiled working directory has none. Run it before shipping doc changes; CI-friendly (non-zero exit on findings, warnings remain non-fatal).',
|
|
231
294
|
help: {
|
|
232
295
|
name: 'memory lint',
|
|
233
|
-
summary: 'strict-parse the bounded memory corpus and validate project
|
|
296
|
+
summary: 'strict-parse the bounded memory corpus and validate project workspace front doors',
|
|
234
297
|
params: [],
|
|
235
298
|
output: [
|
|
236
299
|
{ name: 'checked', type: 'number', required: true, constraint: 'Files linted across all corpora.' },
|
|
237
|
-
{ name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus counts: {memory_stores (files), profile_projects (managed dirs checked for a
|
|
300
|
+
{ name: 'corpora', type: 'object', required: true, constraint: 'Per-corpus counts: {memory_stores (files), profile_projects (managed dirs checked for a workspace front door)}.' },
|
|
238
301
|
{ name: 'findings', type: 'object[]', required: true, constraint: 'One row per failure: {path, error}. Empty when green.' },
|
|
239
302
|
{ name: 'warnings', type: 'string[]', required: true, constraint: 'Non-fatal authoring gaps detected for the directory where lint ran.' },
|
|
240
303
|
],
|
|
@@ -254,13 +317,17 @@ export const lintLeaf = defineLeaf({
|
|
|
254
317
|
// path never yields a doc name, so e.g. the maintainer store shipped
|
|
255
318
|
// at builtin-memory/.crouter can never register) — lint must not
|
|
256
319
|
// flag files the substrate can never load.
|
|
257
|
-
//
|
|
258
|
-
//
|
|
320
|
+
// The resolvable link targets: exact doc names, plus every proper name
|
|
321
|
+
// prefix — a bare-dir [[ref]] is a legal listing link (`crtr memory read
|
|
322
|
+
// <dir>` answers with the directory listing).
|
|
259
323
|
const corpusNames = new Set();
|
|
260
324
|
for (const doc of listAllMemoryDocs(undefined, true, true)) {
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
325
|
+
if (doc.name === '')
|
|
326
|
+
continue;
|
|
327
|
+
corpusNames.add(doc.name);
|
|
328
|
+
const segs = doc.name.split('/');
|
|
329
|
+
for (let i = 1; i < segs.length; i++)
|
|
330
|
+
corpusNames.add(segs.slice(0, i).join('/'));
|
|
264
331
|
}
|
|
265
332
|
const lintDir = (dir) => {
|
|
266
333
|
if (!dir || !pathExists(dir))
|
|
@@ -281,9 +348,9 @@ export const lintLeaf = defineLeaf({
|
|
|
281
348
|
lintDir(pluginMemoryDir(plugin));
|
|
282
349
|
}
|
|
283
350
|
}
|
|
284
|
-
// Nested descendant stores: same schema gate, plus a boot-
|
|
285
|
-
// the boot catalog deliberately never includes nested stores, so a
|
|
286
|
-
//
|
|
351
|
+
// Nested descendant stores: same schema gate, plus a boot-entry warning —
|
|
352
|
+
// the boot catalog deliberately never includes nested stores, so a boot
|
|
353
|
+
// surfaces entry there is inert.
|
|
287
354
|
for (const root of descendantStoreRoots(projectScopeRoots())) {
|
|
288
355
|
const dir = join(root, 'memory');
|
|
289
356
|
lintDir(dir);
|
|
@@ -299,9 +366,8 @@ export const lintLeaf = defineLeaf({
|
|
|
299
366
|
catch {
|
|
300
367
|
continue; // invalid YAML is already a finding from lintDir
|
|
301
368
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
warnings.push(`${file}: nested-store docs never ride the boot catalog, so this \`system-prompt-visibility\` rung is inert — set \`none\``);
|
|
369
|
+
if (fm !== null && parsedSurfaces(fm).some((e) => e.on === 'boot')) {
|
|
370
|
+
warnings.push(`${file}: nested-store docs never ride the boot catalog, so its boot surfaces entries are inert — drop them`);
|
|
305
371
|
}
|
|
306
372
|
}
|
|
307
373
|
}
|
|
@@ -312,10 +378,11 @@ export const lintLeaf = defineLeaf({
|
|
|
312
378
|
lintDir(pluginMemoryDir(plugin));
|
|
313
379
|
}
|
|
314
380
|
}
|
|
315
|
-
// Profile coverage: each managed project has
|
|
316
|
-
// door
|
|
317
|
-
//
|
|
318
|
-
//
|
|
381
|
+
// Profile coverage: each managed project has exactly ONE workspace front
|
|
382
|
+
// door — a doc carrying {on: workspace-open, at: content}, the guide that
|
|
383
|
+
// enters first-message context when cwd/profile mounts the store. Zero
|
|
384
|
+
// means agents open the workspace blind; two means both deliver whole at
|
|
385
|
+
// every open, and the store needs consolidating.
|
|
319
386
|
let profileProjects = 0;
|
|
320
387
|
let profileResolved = false;
|
|
321
388
|
const profileIdOrName = process.env['CRTR_PROFILE_ID'] || getDefaultProfileId(process.cwd());
|
|
@@ -326,38 +393,18 @@ export const lintLeaf = defineLeaf({
|
|
|
326
393
|
lintDir(profileMemoryDir(profileId));
|
|
327
394
|
for (const dir of manifest.projects) {
|
|
328
395
|
profileProjects += 1;
|
|
329
|
-
const
|
|
330
|
-
if (
|
|
396
|
+
const doors = frontDoorDocs(join(dir, '.crouter', 'memory'));
|
|
397
|
+
if (doors.length === 0) {
|
|
331
398
|
findings.push({
|
|
332
399
|
path: dir,
|
|
333
|
-
error: `profile "${manifest.name}" manages this dir but
|
|
400
|
+
error: `profile "${manifest.name}" manages this dir but no doc in its .crouter/memory carries a {on: workspace-open, at: content} surfaces entry — ${FRONT_DOOR_REMEDY}`,
|
|
334
401
|
});
|
|
335
|
-
continue;
|
|
336
|
-
}
|
|
337
|
-
try {
|
|
338
|
-
const fm = parseFrontmatterGeneric(readText(indexPath)).data;
|
|
339
|
-
const problems = [];
|
|
340
|
-
if (fm?.kind !== 'knowledge')
|
|
341
|
-
problems.push('kind must be `knowledge`');
|
|
342
|
-
if (fm?.['system-prompt-visibility'] !== 'none')
|
|
343
|
-
problems.push('system-prompt-visibility must be `none`');
|
|
344
|
-
if (fm?.['file-read-visibility'] !== 'content')
|
|
345
|
-
problems.push('file-read-visibility must be `content`');
|
|
346
|
-
const applies = fm?.['applies-to'];
|
|
347
|
-
const globs = typeof applies === 'string'
|
|
348
|
-
? [applies]
|
|
349
|
-
: Array.isArray(applies) && applies.every((value) => typeof value === 'string')
|
|
350
|
-
? applies
|
|
351
|
-
: [];
|
|
352
|
-
if (!globs.some((glob) => glob.trim() === '.'))
|
|
353
|
-
problems.push('applies-to must include `.`');
|
|
354
|
-
if (problems.length > 0) {
|
|
355
|
-
findings.push({ path: indexPath, error: `invalid project root front door: ${problems.join('; ')}` });
|
|
356
|
-
}
|
|
357
402
|
}
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
403
|
+
else if (doors.length > 1) {
|
|
404
|
+
findings.push({
|
|
405
|
+
path: dir,
|
|
406
|
+
error: `multiple workspace front doors (${doors.length}): ${doors.join(', ')} — exactly one doc per project store may carry {on: workspace-open, at: content}; consolidate the others into it or lower their entries`,
|
|
407
|
+
});
|
|
361
408
|
}
|
|
362
409
|
}
|
|
363
410
|
}
|
|
@@ -369,9 +416,8 @@ export const lintLeaf = defineLeaf({
|
|
|
369
416
|
// Without a selected, resolvable profile there is no managed-project
|
|
370
417
|
// finding to carry this signal, so keep a non-fatal cwd adoption warning.
|
|
371
418
|
if (!profileResolved) {
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
warnings.push(`${workingDirectory}: no .crouter/memory/INDEX.md root front door — author INDEX for this project with kind knowledge, system-prompt-visibility none, file-read-visibility content, and applies-to "."; run \`crtr memory write -h\` first`);
|
|
419
|
+
if (frontDoorDocs(join(workingDirectory, '.crouter', 'memory')).length === 0) {
|
|
420
|
+
warnings.push(`${workingDirectory}: no workspace front door — ${FRONT_DOOR_REMEDY}`);
|
|
375
421
|
}
|
|
376
422
|
}
|
|
377
423
|
const checked = memoryCount;
|
|
@@ -388,7 +434,7 @@ export const lintLeaf = defineLeaf({
|
|
|
388
434
|
checked,
|
|
389
435
|
findings: findings.map((f) => ({ path: f.path, error: f.error })),
|
|
390
436
|
warnings,
|
|
391
|
-
next: 'Fix each doc (quote YAML values containing `: `; use a valid kind
|
|
437
|
+
next: 'Fix each doc (quote YAML values containing `: `; use a valid kind; run `crtr sys migrate` for retired visibility fields; give each surfaces entry a valid on/at and the match its event requires; retarget or drop dangling [[links]]; split an over-length body into [[linked]] surface-less reference docs); give every profile-managed project exactly one workspace front door; then re-run `crtr memory lint`.',
|
|
392
438
|
});
|
|
393
439
|
}
|
|
394
440
|
return {
|
|
@@ -8,7 +8,6 @@ export interface ListedMemoryDoc {
|
|
|
8
8
|
scope: MemoryScope;
|
|
9
9
|
path: string;
|
|
10
10
|
shortForm: string;
|
|
11
|
-
isDir: boolean;
|
|
12
11
|
slash: boolean;
|
|
13
12
|
}
|
|
14
13
|
export declare function listMemoryDocs(kindFilter?: string, scopeFilter?: MemoryScope, quiet?: boolean, docs?: readonly MemoryDoc[]): ListedMemoryDoc[];
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { defineLeaf } from '../../core/command.js';
|
|
2
2
|
import { listAllMemoryDocs } from '../../core/memory-resolver.js';
|
|
3
3
|
import { parseSubstrateDoc } from '../../core/substrate/schema.js';
|
|
4
|
-
import { isIndexName, indexDirOf } from '../../core/substrate/ceiling.js';
|
|
5
4
|
import { paginate } from '../../core/pagination.js';
|
|
6
5
|
import { MEMORY_KINDS, MEMORY_SCOPES, scopeRank } from './shared.js';
|
|
7
6
|
export function listMemoryDocs(kindFilter, scopeFilter, quiet = false, docs = listAllMemoryDocs(scopeFilter, quiet, true)) {
|
|
@@ -21,15 +20,7 @@ export function listMemoryDocs(kindFilter, scopeFilter, quiet = false, docs = li
|
|
|
21
20
|
continue;
|
|
22
21
|
if (kindFilter !== undefined && sub.kind !== kindFilter)
|
|
23
22
|
continue;
|
|
24
|
-
|
|
25
|
-
// slash, flagged is_dir, so the inventory distinguishes it from a doc. A
|
|
26
|
-
// store-root INDEX has no dir path, so it keeps its canonical `INDEX`
|
|
27
|
-
// name — every name in this column must be one `crtr memory read` and a
|
|
28
|
-
// `[[link]]` accept, and a bare `/` is neither.
|
|
29
|
-
const isDir = isIndexName(sub.name);
|
|
30
|
-
const dirPath = isDir ? indexDirOf(sub.name) : '';
|
|
31
|
-
const name = dirPath === '' ? sub.name : dirPath + '/';
|
|
32
|
-
addItem({ name, kind: sub.kind, scope: sub.scope, path: sub.path, shortForm: sub.shortForm, isDir, slash: sub.slash });
|
|
23
|
+
addItem({ name: sub.name, kind: sub.kind, scope: sub.scope, path: sub.path, shortForm: sub.shortForm, slash: sub.slash });
|
|
33
24
|
}
|
|
34
25
|
items.sort((a, b) => {
|
|
35
26
|
const sr = scopeRank(a.scope) - scopeRank(b.scope);
|
|
@@ -57,7 +48,7 @@ export const listLeaf = defineLeaf({
|
|
|
57
48
|
{ kind: 'flag', name: 'cursor', type: 'string', required: false, constraint: 'Opaque token from next_cursor. Omit on first call.' },
|
|
58
49
|
],
|
|
59
50
|
output: [
|
|
60
|
-
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document: {name, short_form, kind, scope
|
|
51
|
+
{ name: 'items', type: 'object[]', required: true, constraint: 'One row per document: {name, short_form, kind, scope}. path is included only with --paths. Every name is readable as-is with `crtr memory read` (a directory segment of a name is readable too — it answers with the directory listing). short_form is the abbreviated hook — shown here and nowhere else. Sorted by scope then kind then name ascending.' },
|
|
61
52
|
{ name: 'next_cursor', type: 'string | null', required: true, constraint: 'Opaque token for the next page; null means no more items.' },
|
|
62
53
|
{ name: 'total', type: 'integer | null', required: true, constraint: 'Exact total across all pages when cheap to compute (always cheap here); never null in practice for this leaf.' },
|
|
63
54
|
{ name: 'follow_up', type: 'string', required: true, constraint: 'Concrete next commands — read a document in full or narrow the inventory.' },
|
|
@@ -81,7 +72,6 @@ export const listLeaf = defineLeaf({
|
|
|
81
72
|
short_form: d.shortForm,
|
|
82
73
|
kind: d.kind,
|
|
83
74
|
scope: d.scope,
|
|
84
|
-
is_dir: d.isDir,
|
|
85
75
|
...(includePaths ? { path: d.path } : {}),
|
|
86
76
|
})),
|
|
87
77
|
next_cursor: result.next_cursor,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const moveLeaf: import("../../core/command.js").LeafDef;
|