@crouton-kit/crouter 0.3.20 → 0.3.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/builtin-memory/crouter-development/marketplaces.md +2 -4
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +3 -5
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +3 -6
- package/dist/builtin-memory/crouter-development/personas.md +2 -6
- package/dist/builtin-memory/crouter-development/plugins.md +2 -4
- package/dist/builtin-memory/design.md +2 -4
- package/dist/builtin-memory/development.md +3 -3
- package/dist/builtin-memory/planning.md +3 -4
- package/dist/builtin-memory/spec.md +3 -10
- package/dist/builtin-personas/orchestration-kernel.md +1 -1
- package/dist/commands/canvas-history/read.d.ts +1 -0
- package/dist/commands/canvas-history/read.js +61 -0
- package/dist/commands/canvas-history/search.d.ts +1 -0
- package/dist/commands/canvas-history/search.js +270 -0
- package/dist/commands/canvas-history/show.d.ts +1 -0
- package/dist/commands/canvas-history/show.js +79 -0
- package/dist/commands/canvas-history.d.ts +2 -0
- package/dist/commands/canvas-history.js +33 -0
- package/dist/commands/canvas.js +3 -2
- package/dist/commands/human/queue.js +2 -2
- package/dist/commands/memory/__tests__/lint-schema.test.js +29 -0
- package/dist/commands/memory/find.js +14 -52
- package/dist/commands/memory/lint.d.ts +7 -0
- package/dist/commands/memory/lint.js +29 -35
- package/dist/commands/memory/list.js +8 -18
- package/dist/commands/memory/read.js +4 -37
- package/dist/commands/memory/shared.js +1 -2
- package/dist/commands/memory/write.js +6 -9
- package/dist/commands/node.js +47 -25
- package/dist/commands/pkg/bridge.d.ts +1 -0
- package/dist/commands/pkg/bridge.js +137 -0
- package/dist/commands/pkg/plugin-inspect.js +7 -6
- package/dist/commands/pkg.js +2 -1
- package/dist/commands/push.js +10 -15
- package/dist/commands/skill/author.js +35 -44
- package/dist/commands/skill/shared.d.ts +1 -5
- package/dist/commands/skill/shared.js +9 -63
- package/dist/commands/skill.js +5 -6
- package/dist/commands/sys/doctor.js +6 -144
- package/dist/commands/tmux-spread.js +16 -1
- package/dist/commands/view-cycle.js +9 -5
- package/dist/commands/view-run.js +3 -2
- package/dist/core/__tests__/broker-dormant-wake.test.js +71 -34
- package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +21 -5
- package/dist/core/__tests__/canvas-inbox-watcher.test.js +21 -5
- package/dist/core/__tests__/child-death-wake.test.js +133 -185
- package/dist/core/__tests__/context-intro.test.js +23 -27
- package/dist/core/__tests__/daemon-liveness.test.js +28 -258
- package/dist/core/__tests__/flagship-lifecycle.test.js +156 -135
- package/dist/core/__tests__/frame-decoder-perf.test.js +51 -25
- package/dist/core/__tests__/{broker-attach-limits.test.js → full/broker-attach-limits.test.js} +62 -35
- package/dist/core/__tests__/{broker-attach-stream.test.js → full/broker-attach-stream.test.js} +54 -31
- package/dist/core/__tests__/full/broker-crash-teardown.test.js +143 -0
- package/dist/core/__tests__/full/broker-dialogs.test.js +153 -0
- package/dist/core/__tests__/full/broker-lifecycle.test.js +116 -0
- package/dist/core/__tests__/{cascade-close.test.js → full/cascade-close.test.js} +4 -3
- package/dist/core/__tests__/full/daemon-liveness-pane.full.test.js +292 -0
- package/dist/core/__tests__/{dead-pane-regression.test.js → full/dead-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/{detach-focus.test.js → full/detach-focus.test.js} +7 -7
- package/dist/core/__tests__/{human-new-window-regression.test.js → full/human-new-window-regression.test.js} +2 -2
- package/dist/core/__tests__/{placement-focus.test.js → full/placement-focus.test.js} +5 -5
- package/dist/core/__tests__/{placement-reconcile.test.js → full/placement-reconcile.test.js} +3 -3
- package/dist/core/__tests__/{placement-revive.test.js → full/placement-revive.test.js} +5 -5
- package/dist/core/__tests__/{placement-teardown.test.js → full/placement-teardown.test.js} +4 -4
- package/dist/core/__tests__/{review-render-pane-regression.test.js → full/review-render-pane-regression.test.js} +2 -2
- package/dist/core/__tests__/full/spike-harness.test.d.ts +1 -0
- package/dist/core/__tests__/full/spike-harness.test.js +117 -0
- package/dist/core/__tests__/grace-clock.test.js +72 -75
- package/dist/core/__tests__/helpers/harness.d.ts +35 -1
- package/dist/core/__tests__/helpers/harness.js +70 -12
- package/dist/core/__tests__/live-mutation-verbs.test.js +100 -105
- package/dist/core/__tests__/live-mutation.test.js +111 -134
- package/dist/core/__tests__/memory.test.js +4 -4
- package/dist/core/__tests__/revive.test.js +96 -86
- package/dist/core/__tests__/subscription-delivery.test.js +116 -138
- package/dist/core/__tests__/wake-origin.test.js +54 -50
- package/dist/core/bootstrap.js +18 -14
- package/dist/core/bridge-map.d.ts +19 -0
- package/dist/core/bridge-map.js +73 -0
- package/dist/core/canvas/history.d.ts +91 -0
- package/dist/core/canvas/history.js +389 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/config.js +3 -22
- package/dist/core/help.d.ts +2 -2
- package/dist/core/help.js +1 -1
- package/dist/core/memory-resolver.d.ts +9 -2
- package/dist/core/memory-resolver.js +47 -7
- package/dist/core/render.d.ts +4 -3
- package/dist/core/render.js +38 -41
- package/dist/core/resolver.d.ts +1 -30
- package/dist/core/resolver.js +6 -546
- package/dist/core/runtime/bearings.d.ts +2 -2
- package/dist/core/runtime/bearings.js +2 -2
- package/dist/core/runtime/persona.js +5 -7
- package/dist/core/scope.d.ts +7 -5
- package/dist/core/scope.js +16 -17
- package/dist/core/substrate/ceiling.d.ts +28 -0
- package/dist/core/substrate/ceiling.js +87 -0
- package/dist/core/substrate/index.d.ts +2 -0
- package/dist/core/substrate/index.js +1 -0
- package/dist/core/substrate/on-read.js +18 -2
- package/dist/core/substrate/render.d.ts +3 -5
- package/dist/core/substrate/render.js +68 -59
- package/dist/core/substrate/schema.d.ts +18 -13
- package/dist/core/substrate/schema.js +12 -11
- package/dist/pi-extensions/canvas-context-intro.js +5 -3
- package/dist/pi-extensions/canvas-inbox-watcher.js +19 -2
- package/dist/types.d.ts +3 -18
- package/dist/types.js +0 -8
- package/package.json +4 -3
- package/dist/builtin-skills/.crouter-plugin/plugin.json +0 -5
- package/dist/builtin-skills/skills/crouter-development/marketplaces/SKILL.md +0 -157
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +0 -106
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +0 -49
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +0 -156
- package/dist/builtin-skills/skills/design/SKILL.md +0 -51
- package/dist/builtin-skills/skills/development/SKILL.md +0 -109
- package/dist/builtin-skills/skills/planning/SKILL.md +0 -59
- package/dist/builtin-skills/skills/spec/SKILL.md +0 -83
- package/dist/commands/skill/state.d.ts +0 -3
- package/dist/commands/skill/state.js +0 -71
- package/dist/core/__tests__/broker-crash-teardown.test.js +0 -116
- package/dist/core/__tests__/broker-dialogs.test.js +0 -126
- package/dist/core/__tests__/broker-lifecycle.test.js +0 -87
- package/dist/core/__tests__/resolver.test.js +0 -181
- package/dist/core/__tests__/spike-harness.test.js +0 -242
- /package/dist/{core/__tests__/broker-attach-limits.test.d.ts → commands/memory/__tests__/lint-schema.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-attach-stream.test.d.ts → full/broker-attach-limits.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-crash-teardown.test.d.ts → full/broker-attach-stream.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-dialogs.test.d.ts → full/broker-crash-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{broker-lifecycle.test.d.ts → full/broker-dialogs.test.d.ts} +0 -0
- /package/dist/core/__tests__/{cascade-close.test.d.ts → full/broker-lifecycle.test.d.ts} +0 -0
- /package/dist/core/__tests__/{dead-pane-regression.test.d.ts → full/cascade-close.test.d.ts} +0 -0
- /package/dist/core/__tests__/{detach-focus.test.d.ts → full/daemon-liveness-pane.full.test.d.ts} +0 -0
- /package/dist/core/__tests__/{human-new-window-regression.test.d.ts → full/dead-pane-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-focus.test.d.ts → full/detach-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-reconcile.test.d.ts → full/human-new-window-regression.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-revive.test.d.ts → full/placement-focus.test.d.ts} +0 -0
- /package/dist/core/__tests__/{placement-teardown.test.d.ts → full/placement-reconcile.test.d.ts} +0 -0
- /package/dist/core/__tests__/{resolver.test.d.ts → full/placement-revive.test.d.ts} +0 -0
- /package/dist/core/__tests__/{review-render-pane-regression.test.d.ts → full/placement-teardown.test.d.ts} +0 -0
- /package/dist/core/__tests__/{spike-harness.test.d.ts → full/review-render-pane-regression.test.d.ts} +0 -0
package/dist/core/scope.js
CHANGED
|
@@ -5,16 +5,9 @@ import { fileURLToPath } from 'node:url';
|
|
|
5
5
|
import { CRTR_DIR_NAME } from '../types.js';
|
|
6
6
|
import { usage } from './errors.js';
|
|
7
7
|
let cachedProjectRoot;
|
|
8
|
-
export function builtinSkillsRoot() {
|
|
9
|
-
// Resolve relative to this file: src/core/scope.ts → src/builtin-skills/ OR dist/core/scope.js → dist/builtin-skills/
|
|
10
|
-
const thisFile = fileURLToPath(import.meta.url);
|
|
11
|
-
const coreDir = dirname(thisFile);
|
|
12
|
-
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
13
|
-
return join(pkgDir, 'builtin-skills');
|
|
14
|
-
}
|
|
15
8
|
export function builtinViewsRoot() {
|
|
16
|
-
// Resolved relative to this file
|
|
17
|
-
//
|
|
9
|
+
// Resolved relative to this file: dist/core/scope.js → dist/builtin-views/
|
|
10
|
+
// (src/ at dev time).
|
|
18
11
|
const thisFile = fileURLToPath(import.meta.url);
|
|
19
12
|
const coreDir = dirname(thisFile);
|
|
20
13
|
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
@@ -22,11 +15,10 @@ export function builtinViewsRoot() {
|
|
|
22
15
|
}
|
|
23
16
|
export function builtinMemoryRoot() {
|
|
24
17
|
// The substrate's shipped built-in documents live in their OWN package dir,
|
|
25
|
-
//
|
|
26
|
-
// builtinSkillsRoot/builtinViewsRoot: dist/core/scope.js → dist/builtin-memory/
|
|
18
|
+
// resolved relative to this file: dist/core/scope.js → dist/builtin-memory/
|
|
27
19
|
// (src/ at dev time). This is a HARD special case (design verdict m2): builtin
|
|
28
|
-
// memory is
|
|
29
|
-
//
|
|
20
|
+
// memory is the only content the builtin scope carries — the scope has no
|
|
21
|
+
// scopeRoot (scopeRoot('builtin') is null).
|
|
30
22
|
const thisFile = fileURLToPath(import.meta.url);
|
|
31
23
|
const coreDir = dirname(thisFile);
|
|
32
24
|
const pkgDir = dirname(coreDir); // src/ or dist/
|
|
@@ -65,8 +57,10 @@ export function projectScopeRoot(startDir) {
|
|
|
65
57
|
return findProjectScopeRoot(startDir);
|
|
66
58
|
}
|
|
67
59
|
export function scopeRoot(scope) {
|
|
60
|
+
// The builtin scope has no scope-root dir: its only content is memory
|
|
61
|
+
// (builtinMemoryRoot) and views (builtinViewsRoot), both hard special cases.
|
|
68
62
|
if (scope === 'builtin')
|
|
69
|
-
return
|
|
63
|
+
return null;
|
|
70
64
|
return scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
71
65
|
}
|
|
72
66
|
export function requireScopeRoot(scope) {
|
|
@@ -98,15 +92,20 @@ export function scopeSkillsDir(scope) {
|
|
|
98
92
|
return root ? join(root, 'skills') : null;
|
|
99
93
|
}
|
|
100
94
|
/** Where substrate memory documents live per scope. Builtin memory is the
|
|
101
|
-
* special case — its own package dir (builtinMemoryRoot,
|
|
102
|
-
*
|
|
103
|
-
*
|
|
95
|
+
* special case — its own package dir (builtinMemoryRoot), since the builtin
|
|
96
|
+
* scope has no scopeRoot — mirroring viewsDir's builtin handling. User and
|
|
97
|
+
* project memory live at scopeRoot(scope)/memory. */
|
|
104
98
|
export function scopeMemoryDir(scope) {
|
|
105
99
|
if (scope === 'builtin')
|
|
106
100
|
return builtinMemoryRoot();
|
|
107
101
|
const root = scope === 'user' ? userScopeRoot() : projectScopeRoot();
|
|
108
102
|
return root ? join(root, 'memory') : null;
|
|
109
103
|
}
|
|
104
|
+
/** Where a plugin's substrate memory documents live: `<plugin.root>/memory`.
|
|
105
|
+
* The resolver mounts these under the virtual `<pluginName>/` namespace. */
|
|
106
|
+
export function pluginMemoryDir(plugin) {
|
|
107
|
+
return join(plugin.root, 'memory');
|
|
108
|
+
}
|
|
110
109
|
/** Where view definition dirs live per scope. Builtin views sit directly under
|
|
111
110
|
* builtinViewsRoot() (no `views/` segment — they ARE the builtin views dir),
|
|
112
111
|
* matching the loader's `<root>/<name>/view.mjs` resolution. */
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Rung, type SubstrateDoc } from './schema.js';
|
|
2
|
+
/** The reserved basename (no extension) that marks a directory's index doc. */
|
|
3
|
+
export declare const INDEX_NAME = "INDEX";
|
|
4
|
+
/** Does this path-derived name denote a directory INDEX (`taste/INDEX`, or a
|
|
5
|
+
* top-level `INDEX`)? */
|
|
6
|
+
export declare function isIndexName(name: string): boolean;
|
|
7
|
+
/** The directory an INDEX doc represents: `taste/INDEX` → `taste`, `INDEX` → ''. */
|
|
8
|
+
export declare function indexDirOf(name: string): string;
|
|
9
|
+
/** The name a doc renders under in agent-facing surfaces: an INDEX doc renders
|
|
10
|
+
* under its directory name (the dir entry); every other doc keeps its name. */
|
|
11
|
+
export declare function displayName(name: string): string;
|
|
12
|
+
/** The two visibility surfaces an INDEX can cap. */
|
|
13
|
+
export type Surface = 'systemPromptVisibility' | 'fileReadVisibility';
|
|
14
|
+
/** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
|
|
15
|
+
* callers MUST pass docs precedence-ordered (project > user > builtin) — the
|
|
16
|
+
* highest-precedence INDEX governs a dir, mirroring the memory resolver. */
|
|
17
|
+
export declare function buildCeilingIndex(docs: SubstrateDoc[]): Map<string, SubstrateDoc>;
|
|
18
|
+
/** A doc's effective rung on `surface` after applying every ancestor INDEX
|
|
19
|
+
* ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
|
|
20
|
+
* hides the whole subtree (min with none = none). */
|
|
21
|
+
export declare function effectiveRung(doc: SubstrateDoc, ceil: Map<string, SubstrateDoc>, surface: Surface): Rung;
|
|
22
|
+
/** Apply ceilings to a doc list for a surface: each doc gets its effective rung
|
|
23
|
+
* on `surface` written back into that field, and INDEX docs are renamed to
|
|
24
|
+
* their dir entry. Returns NEW objects only where something changed (cheap,
|
|
25
|
+
* preserves identity for untouched docs). Does NOT drop `none`-rung docs — the
|
|
26
|
+
* caller filters per surface (boot drops them; node-local intentionally keeps
|
|
27
|
+
* them). */
|
|
28
|
+
export declare function applyCeilings(docs: SubstrateDoc[], surface: Surface): SubstrateDoc[];
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// ceiling.ts — INDEX.md dir-level entries and the subtree rung ceiling.
|
|
2
|
+
//
|
|
3
|
+
// A directory under a memory store may carry an `INDEX.md` with the SAME
|
|
4
|
+
// substrate frontmatter as any doc. That file makes the directory a first-class
|
|
5
|
+
// substrate entry: it renders as ONE entry at dir level (headed by the dir
|
|
6
|
+
// name, not `…/INDEX`), and its rung is a CEILING for everything in its subtree.
|
|
7
|
+
//
|
|
8
|
+
// effective rung of a descendant = min(own rung, every ancestor INDEX's rung)
|
|
9
|
+
//
|
|
10
|
+
// So a dir's INDEX at `preview` lets its files surface at most at `preview`; an
|
|
11
|
+
// INDEX at `none` hides the whole subtree. This is the single, explicit
|
|
12
|
+
// mechanism for collapsing a directory in agent-facing surfaces (boot render +
|
|
13
|
+
// on-read) — there is no parallel auto-hiding heuristic. See design taste:
|
|
14
|
+
// `crtr memory read taste/document-substrate`.
|
|
15
|
+
import { rungRank } from './schema.js';
|
|
16
|
+
/** The reserved basename (no extension) that marks a directory's index doc. */
|
|
17
|
+
export const INDEX_NAME = 'INDEX';
|
|
18
|
+
/** Does this path-derived name denote a directory INDEX (`taste/INDEX`, or a
|
|
19
|
+
* top-level `INDEX`)? */
|
|
20
|
+
export function isIndexName(name) {
|
|
21
|
+
return name === INDEX_NAME || name.endsWith('/' + INDEX_NAME);
|
|
22
|
+
}
|
|
23
|
+
/** The directory an INDEX doc represents: `taste/INDEX` → `taste`, `INDEX` → ''. */
|
|
24
|
+
export function indexDirOf(name) {
|
|
25
|
+
return name === INDEX_NAME ? '' : name.slice(0, name.length - INDEX_NAME.length - 1);
|
|
26
|
+
}
|
|
27
|
+
/** The name a doc renders under in agent-facing surfaces: an INDEX doc renders
|
|
28
|
+
* under its directory name (the dir entry); every other doc keeps its name. */
|
|
29
|
+
export function displayName(name) {
|
|
30
|
+
return isIndexName(name) ? indexDirOf(name) : name;
|
|
31
|
+
}
|
|
32
|
+
/** Build the dir → governing-INDEX map. Precedence is first-occurrence-wins, so
|
|
33
|
+
* callers MUST pass docs precedence-ordered (project > user > builtin) — the
|
|
34
|
+
* highest-precedence INDEX governs a dir, mirroring the memory resolver. */
|
|
35
|
+
export function buildCeilingIndex(docs) {
|
|
36
|
+
const m = new Map();
|
|
37
|
+
for (const d of docs) {
|
|
38
|
+
if (!isIndexName(d.name))
|
|
39
|
+
continue;
|
|
40
|
+
const dir = indexDirOf(d.name);
|
|
41
|
+
if (!m.has(dir))
|
|
42
|
+
m.set(dir, d);
|
|
43
|
+
}
|
|
44
|
+
return m;
|
|
45
|
+
}
|
|
46
|
+
function minRung(a, b) {
|
|
47
|
+
return rungRank(a) <= rungRank(b) ? a : b;
|
|
48
|
+
}
|
|
49
|
+
/** Ancestor directory names of a doc, nearest-last: `a/b/c` → [`a/b`, `a`];
|
|
50
|
+
* `a` → []. An INDEX doc's OWN dir is included (a self-cap is a no-op), so a
|
|
51
|
+
* nested INDEX still inherits its parents' ceilings. */
|
|
52
|
+
function ancestorDirs(name) {
|
|
53
|
+
const parts = name.split('/');
|
|
54
|
+
parts.pop(); // drop the leaf
|
|
55
|
+
const out = [];
|
|
56
|
+
for (let i = parts.length; i > 0; i--)
|
|
57
|
+
out.push(parts.slice(0, i).join('/'));
|
|
58
|
+
return out;
|
|
59
|
+
}
|
|
60
|
+
/** A doc's effective rung on `surface` after applying every ancestor INDEX
|
|
61
|
+
* ceiling: min(own rung, each ancestor INDEX's rung). A `none` ancestor INDEX
|
|
62
|
+
* hides the whole subtree (min with none = none). */
|
|
63
|
+
export function effectiveRung(doc, ceil, surface) {
|
|
64
|
+
let rung = doc[surface];
|
|
65
|
+
for (const dir of ancestorDirs(doc.name)) {
|
|
66
|
+
const idx = ceil.get(dir);
|
|
67
|
+
if (idx)
|
|
68
|
+
rung = minRung(rung, idx[surface]);
|
|
69
|
+
}
|
|
70
|
+
return rung;
|
|
71
|
+
}
|
|
72
|
+
/** Apply ceilings to a doc list for a surface: each doc gets its effective rung
|
|
73
|
+
* on `surface` written back into that field, and INDEX docs are renamed to
|
|
74
|
+
* their dir entry. Returns NEW objects only where something changed (cheap,
|
|
75
|
+
* preserves identity for untouched docs). Does NOT drop `none`-rung docs — the
|
|
76
|
+
* caller filters per surface (boot drops them; node-local intentionally keeps
|
|
77
|
+
* them). */
|
|
78
|
+
export function applyCeilings(docs, surface) {
|
|
79
|
+
const ceil = buildCeilingIndex(docs);
|
|
80
|
+
return docs.map((d) => {
|
|
81
|
+
const rung = effectiveRung(d, ceil, surface);
|
|
82
|
+
const name = displayName(d.name);
|
|
83
|
+
if (rung === d[surface] && name === d.name)
|
|
84
|
+
return d;
|
|
85
|
+
return { ...d, [surface]: rung, name };
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -3,5 +3,7 @@ export type { DocKind, Rung, GatePredicate, SubstrateSchema, SubstrateDoc } from
|
|
|
3
3
|
export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
|
|
4
4
|
export type { NodeConfigSubject } from './subject.js';
|
|
5
5
|
export { gatePasses } from './gate.js';
|
|
6
|
+
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, applyCeilings, } from './ceiling.js';
|
|
7
|
+
export type { Surface } from './ceiling.js';
|
|
6
8
|
export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
|
|
7
9
|
export { renderOnReadDocs } from './on-read.js';
|
|
@@ -14,5 +14,6 @@ KIND_DEFAULT_RUNGS,
|
|
|
14
14
|
parseSubstrateFrontmatter, parseSubstrateDoc, previewLine, } from './schema.js';
|
|
15
15
|
export { scopeForCwd, spineDepth, assembleNodeSubject } from './subject.js';
|
|
16
16
|
export { gatePasses } from './gate.js';
|
|
17
|
+
export { INDEX_NAME, isIndexName, indexDirOf, displayName, buildCeilingIndex, effectiveRung, applyCeilings, } from './ceiling.js';
|
|
17
18
|
export { renderSkillsSection, renderPreferencesSection, renderReferencesBlock } from './render.js';
|
|
18
19
|
export { renderOnReadDocs } from './on-read.js';
|
|
@@ -36,7 +36,7 @@ import { CRTR_DIR_NAME } from '../../types.js';
|
|
|
36
36
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
37
37
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
38
38
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
39
|
-
import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
39
|
+
import { assembleNodeSubject, buildCeilingIndex, displayName, effectiveRung, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
40
40
|
import { cachedSubstrateDocs } from './session-cache.js';
|
|
41
41
|
// Ancestor dirs we never look inside for a `.crouter/memory/` store (the read
|
|
42
42
|
// file may live under a build/dependency tree; `.crouter` is NOT junk here — it
|
|
@@ -264,6 +264,18 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
|
264
264
|
// Outermost-first: the nearest/most-specific doc reads last — closest to the
|
|
265
265
|
// file content that follows it (the applies-to set, order -1, trails).
|
|
266
266
|
candidates.sort((a, b) => b.order - a.order);
|
|
267
|
+
// INDEX ceiling: build the dir → governing-INDEX map over the whole resolved
|
|
268
|
+
// corpus PLUS the candidate docs, so a dir's INDEX caps (or `none`-hides) its
|
|
269
|
+
// subtree on-read exactly as it does at boot. The candidate's own
|
|
270
|
+
// fileReadVisibility and name are overridden by the effective rung / dir entry.
|
|
271
|
+
let ceil;
|
|
272
|
+
try {
|
|
273
|
+
const resolved = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
|
|
274
|
+
ceil = buildCeilingIndex([...resolved, ...candidates.map((c) => c.doc)]);
|
|
275
|
+
}
|
|
276
|
+
catch {
|
|
277
|
+
ceil = buildCeilingIndex(candidates.map((c) => c.doc));
|
|
278
|
+
}
|
|
267
279
|
const rendered = [];
|
|
268
280
|
for (const c of candidates) {
|
|
269
281
|
// Never re-surface the doc the agent is literally reading.
|
|
@@ -276,7 +288,11 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
|
|
|
276
288
|
try {
|
|
277
289
|
if (!gatePasses(c.doc, subject))
|
|
278
290
|
continue; // gated out for this node
|
|
279
|
-
|
|
291
|
+
const rung = effectiveRung(c.doc, ceil, 'fileReadVisibility');
|
|
292
|
+
const doc = rung === c.doc.fileReadVisibility && displayName(c.doc.name) === c.doc.name
|
|
293
|
+
? c.doc
|
|
294
|
+
: { ...c.doc, fileReadVisibility: rung, name: displayName(c.doc.name) };
|
|
295
|
+
block = renderDocEnvelope(doc, absReadFile);
|
|
280
296
|
}
|
|
281
297
|
catch {
|
|
282
298
|
continue; // a single bad doc never breaks the read
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
/** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
|
|
2
2
|
* rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
|
|
3
|
-
* one compact catalog
|
|
4
|
-
*
|
|
5
|
-
* Returns '' when nothing is eligible. */
|
|
3
|
+
* one compact, plugin-grouped catalog; `preview`/`content`-rung skills each get
|
|
4
|
+
* a `###` sub-section. Returns '' when nothing is eligible. */
|
|
6
5
|
export declare function renderSkillsSection(nodeId: string): string;
|
|
7
6
|
/** The `## Preferences` system-prompt section: every eligible `kind: preference`
|
|
8
7
|
* doc as its own `###` sub-section, at its `system-prompt-visibility` (the
|
|
@@ -14,8 +13,7 @@ export declare function renderPreferencesSection(nodeId: string): string;
|
|
|
14
13
|
* drops it when ''). Holds every eligible `kind: reference` resolver doc at its
|
|
15
14
|
* `system-prompt-visibility` (reference boot default is `none`, so only
|
|
16
15
|
* author-promoted references show) PLUS the node-local memory docs (any kind),
|
|
17
|
-
* each a `###` sub-section.
|
|
18
|
-
* nothing is eligible.
|
|
16
|
+
* each a `###` sub-section. Returns '' when nothing is eligible.
|
|
19
17
|
*
|
|
20
18
|
* DEFENSIVE: each doc is rendered in its own try/catch so a single malformed
|
|
21
19
|
* doc drops only itself (with a loud stderr warning naming the offending path),
|
|
@@ -5,8 +5,7 @@
|
|
|
5
5
|
// (renderPreferencesSection) — strings the D2 `before_agent_start` extension
|
|
6
6
|
// splices into the system prompt (built by a sibling AFTER this module);
|
|
7
7
|
// • the `<crtr-context>` half — `## References` (renderReferencesBlock) — the
|
|
8
|
-
// string wired into bearings.ts's session_start message
|
|
9
|
-
// `<memory>` block.
|
|
8
|
+
// string wired into bearings.ts's session_start message.
|
|
10
9
|
//
|
|
11
10
|
// Every doc flows through the same pipeline (design §4/§6/§9):
|
|
12
11
|
// MemoryDoc → parseSubstrateDoc → (null-filter non-substrate) →
|
|
@@ -19,14 +18,13 @@
|
|
|
19
18
|
// null for a non-substrate doc and is `.filter()`ed out; per-file loads are
|
|
20
19
|
// wrapped so one bad file is skipped, not fatal.
|
|
21
20
|
import { relative, sep } from 'node:path';
|
|
22
|
-
import {
|
|
23
|
-
import { listAllPlugins, listAllSkills } from '../resolver.js';
|
|
21
|
+
import { listAllPlugins } from '../resolver.js';
|
|
24
22
|
import { renderCatalogSection } from '../../commands/skill/shared.js';
|
|
25
23
|
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
26
24
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
27
25
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
28
26
|
import { memoryDir } from '../runtime/memory.js';
|
|
29
|
-
import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
27
|
+
import { applyCeilings, assembleNodeSubject, gatePasses, isIndexName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
30
28
|
import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './session-cache.js';
|
|
31
29
|
// ---------------------------------------------------------------------------
|
|
32
30
|
// The shared per-doc pipeline.
|
|
@@ -36,7 +34,12 @@ import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './ses
|
|
|
36
34
|
* system-prompt rung above `none`. Resolver = user + project + builtin scopes
|
|
37
35
|
* (precedence-ordered); node-local is loaded separately (see nodeLocalDocs).
|
|
38
36
|
* Uses the per-session cache so the full corpus is scanned+parsed at most once
|
|
39
|
-
* per session across the three boot-render calls.
|
|
37
|
+
* per session across the three boot-render calls.
|
|
38
|
+
*
|
|
39
|
+
* Ceilings are applied over the WHOLE corpus (cross-kind) BEFORE the kind
|
|
40
|
+
* filter: an INDEX.md renders as its dir entry (`taste`, not `taste/INDEX`) at
|
|
41
|
+
* its own rung, and every descendant's rung is capped by its ancestor INDEX
|
|
42
|
+
* rungs — a `none` dir hides its whole subtree. */
|
|
40
43
|
function resolverDocs(subject, kind) {
|
|
41
44
|
let docs;
|
|
42
45
|
try {
|
|
@@ -45,19 +48,25 @@ function resolverDocs(subject, kind) {
|
|
|
45
48
|
catch {
|
|
46
49
|
return [];
|
|
47
50
|
}
|
|
48
|
-
return docs
|
|
51
|
+
return applyCeilings(docs, 'systemPromptVisibility')
|
|
49
52
|
.filter((d) => d.kind === kind)
|
|
50
53
|
.filter((d) => gatePasses(d, subject))
|
|
51
|
-
.filter((d) => d.systemPromptVisibility !== 'none')
|
|
54
|
+
.filter((d) => d.systemPromptVisibility !== 'none')
|
|
55
|
+
// Re-sort after the INDEX rename (taste/INDEX → taste) so a dir entry sorts
|
|
56
|
+
// immediately BEFORE its children, keeping scope precedence (project > user
|
|
57
|
+
// > builtin) intact.
|
|
58
|
+
.sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope) || a.name.localeCompare(b.name));
|
|
59
|
+
}
|
|
60
|
+
function scopeRank(scope) {
|
|
61
|
+
return scope === 'project' ? 0 : scope === 'user' ? 1 : 2;
|
|
52
62
|
}
|
|
53
63
|
/** The node-local memory docs eligible at boot. Node-local memory lives in the
|
|
54
64
|
* canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
|
|
55
65
|
* so it is loaded directly here, its raw frontmatter run through
|
|
56
66
|
* parseSubstrateFrontmatter (mirroring the resolver pipeline), then gate-passed.
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* replacement for the old node-local `<memory>` stanza.
|
|
67
|
+
* Non-substrate files (those with no `kind`) parse to null and drop out.
|
|
68
|
+
* Returned across ALL kinds — node-local is the catch-all this-node store and
|
|
69
|
+
* rides into the references block.
|
|
61
70
|
*
|
|
62
71
|
* IMPORTANT: node-local docs are NOT filtered by `systemPromptVisibility` rung.
|
|
63
72
|
* A migrated node-local reference defaults to rung `none`, which would make it
|
|
@@ -116,50 +125,49 @@ function renderSubSection(d) {
|
|
|
116
125
|
// ---------------------------------------------------------------------------
|
|
117
126
|
// 1. Skills section — `## Skills` (system prompt).
|
|
118
127
|
// ---------------------------------------------------------------------------
|
|
119
|
-
/** The compact, group-collapsed `name`-rung catalog
|
|
120
|
-
*
|
|
121
|
-
*
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
pluginSkills = listAllSkills().filter((s) => s.enabled && s.plugin !== SCOPE_SKILL_PLUGIN && s.scope !== 'builtin');
|
|
131
|
-
}
|
|
132
|
-
catch {
|
|
133
|
-
pluginSkills = [];
|
|
134
|
-
}
|
|
135
|
-
const entries = [
|
|
136
|
-
...pluginSkills.map((s) => ({ scope: s.scope, plugin: s.plugin, name: s.name })),
|
|
137
|
-
...nameRungSkillDocs.map((d) => ({ scope: d.scope, plugin: SCOPE_SKILL_PLUGIN, name: d.name })),
|
|
138
|
-
];
|
|
139
|
-
if (entries.length === 0)
|
|
140
|
-
return '';
|
|
141
|
-
// Group by scope+plugin → forest-root names (drop nested children), mirroring
|
|
142
|
-
// buildSkillCatalog's source construction.
|
|
128
|
+
/** The compact, group-collapsed `name`-rung catalog of substrate `skill` docs.
|
|
129
|
+
* Every leaf is a migrated/generated substrate doc (native, builtin, or a
|
|
130
|
+
* plugin's `<pluginName>/` subtree) — there is no second, resolver-provided
|
|
131
|
+
* skill corpus. Each doc self-groups by its top-dir segment: a name with a
|
|
132
|
+
* slash sources to its top segment (the plugin name); a bare name sources to
|
|
133
|
+
* '' (a scope-local native/builtin skill). A plugin whose INDEX is elevated to
|
|
134
|
+
* preview/content renders as its own `### <plugin>` subsection, so its catalog
|
|
135
|
+
* group is dropped here (via `elevatedSources`) to avoid a double render.
|
|
136
|
+
* Reuses skill/shared.ts's renderCatalogSection group-collapse. Returns ''
|
|
137
|
+
* when nothing is in the catalog. */
|
|
138
|
+
function renderSkillCatalog(nameRungSkillDocs, elevatedSources) {
|
|
143
139
|
const bySource = new Map();
|
|
144
|
-
for (const
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
if (
|
|
148
|
-
|
|
140
|
+
for (const d of nameRungSkillDocs) {
|
|
141
|
+
// INDEX docs are structural ceilings (already renamed to their dir entry by
|
|
142
|
+
// applyCeilings), never catalog leaves — drop defensively.
|
|
143
|
+
if (isIndexName(d.name))
|
|
144
|
+
continue;
|
|
145
|
+
const slash = d.name.indexOf('/');
|
|
146
|
+
const plugin = slash === -1 ? '' : d.name.slice(0, slash);
|
|
147
|
+
const leaf = slash === -1 ? d.name : d.name.slice(slash + 1);
|
|
148
|
+
// A plugin represented by its own elevated ### subsection is dropped from the
|
|
149
|
+
// catalog so it is not rendered twice.
|
|
150
|
+
if (plugin !== '' && elevatedSources.has(plugin))
|
|
151
|
+
continue;
|
|
152
|
+
const key = `${d.scope}\t${plugin}`;
|
|
153
|
+
const src = bySource.get(key);
|
|
154
|
+
if (src)
|
|
155
|
+
src.roots.push(leaf);
|
|
149
156
|
else
|
|
150
|
-
bySource.set(key, [
|
|
157
|
+
bySource.set(key, { scope: d.scope, plugin, roots: [leaf] });
|
|
151
158
|
}
|
|
159
|
+
if (bySource.size === 0)
|
|
160
|
+
return '';
|
|
152
161
|
const projectSources = [];
|
|
153
162
|
const userSources = [];
|
|
154
|
-
for (const
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
.filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
|
|
163
|
+
for (const { scope, plugin, roots } of bySource.values()) {
|
|
164
|
+
// Drop nested children so each source contributes only its top-level skills.
|
|
165
|
+
const top = roots
|
|
166
|
+
.filter((n) => !roots.some((m) => m !== n && n.startsWith(m + '/')))
|
|
159
167
|
.sort();
|
|
160
|
-
if (
|
|
168
|
+
if (top.length === 0)
|
|
161
169
|
continue;
|
|
162
|
-
(scope === 'project' ? projectSources : userSources).push({ plugin, roots });
|
|
170
|
+
(scope === 'project' ? projectSources : userSources).push({ plugin, roots: top });
|
|
163
171
|
}
|
|
164
172
|
const descriptions = new Map();
|
|
165
173
|
try {
|
|
@@ -182,19 +190,21 @@ function renderSkillCatalog(nameRungSkillDocs) {
|
|
|
182
190
|
}
|
|
183
191
|
/** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
|
|
184
192
|
* rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
|
|
185
|
-
* one compact catalog
|
|
186
|
-
*
|
|
187
|
-
* Returns '' when nothing is eligible. */
|
|
193
|
+
* one compact, plugin-grouped catalog; `preview`/`content`-rung skills each get
|
|
194
|
+
* a `###` sub-section. Returns '' when nothing is eligible. */
|
|
188
195
|
export function renderSkillsSection(nodeId) {
|
|
189
196
|
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
190
197
|
if (subject === null)
|
|
191
198
|
return '';
|
|
192
199
|
const skills = resolverDocs(subject, 'skill');
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
const
|
|
200
|
+
const elevated = skills.filter((d) => d.systemPromptVisibility !== 'name');
|
|
201
|
+
// A plugin whose INDEX is elevated surfaces as its own `### <plugin>`
|
|
202
|
+
// subsection (its display name is the plugin/dir name after ceiling rename);
|
|
203
|
+
// the catalog drops that plugin's group so it is not rendered twice.
|
|
204
|
+
const elevatedSources = new Set(elevated.map((d) => d.name));
|
|
205
|
+
const catalog = renderSkillCatalog(skills.filter((d) => d.systemPromptVisibility === 'name'), elevatedSources);
|
|
206
|
+
const elevatedBlocks = elevated.map(renderSubSection);
|
|
207
|
+
const blocks = [catalog, ...elevatedBlocks].filter((s) => s !== '');
|
|
198
208
|
if (blocks.length === 0)
|
|
199
209
|
return '';
|
|
200
210
|
return `## Skills\n\n${blocks.join('\n\n')}`;
|
|
@@ -225,8 +235,7 @@ export function renderPreferencesSection(nodeId) {
|
|
|
225
235
|
* drops it when ''). Holds every eligible `kind: reference` resolver doc at its
|
|
226
236
|
* `system-prompt-visibility` (reference boot default is `none`, so only
|
|
227
237
|
* author-promoted references show) PLUS the node-local memory docs (any kind),
|
|
228
|
-
* each a `###` sub-section.
|
|
229
|
-
* nothing is eligible.
|
|
238
|
+
* each a `###` sub-section. Returns '' when nothing is eligible.
|
|
230
239
|
*
|
|
231
240
|
* DEFENSIVE: each doc is rendered in its own try/catch so a single malformed
|
|
232
241
|
* doc drops only itself (with a loud stderr warning naming the offending path),
|
|
@@ -21,15 +21,17 @@ export declare const KIND_DEFAULT_RUNGS: Record<DocKind, {
|
|
|
21
21
|
* `all`/`any`/`not` combinators (design §4). */
|
|
22
22
|
export type GatePredicate = Record<string, unknown>;
|
|
23
23
|
/** The frontmatter-derived schema of a substrate document, with kind-aware
|
|
24
|
-
* defaults applied. Required fields (`kind`/`when
|
|
25
|
-
* resolved to concrete typed values. */
|
|
24
|
+
* defaults applied. Required fields (`kind`/`when-and-why-to-read`) and
|
|
25
|
+
* optionals all resolved to concrete typed values. */
|
|
26
26
|
export interface SubstrateSchema {
|
|
27
27
|
/** Which of the three semantic kinds. */
|
|
28
28
|
kind: DocKind;
|
|
29
|
-
/**
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
29
|
+
/** The read-routing line — a single sentence answering WHEN to read this doc
|
|
30
|
+
* and WHY it is worth the read: "When <circumstance>, this <kind> should be
|
|
31
|
+
* read <because <payoff>>." (design §4). This is read-routing — why an agent
|
|
32
|
+
* should spend the read — NEVER justification of why the content should be
|
|
33
|
+
* obeyed. It IS the preview verbatim. Frontmatter key `when-and-why-to-read`. */
|
|
34
|
+
whenAndWhyToRead: string;
|
|
33
35
|
/** Human-facing abbreviation for `crtr memory list`. NEVER loaded into an
|
|
34
36
|
* agent's context (design §3). Empty string when absent. */
|
|
35
37
|
shortForm: string;
|
|
@@ -62,15 +64,18 @@ export interface SubstrateDoc extends SubstrateSchema {
|
|
|
62
64
|
* resolver) into a typed schema with defaults applied. Returns `null` when the
|
|
63
65
|
* record is absent or carries no valid `kind` — i.e. it is not a substrate
|
|
64
66
|
* document and cannot be classified or defaulted. Tolerant of every other
|
|
65
|
-
* imperfection (missing `when
|
|
66
|
-
* the kind default), so a renderer mapping over many docs never
|
|
67
|
+
* imperfection (a missing `when-and-why-to-read` defaults to '', a bad rung
|
|
68
|
+
* falls back to the kind default), so a renderer mapping over many docs never
|
|
69
|
+
* throws. Authoring-time enforcement of the field lives in `crtr memory lint`. */
|
|
67
70
|
export declare function parseSubstrateFrontmatter(fm: Record<string, unknown> | null): SubstrateSchema | null;
|
|
68
71
|
/** Parse a resolved MemoryDoc into a fully-typed SubstrateDoc (schema + the
|
|
69
72
|
* resolver's name/scope/path/body). Returns `null` for a non-substrate doc
|
|
70
73
|
* (no valid `kind`), so callers can `docs.map(parseSubstrateDoc).filter(...)`. */
|
|
71
74
|
export declare function parseSubstrateDoc(doc: MemoryDoc): SubstrateDoc | null;
|
|
72
|
-
/** The
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
75
|
+
/** The `preview`-rung routing line (design §4): the `when-and-why-to-read`
|
|
76
|
+
* field rendered essentially verbatim — it is already authored as the complete
|
|
77
|
+
* routing sentence ("When …, this <kind> should be read …"), so there is no
|
|
78
|
+
* template to compose. Both boot and on-read render this identical line, so it
|
|
79
|
+
* lives once here to prevent drift. Light cleanup normalizes the trailing
|
|
80
|
+
* period. */
|
|
81
|
+
export declare function previewLine(doc: Pick<SubstrateSchema, 'whenAndWhyToRead'>): string;
|
|
@@ -50,8 +50,9 @@ export const KIND_DEFAULT_RUNGS = {
|
|
|
50
50
|
* resolver) into a typed schema with defaults applied. Returns `null` when the
|
|
51
51
|
* record is absent or carries no valid `kind` — i.e. it is not a substrate
|
|
52
52
|
* document and cannot be classified or defaulted. Tolerant of every other
|
|
53
|
-
* imperfection (missing `when
|
|
54
|
-
* the kind default), so a renderer mapping over many docs never
|
|
53
|
+
* imperfection (a missing `when-and-why-to-read` defaults to '', a bad rung
|
|
54
|
+
* falls back to the kind default), so a renderer mapping over many docs never
|
|
55
|
+
* throws. Authoring-time enforcement of the field lives in `crtr memory lint`. */
|
|
55
56
|
export function parseSubstrateFrontmatter(fm) {
|
|
56
57
|
if (fm === null)
|
|
57
58
|
return null;
|
|
@@ -61,8 +62,7 @@ export function parseSubstrateFrontmatter(fm) {
|
|
|
61
62
|
const defaults = KIND_DEFAULT_RUNGS[kind];
|
|
62
63
|
return {
|
|
63
64
|
kind,
|
|
64
|
-
|
|
65
|
-
why: strField(fm.why),
|
|
65
|
+
whenAndWhyToRead: strField(fm['when-and-why-to-read']),
|
|
66
66
|
shortForm: strField(fm['short-form']),
|
|
67
67
|
systemPromptVisibility: parseRung(fm['system-prompt-visibility'], defaults.systemPrompt),
|
|
68
68
|
fileReadVisibility: parseRung(fm['file-read-visibility'], defaults.fileRead),
|
|
@@ -79,14 +79,15 @@ export function parseSubstrateDoc(doc) {
|
|
|
79
79
|
return null;
|
|
80
80
|
return { ...schema, name: doc.name, scope: doc.scope, path: doc.path, body: doc.body };
|
|
81
81
|
}
|
|
82
|
-
/** The
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
82
|
+
/** The `preview`-rung routing line (design §4): the `when-and-why-to-read`
|
|
83
|
+
* field rendered essentially verbatim — it is already authored as the complete
|
|
84
|
+
* routing sentence ("When …, this <kind> should be read …"), so there is no
|
|
85
|
+
* template to compose. Both boot and on-read render this identical line, so it
|
|
86
|
+
* lives once here to prevent drift. Light cleanup normalizes the trailing
|
|
87
|
+
* period. */
|
|
86
88
|
export function previewLine(doc) {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
return `${when}, read this ${doc.kind}. ${why}.`;
|
|
89
|
+
const line = doc.whenAndWhyToRead.trim().replace(/\.+$/, '');
|
|
90
|
+
return line === '' ? '' : `${line}.`;
|
|
90
91
|
}
|
|
91
92
|
// ---------------------------------------------------------------------------
|
|
92
93
|
// Field coercion helpers (private).
|
|
@@ -15,9 +15,11 @@
|
|
|
15
15
|
//
|
|
16
16
|
// The block carries: the path to the node's own context dir and the framing for
|
|
17
17
|
// what belongs there (a shared document store for the other nodes). EVERY node
|
|
18
|
-
// also gets a
|
|
19
|
-
//
|
|
20
|
-
//
|
|
18
|
+
// also gets a `## References` block rendered from the document substrate —
|
|
19
|
+
// eligible `reference` docs at their system-prompt visibility rung, plus the
|
|
20
|
+
// node's own node-local substrate docs — so the bearings name what to read on
|
|
21
|
+
// demand. (Skills and preferences surface as their own `## Skills` /
|
|
22
|
+
// `## Preferences` sections of the system prompt, not in this block.) An
|
|
21
23
|
// orchestrator additionally gets the across-refresh-cycles framing (the one
|
|
22
24
|
// thing a terminal worker's bearings drop). The prose lives in
|
|
23
25
|
// core/runtime/bearings.ts (shared with the promotion guidance dump).
|
|
@@ -43,8 +43,24 @@ import { getNode } from '../core/canvas/index.js';
|
|
|
43
43
|
// one watcher is live. Pattern copied verbatim from agent-inbox-watcher.ts.
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
let liveTimer;
|
|
46
|
-
const
|
|
47
|
-
const
|
|
46
|
+
const DEFAULT_TICK_MS = 800; // polling cadence
|
|
47
|
+
const DEFAULT_DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
|
|
48
|
+
// Testability seam: a test can inject a much shorter cadence so it doesn't have
|
|
49
|
+
// to sleep against the real ~800ms/1000ms wall-clock timing. Read per-registration
|
|
50
|
+
// (not as an import-time module const) so each registerCanvasInboxWatcher() call
|
|
51
|
+
// picks up the env current at that moment. Unset → production defaults.
|
|
52
|
+
function resolveCadence() {
|
|
53
|
+
const parse = (raw, fallback) => {
|
|
54
|
+
if (raw === undefined)
|
|
55
|
+
return fallback;
|
|
56
|
+
const n = Number(raw);
|
|
57
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
58
|
+
};
|
|
59
|
+
return {
|
|
60
|
+
tickMs: parse(process.env['CRTR_WATCHER_TICK_MS'], DEFAULT_TICK_MS),
|
|
61
|
+
debounceMs: parse(process.env['CRTR_WATCHER_DEBOUNCE_MS'], DEFAULT_DEBOUNCE_MS),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
48
64
|
// ---------------------------------------------------------------------------
|
|
49
65
|
// Extension
|
|
50
66
|
// ---------------------------------------------------------------------------
|
|
@@ -56,6 +72,7 @@ const DEBOUNCE_MS = 1000; // flush once the burst has been quiet for this long
|
|
|
56
72
|
* the module-level liveTimer guard is the actual stacking prevention.
|
|
57
73
|
*/
|
|
58
74
|
export function registerCanvasInboxWatcher(pi) {
|
|
75
|
+
const { tickMs: TICK_MS, debounceMs: DEBOUNCE_MS } = resolveCadence();
|
|
59
76
|
// Capture the latest event context so isIdle() is readable inside the timer
|
|
60
77
|
// callback, which has no ctx of its own.
|
|
61
78
|
let lastCtx;
|