@crouton-kit/crouter 0.3.17 → 0.3.19
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/build-root.js +6 -0
- package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
- package/dist/builtin-memory/crouter-development/personas.md +117 -0
- package/dist/builtin-memory/crouter-development/plugins.md +163 -0
- package/dist/builtin-memory/design.md +60 -0
- package/dist/builtin-memory/development.md +116 -0
- package/dist/builtin-memory/planning.md +66 -0
- package/dist/builtin-memory/spec.md +99 -0
- package/dist/builtin-personas/design/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/design/orchestrator.md +1 -1
- package/dist/builtin-personas/developer/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/developer/orchestrator.md +1 -1
- package/dist/builtin-personas/explore/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/general/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/lifecycle/resident.md +1 -1
- package/dist/builtin-personas/lifecycle/terminal.md +5 -2
- package/dist/builtin-personas/orchestration-kernel.md +15 -24
- package/dist/builtin-personas/plan/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/plan/orchestrator.md +1 -1
- package/dist/builtin-personas/plan/reviewers/architecture-fit/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/code-smells/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/pattern-consistency/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/requirements-coverage/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/plan/reviewers/security/{base.md → PERSONA.md} +1 -1
- package/dist/builtin-personas/review/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/runtime-base.md +6 -3
- package/dist/builtin-personas/spec/{base.md → PERSONA.md} +4 -0
- package/dist/builtin-personas/spec/orchestrator.md +1 -1
- package/dist/builtin-personas/waiting.md +8 -0
- package/dist/builtin-skills/skills/crouter-development/personas/SKILL.md +24 -14
- package/dist/builtin-skills/skills/crouter-development/personas/base-prompt/SKILL.md +4 -4
- package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
- package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
- package/dist/builtin-views/_lib/states.mjs +161 -0
- package/dist/builtin-views/canvas/client.mjs +303 -0
- package/dist/builtin-views/canvas/view.mjs +576 -0
- package/dist/builtin-views/git-pr/client.mjs +440 -0
- package/dist/builtin-views/git-pr/view.mjs +675 -0
- package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
- package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
- package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
- package/dist/builtin-views/inbox/view.mjs +889 -0
- package/dist/builtin-views/linkedin/client.mjs +610 -0
- package/dist/builtin-views/linkedin/view.mjs +1171 -0
- package/dist/clients/attach/attach-cmd.d.ts +2 -0
- package/dist/clients/attach/attach-cmd.js +354 -0
- package/dist/clients/attach/chat-view.d.ts +77 -0
- package/dist/clients/attach/chat-view.js +450 -0
- package/dist/clients/attach/clipboard-image.d.ts +16 -0
- package/dist/clients/attach/clipboard-image.js +113 -0
- package/dist/clients/attach/config-load.d.ts +31 -0
- package/dist/clients/attach/config-load.js +113 -0
- package/dist/clients/attach/extension-dialogs.d.ts +29 -0
- package/dist/clients/attach/extension-dialogs.js +101 -0
- package/dist/clients/attach/input-controller.d.ts +54 -0
- package/dist/clients/attach/input-controller.js +204 -0
- package/dist/clients/attach/slash-commands.d.ts +36 -0
- package/dist/clients/attach/slash-commands.js +200 -0
- package/dist/clients/attach/view-socket.d.ts +48 -0
- package/dist/clients/attach/view-socket.js +126 -0
- package/dist/commands/attention.js +3 -3
- package/dist/commands/canvas-prune.js +1 -1
- package/dist/commands/daemon.js +4 -3
- package/dist/commands/human/prompts.js +4 -10
- package/dist/commands/human/queue.js +43 -8
- package/dist/commands/human/shared.d.ts +28 -1
- package/dist/commands/human/shared.js +48 -10
- package/dist/commands/memory/find.d.ts +1 -0
- package/dist/commands/memory/find.js +180 -0
- package/dist/commands/memory/lint.d.ts +1 -0
- package/dist/commands/memory/lint.js +140 -0
- package/dist/commands/memory/list.d.ts +1 -0
- package/dist/commands/memory/list.js +79 -0
- package/dist/commands/memory/read.js +103 -0
- package/dist/commands/memory/shared.d.ts +30 -0
- package/dist/commands/memory/shared.js +122 -0
- package/dist/commands/memory/write.d.ts +1 -0
- package/dist/commands/memory/write.js +85 -0
- package/dist/commands/memory.d.ts +2 -0
- package/dist/commands/memory.js +27 -0
- package/dist/commands/node.d.ts +3 -2
- package/dist/commands/node.js +660 -65
- package/dist/commands/pkg/market-manage.js +1 -1
- package/dist/commands/push.js +6 -6
- package/dist/commands/revive.js +1 -1
- package/dist/commands/skill/author.js +1 -1
- package/dist/commands/skill/shared.d.ts +1 -8
- package/dist/commands/skill/shared.js +2 -55
- package/dist/commands/skill.js +9 -14
- package/dist/commands/sys/doctor.js +1 -1
- package/dist/commands/sys/update.js +1 -1
- package/dist/commands/view-cycle.d.ts +2 -0
- package/dist/commands/view-cycle.js +125 -0
- package/dist/commands/view-list.d.ts +2 -0
- package/dist/commands/view-list.js +66 -0
- package/dist/commands/view-new.d.ts +2 -0
- package/dist/commands/view-new.js +70 -0
- package/dist/commands/view-pick.d.ts +2 -0
- package/dist/commands/view-pick.js +119 -0
- package/dist/commands/view-run.d.ts +2 -0
- package/dist/commands/view-run.js +191 -0
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +29 -0
- package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
- package/dist/core/__tests__/broker-lifecycle.test.js +677 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
- package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
- package/dist/core/__tests__/cascade-close.test.js +12 -2
- package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
- package/dist/core/__tests__/child-death-wake.test.js +245 -0
- package/dist/core/__tests__/context-intro.test.js +76 -62
- package/dist/core/__tests__/daemon-boot.test.js +14 -5
- package/dist/core/__tests__/daemon-liveness.test.js +34 -9
- package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
- package/dist/core/__tests__/detach-focus.test.js +206 -0
- package/dist/core/__tests__/draw-style.test.d.ts +1 -0
- package/dist/core/__tests__/draw-style.test.js +258 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
- package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
- package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
- package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
- package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
- package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
- package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
- package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
- package/dist/core/__tests__/helpers/harness.d.ts +9 -0
- package/dist/core/__tests__/helpers/harness.js +111 -1
- package/dist/core/__tests__/home-session.test.js +41 -8
- package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
- package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
- package/dist/core/__tests__/human-surface-target.test.d.ts +1 -0
- package/dist/core/__tests__/human-surface-target.test.js +98 -0
- package/dist/core/__tests__/kickoff.test.js +37 -3
- package/dist/core/__tests__/live-mutation.test.js +50 -33
- package/dist/core/__tests__/memory.test.js +23 -115
- package/dist/core/__tests__/persona-subkind.test.js +18 -15
- package/dist/core/__tests__/placement-focus.test.js +5 -0
- package/dist/core/__tests__/placement-teardown.test.js +54 -11
- package/dist/core/__tests__/relaunch.test.js +4 -3
- package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
- package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
- package/dist/core/__tests__/spawn-root.test.js +10 -0
- package/dist/core/__tests__/spike-harness.test.js +1 -0
- package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
- package/dist/core/__tests__/wake-bearings.test.js +156 -0
- package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
- package/dist/core/__tests__/wake-origin.test.js +110 -0
- package/dist/core/bootstrap.js +1 -1
- package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
- package/dist/core/canvas/browse/app.js +24 -2
- package/dist/core/canvas/browse/model.d.ts +38 -2
- package/dist/core/canvas/browse/model.js +134 -10
- package/dist/core/canvas/browse/render.d.ts +6 -12
- package/dist/core/canvas/browse/render.js +72 -104
- package/dist/core/canvas/canvas.js +15 -22
- package/dist/core/canvas/db.js +46 -0
- package/dist/core/canvas/index.d.ts +1 -0
- package/dist/core/canvas/index.js +1 -0
- package/dist/core/canvas/paths.d.ts +4 -1
- package/dist/core/canvas/paths.js +10 -4
- package/dist/core/canvas/pid.d.ts +4 -0
- package/dist/core/canvas/pid.js +23 -0
- package/dist/core/canvas/render.d.ts +11 -2
- package/dist/core/canvas/render.js +69 -0
- package/dist/core/canvas/types.d.ts +85 -3
- package/dist/core/canvas/types.js +2 -2
- package/dist/core/canvas/wakeups.d.ts +76 -0
- package/dist/core/canvas/wakeups.js +185 -0
- package/dist/core/config.js +4 -1
- package/dist/core/frontmatter.js +37 -124
- package/dist/core/help.d.ts +6 -0
- package/dist/core/help.js +7 -0
- package/dist/core/memory-resolver.d.ts +49 -0
- package/dist/core/memory-resolver.js +141 -0
- package/dist/core/personas/index.d.ts +4 -3
- package/dist/core/personas/index.js +3 -2
- package/dist/core/personas/loader.d.ts +41 -16
- package/dist/core/personas/loader.js +133 -29
- package/dist/core/personas/resolve.d.ts +4 -4
- package/dist/core/personas/resolve.js +28 -16
- package/dist/core/predicate.d.ts +63 -0
- package/dist/core/predicate.js +189 -0
- package/dist/core/resolver.js +26 -5
- package/dist/core/runtime/bearings.d.ts +53 -12
- package/dist/core/runtime/bearings.js +132 -59
- package/dist/core/runtime/broker-cli.d.ts +1 -0
- package/dist/core/runtime/broker-cli.js +46 -0
- package/dist/core/runtime/broker-protocol.d.ts +332 -0
- package/dist/core/runtime/broker-protocol.js +153 -0
- package/dist/core/runtime/broker-sdk.d.ts +48 -0
- package/dist/core/runtime/broker-sdk.js +72 -0
- package/dist/core/runtime/broker.d.ts +55 -0
- package/dist/core/runtime/broker.js +1128 -0
- package/dist/core/runtime/close.js +35 -6
- package/dist/core/runtime/host.d.ts +53 -0
- package/dist/core/runtime/host.js +186 -0
- package/dist/core/runtime/kickoff.d.ts +2 -1
- package/dist/core/runtime/kickoff.js +91 -5
- package/dist/core/runtime/launch.d.ts +45 -2
- package/dist/core/runtime/launch.js +65 -2
- package/dist/core/runtime/lifecycle.js +23 -6
- package/dist/core/runtime/memory.d.ts +2 -42
- package/dist/core/runtime/memory.js +11 -162
- package/dist/core/runtime/nodes.d.ts +33 -0
- package/dist/core/runtime/nodes.js +59 -1
- package/dist/core/runtime/persona.js +21 -11
- package/dist/core/runtime/pi-vendored.d.ts +18 -0
- package/dist/core/runtime/pi-vendored.js +49 -0
- package/dist/core/runtime/placement.d.ts +42 -14
- package/dist/core/runtime/placement.js +228 -38
- package/dist/core/runtime/promote.d.ts +0 -6
- package/dist/core/runtime/promote.js +1 -12
- package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
- package/dist/core/runtime/{demote.js → recycle.js} +27 -11
- package/dist/core/runtime/reset.js +8 -6
- package/dist/core/runtime/revive.d.ts +2 -0
- package/dist/core/runtime/revive.js +34 -34
- package/dist/core/runtime/spawn.d.ts +19 -0
- package/dist/core/runtime/spawn.js +75 -22
- package/dist/core/runtime/stop-guard.d.ts +1 -1
- package/dist/core/runtime/stop-guard.js +6 -1
- package/dist/core/runtime/tmux-chrome.d.ts +1 -1
- package/dist/core/runtime/tmux-chrome.js +1 -1
- package/dist/core/runtime/tmux.d.ts +28 -0
- package/dist/core/runtime/tmux.js +80 -6
- package/dist/core/scope.d.ts +11 -0
- package/dist/core/scope.js +39 -0
- package/dist/core/spawn.d.ts +25 -1
- package/dist/core/spawn.js +72 -7
- package/dist/core/substrate/gate.d.ts +13 -0
- package/dist/core/substrate/gate.js +21 -0
- package/dist/core/substrate/index.d.ts +7 -0
- package/dist/core/substrate/index.js +18 -0
- package/dist/core/substrate/on-read.d.ts +14 -0
- package/dist/core/substrate/on-read.js +292 -0
- package/dist/core/substrate/render.d.ts +25 -0
- package/dist/core/substrate/render.js +256 -0
- package/dist/core/substrate/schema.d.ts +76 -0
- package/dist/core/substrate/schema.js +124 -0
- package/dist/core/substrate/session-cache.d.ts +30 -0
- package/dist/core/substrate/session-cache.js +77 -0
- package/dist/core/substrate/subject.d.ts +41 -0
- package/dist/core/substrate/subject.js +54 -0
- package/dist/core/tui/contract.d.ts +83 -0
- package/dist/core/tui/contract.js +8 -0
- package/dist/core/tui/draw.d.ts +96 -0
- package/dist/core/tui/draw.js +339 -0
- package/dist/core/tui/host.d.ts +29 -0
- package/dist/core/tui/host.js +379 -0
- package/dist/core/tui/loader.d.ts +16 -0
- package/dist/core/tui/loader.js +94 -0
- package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
- package/dist/core/wake.d.ts +86 -0
- package/dist/core/wake.js +308 -0
- package/dist/daemon/crtrd.d.ts +29 -4
- package/dist/daemon/crtrd.js +662 -46
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
- package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
- package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
- package/dist/pi-extensions/canvas-commands.d.ts +3 -0
- package/dist/pi-extensions/canvas-commands.js +10 -0
- package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
- package/dist/pi-extensions/canvas-context-intro.js +55 -15
- package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
- package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
- package/dist/pi-extensions/canvas-nav.d.ts +3 -0
- package/dist/pi-extensions/canvas-nav.js +145 -34
- package/dist/pi-extensions/canvas-stophook.js +17 -8
- package/dist/pi-extensions/canvas-view.d.ts +21 -0
- package/dist/pi-extensions/canvas-view.js +75 -0
- package/dist/prompts/skill.js +19 -26
- package/dist/prompts/view.d.ts +7 -0
- package/dist/prompts/view.js +101 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.js +1 -0
- package/package.json +9 -4
- package/dist/commands/skill/find.d.ts +0 -4
- package/dist/commands/skill/find.js +0 -257
- package/dist/commands/skill/read.js +0 -91
- /package/dist/commands/{skill → memory}/read.d.ts +0 -0
- /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
// render.ts — the pure boot-render functions for the document substrate.
|
|
2
|
+
//
|
|
3
|
+
// Two boot targets, three functions:
|
|
4
|
+
// • the SYSTEM-PROMPT half — `## Skills` (renderSkillsSection) + `## Preferences`
|
|
5
|
+
// (renderPreferencesSection) — strings the D2 `before_agent_start` extension
|
|
6
|
+
// splices into the system prompt (built by a sibling AFTER this module);
|
|
7
|
+
// • the `<crtr-context>` half — `## References` (renderReferencesBlock) — the
|
|
8
|
+
// string wired into bearings.ts's session_start message, REPLACING the old
|
|
9
|
+
// `<memory>` block.
|
|
10
|
+
//
|
|
11
|
+
// Every doc flows through the same pipeline (design §4/§6/§9):
|
|
12
|
+
// MemoryDoc → parseSubstrateDoc → (null-filter non-substrate) →
|
|
13
|
+
// gatePasses(doc, assembleNodeSubject(nodeId)) → render at the doc's
|
|
14
|
+
// `system-prompt-visibility` rung.
|
|
15
|
+
//
|
|
16
|
+
// Pure + defensive: reads the resolver, canvas-db (subject assembly), and the
|
|
17
|
+
// node-local memory dir; performs no writes and no side effects. A single
|
|
18
|
+
// malformed doc must never throw the whole render — parseSubstrateDoc returns
|
|
19
|
+
// null for a non-substrate doc and is `.filter()`ed out; per-file loads are
|
|
20
|
+
// wrapped so one bad file is skipped, not fatal.
|
|
21
|
+
import { relative, sep } from 'node:path';
|
|
22
|
+
import { SCOPE_SKILL_PLUGIN } from '../../types.js';
|
|
23
|
+
import { listAllPlugins, listAllSkills } from '../resolver.js';
|
|
24
|
+
import { renderCatalogSection } from '../../commands/skill/shared.js';
|
|
25
|
+
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
26
|
+
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
27
|
+
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
28
|
+
import { memoryDir } from '../runtime/memory.js';
|
|
29
|
+
import { assembleNodeSubject, gatePasses, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, } from './index.js';
|
|
30
|
+
import { cachedAllPlugins, cachedNodeSubject, cachedSubstrateDocs, } from './session-cache.js';
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// The shared per-doc pipeline.
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
/** The resolver-provided substrate docs of one `kind`, eligible at boot for
|
|
35
|
+
* `subject`: parsed (non-substrate docs null-filtered), gate-passed, and at a
|
|
36
|
+
* system-prompt rung above `none`. Resolver = user + project + builtin scopes
|
|
37
|
+
* (precedence-ordered); node-local is loaded separately (see nodeLocalDocs).
|
|
38
|
+
* Uses the per-session cache so the full corpus is scanned+parsed at most once
|
|
39
|
+
* per session across the three boot-render calls. */
|
|
40
|
+
function resolverDocs(subject, kind) {
|
|
41
|
+
let docs;
|
|
42
|
+
try {
|
|
43
|
+
docs = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
return docs
|
|
49
|
+
.filter((d) => d.kind === kind)
|
|
50
|
+
.filter((d) => gatePasses(d, subject))
|
|
51
|
+
.filter((d) => d.systemPromptVisibility !== 'none');
|
|
52
|
+
}
|
|
53
|
+
/** The node-local memory docs eligible at boot. Node-local memory lives in the
|
|
54
|
+
* canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
|
|
55
|
+
* so it is loaded directly here, its raw frontmatter run through
|
|
56
|
+
* parseSubstrateFrontmatter (mirroring the resolver pipeline), then gate-passed.
|
|
57
|
+
* Old-format / non-substrate files (incl. the MEMORY.md index, which has no
|
|
58
|
+
* `kind`) parse to null and drop out. Returned across ALL kinds — node-local is
|
|
59
|
+
* the catch-all this-node store and rides into the references block as the
|
|
60
|
+
* replacement for the old node-local `<memory>` stanza.
|
|
61
|
+
*
|
|
62
|
+
* IMPORTANT: node-local docs are NOT filtered by `systemPromptVisibility` rung.
|
|
63
|
+
* A migrated node-local reference defaults to rung `none`, which would make it
|
|
64
|
+
* invisible — but the design explicitly says "node-local rides into references"
|
|
65
|
+
* without qualification. Suppressing them by rung contradicts that contract.
|
|
66
|
+
* A `none`-rung node-local doc renders as a `### <name>` title stub (the `name`
|
|
67
|
+
* rung fallback in renderSubSection), which is the minimum meaningful surface.
|
|
68
|
+
* Only gate evaluation removes a node-local doc from the block. */
|
|
69
|
+
function nodeLocalDocs(nodeId, subject) {
|
|
70
|
+
const dir = memoryDir(nodeId);
|
|
71
|
+
if (!pathExists(dir))
|
|
72
|
+
return [];
|
|
73
|
+
const out = [];
|
|
74
|
+
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
75
|
+
const name = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
|
|
76
|
+
if (!name)
|
|
77
|
+
continue;
|
|
78
|
+
try {
|
|
79
|
+
const { data, body } = parseFrontmatterGeneric(readText(file));
|
|
80
|
+
const schema = parseSubstrateFrontmatter(data);
|
|
81
|
+
if (schema === null)
|
|
82
|
+
continue;
|
|
83
|
+
// node-local is NOT a resolver scope; `scope` is a placeholder never read
|
|
84
|
+
// by gate eval (keyed off the NODE subject, not the doc) nor by the
|
|
85
|
+
// renderers (keyed off name / body / rung).
|
|
86
|
+
out.push({ ...schema, name, scope: 'user', path: file, body });
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// A single malformed file is skipped, never fatal to the render.
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Gate-filter only: rung is NOT filtered (see comment above).
|
|
94
|
+
return out.filter((d) => gatePasses(d, subject));
|
|
95
|
+
}
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Sub-section render (preview / content / name) — the per-doc `###` block used
|
|
98
|
+
// by every kind EXCEPT name-rung skills (those fold into the compact catalog).
|
|
99
|
+
// ---------------------------------------------------------------------------
|
|
100
|
+
/** One doc rendered as its own `### <name>` sub-section, at its system-prompt
|
|
101
|
+
* rung: `preview` → the generated routing line; `content` → the full body;
|
|
102
|
+
* `name` → the title alone (`none` is filtered upstream, never reaches here). */
|
|
103
|
+
function renderSubSection(d) {
|
|
104
|
+
const header = `### ${d.name}`;
|
|
105
|
+
switch (d.systemPromptVisibility) {
|
|
106
|
+
case 'preview':
|
|
107
|
+
return `${header}\n${previewLine(d)}`;
|
|
108
|
+
case 'content': {
|
|
109
|
+
const body = d.body.trim();
|
|
110
|
+
return body === '' ? header : `${header}\n${body}`;
|
|
111
|
+
}
|
|
112
|
+
default: // 'name'
|
|
113
|
+
return header;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
// 1. Skills section — `## Skills` (system prompt).
|
|
118
|
+
// ---------------------------------------------------------------------------
|
|
119
|
+
/** The compact, group-collapsed `name`-rung catalog: the UNION of name-rung
|
|
120
|
+
* substrate `skill` docs (the migrated scope-local + builtin skills, rendered
|
|
121
|
+
* as scope-local `_` entries) WITH plugin/marketplace skills (which stay
|
|
122
|
+
* resolver-provided, NOT migrated — named-plugin skills at user/project scope;
|
|
123
|
+
* the SCOPE_SKILL_PLUGIN sentinel and builtin scope are EXCLUDED because they
|
|
124
|
+
* become substrate docs). Reuses skill/shared.ts's renderCatalogSection
|
|
125
|
+
* group-collapse so the boot catalog matches today's `<skills count=N>` shape.
|
|
126
|
+
* Returns '' when nothing is in the catalog. */
|
|
127
|
+
function renderSkillCatalog(nameRungSkillDocs) {
|
|
128
|
+
let pluginSkills;
|
|
129
|
+
try {
|
|
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.
|
|
143
|
+
const bySource = new Map();
|
|
144
|
+
for (const e of entries) {
|
|
145
|
+
const key = `${e.scope}\t${e.plugin}`;
|
|
146
|
+
const arr = bySource.get(key);
|
|
147
|
+
if (arr)
|
|
148
|
+
arr.push(e);
|
|
149
|
+
else
|
|
150
|
+
bySource.set(key, [e]);
|
|
151
|
+
}
|
|
152
|
+
const projectSources = [];
|
|
153
|
+
const userSources = [];
|
|
154
|
+
for (const [key, group] of bySource) {
|
|
155
|
+
const [scope, plugin] = key.split('\t');
|
|
156
|
+
const names = group.map((g) => g.name);
|
|
157
|
+
const roots = names
|
|
158
|
+
.filter((n) => !names.some((m) => m !== n && n.startsWith(m + '/')))
|
|
159
|
+
.sort();
|
|
160
|
+
if (roots.length === 0)
|
|
161
|
+
continue;
|
|
162
|
+
(scope === 'project' ? projectSources : userSources).push({ plugin, roots });
|
|
163
|
+
}
|
|
164
|
+
const descriptions = new Map();
|
|
165
|
+
try {
|
|
166
|
+
for (const p of cachedAllPlugins(listAllPlugins)) {
|
|
167
|
+
if (p.manifest.description)
|
|
168
|
+
descriptions.set(p.name, p.manifest.description);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
// descriptions are an optional suffix; render without them on failure.
|
|
173
|
+
}
|
|
174
|
+
const body = [];
|
|
175
|
+
renderCatalogSection('Project', projectSources, descriptions, body);
|
|
176
|
+
renderCatalogSection('User', userSources, descriptions, body);
|
|
177
|
+
// renderCatalogSection leads each section with a blank separator; drop it so
|
|
178
|
+
// the catalog starts on its first real line.
|
|
179
|
+
while (body.length > 0 && body[0] === '')
|
|
180
|
+
body.shift();
|
|
181
|
+
return body.length === 0 ? '' : body.join('\n');
|
|
182
|
+
}
|
|
183
|
+
/** The `## Skills` system-prompt section: every eligible `kind: skill` doc,
|
|
184
|
+
* rendered at its `system-prompt-visibility`. `name`-rung skills collapse into
|
|
185
|
+
* one compact catalog (group-collapsed, unioned with plugin/marketplace
|
|
186
|
+
* skills); `preview`/`content`-rung skills each get a `###` sub-section.
|
|
187
|
+
* Returns '' when nothing is eligible. */
|
|
188
|
+
export function renderSkillsSection(nodeId) {
|
|
189
|
+
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
190
|
+
if (subject === null)
|
|
191
|
+
return '';
|
|
192
|
+
const skills = resolverDocs(subject, 'skill');
|
|
193
|
+
const catalog = renderSkillCatalog(skills.filter((d) => d.systemPromptVisibility === 'name'));
|
|
194
|
+
const elevated = skills
|
|
195
|
+
.filter((d) => d.systemPromptVisibility !== 'name')
|
|
196
|
+
.map(renderSubSection);
|
|
197
|
+
const blocks = [catalog, ...elevated].filter((s) => s !== '');
|
|
198
|
+
if (blocks.length === 0)
|
|
199
|
+
return '';
|
|
200
|
+
return `## Skills\n\n${blocks.join('\n\n')}`;
|
|
201
|
+
}
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
// 2. Preferences section — `## Preferences` (system prompt).
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
/** The `## Preferences` system-prompt section: every eligible `kind: preference`
|
|
206
|
+
* doc as its own `###` sub-section, at its `system-prompt-visibility` (the
|
|
207
|
+
* preference default rung is `preview` → the routing line). Returns '' when
|
|
208
|
+
* nothing is eligible. */
|
|
209
|
+
export function renderPreferencesSection(nodeId) {
|
|
210
|
+
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
211
|
+
if (subject === null)
|
|
212
|
+
return '';
|
|
213
|
+
const subs = resolverDocs(subject, 'preference')
|
|
214
|
+
.map(renderSubSection)
|
|
215
|
+
.filter((s) => s !== '');
|
|
216
|
+
if (subs.length === 0)
|
|
217
|
+
return '';
|
|
218
|
+
return `## Preferences\n\n${subs.join('\n\n')}`;
|
|
219
|
+
}
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// 3. References block — `## References` (inside the <crtr-context> message).
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
/** The `## References` block embedded INSIDE the `<crtr-context>` session_start
|
|
224
|
+
* message (the bearings caller pushes the returned string into the block, or
|
|
225
|
+
* drops it when ''). Holds every eligible `kind: reference` resolver doc at its
|
|
226
|
+
* `system-prompt-visibility` (reference boot default is `none`, so only
|
|
227
|
+
* author-promoted references show) PLUS the node-local memory docs (any kind),
|
|
228
|
+
* each a `###` sub-section. Replaces the old `<memory>` block. Returns '' when
|
|
229
|
+
* nothing is eligible.
|
|
230
|
+
*
|
|
231
|
+
* DEFENSIVE: each doc is rendered in its own try/catch so a single malformed
|
|
232
|
+
* doc drops only itself (with a loud stderr warning naming the offending path),
|
|
233
|
+
* never silently swallowing the entire block (identity included). Per the CTO
|
|
234
|
+
* ruling, strictness lives at the COLLECTION layer (memory-resolver.ts); this
|
|
235
|
+
* catch is error ISOLATION at the render layer, not a fallback parser. */
|
|
236
|
+
export function renderReferencesBlock(nodeId) {
|
|
237
|
+
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
238
|
+
if (subject === null)
|
|
239
|
+
return '';
|
|
240
|
+
const docs = [...resolverDocs(subject, 'reference'), ...nodeLocalDocs(nodeId, subject)];
|
|
241
|
+
const subs = [];
|
|
242
|
+
for (const d of docs) {
|
|
243
|
+
try {
|
|
244
|
+
const rendered = renderSubSection(d);
|
|
245
|
+
if (rendered !== '')
|
|
246
|
+
subs.push(rendered);
|
|
247
|
+
}
|
|
248
|
+
catch (e) {
|
|
249
|
+
const msg = (e instanceof Error ? e.message : String(e)).split('\n')[0];
|
|
250
|
+
process.stderr.write(`[crtr substrate] renderReferencesBlock: skipping doc "${d.path}": ${msg}\n`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
if (subs.length === 0)
|
|
254
|
+
return '';
|
|
255
|
+
return `## References\n\n${subs.join('\n\n')}`;
|
|
256
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { Scope } from '../../types.js';
|
|
2
|
+
import type { MemoryDoc } from '../memory-resolver.js';
|
|
3
|
+
export declare const KINDS: readonly ["skill", "reference", "preference"];
|
|
4
|
+
export type DocKind = (typeof KINDS)[number];
|
|
5
|
+
/** Is `v` one of the three valid document kinds? */
|
|
6
|
+
export declare function isDocKind(v: unknown): v is DocKind;
|
|
7
|
+
export declare const RUNGS: readonly ["none", "name", "preview", "content"];
|
|
8
|
+
export type Rung = (typeof RUNGS)[number];
|
|
9
|
+
/** Ordinal of a rung on the ladder (none=0 … content=3). */
|
|
10
|
+
export declare function rungRank(r: Rung): number;
|
|
11
|
+
/** Does rung `r` disclose at least as much as `min`? — e.g.
|
|
12
|
+
* `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
|
|
13
|
+
export declare function rungAtLeast(r: Rung, min: Rung): boolean;
|
|
14
|
+
export declare const KIND_DEFAULT_RUNGS: Record<DocKind, {
|
|
15
|
+
systemPrompt: Rung;
|
|
16
|
+
fileRead: Rung;
|
|
17
|
+
}>;
|
|
18
|
+
/** A gate predicate tree, evaluated by predicate.ts (`evalCondition`) against
|
|
19
|
+
* the node-config subject. Typed loosely on purpose — the matcher engine owns
|
|
20
|
+
* validation; structurally it is a field→matcher map with optional
|
|
21
|
+
* `all`/`any`/`not` combinators (design §4). */
|
|
22
|
+
export type GatePredicate = Record<string, unknown>;
|
|
23
|
+
/** The frontmatter-derived schema of a substrate document, with kind-aware
|
|
24
|
+
* defaults applied. Required fields (`kind`/`when`/`why`) and optionals all
|
|
25
|
+
* resolved to concrete typed values. */
|
|
26
|
+
export interface SubstrateSchema {
|
|
27
|
+
/** Which of the three semantic kinds. */
|
|
28
|
+
kind: DocKind;
|
|
29
|
+
/** Routing condition — "When you are in a situation like X…" (design §4). */
|
|
30
|
+
when: string;
|
|
31
|
+
/** Payoff — "…because Z." Half of the generated preview line. */
|
|
32
|
+
why: string;
|
|
33
|
+
/** Human-facing abbreviation for `crtr memory list`. NEVER loaded into an
|
|
34
|
+
* agent's context (design §3). Empty string when absent. */
|
|
35
|
+
shortForm: string;
|
|
36
|
+
/** How much surfaces at boot (system prompt / autoloaded context). */
|
|
37
|
+
systemPromptVisibility: Rung;
|
|
38
|
+
/** How much surfaces on-read (when a related file is read). */
|
|
39
|
+
fileReadVisibility: Rung;
|
|
40
|
+
/** Optional eligibility predicate over the node's own config. Absent ⇒ always
|
|
41
|
+
* eligible. An empty `{}` is carried as-is and is inert (never matches) — see
|
|
42
|
+
* `gatePasses`. */
|
|
43
|
+
gate?: GatePredicate;
|
|
44
|
+
/** Optional glob list narrowing the on-read trigger to matching read files.
|
|
45
|
+
* Absent ⇒ positional trigger only. A single glob is normalized to a 1-list. */
|
|
46
|
+
appliesTo?: string[];
|
|
47
|
+
}
|
|
48
|
+
/** A fully-resolved substrate document: the parsed schema PLUS the resolver's
|
|
49
|
+
* path-derived identity and body. This single object flows through the whole
|
|
50
|
+
* pipeline (gate eval → boot/on-read render), so a renderer never re-parses. */
|
|
51
|
+
export interface SubstrateDoc extends SubstrateSchema {
|
|
52
|
+
/** Path-derived identity, e.g. `taste/document-substrate` (resolver-supplied). */
|
|
53
|
+
name: string;
|
|
54
|
+
/** The scope this doc resolved from. */
|
|
55
|
+
scope: Scope;
|
|
56
|
+
/** Absolute path to the source .md. */
|
|
57
|
+
path: string;
|
|
58
|
+
/** Document body, frontmatter stripped. */
|
|
59
|
+
body: string;
|
|
60
|
+
}
|
|
61
|
+
/** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
|
|
62
|
+
* resolver) into a typed schema with defaults applied. Returns `null` when the
|
|
63
|
+
* record is absent or carries no valid `kind` — i.e. it is not a substrate
|
|
64
|
+
* document and cannot be classified or defaulted. Tolerant of every other
|
|
65
|
+
* imperfection (missing `when`/`why` default to '', a bad rung falls back to
|
|
66
|
+
* the kind default), so a renderer mapping over many docs never throws. */
|
|
67
|
+
export declare function parseSubstrateFrontmatter(fm: Record<string, unknown> | null): SubstrateSchema | null;
|
|
68
|
+
/** Parse a resolved MemoryDoc into a fully-typed SubstrateDoc (schema + the
|
|
69
|
+
* resolver's name/scope/path/body). Returns `null` for a non-substrate doc
|
|
70
|
+
* (no valid `kind`), so callers can `docs.map(parseSubstrateDoc).filter(...)`. */
|
|
71
|
+
export declare function parseSubstrateDoc(doc: MemoryDoc): SubstrateDoc | null;
|
|
72
|
+
/** The generated `preview`-rung routing line (design §4), composed from the two
|
|
73
|
+
* required prose fields and the kind: `"{when}, read this {kind}. {why}."`.
|
|
74
|
+
* Both boot and on-read render render this identical line, so it lives once
|
|
75
|
+
* here to prevent drift. Light cleanup avoids doubled punctuation. */
|
|
76
|
+
export declare function previewLine(doc: Pick<SubstrateSchema, 'kind' | 'when' | 'why'>): string;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// schema.ts — the typed shape of a substrate document's frontmatter, the
|
|
2
|
+
// visibility ladder, the kind→default-rungs table, and the parse/validate
|
|
3
|
+
// function that turns a resolved MemoryDoc into a fully-typed SubstrateDoc with
|
|
4
|
+
// defaults applied. This is the keystone every downstream track (CLI verbs,
|
|
5
|
+
// boot render, on-read render, migrator) builds against — pure and side-effect
|
|
6
|
+
// free. See design-substrate.md §4 (schema) + §9 (defaults).
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Kinds — the three semantic kinds (design §3). `kind` is data, not a fork.
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
export const KINDS = ['skill', 'reference', 'preference'];
|
|
11
|
+
/** Is `v` one of the three valid document kinds? */
|
|
12
|
+
export function isDocKind(v) {
|
|
13
|
+
return typeof v === 'string' && KINDS.includes(v);
|
|
14
|
+
}
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// The 4-rung visibility ladder (design §4). A strict, monotone ordering where
|
|
17
|
+
// each rung is a superset of disclosure over the one before:
|
|
18
|
+
// none < name < preview < content
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
export const RUNGS = ['none', 'name', 'preview', 'content'];
|
|
21
|
+
/** Ordinal of a rung on the ladder (none=0 … content=3). */
|
|
22
|
+
export function rungRank(r) {
|
|
23
|
+
return RUNGS.indexOf(r);
|
|
24
|
+
}
|
|
25
|
+
/** Does rung `r` disclose at least as much as `min`? — e.g.
|
|
26
|
+
* `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
|
|
27
|
+
export function rungAtLeast(r, min) {
|
|
28
|
+
return rungRank(r) >= rungRank(min);
|
|
29
|
+
}
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// The kind→default-rungs table (design §9). When an author omits a visibility
|
|
32
|
+
// field, its default is a function of `kind` — chosen so the common case is
|
|
33
|
+
// correct with no thought and the migration is behavior-preserving for skills
|
|
34
|
+
// and preferences. One data row per kind, NOT a code fork.
|
|
35
|
+
//
|
|
36
|
+
// kind system-prompt-visibility file-read-visibility
|
|
37
|
+
// skill name none
|
|
38
|
+
// preference preview none
|
|
39
|
+
// reference none preview
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
export const KIND_DEFAULT_RUNGS = {
|
|
42
|
+
skill: { systemPrompt: 'name', fileRead: 'none' },
|
|
43
|
+
preference: { systemPrompt: 'preview', fileRead: 'none' },
|
|
44
|
+
reference: { systemPrompt: 'none', fileRead: 'preview' },
|
|
45
|
+
};
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// Parse / validate.
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
/** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
|
|
50
|
+
* resolver) into a typed schema with defaults applied. Returns `null` when the
|
|
51
|
+
* record is absent or carries no valid `kind` — i.e. it is not a substrate
|
|
52
|
+
* document and cannot be classified or defaulted. Tolerant of every other
|
|
53
|
+
* imperfection (missing `when`/`why` default to '', a bad rung falls back to
|
|
54
|
+
* the kind default), so a renderer mapping over many docs never throws. */
|
|
55
|
+
export function parseSubstrateFrontmatter(fm) {
|
|
56
|
+
if (fm === null)
|
|
57
|
+
return null;
|
|
58
|
+
if (!isDocKind(fm.kind))
|
|
59
|
+
return null;
|
|
60
|
+
const kind = fm.kind;
|
|
61
|
+
const defaults = KIND_DEFAULT_RUNGS[kind];
|
|
62
|
+
return {
|
|
63
|
+
kind,
|
|
64
|
+
when: strField(fm.when),
|
|
65
|
+
why: strField(fm.why),
|
|
66
|
+
shortForm: strField(fm['short-form']),
|
|
67
|
+
systemPromptVisibility: parseRung(fm['system-prompt-visibility'], defaults.systemPrompt),
|
|
68
|
+
fileReadVisibility: parseRung(fm['file-read-visibility'], defaults.fileRead),
|
|
69
|
+
gate: parseGate(fm.gate),
|
|
70
|
+
appliesTo: parseAppliesTo(fm['applies-to']),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/** Parse a resolved MemoryDoc into a fully-typed SubstrateDoc (schema + the
|
|
74
|
+
* resolver's name/scope/path/body). Returns `null` for a non-substrate doc
|
|
75
|
+
* (no valid `kind`), so callers can `docs.map(parseSubstrateDoc).filter(...)`. */
|
|
76
|
+
export function parseSubstrateDoc(doc) {
|
|
77
|
+
const schema = parseSubstrateFrontmatter(doc.frontmatter);
|
|
78
|
+
if (schema === null)
|
|
79
|
+
return null;
|
|
80
|
+
return { ...schema, name: doc.name, scope: doc.scope, path: doc.path, body: doc.body };
|
|
81
|
+
}
|
|
82
|
+
/** The generated `preview`-rung routing line (design §4), composed from the two
|
|
83
|
+
* required prose fields and the kind: `"{when}, read this {kind}. {why}."`.
|
|
84
|
+
* Both boot and on-read render render this identical line, so it lives once
|
|
85
|
+
* here to prevent drift. Light cleanup avoids doubled punctuation. */
|
|
86
|
+
export function previewLine(doc) {
|
|
87
|
+
const when = doc.when.trim().replace(/[.,]+$/, '');
|
|
88
|
+
const why = doc.why.trim().replace(/\.+$/, '');
|
|
89
|
+
return `${when}, read this ${doc.kind}. ${why}.`;
|
|
90
|
+
}
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Field coercion helpers (private).
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
function strField(v) {
|
|
95
|
+
return typeof v === 'string' ? v : '';
|
|
96
|
+
}
|
|
97
|
+
/** Resolve a visibility field to a ladder rung, falling back to the kind
|
|
98
|
+
* default when absent/invalid. */
|
|
99
|
+
function parseRung(v, fallback) {
|
|
100
|
+
return typeof v === 'string' && RUNGS.includes(v)
|
|
101
|
+
? v
|
|
102
|
+
: fallback;
|
|
103
|
+
}
|
|
104
|
+
/** A gate is engaged only when frontmatter carries a non-null, non-array object
|
|
105
|
+
* (the predicate vocabulary's field→matcher map). Anything else (absent, null,
|
|
106
|
+
* scalar, array) ⇒ no gate ⇒ always eligible — the safe default that never
|
|
107
|
+
* silently hides a doc. An empty `{}` IS carried (and is inert per design §4). */
|
|
108
|
+
function parseGate(v) {
|
|
109
|
+
return v !== null && typeof v === 'object' && !Array.isArray(v)
|
|
110
|
+
? v
|
|
111
|
+
: undefined;
|
|
112
|
+
}
|
|
113
|
+
/** Normalize `applies-to` to a non-empty glob list, or undefined (positional
|
|
114
|
+
* trigger only). Accepts a single string or an array of strings. */
|
|
115
|
+
function parseAppliesTo(v) {
|
|
116
|
+
if (typeof v === 'string') {
|
|
117
|
+
return v.trim() === '' ? undefined : [v];
|
|
118
|
+
}
|
|
119
|
+
if (Array.isArray(v)) {
|
|
120
|
+
const globs = v.filter((g) => typeof g === 'string' && g.trim() !== '');
|
|
121
|
+
return globs.length > 0 ? globs : undefined;
|
|
122
|
+
}
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MemoryDoc } from '../memory-resolver.js';
|
|
2
|
+
import type { SubstrateDoc } from './schema.js';
|
|
3
|
+
type ListAllMemoryDocsFn = () => MemoryDoc[];
|
|
4
|
+
type ParseSubstrateDocFn = (doc: MemoryDoc) => SubstrateDoc | null;
|
|
5
|
+
type ListAllPluginsFn = () => {
|
|
6
|
+
name: string;
|
|
7
|
+
manifest: {
|
|
8
|
+
description?: string;
|
|
9
|
+
};
|
|
10
|
+
}[];
|
|
11
|
+
type AssembleNodeSubjectFn = (id: string) => import('./subject.js').NodeConfigSubject | null;
|
|
12
|
+
/** Called by canvas-doc-substrate.ts on every `session_start`.
|
|
13
|
+
* Resets all cached values so the next access triggers a fresh scan. */
|
|
14
|
+
export declare function clearSessionCache(): void;
|
|
15
|
+
/** All memory docs (listAllMemoryDocs()), scanned once per session.
|
|
16
|
+
* `listFn` is injected by the caller to avoid circular imports at module init. */
|
|
17
|
+
export declare function cachedAllMemoryDocs(listFn: ListAllMemoryDocsFn): MemoryDoc[];
|
|
18
|
+
/** allMemoryDocs mapped through parseSubstrateDoc + null-filtered.
|
|
19
|
+
* Both render.ts and on-read.ts consume this. */
|
|
20
|
+
export declare function cachedSubstrateDocs(listFn: ListAllMemoryDocsFn, parseFn: ParseSubstrateDocFn): SubstrateDoc[];
|
|
21
|
+
/** listAllPlugins() result, cached per session. */
|
|
22
|
+
export declare function cachedAllPlugins(listFn: ListAllPluginsFn): {
|
|
23
|
+
name: string;
|
|
24
|
+
manifest: {
|
|
25
|
+
description?: string;
|
|
26
|
+
};
|
|
27
|
+
}[];
|
|
28
|
+
/** assembleNodeSubject(nodeId), cached per (session × nodeId). */
|
|
29
|
+
export declare function cachedNodeSubject(nodeId: string, assembleFn: AssembleNodeSubjectFn): import('./subject.js').NodeConfigSubject | null;
|
|
30
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
// session-cache.ts — a per-session parse cache for the document substrate.
|
|
2
|
+
//
|
|
3
|
+
// Both substrate hooks (boot render in render.ts, on-read in on-read.ts) re-walk
|
|
4
|
+
// and re-YAML-parse the full corpus on every invocation without memoization. At
|
|
5
|
+
// the two highest-frequency hook sites this is O(reads × corpus) and O(turns ×
|
|
6
|
+
// corpus) respectively. This module provides a single session-scoped cache that
|
|
7
|
+
// the hooks read from, so the corpus is scanned and parsed AT MOST ONCE per
|
|
8
|
+
// session turn/read burst.
|
|
9
|
+
//
|
|
10
|
+
// Lifecycle:
|
|
11
|
+
// • clearSessionCache() — called on every `session_start` by canvas-doc-
|
|
12
|
+
// substrate.ts. It does NOT need to be called before the first use: a
|
|
13
|
+
// cold cache (null) triggers a fresh scan on the next read.
|
|
14
|
+
// • All getters lazily populate on first call after a clear.
|
|
15
|
+
// • The cache is a module-level singleton (one JS module instance per pi
|
|
16
|
+
// process, and each pi process hosts exactly one canvas node session).
|
|
17
|
+
//
|
|
18
|
+
// Cached entries:
|
|
19
|
+
// • allMemoryDocs — the full listAllMemoryDocs() corpus (parsed MemoryDocs,
|
|
20
|
+
// already YAML-parsed by the resolver). Shared by both
|
|
21
|
+
// render.ts and on-read.ts so each session call site only
|
|
22
|
+
// pays the filesystem walk+parse ONCE.
|
|
23
|
+
// • substrateDocs — allMemoryDocs mapped through parseSubstrateDoc and
|
|
24
|
+
// null-filtered. Used by render.ts's resolverDocs and
|
|
25
|
+
// on-read.ts's appliesToCandidates.
|
|
26
|
+
// • allPlugins — listAllPlugins() result. renderSkillCatalog was calling
|
|
27
|
+
// listAllPlugins() twice per turn (once via listAllSkills,
|
|
28
|
+
// once directly for descriptions).
|
|
29
|
+
// • nodeSubjects — assembleNodeSubject() result keyed by nodeId. The three
|
|
30
|
+
// boot-render functions each call it; caching avoids the
|
|
31
|
+
// meta.json read + sqlite spine-walk happening 2-3x per turn.
|
|
32
|
+
const _cache = {
|
|
33
|
+
allMemoryDocs: null,
|
|
34
|
+
substrateDocs: null,
|
|
35
|
+
allPlugins: null,
|
|
36
|
+
nodeSubjects: new Map(),
|
|
37
|
+
};
|
|
38
|
+
/** Called by canvas-doc-substrate.ts on every `session_start`.
|
|
39
|
+
* Resets all cached values so the next access triggers a fresh scan. */
|
|
40
|
+
export function clearSessionCache() {
|
|
41
|
+
_cache.allMemoryDocs = null;
|
|
42
|
+
_cache.substrateDocs = null;
|
|
43
|
+
_cache.allPlugins = null;
|
|
44
|
+
_cache.nodeSubjects.clear();
|
|
45
|
+
}
|
|
46
|
+
/** All memory docs (listAllMemoryDocs()), scanned once per session.
|
|
47
|
+
* `listFn` is injected by the caller to avoid circular imports at module init. */
|
|
48
|
+
export function cachedAllMemoryDocs(listFn) {
|
|
49
|
+
if (_cache.allMemoryDocs === null) {
|
|
50
|
+
_cache.allMemoryDocs = listFn();
|
|
51
|
+
}
|
|
52
|
+
return _cache.allMemoryDocs;
|
|
53
|
+
}
|
|
54
|
+
/** allMemoryDocs mapped through parseSubstrateDoc + null-filtered.
|
|
55
|
+
* Both render.ts and on-read.ts consume this. */
|
|
56
|
+
export function cachedSubstrateDocs(listFn, parseFn) {
|
|
57
|
+
if (_cache.substrateDocs === null) {
|
|
58
|
+
_cache.substrateDocs = cachedAllMemoryDocs(listFn)
|
|
59
|
+
.map(parseFn)
|
|
60
|
+
.filter((d) => d !== null);
|
|
61
|
+
}
|
|
62
|
+
return _cache.substrateDocs;
|
|
63
|
+
}
|
|
64
|
+
/** listAllPlugins() result, cached per session. */
|
|
65
|
+
export function cachedAllPlugins(listFn) {
|
|
66
|
+
if (_cache.allPlugins === null) {
|
|
67
|
+
_cache.allPlugins = listFn();
|
|
68
|
+
}
|
|
69
|
+
return _cache.allPlugins;
|
|
70
|
+
}
|
|
71
|
+
/** assembleNodeSubject(nodeId), cached per (session × nodeId). */
|
|
72
|
+
export function cachedNodeSubject(nodeId, assembleFn) {
|
|
73
|
+
if (!_cache.nodeSubjects.has(nodeId)) {
|
|
74
|
+
_cache.nodeSubjects.set(nodeId, assembleFn(nodeId));
|
|
75
|
+
}
|
|
76
|
+
return _cache.nodeSubjects.get(nodeId) ?? null;
|
|
77
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type Mode, type Lifecycle } from '../canvas/index.js';
|
|
2
|
+
/** The gate input: a node's configuration as a structured object. Field names
|
|
3
|
+
* and shape are the predicate vocabulary's dotted paths (`orchestration.depth`,
|
|
4
|
+
* `hasManager`, …). There is intentionally NO `tags` field — NodeMeta has none. */
|
|
5
|
+
export interface NodeConfigSubject {
|
|
6
|
+
/** The node's role, free-form (developer | explore | design | …). NodeMeta.kind. */
|
|
7
|
+
kind: string;
|
|
8
|
+
/** base (hands-on worker) | orchestrator (delegating manager). NodeMeta.mode. */
|
|
9
|
+
mode: Mode;
|
|
10
|
+
/** terminal (worker) | resident (manager). NodeMeta.lifecycle. */
|
|
11
|
+
lifecycle: Lifecycle;
|
|
12
|
+
/** Spine position: does this node have a manager? (= parent !== null). */
|
|
13
|
+
hasManager: boolean;
|
|
14
|
+
/** The node's working directory on disk. NodeMeta.cwd. */
|
|
15
|
+
cwd: string;
|
|
16
|
+
/** DERIVED: the scope scope.ts resolves for `cwd` — `project` when a
|
|
17
|
+
* nearest-ancestor `.crouter/` exists at/above cwd, else `user`. */
|
|
18
|
+
scope: 'user' | 'project';
|
|
19
|
+
/** DERIVED orchestration metrics. */
|
|
20
|
+
orchestration: {
|
|
21
|
+
/** Spine distance (hops) from this node up to its root orchestrator. The
|
|
22
|
+
* root itself is 0; a direct child of the root is 1; etc. */
|
|
23
|
+
depth: number;
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
/** The scope a cwd resolves into: `project` when scope.ts finds a
|
|
27
|
+
* nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
|
|
28
|
+
* resolver (`projectScopeRoot`). NOTE: `findProjectScopeRoot` is process-cached
|
|
29
|
+
* on first call, so for a node assembling its OWN subject (cwd === process.cwd)
|
|
30
|
+
* this is exact; resolving an arbitrary unrelated cwd in the same process would
|
|
31
|
+
* return the cached root — fine for per-node subject assembly. */
|
|
32
|
+
export declare function scopeForCwd(cwd: string): 'user' | 'project';
|
|
33
|
+
/** Spine distance from `nodeId` up to its root orchestrator: count the hops
|
|
34
|
+
* walking the `parent` chain (canvas-db rows) until a row with no parent (the
|
|
35
|
+
* root). The root is depth 0. Cycle-guarded (parents must not cycle, but never
|
|
36
|
+
* loop forever) — mirrors `rootOfSpine` in runtime/placement.ts. */
|
|
37
|
+
export declare function spineDepth(nodeId: string): number;
|
|
38
|
+
/** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the two
|
|
39
|
+
* derived fields. Returns `null` when the node has no canvas-db row (an
|
|
40
|
+
* unknown id). Pure aside from the canvas-db reads. */
|
|
41
|
+
export declare function assembleNodeSubject(nodeId: string): NodeConfigSubject | null;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// subject.ts — the node-config SUBJECT a document's `gate` predicate is
|
|
2
|
+
// evaluated against. Assembled per node from its canvas-db `NodeMeta` plus two
|
|
3
|
+
// DERIVED fields: `scope` (the scope.ts resolution for the node's cwd) and
|
|
4
|
+
// `orchestration.depth` (the node's spine distance to its root orchestrator).
|
|
5
|
+
// See design-substrate.md §4 + plan-substrate.md §2. Reads canvas-db; otherwise
|
|
6
|
+
// a thin, pure assembler.
|
|
7
|
+
import { getNode, getRow } from '../canvas/index.js';
|
|
8
|
+
import { projectScopeRoot } from '../scope.js';
|
|
9
|
+
/** The scope a cwd resolves into: `project` when scope.ts finds a
|
|
10
|
+
* nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
|
|
11
|
+
* resolver (`projectScopeRoot`). NOTE: `findProjectScopeRoot` is process-cached
|
|
12
|
+
* on first call, so for a node assembling its OWN subject (cwd === process.cwd)
|
|
13
|
+
* this is exact; resolving an arbitrary unrelated cwd in the same process would
|
|
14
|
+
* return the cached root — fine for per-node subject assembly. */
|
|
15
|
+
export function scopeForCwd(cwd) {
|
|
16
|
+
return projectScopeRoot(cwd) !== null ? 'project' : 'user';
|
|
17
|
+
}
|
|
18
|
+
/** Spine distance from `nodeId` up to its root orchestrator: count the hops
|
|
19
|
+
* walking the `parent` chain (canvas-db rows) until a row with no parent (the
|
|
20
|
+
* root). The root is depth 0. Cycle-guarded (parents must not cycle, but never
|
|
21
|
+
* loop forever) — mirrors `rootOfSpine` in runtime/placement.ts. */
|
|
22
|
+
export function spineDepth(nodeId) {
|
|
23
|
+
let cur = nodeId;
|
|
24
|
+
const seen = new Set();
|
|
25
|
+
let hops = 0;
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (seen.has(cur))
|
|
28
|
+
break;
|
|
29
|
+
seen.add(cur);
|
|
30
|
+
const row = getRow(cur);
|
|
31
|
+
if (row === null || row.parent == null)
|
|
32
|
+
break;
|
|
33
|
+
cur = row.parent;
|
|
34
|
+
hops++;
|
|
35
|
+
}
|
|
36
|
+
return hops;
|
|
37
|
+
}
|
|
38
|
+
/** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the two
|
|
39
|
+
* derived fields. Returns `null` when the node has no canvas-db row (an
|
|
40
|
+
* unknown id). Pure aside from the canvas-db reads. */
|
|
41
|
+
export function assembleNodeSubject(nodeId) {
|
|
42
|
+
const meta = getNode(nodeId);
|
|
43
|
+
if (meta === null)
|
|
44
|
+
return null;
|
|
45
|
+
return {
|
|
46
|
+
kind: meta.kind,
|
|
47
|
+
mode: meta.mode,
|
|
48
|
+
lifecycle: meta.lifecycle,
|
|
49
|
+
hasManager: (meta.parent ?? null) !== null,
|
|
50
|
+
cwd: meta.cwd,
|
|
51
|
+
scope: scopeForCwd(meta.cwd),
|
|
52
|
+
orchestration: { depth: spineDepth(nodeId) },
|
|
53
|
+
};
|
|
54
|
+
}
|