@crouton-kit/crouter 0.3.36 → 0.3.38
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 +3 -6
- package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +8 -1
- package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
- package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
- package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
- package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
- package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
- package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
- package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
- package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
- package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
- package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
- package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
- package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
- package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
- package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
- package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
- package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
- package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
- package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
- package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
- package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
- package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
- package/dist/builtin-memory/internal/INDEX.md +1 -1
- package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
- package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
- package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
- package/dist/builtin-views/canvas/core.mjs +3 -2
- package/dist/builtin-views/prompt-review/core.mjs +6 -134
- package/dist/builtin-views/settings/core.mjs +26 -94
- package/dist/builtin-views/settings/text.mjs +0 -5
- package/dist/builtin-views/settings/tui.mjs +1 -7
- package/dist/builtin-views/settings/web.jsx +3 -49
- package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
- package/dist/clients/attach/attach-cmd.js +429 -432
- package/dist/clients/web/web-cmd.js +7 -7
- package/dist/commands/__tests__/human.test.js +1 -15
- package/dist/commands/attention.js +9 -6
- package/dist/commands/canvas-history/search.js +2 -2
- package/dist/commands/canvas-history/show.js +3 -3
- package/dist/commands/canvas-history.js +1 -1
- package/dist/commands/canvas-issue.js +1 -0
- package/dist/commands/canvas-snapshot.js +1 -0
- package/dist/commands/canvas.js +10 -11
- package/dist/commands/chord.js +1 -0
- package/dist/commands/human/prompts.js +0 -1
- package/dist/commands/human/queue.js +1 -0
- package/dist/commands/memory/read.js +3 -3
- package/dist/commands/memory/shared.d.ts +13 -8
- package/dist/commands/memory/shared.js +22 -8
- package/dist/commands/memory/write.js +5 -3
- package/dist/commands/node-snapshot.js +1 -0
- package/dist/commands/node.js +52 -22
- package/dist/commands/profile/add-project.d.ts +1 -0
- package/dist/commands/profile/add-project.js +42 -0
- package/dist/commands/profile/delete.d.ts +1 -0
- package/dist/commands/profile/delete.js +39 -0
- package/dist/commands/profile/list.d.ts +1 -0
- package/dist/commands/profile/list.js +35 -0
- package/dist/commands/profile/new.d.ts +1 -0
- package/dist/commands/profile/new.js +50 -0
- package/dist/commands/profile/remove-project.d.ts +1 -0
- package/dist/commands/profile/remove-project.js +42 -0
- package/dist/commands/profile/rename.d.ts +1 -0
- package/dist/commands/profile/rename.js +42 -0
- package/dist/commands/profile/show.d.ts +1 -0
- package/dist/commands/profile/show.js +50 -0
- package/dist/commands/profile.d.ts +2 -0
- package/dist/commands/profile.js +33 -0
- package/dist/commands/push.d.ts +0 -1
- package/dist/commands/push.js +2 -228
- package/dist/commands/revive.js +2 -2
- package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
- package/dist/commands/surface.js +3 -2
- package/dist/commands/sys/config.js +6 -21
- package/dist/commands/sys/daemon.d.ts +2 -0
- package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
- package/dist/commands/sys/doctor.js +1 -0
- package/dist/commands/sys/prompt-review.js +90 -17
- package/dist/commands/sys.js +4 -3
- package/dist/core/__tests__/daemon-boot.test.js +1 -1
- package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
- package/dist/core/__tests__/daemon-wedge.test.js +87 -0
- package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
- package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
- package/dist/core/__tests__/history-inbox.test.js +105 -0
- package/dist/core/__tests__/kickoff.test.js +3 -3
- package/dist/core/__tests__/revive.test.js +39 -1
- package/dist/core/canvas/canvas.js +10 -9
- package/dist/core/canvas/db.js +11 -0
- package/dist/core/canvas/history.d.ts +1 -1
- package/dist/core/canvas/history.js +91 -3
- package/dist/core/canvas/paths.d.ts +6 -0
- package/dist/core/canvas/paths.js +9 -0
- package/dist/core/canvas/status-glyph.js +1 -0
- package/dist/core/canvas/types.d.ts +10 -0
- package/dist/core/config.d.ts +44 -1
- package/dist/core/config.js +155 -12
- package/dist/core/fault-classifier.d.ts +2 -2
- package/dist/core/feed/inbox.d.ts +7 -12
- package/dist/core/feed/inbox.js +45 -29
- package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
- package/dist/core/memory-resolver.d.ts +26 -15
- package/dist/core/memory-resolver.js +158 -40
- package/dist/core/preview-registry.js +14 -112
- package/dist/core/profiles/manifest.d.ts +33 -0
- package/dist/core/profiles/manifest.js +332 -0
- package/dist/core/profiles/select.d.ts +11 -0
- package/dist/core/profiles/select.js +115 -0
- package/dist/core/render.js +1 -1
- package/dist/core/runtime/bearings.d.ts +9 -0
- package/dist/core/runtime/bearings.js +31 -0
- package/dist/core/runtime/busy.d.ts +15 -0
- package/dist/core/runtime/busy.js +32 -1
- package/dist/core/runtime/close.d.ts +10 -0
- package/dist/core/runtime/close.js +18 -13
- package/dist/core/runtime/fault-recovery.js +4 -0
- package/dist/core/runtime/front-door.js +9 -2
- package/dist/core/runtime/kickoff.js +5 -7
- package/dist/core/runtime/launch.d.ts +25 -7
- package/dist/core/runtime/launch.js +42 -55
- package/dist/core/runtime/nodes.d.ts +7 -0
- package/dist/core/runtime/nodes.js +6 -0
- package/dist/core/runtime/persona.js +18 -32
- package/dist/core/runtime/placement.js +1 -1
- package/dist/core/runtime/revive.js +9 -1
- package/dist/core/runtime/spawn.d.ts +13 -0
- package/dist/core/runtime/spawn.js +25 -0
- package/dist/core/runtime/tmux.d.ts +2 -2
- package/dist/core/runtime/tmux.js +2 -2
- package/dist/core/scope.d.ts +14 -7
- package/dist/core/scope.js +76 -20
- package/dist/core/substrate/index.d.ts +3 -3
- package/dist/core/substrate/index.js +5 -3
- package/dist/core/substrate/on-read.js +3 -2
- package/dist/core/substrate/render.d.ts +16 -11
- package/dist/core/substrate/render.js +249 -62
- package/dist/core/substrate/schema.d.ts +11 -3
- package/dist/core/substrate/schema.js +25 -0
- package/dist/core/substrate/subject.d.ts +15 -2
- package/dist/core/substrate/subject.js +24 -2
- package/dist/daemon/crtrd-cli.js +1 -1
- package/dist/daemon/crtrd.d.ts +24 -0
- package/dist/daemon/crtrd.js +213 -5
- package/dist/daemon/manage.js +2 -2
- package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
- package/dist/pi-extensions/canvas-nav.js +2 -2
- package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
- package/dist/pi-extensions/canvas-stophook.js +59 -2
- package/dist/types.d.ts +57 -1
- package/dist/types.js +83 -1
- package/dist/web-client/assets/{index-IAJVtuVe.js → index-CbO8L0mN.js} +4 -4
- package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
- package/dist/web-client/index.html +2 -2
- package/package.json +2 -2
- package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
- package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
- package/dist/builtin-memory/crouter-development/personas.md +0 -112
- package/dist/builtin-personas/spine/no-manager.md +0 -2
- package/dist/commands/daemon.d.ts +0 -2
- package/dist/core/personas/index.d.ts +0 -13
- package/dist/core/personas/index.js +0 -11
- package/dist/core/personas/loader.d.ts +0 -159
- package/dist/core/personas/loader.js +0 -327
- package/dist/core/personas/resolve.d.ts +0 -59
- package/dist/core/personas/resolve.js +0 -376
- package/dist/web-client/assets/index-DUThOUzU.css +0 -2
- /package/dist/commands/{canvas-tmux-spread.d.ts → surface-tmux-spread.d.ts} +0 -0
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
// render.ts — the pure boot-render functions for the document substrate.
|
|
2
2
|
//
|
|
3
3
|
// Two boot targets, one shape per kind: each kind renders as ONE wrapped block —
|
|
4
|
-
// intro prose →
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
4
|
+
// intro prose → GROUPED content → an update directive. The render is grouped
|
|
5
|
+
// by visibility rung, not interleaved: `content`-rung docs render FIRST as
|
|
6
|
+
// clean prose bodies (verbatim, no tree chrome, no name label — this is what
|
|
7
|
+
// makes content-rung the persona-prose mechanism), in general→specific order;
|
|
8
|
+
// then the `preview`+`name`(+hidden `none`) docs render as ONE catalog tree,
|
|
9
|
+
// each at its own rung (preview → a `# read when:` routing line; name → a bare
|
|
10
|
+
// entry); a `none`-rung doc leaks only into its dir's `[+N more]` count, never
|
|
11
|
+
// a name — EXCEPT a node-local doc, whose contract is "node-local rides into
|
|
12
|
+
// knowledge" unconditionally: renderKnowledgeBlock floors a rung-none
|
|
13
|
+
// node-local doc up to `name` so it still shows its bare name rather than
|
|
14
|
+
// collapsing into a hidden count. Only gate evaluation removes a node-local
|
|
15
|
+
// doc outright.
|
|
8
16
|
//
|
|
9
17
|
// • the SYSTEM-PROMPT half — `<memory kind="preference">`
|
|
10
18
|
// (renderPreferencesSection) — the always-present memory-usage guidance plus
|
|
@@ -17,11 +25,22 @@
|
|
|
17
25
|
// message as a SIBLING of `<crtr-bearings>` (never nested under the context
|
|
18
26
|
// dir — the catalog spans all scopes and belongs to no one directory).
|
|
19
27
|
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
28
|
+
// The pipeline is explicitly TWO-STEP and must never run in the other order:
|
|
29
|
+
// 1. SELECT WINNERS — MemoryDoc → parseSubstrateDoc → (null-filter
|
|
30
|
+
// non-substrate) → ceiling-capped effective rung → gatePasses →
|
|
31
|
+
// first-wins DEDUP by name over the resolver's precedence order (nearest
|
|
32
|
+
// project > user > builtin). Node-local docs follow the same gate rule,
|
|
33
|
+
// but the knowledge-block render floors a rung-none node-local doc's
|
|
34
|
+
// EFFECTIVE rung to `name` — the one rung-floor exemption, scoped to the
|
|
35
|
+
// node-local store only.
|
|
36
|
+
// 2. RENDER IN DISPLAY ORDER — split winners into content vs. the rest;
|
|
37
|
+
// order content general→specific (builtin, user, project outermost→
|
|
38
|
+
// nearest, node-local last) then by tree position then filename; place
|
|
39
|
+
// the rest into the catalog tree (position-ordered, as before).
|
|
40
|
+
// Sorting into display order BEFORE step 1's dedup would let a
|
|
41
|
+
// later-in-display-order builtin doc's array position mask a project/user
|
|
42
|
+
// override — dedup precedence and display order are deliberately different
|
|
43
|
+
// orderings and must not be conflated.
|
|
25
44
|
//
|
|
26
45
|
// Pure + defensive: reads the resolver, canvas-db (subject assembly), and the
|
|
27
46
|
// node-local memory dir; performs no writes and no side effects. A single
|
|
@@ -29,28 +48,47 @@
|
|
|
29
48
|
// (parseSubstrateDoc returns null for a non-substrate doc; per-file node-local
|
|
30
49
|
// loads are wrapped), and tree construction is pure string work.
|
|
31
50
|
import { relative, sep } from 'node:path';
|
|
32
|
-
import { listAllMemoryDocs } from '../memory-resolver.js';
|
|
51
|
+
import { listAllMemoryDocs, resolveMemoryDoc } from '../memory-resolver.js';
|
|
52
|
+
import { subKindsAvailableTo } from '../config.js';
|
|
33
53
|
import { parseFrontmatterGeneric } from '../frontmatter.js';
|
|
34
54
|
import { pathExists, readText, walkFiles } from '../fs-utils.js';
|
|
35
55
|
import { memoryDir } from '../runtime/memory.js';
|
|
36
|
-
import {
|
|
56
|
+
import { projectScopeRoots } from '../scope.js';
|
|
57
|
+
import { assembleNodeSubject, buildCeilingIndex, effectiveRung, gatePasses, indexDirOf, isIndexName, normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, rungRank, } from './index.js';
|
|
37
58
|
import { cachedNodeSubject, cachedSubstrateDocs } from './session-cache.js';
|
|
38
59
|
// ---------------------------------------------------------------------------
|
|
39
|
-
//
|
|
60
|
+
// Step 1 — winner selection (ceiling + gate + first-wins dedup).
|
|
40
61
|
// ---------------------------------------------------------------------------
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
62
|
+
/** First-wins dedup by (already-normalized) name, preserving the input's
|
|
63
|
+
* precedence order. The array's ORDER is the precedence signal — callers must
|
|
64
|
+
* pass docs already ordered highest-precedence-first; this function never
|
|
65
|
+
* reorders, only drops later duplicates. */
|
|
66
|
+
function dedupeFirstWins(docs) {
|
|
67
|
+
const seen = new Set();
|
|
68
|
+
const out = [];
|
|
69
|
+
for (const d of docs) {
|
|
70
|
+
if (seen.has(d.name))
|
|
71
|
+
continue;
|
|
72
|
+
seen.add(d.name);
|
|
73
|
+
out.push(d);
|
|
74
|
+
}
|
|
75
|
+
return out;
|
|
76
|
+
}
|
|
77
|
+
/** STEP 1 — the resolver-provided WINNER docs of one `kind`, eligible at boot
|
|
78
|
+
* for `subject`: parsed (non-substrate docs null-filtered), ceiling-capped,
|
|
79
|
+
* gate-passed, then first-wins DEDUPED by name over the resolver's precedence
|
|
80
|
+
* order (nearest project > user > builtin) — at the EFFECTIVE system-prompt
|
|
81
|
+
* rung, INCLUDING `none`-rung docs (the tree counts them into `[+N more]`).
|
|
82
|
+
* This is winner selection ONLY; display ordering is a separate, later step
|
|
83
|
+
* (see renderGrouped) so a builtin doc can never mask a project/user override
|
|
84
|
+
* by virtue of sorting first. Uses the per-session cache so the full corpus
|
|
85
|
+
* is scanned + parsed at most once per session across the boot-render calls.
|
|
48
86
|
*
|
|
49
87
|
* Ceilings are computed over the WHOLE corpus (cross-kind) BEFORE the kind
|
|
50
88
|
* filter, and the effective rung is written back into systemPromptVisibility —
|
|
51
89
|
* but the doc's NAME is left intact (the tree needs the real `taste/INDEX` path
|
|
52
90
|
* to place an `INDEX.md` child; renaming INDEX docs to their dir would lose it). */
|
|
53
|
-
function
|
|
91
|
+
function selectWinners(subject, kind) {
|
|
54
92
|
let docs;
|
|
55
93
|
try {
|
|
56
94
|
docs = cachedSubstrateDocs(listAllMemoryDocs, parseSubstrateDoc);
|
|
@@ -59,17 +97,95 @@ function effectiveDocs(subject, kind) {
|
|
|
59
97
|
return [];
|
|
60
98
|
}
|
|
61
99
|
const ceil = buildCeilingIndex(docs);
|
|
62
|
-
|
|
100
|
+
const eligible = docs
|
|
63
101
|
.map((d) => {
|
|
64
102
|
const rung = effectiveRung(d, ceil, 'systemPromptVisibility');
|
|
65
103
|
return rung === d.systemPromptVisibility ? d : { ...d, systemPromptVisibility: rung };
|
|
66
104
|
})
|
|
67
105
|
.filter((d) => d.kind === kind)
|
|
68
|
-
.filter((d) => gatePasses(d, subject))
|
|
69
|
-
|
|
106
|
+
.filter((d) => gatePasses(d, subject));
|
|
107
|
+
return dedupeFirstWins(eligible);
|
|
70
108
|
}
|
|
71
|
-
|
|
72
|
-
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
// Step 2 — display ordering + grouped render.
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
/** Content-prose ordering rank: general → specific. builtin=0, user=1,
|
|
113
|
+
* profile=2, project ranks start at 3 and climb by root nearness — the
|
|
114
|
+
* OUTERMOST ancestor `.crouter/` root ranks lowest (renders first), the
|
|
115
|
+
* NEAREST root ranks highest (renders last) — the mirror of the resolver's
|
|
116
|
+
* nearest-first dedup precedence. `isNodeLocal` docs rank last of all (the
|
|
117
|
+
* most specific, this-node-only store). This is display order ONLY — the
|
|
118
|
+
* resolver's REPLACEMENT precedence (first-wins dedup, step 1) is the
|
|
119
|
+
* opposite direction for project/profile: project stack > profile > user >
|
|
120
|
+
* builtin, nearest project strongest; the two orderings are deliberately
|
|
121
|
+
* different (see the module comment). */
|
|
122
|
+
function scopeGeneralityRank(doc, isNodeLocal) {
|
|
123
|
+
if (isNodeLocal)
|
|
124
|
+
return 1000;
|
|
125
|
+
if (doc.scope === 'builtin')
|
|
126
|
+
return 0;
|
|
127
|
+
if (doc.scope === 'user')
|
|
128
|
+
return 1;
|
|
129
|
+
if (doc.scope === 'profile')
|
|
130
|
+
return 2;
|
|
131
|
+
const roots = projectScopeRoots(); // nearest-first
|
|
132
|
+
if (roots.length === 0)
|
|
133
|
+
return 3;
|
|
134
|
+
const nearFirstIdx = roots.findIndex((root) => doc.path.startsWith(root + sep));
|
|
135
|
+
const idx = nearFirstIdx === -1 ? 0 : nearFirstIdx;
|
|
136
|
+
return 3 + (roots.length - 1 - idx); // outermost (largest idx) -> smallest rank
|
|
137
|
+
}
|
|
138
|
+
/** The doc's RAW physical path segments (numeric prefix intact), recovered
|
|
139
|
+
* from its absolute `path` using its (already-normalized) `name`'s segment
|
|
140
|
+
* COUNT — normalization strips a prefix from a segment but never merges or
|
|
141
|
+
* drops a segment, so the last N components of `path` are always the doc's
|
|
142
|
+
* physical segments. This is what lets `tree position` sort respect an
|
|
143
|
+
* author's `NN-` ordering pin even though the pin is invisible in `name`. */
|
|
144
|
+
function physicalSegments(doc) {
|
|
145
|
+
const n = doc.name.split('/').length;
|
|
146
|
+
const raw = doc.path.split(sep).slice(-n);
|
|
147
|
+
if (raw.length > 0)
|
|
148
|
+
raw[raw.length - 1] = raw[raw.length - 1].replace(/\.md$/i, '');
|
|
149
|
+
return raw;
|
|
150
|
+
}
|
|
151
|
+
/** "Tree position" comparator for content-prose ordering within a store:
|
|
152
|
+
* compares two docs' RAW physical segments element-by-element (a shallower
|
|
153
|
+
* path sorts before a path nested under it at the first differing segment). */
|
|
154
|
+
function compareTreePosition(a, b) {
|
|
155
|
+
const as = physicalSegments(a);
|
|
156
|
+
const bs = physicalSegments(b);
|
|
157
|
+
const len = Math.min(as.length, bs.length);
|
|
158
|
+
for (let i = 0; i < len; i++) {
|
|
159
|
+
const c = as[i].localeCompare(bs[i]);
|
|
160
|
+
if (c !== 0)
|
|
161
|
+
return c;
|
|
162
|
+
}
|
|
163
|
+
return as.length - bs.length;
|
|
164
|
+
}
|
|
165
|
+
/** STEP 2 — split deduped `winners` by rung and render each group in its
|
|
166
|
+
* display order: `content` docs first, as clean prose (general→specific,
|
|
167
|
+
* then tree position, then filename — no tree chrome, no name label); every
|
|
168
|
+
* other winner (preview/name/hidden-none) folds into ONE catalog tree exactly
|
|
169
|
+
* as before. `nodeLocalSet` marks which winners came from the node-local
|
|
170
|
+
* store (ranked most-specific in content ordering); omit it for a pure
|
|
171
|
+
* resolver render. */
|
|
172
|
+
function renderGrouped(winners, rootLabel, nodeLocalSet = new Set()) {
|
|
173
|
+
const content = [];
|
|
174
|
+
const rest = [];
|
|
175
|
+
for (const d of winners) {
|
|
176
|
+
if (d.systemPromptVisibility === 'content')
|
|
177
|
+
content.push(d);
|
|
178
|
+
else
|
|
179
|
+
rest.push(d);
|
|
180
|
+
}
|
|
181
|
+
content.sort((a, b) => scopeGeneralityRank(a, nodeLocalSet.has(a)) - scopeGeneralityRank(b, nodeLocalSet.has(b)) ||
|
|
182
|
+
compareTreePosition(a, b) ||
|
|
183
|
+
a.name.localeCompare(b.name));
|
|
184
|
+
const contentProse = content
|
|
185
|
+
.map((d) => d.body.trim())
|
|
186
|
+
.filter((b) => b !== '')
|
|
187
|
+
.join('\n\n');
|
|
188
|
+
return { contentProse, tree: buildTree(rest, rootLabel), contentDocs: content };
|
|
73
189
|
}
|
|
74
190
|
/** The node-local memory docs eligible at boot. Node-local memory lives in the
|
|
75
191
|
* canvas home (`nodes/<id>/context/memory/`) and is OUTSIDE the scope resolver,
|
|
@@ -79,18 +195,21 @@ function scopeRank(scope) {
|
|
|
79
195
|
* Returned across ALL kinds — node-local is the catch-all this-node store and
|
|
80
196
|
* rides into the knowledge block.
|
|
81
197
|
*
|
|
82
|
-
*
|
|
83
|
-
* is
|
|
84
|
-
* node-local
|
|
85
|
-
*
|
|
86
|
-
*
|
|
198
|
+
* Node-local docs follow the same GATE rule as every other store — gate
|
|
199
|
+
* evaluation is the only thing that removes a doc here. Rung is different:
|
|
200
|
+
* node-local's contract is "node-local rides into knowledge" unconditionally,
|
|
201
|
+
* so a `none`-rung node-local doc is retained AT its parsed rung by this
|
|
202
|
+
* function (never rewritten here) — it is the caller (renderKnowledgeBlock)
|
|
203
|
+
* that floors a rung-none node-local doc up to `name` so it still shows its
|
|
204
|
+
* bare name instead of collapsing into a `[+N more]` count. */
|
|
87
205
|
function nodeLocalDocs(nodeId, subject) {
|
|
88
206
|
const dir = memoryDir(nodeId);
|
|
89
207
|
if (!pathExists(dir))
|
|
90
208
|
return [];
|
|
91
209
|
const out = [];
|
|
92
210
|
for (const file of walkFiles(dir, (n) => n.endsWith('.md'))) {
|
|
93
|
-
const
|
|
211
|
+
const raw = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
|
|
212
|
+
const name = normalizeDocName(raw);
|
|
94
213
|
if (!name)
|
|
95
214
|
continue;
|
|
96
215
|
try {
|
|
@@ -108,7 +227,6 @@ function nodeLocalDocs(nodeId, subject) {
|
|
|
108
227
|
continue;
|
|
109
228
|
}
|
|
110
229
|
}
|
|
111
|
-
// Gate-filter only: rung is NOT filtered (see comment above).
|
|
112
230
|
return out.filter((d) => gatePasses(d, subject));
|
|
113
231
|
}
|
|
114
232
|
function newDir(path) {
|
|
@@ -240,14 +358,15 @@ function renderChildren(dir, childPrefix, lines) {
|
|
|
240
358
|
}
|
|
241
359
|
});
|
|
242
360
|
}
|
|
243
|
-
/** Build the
|
|
244
|
-
* Returns '' when there are no docs at all (the
|
|
361
|
+
/** Build the catalog tree for a set of already-deduped, non-content winners,
|
|
362
|
+
* headed by `rootLabel`. Returns '' when there are no docs at all (the
|
|
363
|
+
* empty-tree contract).
|
|
245
364
|
*
|
|
246
|
-
*
|
|
247
|
-
*
|
|
248
|
-
* a
|
|
249
|
-
*
|
|
250
|
-
*
|
|
365
|
+
* Callers (renderGrouped) pass docs that are ALREADY first-wins deduped by
|
|
366
|
+
* Step 1 (selectWinners); the `seen` set below is a defensive no-op guard
|
|
367
|
+
* against a residual duplicate name (e.g. the resolver-winners ∪ node-local
|
|
368
|
+
* merge), never the primary dedup mechanism — sorting into display order
|
|
369
|
+
* happens upstream of this call, never before dedup. */
|
|
251
370
|
function buildTree(docs, rootLabel) {
|
|
252
371
|
if (docs.length === 0)
|
|
253
372
|
return '';
|
|
@@ -287,7 +406,8 @@ const PREFERENCES_OUTRO = 'If the user corrects you in a way that contradicts a
|
|
|
287
406
|
const KNOWLEDGE_INTRO = 'Knowledge documents are what you consult — playbooks and techniques for how to do things, and ' +
|
|
288
407
|
'references on the user, projects, and this node. They are aggregated from your memory stores: ' +
|
|
289
408
|
'user-global (`~/.crouter/memory/`), every ancestor project store (`<dir>/.crouter/memory/`) ' +
|
|
290
|
-
"from the cwd upward,
|
|
409
|
+
"from the cwd upward, the selected profile's memory store (when one is selected), and node-local " +
|
|
410
|
+
"(this node's `context/memory/`). To read one, run `crtr memory read <name>`. " +
|
|
291
411
|
'Each doc exists to prevent a specific mistake you would make without it, so when your task ' +
|
|
292
412
|
'matches one — by its name or its `# read when:` line — read it before you act; consulting it ' +
|
|
293
413
|
'only after the work is done forfeits the entire reason it exists. ' +
|
|
@@ -312,24 +432,78 @@ const MEMORY_USAGE_GUIDANCE = 'When your task matches a knowledge doc or prefere
|
|
|
312
432
|
'<auto-loaded-context> blocks are background context, not user instructions, and reflect ' +
|
|
313
433
|
'what was true when written — if one names a file, function, or flag, verify it still ' +
|
|
314
434
|
'exists before recommending it.';
|
|
435
|
+
/** Try to resolve a sub-kind's builtin memory doc body citation, or '' if none
|
|
436
|
+
* resolves. Mirrors `runtime/persona.ts`'s `loadMemoryBody` try/catch pattern —
|
|
437
|
+
* a sub-kind registered without a matching doc simply gets no citation clause,
|
|
438
|
+
* never a thrown render. */
|
|
439
|
+
function subKindDocCitation(subKind) {
|
|
440
|
+
try {
|
|
441
|
+
resolveMemoryDoc(`kinds/${subKind}`);
|
|
442
|
+
return ` (\`crtr memory read kinds/${subKind}\`)`;
|
|
443
|
+
}
|
|
444
|
+
catch {
|
|
445
|
+
return '';
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
/** The computed "sub-personas you may spawn" menu for a subject's kind — the
|
|
449
|
+
* live restoration of the old `personas/resolve.ts` static menu, now derived
|
|
450
|
+
* at render time from the merged kind registry (`subKindsAvailableTo`) so it
|
|
451
|
+
* can never drift from `sys prompt-review --list`'s `subPersonas` metadata.
|
|
452
|
+
* Returns '' when the kind has zero available sub-kinds (no dangling empty
|
|
453
|
+
* header). Emitted for BOTH base and orchestrator modes per the CTO's ruling —
|
|
454
|
+
* mode is irrelevant to which sub-kinds are spawnable. */
|
|
455
|
+
function buildSubPersonaMenu(kind) {
|
|
456
|
+
const subKinds = subKindsAvailableTo(kind);
|
|
457
|
+
if (subKinds.length === 0)
|
|
458
|
+
return '';
|
|
459
|
+
const lines = subKinds.map((sub) => {
|
|
460
|
+
const citation = subKindDocCitation(sub.kind);
|
|
461
|
+
return `- \`${sub.kind}\` — ${sub.whenToUse}${citation}`;
|
|
462
|
+
});
|
|
463
|
+
return (`## Sub-personas you may spawn\n\n` +
|
|
464
|
+
`These specialist sub-personas are available to the ${kind} kind. Spawn one with ` +
|
|
465
|
+
'`crtr node new --kind <sub> "<scope>"`, and treat it as its own scope instead of ' +
|
|
466
|
+
'dragging unrelated context into it.\n\n' +
|
|
467
|
+
lines.join('\n'));
|
|
468
|
+
}
|
|
315
469
|
// ---------------------------------------------------------------------------
|
|
316
470
|
// 1. Preferences — `<memory kind="preference">` (system prompt).
|
|
317
471
|
// ---------------------------------------------------------------------------
|
|
318
472
|
/** The system-prompt memory block: the always-present memory-usage guidance,
|
|
319
|
-
* then (when any are eligible) the `kind: preference` docs
|
|
320
|
-
*
|
|
321
|
-
* ALWAYS returns a non-empty `<memory
|
|
322
|
-
*
|
|
323
|
-
* no preference is eligible. */
|
|
324
|
-
|
|
473
|
+
* then (when any are eligible) the `kind: preference` docs GROUPED by rung —
|
|
474
|
+
* content-rung docs first as clean prose (general→specific), then the
|
|
475
|
+
* preview/name catalog tree. ALWAYS returns a non-empty `<memory
|
|
476
|
+
* kind="preference">` block — the guidance frame is unconditional, so the
|
|
477
|
+
* system-prompt splice is never empty even when no preference is eligible. */
|
|
478
|
+
function buildPreferencesBlock(subject) {
|
|
325
479
|
let body = MEMORY_USAGE_GUIDANCE;
|
|
326
|
-
|
|
480
|
+
let contentDocs = [];
|
|
481
|
+
let tree = '';
|
|
327
482
|
if (subject !== null) {
|
|
328
|
-
const
|
|
329
|
-
|
|
330
|
-
|
|
483
|
+
const winners = selectWinners(subject, 'preference');
|
|
484
|
+
const grouped = renderGrouped(winners, 'preferences');
|
|
485
|
+
contentDocs = grouped.contentDocs;
|
|
486
|
+
tree = grouped.tree;
|
|
487
|
+
const subPersonaMenu = buildSubPersonaMenu(subject.kind);
|
|
488
|
+
if (grouped.contentProse !== '' || grouped.tree !== '' || subPersonaMenu !== '') {
|
|
489
|
+
body += `\n\n${PREFERENCES_INTRO}`;
|
|
490
|
+
if (grouped.contentProse !== '')
|
|
491
|
+
body += `\n\n${grouped.contentProse}`;
|
|
492
|
+
if (subPersonaMenu !== '')
|
|
493
|
+
body += `\n\n${subPersonaMenu}`;
|
|
494
|
+
if (grouped.tree !== '')
|
|
495
|
+
body += `\n\n${grouped.tree}`;
|
|
496
|
+
body += `\n\n${PREFERENCES_OUTRO}`;
|
|
497
|
+
}
|
|
331
498
|
}
|
|
332
|
-
return `<memory kind="preference">\n${body}\n</memory
|
|
499
|
+
return { block: `<memory kind="preference">\n${body}\n</memory>`, contentDocs, tree };
|
|
500
|
+
}
|
|
501
|
+
export function renderPreferencesSection(nodeId) {
|
|
502
|
+
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
503
|
+
return buildPreferencesBlock(subject).block;
|
|
504
|
+
}
|
|
505
|
+
export function renderPreferencesForSubject(subject) {
|
|
506
|
+
return buildPreferencesBlock(subject);
|
|
333
507
|
}
|
|
334
508
|
// ---------------------------------------------------------------------------
|
|
335
509
|
// 2. Knowledge — `<memory kind="knowledge">` (the first-message bearings).
|
|
@@ -337,20 +511,33 @@ export function renderPreferencesSection(nodeId) {
|
|
|
337
511
|
/** The `<memory kind="knowledge">` block embedded in the session_start bearings
|
|
338
512
|
* message (bearings.ts pushes it as a SIBLING of `<crtr-bearings>`, or drops it
|
|
339
513
|
* when ''). The consultable catalog: every eligible `kind: knowledge` resolver
|
|
340
|
-
* doc
|
|
341
|
-
*
|
|
342
|
-
*
|
|
343
|
-
*
|
|
344
|
-
*
|
|
514
|
+
* doc PLUS the node-local memory docs (any kind) — resolver docs win
|
|
515
|
+
* first-wins dedup over a same-named node-local doc. GROUPED by rung exactly
|
|
516
|
+
* like the preference block: content prose first (general→specific,
|
|
517
|
+
* node-local last), then the preview/name catalog tree. Node-local docs are
|
|
518
|
+
* floored HERE — a rung-none node-local doc's effective rung is bumped to
|
|
519
|
+
* `name` before grouping, so it still surfaces as a bare-name tree entry
|
|
520
|
+
* rather than collapsing into a `[+N more]` count (its body still never
|
|
521
|
+
* renders: floored docs never reach the `content` rung). Procedural guidance
|
|
522
|
+
* and factual references both live here as `knowledge`. Returns '' when
|
|
523
|
+
* nothing is eligible. */
|
|
345
524
|
export function renderKnowledgeBlock(nodeId) {
|
|
346
525
|
const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
|
|
347
526
|
if (subject === null)
|
|
348
527
|
return '';
|
|
349
|
-
const nodeLocal = nodeLocalDocs(nodeId, subject).map((d) => rungRank(d.systemPromptVisibility) >= rungRank('name')
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
528
|
+
const nodeLocal = nodeLocalDocs(nodeId, subject).map((d) => rungRank(d.systemPromptVisibility) >= rungRank('name') ? d : { ...d, systemPromptVisibility: 'name' });
|
|
529
|
+
const nodeLocalSet = new Set(nodeLocal);
|
|
530
|
+
// Resolver winners first: first-wins dedup keeps the resolver's copy over a
|
|
531
|
+
// same-named node-local doc, matching the original precedence.
|
|
532
|
+
const winners = dedupeFirstWins([...selectWinners(subject, 'knowledge'), ...nodeLocal]);
|
|
533
|
+
const { contentProse, tree } = renderGrouped(winners, 'knowledge', nodeLocalSet);
|
|
534
|
+
if (contentProse === '' && tree === '')
|
|
354
535
|
return '';
|
|
355
|
-
|
|
536
|
+
let out = KNOWLEDGE_INTRO;
|
|
537
|
+
if (contentProse !== '')
|
|
538
|
+
out += `\n\n${contentProse}`;
|
|
539
|
+
if (tree !== '')
|
|
540
|
+
out += `\n\n${tree}`;
|
|
541
|
+
out += `\n\n${KNOWLEDGE_OUTRO}`;
|
|
542
|
+
return `<memory kind="knowledge">\n${out}\n</memory>`;
|
|
356
543
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
import type { MemoryDoc } from '../memory-resolver.js';
|
|
1
|
+
import type { MemoryDoc, MemoryScope } from '../memory-resolver.js';
|
|
3
2
|
export declare const KINDS: readonly ["knowledge", "preference"];
|
|
4
3
|
export type DocKind = (typeof KINDS)[number];
|
|
5
4
|
/** Is `v` one of the two valid document kinds? */
|
|
@@ -12,6 +11,15 @@ export declare function rungRank(r: Rung): number;
|
|
|
12
11
|
* `rungAtLeast(doc.systemPromptVisibility, 'name')` ⇒ "shows at boot at all". */
|
|
13
12
|
export declare function rungAtLeast(r: Rung, min: Rung): boolean;
|
|
14
13
|
export declare const FALLBACK_RUNG: Rung;
|
|
14
|
+
/** Strip an optional `NN-` ordering prefix from ONE path segment (file or
|
|
15
|
+
* directory display name). `00-runtime-base` -> `runtime-base`; `spine` (no
|
|
16
|
+
* prefix) is unchanged. */
|
|
17
|
+
export declare function normalizeNameSegment(segment: string): string;
|
|
18
|
+
/** Normalize a full slash-separated path-derived doc name by stripping the
|
|
19
|
+
* optional numeric prefix from EVERY segment: `01-spine/00-has-manager` ->
|
|
20
|
+
* `spine/has-manager`. This is the identity a doc displays, dedups, and
|
|
21
|
+
* resolves under — distinct from its physical path, which keeps the prefix. */
|
|
22
|
+
export declare function normalizeDocName(name: string): string;
|
|
15
23
|
/** A gate predicate tree, evaluated by predicate.ts (`evalCondition`) against
|
|
16
24
|
* the node-config subject. Typed loosely on purpose — the matcher engine owns
|
|
17
25
|
* validation; structurally it is a field→matcher map with optional
|
|
@@ -58,7 +66,7 @@ export interface SubstrateDoc extends SubstrateSchema {
|
|
|
58
66
|
/** Path-derived identity, e.g. `taste/document-substrate` (resolver-supplied). */
|
|
59
67
|
name: string;
|
|
60
68
|
/** The scope this doc resolved from. */
|
|
61
|
-
scope:
|
|
69
|
+
scope: MemoryScope;
|
|
62
70
|
/** Absolute path to the source .md. */
|
|
63
71
|
path: string;
|
|
64
72
|
/** Document body, frontmatter stripped. */
|
|
@@ -44,6 +44,31 @@ export function rungAtLeast(r, min) {
|
|
|
44
44
|
// ---------------------------------------------------------------------------
|
|
45
45
|
export const FALLBACK_RUNG = 'none';
|
|
46
46
|
// ---------------------------------------------------------------------------
|
|
47
|
+
// Display-name normalization — the optional `NN-` ordering pin. A doc's
|
|
48
|
+
// physical path may carry a two-digit numeric prefix on a file or directory
|
|
49
|
+
// segment (`00-runtime-base.md`, `01-spine/`) purely to pin structural order
|
|
50
|
+
// against natural sort (design: builtin prompt corpus, used sparingly). The
|
|
51
|
+
// prefix is NEVER part of the doc's identity: every identity-facing surface —
|
|
52
|
+
// MemoryDoc.name, direct lookup, leaf fallback, `crtr memory read`/`list`,
|
|
53
|
+
// prompt render, on-read display — derives from the NORMALIZED segments, so
|
|
54
|
+
// `00-runtime-base` displays/dedups/resolves as `runtime-base`. Only the
|
|
55
|
+
// physical path keeps the prefix.
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
const NUMERIC_PREFIX_RE = /^\d{2}-/;
|
|
58
|
+
/** Strip an optional `NN-` ordering prefix from ONE path segment (file or
|
|
59
|
+
* directory display name). `00-runtime-base` -> `runtime-base`; `spine` (no
|
|
60
|
+
* prefix) is unchanged. */
|
|
61
|
+
export function normalizeNameSegment(segment) {
|
|
62
|
+
return segment.replace(NUMERIC_PREFIX_RE, '');
|
|
63
|
+
}
|
|
64
|
+
/** Normalize a full slash-separated path-derived doc name by stripping the
|
|
65
|
+
* optional numeric prefix from EVERY segment: `01-spine/00-has-manager` ->
|
|
66
|
+
* `spine/has-manager`. This is the identity a doc displays, dedups, and
|
|
67
|
+
* resolves under — distinct from its physical path, which keeps the prefix. */
|
|
68
|
+
export function normalizeDocName(name) {
|
|
69
|
+
return name.split('/').map(normalizeNameSegment).join('/');
|
|
70
|
+
}
|
|
71
|
+
// ---------------------------------------------------------------------------
|
|
47
72
|
// Parse / validate.
|
|
48
73
|
// ---------------------------------------------------------------------------
|
|
49
74
|
/** Parse a raw frontmatter record (from `parseFrontmatterGeneric`, via the
|
|
@@ -22,6 +22,12 @@ export interface NodeConfigSubject {
|
|
|
22
22
|
* root itself is 0; a direct child of the root is 1; etc. */
|
|
23
23
|
depth: number;
|
|
24
24
|
};
|
|
25
|
+
/** DERIVED: `meta.profile_id` resolved to its manifest's mutable `name` (NOT
|
|
26
|
+
* the stable directory id — gate authors write `gate: {profile: <name>}`).
|
|
27
|
+
* null for a root/no-profile node, and null (never throwing) when the
|
|
28
|
+
* profile has been deleted or its manifest is unreadable — this is a HOT,
|
|
29
|
+
* every-doc-render path and must never brick boot/on-read gating. */
|
|
30
|
+
profile: string | null;
|
|
25
31
|
}
|
|
26
32
|
/** The scope a cwd resolves into: `project` when scope.ts finds a
|
|
27
33
|
* nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
|
|
@@ -35,7 +41,14 @@ export declare function scopeForCwd(cwd: string): 'user' | 'project';
|
|
|
35
41
|
* root). The root is depth 0. Cycle-guarded (parents must not cycle, but never
|
|
36
42
|
* loop forever) — mirrors `rootOfSpine` in runtime/placement.ts. */
|
|
37
43
|
export declare function spineDepth(nodeId: string): number;
|
|
38
|
-
/**
|
|
44
|
+
/** Resolve `profile_id` to the manifest's mutable `name` for the gate subject.
|
|
45
|
+
* NEVER throws: a missing/deleted profile resolves to `null` silently (the
|
|
46
|
+
* ordinary lifecycle of a node whose profile was deleted out from under it);
|
|
47
|
+
* any OTHER failure (e.g. an unreadable/corrupt manifest slipping past
|
|
48
|
+
* `loadProfileManifest`'s own tolerance) is logged and still resolves to
|
|
49
|
+
* `null` rather than throwing — this runs on every gated-doc render. */
|
|
50
|
+
export declare function profileNameFor(profileId: string | null | undefined): string | null;
|
|
51
|
+
/** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the
|
|
39
52
|
* derived fields. Returns `null` when the node has no canvas-db row (an
|
|
40
|
-
* unknown id). Pure aside from the canvas-db reads. */
|
|
53
|
+
* unknown id). Pure aside from the canvas-db + profile-manifest reads. */
|
|
41
54
|
export declare function assembleNodeSubject(nodeId: string): NodeConfigSubject | null;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
// a thin, pure assembler.
|
|
7
7
|
import { getNode, getRow } from '../canvas/index.js';
|
|
8
8
|
import { projectScopeRoot } from '../scope.js';
|
|
9
|
+
import { loadProfileManifest } from '../profiles/manifest.js';
|
|
10
|
+
import { CrtrError } from '../errors.js';
|
|
9
11
|
/** The scope a cwd resolves into: `project` when scope.ts finds a
|
|
10
12
|
* nearest-ancestor `.crouter/` at/above it, else `user`. Reuses the existing
|
|
11
13
|
* resolver (`projectScopeRoot`). NOTE: `findProjectScopeRoot` is process-cached
|
|
@@ -35,9 +37,28 @@ export function spineDepth(nodeId) {
|
|
|
35
37
|
}
|
|
36
38
|
return hops;
|
|
37
39
|
}
|
|
38
|
-
/**
|
|
40
|
+
/** Resolve `profile_id` to the manifest's mutable `name` for the gate subject.
|
|
41
|
+
* NEVER throws: a missing/deleted profile resolves to `null` silently (the
|
|
42
|
+
* ordinary lifecycle of a node whose profile was deleted out from under it);
|
|
43
|
+
* any OTHER failure (e.g. an unreadable/corrupt manifest slipping past
|
|
44
|
+
* `loadProfileManifest`'s own tolerance) is logged and still resolves to
|
|
45
|
+
* `null` rather than throwing — this runs on every gated-doc render. */
|
|
46
|
+
export function profileNameFor(profileId) {
|
|
47
|
+
if (profileId === undefined || profileId === null || profileId === '')
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
return loadProfileManifest(profileId).manifest.name;
|
|
51
|
+
}
|
|
52
|
+
catch (err) {
|
|
53
|
+
if (err instanceof CrtrError && err.code === 'not_found')
|
|
54
|
+
return null;
|
|
55
|
+
console.warn(`[subject] profile "${profileId}" failed to resolve for node subject assembly: ${err.message}`);
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the
|
|
39
60
|
* derived fields. Returns `null` when the node has no canvas-db row (an
|
|
40
|
-
* unknown id). Pure aside from the canvas-db reads. */
|
|
61
|
+
* unknown id). Pure aside from the canvas-db + profile-manifest reads. */
|
|
41
62
|
export function assembleNodeSubject(nodeId) {
|
|
42
63
|
const meta = getNode(nodeId);
|
|
43
64
|
if (meta === null)
|
|
@@ -50,5 +71,6 @@ export function assembleNodeSubject(nodeId) {
|
|
|
50
71
|
cwd: meta.cwd,
|
|
51
72
|
scope: scopeForCwd(meta.cwd),
|
|
52
73
|
orchestration: { depth: spineDepth(nodeId) },
|
|
74
|
+
profile: profileNameFor(meta.profile_id),
|
|
53
75
|
};
|
|
54
76
|
}
|
package/dist/daemon/crtrd-cli.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// crtrd entry point — spawned detached by `crtr
|
|
1
|
+
// crtrd entry point — spawned detached by `crtr sys daemon start` and by bin/crtrd.
|
|
2
2
|
// Calls runDaemon() and never returns (the loop drives via setTimeout).
|
|
3
3
|
import { runDaemon } from './crtrd.js';
|
|
4
4
|
runDaemon();
|
package/dist/daemon/crtrd.d.ts
CHANGED
|
@@ -19,6 +19,30 @@ export declare const YIELD_STALL_GRACE_MS: number;
|
|
|
19
19
|
* after running `node yield` mid-turn, and a working engine must never be
|
|
20
20
|
* killed), and the state has persisted past YIELD_STALL_GRACE_MS. */
|
|
21
21
|
export declare function yieldStallVerdict(piPidAlive: boolean | null, intent: NodeRow['intent'], busy: boolean, stalledFor: number | null): 'leave' | 'pending' | 'kill';
|
|
22
|
+
export declare const WEDGE_QUIET_MS: number;
|
|
23
|
+
export declare const WEDGE_CPU_MAX_PERCENT = 2;
|
|
24
|
+
export type WedgeVerdict = 'leave' | 'pending' | 'wedged';
|
|
25
|
+
/** Pure wedge decision — mirrors livenessVerdict/yieldStallVerdict: a total
|
|
26
|
+
* function of the observed signals, no clock or process access of its own.
|
|
27
|
+
* `busy` false — the turn already ended (however it routed) — is never a
|
|
28
|
+
* wedge, regardless of how stale the marker looks. `quietForMs` null means
|
|
29
|
+
* the heartbeat marker couldn't be read (treated as “not enough evidence”, not
|
|
30
|
+
* “wedged”). `cpuPercent` null (sample failed/unavailable) or above the
|
|
31
|
+
* ceiling — EITHER keeps the verdict at 'pending' (keep watching next tick)
|
|
32
|
+
* rather than firing on an unconfirmed or contradicted signal. */
|
|
33
|
+
export declare function wedgeVerdict(input: {
|
|
34
|
+
busy: boolean;
|
|
35
|
+
quietForMs: number | null;
|
|
36
|
+
cpuPercent: number | null;
|
|
37
|
+
}): WedgeVerdict;
|
|
38
|
+
/** Pure: sum CPU% across a process's entire tree (itself + every descendant).
|
|
39
|
+
* Parses `ps -ax -o pid=,ppid=,pcpu=` output, builds a ppid→children map, and
|
|
40
|
+
* DFS's from `rootPid` collecting root + all descendants; malformed/blank
|
|
41
|
+
* lines are skipped. Returns `null` if `rootPid` never appears in the output
|
|
42
|
+
* (can't measure at all — treated as UNKNOWN by wedgeVerdict, never as
|
|
43
|
+
* “low”) or the output has no parsable rows. Exported for direct unit testing
|
|
44
|
+
* — no process access of its own, mirroring wedgeVerdict's purity. */
|
|
45
|
+
export declare function sumTreeCpu(psOutput: string, rootPid: number): number | null;
|
|
22
46
|
export type LivenessVerdict = 'leave' | 'pending' | 'revive';
|
|
23
47
|
/** Decide what to do with a node whose engine pid is DEAD, from how long it's
|
|
24
48
|
* been dead. Pure — the time/revive side effects live in handleNodeLiveness;
|