@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.
Files changed (177) hide show
  1. package/dist/build-root.js +3 -6
  2. package/dist/{builtin-personas/runtime-base.md → builtin-memory/00-runtime-base.md} +8 -1
  3. package/dist/{builtin-personas/spine/has-manager.md → builtin-memory/01-spine/00-has-manager.md} +8 -0
  4. package/dist/builtin-memory/01-spine/01-no-manager.md +10 -0
  5. package/dist/{builtin-personas/lifecycle/terminal.md → builtin-memory/02-lifecycle/00-terminal.md} +8 -0
  6. package/dist/{builtin-personas/lifecycle/resident.md → builtin-memory/02-lifecycle/01-resident.md} +8 -0
  7. package/dist/{builtin-personas/waiting.md → builtin-memory/03-waiting.md} +7 -0
  8. package/dist/{builtin-personas/orchestration-kernel.md → builtin-memory/04-orchestration-kernel.md} +8 -0
  9. package/dist/{builtin-personas/advisor/PERSONA.md → builtin-memory/05-kinds/advisor/00-base.md} +5 -2
  10. package/dist/{builtin-personas/design/PERSONA.md → builtin-memory/05-kinds/design/00-base.md} +5 -2
  11. package/dist/{builtin-personas/design/orchestrator.md → builtin-memory/05-kinds/design/01-orchestrator.md} +5 -4
  12. package/dist/{builtin-personas/developer/PERSONA.md → builtin-memory/05-kinds/developer/00-base.md} +5 -2
  13. package/dist/{builtin-personas/developer/orchestrator.md → builtin-memory/05-kinds/developer/01-orchestrator.md} +5 -4
  14. package/dist/{builtin-personas/explore/PERSONA.md → builtin-memory/05-kinds/explore/00-base.md} +5 -2
  15. package/dist/{builtin-personas/explore/orchestrator.md → builtin-memory/05-kinds/explore/01-orchestrator.md} +5 -3
  16. package/dist/{builtin-personas/general/PERSONA.md → builtin-memory/05-kinds/general/00-base.md} +5 -2
  17. package/dist/{builtin-personas/general/orchestrator.md → builtin-memory/05-kinds/general/01-orchestrator.md} +5 -3
  18. package/dist/{builtin-personas/plan/PERSONA.md → builtin-memory/05-kinds/plan/00-base.md} +5 -2
  19. package/dist/{builtin-personas/plan/orchestrator.md → builtin-memory/05-kinds/plan/01-orchestrator.md} +5 -4
  20. package/dist/{builtin-personas/plan/reviewers/architecture-fit/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/architecture-fit.md} +5 -2
  21. package/dist/{builtin-personas/plan/reviewers/code-smells/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/code-smells.md} +5 -2
  22. package/dist/{builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md} +5 -2
  23. package/dist/{builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md} +5 -2
  24. package/dist/{builtin-personas/plan/reviewers/security/PERSONA.md → builtin-memory/05-kinds/plan/reviewers/security.md} +5 -2
  25. package/dist/{builtin-personas/product/PERSONA.md → builtin-memory/05-kinds/product/00-base.md} +5 -2
  26. package/dist/{builtin-personas/product/orchestrator.md → builtin-memory/05-kinds/product/01-orchestrator.md} +5 -4
  27. package/dist/{builtin-personas/product/teardown/PERSONA.md → builtin-memory/05-kinds/product/teardown.md} +5 -3
  28. package/dist/{builtin-personas/review/PERSONA.md → builtin-memory/05-kinds/review/00-base.md} +5 -2
  29. package/dist/{builtin-personas/review/orchestrator.md → builtin-memory/05-kinds/review/01-orchestrator.md} +5 -3
  30. package/dist/{builtin-personas/spec/PERSONA.md → builtin-memory/05-kinds/spec/00-base.md} +5 -2
  31. package/dist/{builtin-personas/spec/orchestrator.md → builtin-memory/05-kinds/spec/01-orchestrator.md} +5 -4
  32. package/dist/{builtin-personas/spec/requirements/PERSONA.md → builtin-memory/05-kinds/spec/requirements.md} +5 -2
  33. package/dist/builtin-memory/internal/INDEX.md +1 -1
  34. package/dist/builtin-memory/internal/examples/imessage-assistant.md +2 -2
  35. package/dist/builtin-memory/internal/nodes-and-canvas.md +1 -1
  36. package/dist/builtin-pi-packages/pi-mode-switch/extensions/index.ts +4 -4
  37. package/dist/builtin-views/canvas/core.mjs +3 -2
  38. package/dist/builtin-views/prompt-review/core.mjs +6 -134
  39. package/dist/builtin-views/settings/core.mjs +26 -94
  40. package/dist/builtin-views/settings/text.mjs +0 -5
  41. package/dist/builtin-views/settings/tui.mjs +1 -7
  42. package/dist/builtin-views/settings/web.jsx +3 -49
  43. package/dist/clients/attach/__tests__/crtr-output-render.test.js +21 -205
  44. package/dist/clients/attach/attach-cmd.js +429 -432
  45. package/dist/clients/web/web-cmd.js +7 -7
  46. package/dist/commands/__tests__/human.test.js +1 -15
  47. package/dist/commands/attention.js +9 -6
  48. package/dist/commands/canvas-history/search.js +2 -2
  49. package/dist/commands/canvas-history/show.js +3 -3
  50. package/dist/commands/canvas-history.js +1 -1
  51. package/dist/commands/canvas-issue.js +1 -0
  52. package/dist/commands/canvas-snapshot.js +1 -0
  53. package/dist/commands/canvas.js +10 -11
  54. package/dist/commands/chord.js +1 -0
  55. package/dist/commands/human/prompts.js +0 -1
  56. package/dist/commands/human/queue.js +1 -0
  57. package/dist/commands/memory/read.js +3 -3
  58. package/dist/commands/memory/shared.d.ts +13 -8
  59. package/dist/commands/memory/shared.js +22 -8
  60. package/dist/commands/memory/write.js +5 -3
  61. package/dist/commands/node-snapshot.js +1 -0
  62. package/dist/commands/node.js +52 -22
  63. package/dist/commands/profile/add-project.d.ts +1 -0
  64. package/dist/commands/profile/add-project.js +42 -0
  65. package/dist/commands/profile/delete.d.ts +1 -0
  66. package/dist/commands/profile/delete.js +39 -0
  67. package/dist/commands/profile/list.d.ts +1 -0
  68. package/dist/commands/profile/list.js +35 -0
  69. package/dist/commands/profile/new.d.ts +1 -0
  70. package/dist/commands/profile/new.js +50 -0
  71. package/dist/commands/profile/remove-project.d.ts +1 -0
  72. package/dist/commands/profile/remove-project.js +42 -0
  73. package/dist/commands/profile/rename.d.ts +1 -0
  74. package/dist/commands/profile/rename.js +42 -0
  75. package/dist/commands/profile/show.d.ts +1 -0
  76. package/dist/commands/profile/show.js +50 -0
  77. package/dist/commands/profile.d.ts +2 -0
  78. package/dist/commands/profile.js +33 -0
  79. package/dist/commands/push.d.ts +0 -1
  80. package/dist/commands/push.js +2 -228
  81. package/dist/commands/revive.js +2 -2
  82. package/dist/commands/{canvas-tmux-spread.js → surface-tmux-spread.js} +9 -6
  83. package/dist/commands/surface.js +3 -2
  84. package/dist/commands/sys/config.js +6 -21
  85. package/dist/commands/sys/daemon.d.ts +2 -0
  86. package/dist/commands/{daemon.js → sys/daemon.js} +12 -12
  87. package/dist/commands/sys/doctor.js +1 -0
  88. package/dist/commands/sys/prompt-review.js +90 -17
  89. package/dist/commands/sys.js +4 -3
  90. package/dist/core/__tests__/daemon-boot.test.js +1 -1
  91. package/dist/core/__tests__/daemon-wedge.test.d.ts +1 -0
  92. package/dist/core/__tests__/daemon-wedge.test.js +87 -0
  93. package/dist/core/__tests__/hearth-bootstrap.test.js +2 -2
  94. package/dist/core/__tests__/history-inbox.test.d.ts +1 -0
  95. package/dist/core/__tests__/history-inbox.test.js +105 -0
  96. package/dist/core/__tests__/kickoff.test.js +3 -3
  97. package/dist/core/__tests__/revive.test.js +39 -1
  98. package/dist/core/canvas/canvas.js +10 -9
  99. package/dist/core/canvas/db.js +11 -0
  100. package/dist/core/canvas/history.d.ts +1 -1
  101. package/dist/core/canvas/history.js +91 -3
  102. package/dist/core/canvas/paths.d.ts +6 -0
  103. package/dist/core/canvas/paths.js +9 -0
  104. package/dist/core/canvas/status-glyph.js +1 -0
  105. package/dist/core/canvas/types.d.ts +10 -0
  106. package/dist/core/config.d.ts +44 -1
  107. package/dist/core/config.js +155 -12
  108. package/dist/core/fault-classifier.d.ts +2 -2
  109. package/dist/core/feed/inbox.d.ts +7 -12
  110. package/dist/core/feed/inbox.js +45 -29
  111. package/dist/core/hearth/providers/blaxel-bootstrap.js +1 -1
  112. package/dist/core/memory-resolver.d.ts +26 -15
  113. package/dist/core/memory-resolver.js +158 -40
  114. package/dist/core/preview-registry.js +14 -112
  115. package/dist/core/profiles/manifest.d.ts +33 -0
  116. package/dist/core/profiles/manifest.js +332 -0
  117. package/dist/core/profiles/select.d.ts +11 -0
  118. package/dist/core/profiles/select.js +115 -0
  119. package/dist/core/render.js +1 -1
  120. package/dist/core/runtime/bearings.d.ts +9 -0
  121. package/dist/core/runtime/bearings.js +31 -0
  122. package/dist/core/runtime/busy.d.ts +15 -0
  123. package/dist/core/runtime/busy.js +32 -1
  124. package/dist/core/runtime/close.d.ts +10 -0
  125. package/dist/core/runtime/close.js +18 -13
  126. package/dist/core/runtime/fault-recovery.js +4 -0
  127. package/dist/core/runtime/front-door.js +9 -2
  128. package/dist/core/runtime/kickoff.js +5 -7
  129. package/dist/core/runtime/launch.d.ts +25 -7
  130. package/dist/core/runtime/launch.js +42 -55
  131. package/dist/core/runtime/nodes.d.ts +7 -0
  132. package/dist/core/runtime/nodes.js +6 -0
  133. package/dist/core/runtime/persona.js +18 -32
  134. package/dist/core/runtime/placement.js +1 -1
  135. package/dist/core/runtime/revive.js +9 -1
  136. package/dist/core/runtime/spawn.d.ts +13 -0
  137. package/dist/core/runtime/spawn.js +25 -0
  138. package/dist/core/runtime/tmux.d.ts +2 -2
  139. package/dist/core/runtime/tmux.js +2 -2
  140. package/dist/core/scope.d.ts +14 -7
  141. package/dist/core/scope.js +76 -20
  142. package/dist/core/substrate/index.d.ts +3 -3
  143. package/dist/core/substrate/index.js +5 -3
  144. package/dist/core/substrate/on-read.js +3 -2
  145. package/dist/core/substrate/render.d.ts +16 -11
  146. package/dist/core/substrate/render.js +249 -62
  147. package/dist/core/substrate/schema.d.ts +11 -3
  148. package/dist/core/substrate/schema.js +25 -0
  149. package/dist/core/substrate/subject.d.ts +15 -2
  150. package/dist/core/substrate/subject.js +24 -2
  151. package/dist/daemon/crtrd-cli.js +1 -1
  152. package/dist/daemon/crtrd.d.ts +24 -0
  153. package/dist/daemon/crtrd.js +213 -5
  154. package/dist/daemon/manage.js +2 -2
  155. package/dist/pi-extensions/canvas-doc-substrate.js +2 -3
  156. package/dist/pi-extensions/canvas-nav.js +2 -2
  157. package/dist/pi-extensions/canvas-stophook.d.ts +1 -1
  158. package/dist/pi-extensions/canvas-stophook.js +59 -2
  159. package/dist/types.d.ts +57 -1
  160. package/dist/types.js +83 -1
  161. package/dist/web-client/assets/{index-IAJVtuVe.js → index-CbO8L0mN.js} +4 -4
  162. package/dist/web-client/assets/index-DwO46Cs5.css +2 -0
  163. package/dist/web-client/index.html +2 -2
  164. package/package.json +2 -2
  165. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +0 -56
  166. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +0 -57
  167. package/dist/builtin-memory/crouter-development/personas.md +0 -112
  168. package/dist/builtin-personas/spine/no-manager.md +0 -2
  169. package/dist/commands/daemon.d.ts +0 -2
  170. package/dist/core/personas/index.d.ts +0 -13
  171. package/dist/core/personas/index.js +0 -11
  172. package/dist/core/personas/loader.d.ts +0 -159
  173. package/dist/core/personas/loader.js +0 -327
  174. package/dist/core/personas/resolve.d.ts +0 -59
  175. package/dist/core/personas/resolve.js +0 -376
  176. package/dist/web-client/assets/index-DUThOUzU.css +0 -2
  177. /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 → a file tree → an update directive. The tree discloses each doc
5
- // at its effective rung (name bare entry; preview a `# read when:` routing
6
- // line; content the full body indented beneath the entry); hidden (`none`-rung)
7
- // docs leak only a `[+N more]` count under their dir, never a name.
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
- // Every doc flows through the same pipeline: MemoryDoc parseSubstrateDoc
21
- // (null-filter non-substrate) ceiling-capped effective runggatePasses →
22
- // tree placement. INDEX docs render as an explicit `INDEX.md` child line under
23
- // their dir (teaching the convention); a `none`-rung INDEX still hides its whole
24
- // subtree (its descendants roll up as hidden counts).
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 { assembleNodeSubject, buildCeilingIndex, effectiveRung, gatePasses, indexDirOf, isIndexName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, rungRank, } from './index.js';
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
- // The shared per-doc pipeline.
60
+ // Step 1 — winner selection (ceiling + gate + first-wins dedup).
40
61
  // ---------------------------------------------------------------------------
41
- /** The resolver-provided substrate docs of one `kind`, eligible at boot for
42
- * `subject`: parsed (non-substrate docs null-filtered), ceiling-capped, and
43
- * gate-passed at their EFFECTIVE system-prompt rung, INCLUDING `none`-rung
44
- * docs (the tree counts them into `[+N more]`). Resolver = user + project +
45
- * builtin scopes (precedence-ordered); node-local is loaded separately (see
46
- * nodeLocalDocs). Uses the per-session cache so the full corpus is scanned +
47
- * parsed at most once per session across the boot-render calls.
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 effectiveDocs(subject, kind) {
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
- return docs
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
- .sort((a, b) => scopeRank(a.scope) - scopeRank(b.scope) || a.name.localeCompare(b.name));
106
+ .filter((d) => gatePasses(d, subject));
107
+ return dedupeFirstWins(eligible);
70
108
  }
71
- function scopeRank(scope) {
72
- return scope === 'project' ? 0 : scope === 'user' ? 1 : 2;
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
- * IMPORTANT: node-local docs are NOT filtered or capped by rung. Their contract
83
- * is "node-local rides into knowledge" without qualification, so a `none`-rung
84
- * node-local doc must still surface the caller floors it to `name` so it
85
- * renders its name (never collapsing into a hidden count). Only gate evaluation
86
- * removes a node-local doc from the block. */
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 name = relative(dir, file).replace(/\.md$/i, '').split(sep).join('/');
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 file tree for a kind's eligible docs, headed by `rootLabel`.
244
- * Returns '' when there are no docs at all (the empty-tree contract).
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
- * `docs` arrives in precedence order (project > user > builtin, node-local last)
247
- * WITHOUT cross-scope dedup `listAllMemoryDocs` returns every scope's hit for
248
- * a path-derived name and leaves first-wins dedup to the caller. So a name
249
- * present in two scopes is deduped here (first occurrence wins); otherwise it
250
- * would render as two identical sibling lines and double-count into `[+N more]`. */
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, and node-local (this node's `context/memory/`). To read one, run `crtr memory read <name>`. " +
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 in one tree at their
320
- * effective rung (the preference default rung is `preview` → the routing line).
321
- * ALWAYS returns a non-empty `<memory kind="preference">` block — the guidance
322
- * frame is unconditional, so the system-prompt splice is never empty even when
323
- * no preference is eligible. */
324
- export function renderPreferencesSection(nodeId) {
473
+ * then (when any are eligible) the `kind: preference` docs GROUPED by rung
474
+ * content-rung docs first as clean prose (generalspecific), 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
- const subject = cachedNodeSubject(nodeId, assembleNodeSubject);
480
+ let contentDocs = [];
481
+ let tree = '';
327
482
  if (subject !== null) {
328
- const tree = buildTree(effectiveDocs(subject, 'preference'), 'preferences');
329
- if (tree !== '')
330
- body += `\n\n${PREFERENCES_INTRO}\n\n${tree}\n\n${PREFERENCES_OUTRO}`;
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 at its effective rung (most surface only as `[+N more]` counts unless
341
- * author-promoted) PLUS the node-local memory docs (any kind), the latter
342
- * floored to `name` so a `none`-rung node-local doc still shows its name rather
343
- * than collapsing into a count. Procedural guidance and factual references both
344
- * live here as `knowledge`. Returns '' when nothing is eligible. */
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
- ? d
351
- : { ...d, systemPromptVisibility: 'name' });
352
- const tree = buildTree([...effectiveDocs(subject, 'knowledge'), ...nodeLocal], 'knowledge');
353
- if (tree === '')
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
- return `<memory kind="knowledge">\n${KNOWLEDGE_INTRO}\n\n${tree}\n\n${KNOWLEDGE_OUTRO}\n</memory>`;
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 { Scope } from '../../types.js';
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: 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
- /** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the two
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
- /** Assemble the node-config subject for `nodeId` from its `NodeMeta` + the two
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
  }
@@ -1,4 +1,4 @@
1
- // crtrd entry point — spawned detached by `crtr canvas daemon start` and by bin/crtrd.
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();
@@ -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;