@north-light/crouter 0.3.207 → 0.3.209

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 (172) hide show
  1. package/dist/api/client.d.ts +3 -1
  2. package/dist/api/client.js +4 -0
  3. package/dist/api/dto/broker-ops.d.ts +2 -12
  4. package/dist/api/dto/nodes.d.ts +16 -0
  5. package/dist/api/routes.d.ts +1 -0
  6. package/dist/api/routes.js +1 -0
  7. package/dist/builtin-memory/00-runtime-base.md +3 -2
  8. package/dist/builtin-memory/01-spine/00-has-manager.md +3 -2
  9. package/dist/builtin-memory/01-spine/01-no-manager.md +3 -2
  10. package/dist/builtin-memory/02-lifecycle/00-terminal.md +3 -2
  11. package/dist/builtin-memory/02-lifecycle/01-resident.md +3 -2
  12. package/dist/builtin-memory/04-base-worker.md +3 -2
  13. package/dist/builtin-memory/04-orchestration-kernel.md +3 -2
  14. package/dist/builtin-memory/05-kinds/advisor/00-base.md +3 -2
  15. package/dist/builtin-memory/05-kinds/advisor/01-orchestrator.md +3 -2
  16. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -2
  17. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +3 -2
  18. package/dist/builtin-memory/05-kinds/developer/00-base.md +3 -2
  19. package/dist/builtin-memory/05-kinds/developer/01-orchestrator.md +3 -2
  20. package/dist/builtin-memory/05-kinds/explore/00-base.md +3 -2
  21. package/dist/builtin-memory/05-kinds/explore/01-orchestrator.md +3 -2
  22. package/dist/builtin-memory/05-kinds/general/00-base.md +3 -2
  23. package/dist/builtin-memory/05-kinds/general/01-orchestrator.md +3 -2
  24. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -2
  25. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +3 -2
  26. package/dist/builtin-memory/05-kinds/plan/reviewers/00-base.md +3 -2
  27. package/dist/builtin-memory/05-kinds/plan/reviewers/architecture-fit.md +3 -2
  28. package/dist/builtin-memory/05-kinds/plan/reviewers/code-smells.md +3 -2
  29. package/dist/builtin-memory/05-kinds/plan/reviewers/pattern-consistency.md +3 -2
  30. package/dist/builtin-memory/05-kinds/plan/reviewers/requirements-coverage.md +3 -2
  31. package/dist/builtin-memory/05-kinds/plan/reviewers/security.md +3 -2
  32. package/dist/builtin-memory/05-kinds/review/00-base.md +3 -2
  33. package/dist/builtin-memory/05-kinds/review/01-orchestrator.md +3 -2
  34. package/dist/builtin-memory/05-kinds/review/companion/00-base.md +3 -2
  35. package/dist/builtin-memory/05-kinds/spec/00-base.md +3 -2
  36. package/dist/builtin-memory/05-kinds/spec/01-orchestrator.md +3 -2
  37. package/dist/builtin-memory/05-kinds/spec/requirements.md +3 -2
  38. package/dist/builtin-memory/advisor/council.md +0 -2
  39. package/dist/builtin-memory/design.md +3 -2
  40. package/dist/builtin-memory/development.md +3 -2
  41. package/dist/builtin-memory/insights/capture.md +1 -3
  42. package/dist/builtin-memory/insights/init.md +1 -3
  43. package/dist/builtin-memory/insights/listen.md +3 -2
  44. package/dist/builtin-memory/internal/INDEX.md +5 -4
  45. package/dist/builtin-memory/internal/agent-shaping.md +5 -4
  46. package/dist/builtin-memory/internal/examples/INDEX.md +3 -2
  47. package/dist/builtin-memory/internal/examples/imessage-assistant.md +3 -2
  48. package/dist/builtin-memory/internal/marketplaces.md +3 -2
  49. package/dist/builtin-memory/internal/memory-loading.md +31 -20
  50. package/dist/builtin-memory/internal/nodes-and-canvas.md +3 -2
  51. package/dist/builtin-memory/internal/plugins.md +7 -6
  52. package/dist/builtin-memory/internal/storage-tiers.md +3 -2
  53. package/dist/builtin-memory/plan/roadmap.md +3 -2
  54. package/dist/builtin-memory/spec/guide.md +0 -2
  55. package/dist/builtin-memory/spec/requirements.md +0 -2
  56. package/dist/builtin-memory/spec/roadmap.md +3 -2
  57. package/dist/builtin-memory/testing.md +1 -3
  58. package/dist/builtin-memory/wedged-child-on-runaway-bash.md +3 -2
  59. package/dist/builtin-pi-packages/pi-crtr-extensions/README.md +1 -1
  60. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/frontmatter-rules/index.ts +3 -3
  61. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +1 -4
  62. package/dist/clients/attach/__tests__/group-activity.test.js +8 -1
  63. package/dist/clients/attach/render/card-presentation.d.ts +31 -0
  64. package/dist/clients/attach/render/card-presentation.js +163 -0
  65. package/dist/clients/attach/render/chat-view.d.ts +0 -2
  66. package/dist/clients/attach/render/chat-view.js +12 -23
  67. package/dist/clients/attach/render/context-message.d.ts +5 -9
  68. package/dist/clients/attach/render/context-message.js +20 -43
  69. package/dist/clients/attach/render/group-activity.d.ts +28 -4
  70. package/dist/clients/attach/render/group-activity.js +65 -11
  71. package/dist/clients/attach/render/group-recap.d.ts +3 -0
  72. package/dist/clients/attach/render/group-recap.js +17 -0
  73. package/dist/clients/attach/viewer.js +516 -516
  74. package/dist/commands/memory/delete.js +3 -18
  75. package/dist/commands/memory/edit.js +23 -30
  76. package/dist/commands/memory/history.js +1 -1
  77. package/dist/commands/memory/lint.d.ts +7 -8
  78. package/dist/commands/memory/lint.js +178 -132
  79. package/dist/commands/memory/list.d.ts +0 -1
  80. package/dist/commands/memory/list.js +2 -12
  81. package/dist/commands/memory/move.d.ts +1 -0
  82. package/dist/commands/memory/move.js +195 -0
  83. package/dist/commands/memory/read.js +135 -141
  84. package/dist/commands/memory/shared.d.ts +18 -17
  85. package/dist/commands/memory/shared.js +93 -39
  86. package/dist/commands/memory/write.js +23 -33
  87. package/dist/commands/memory.js +5 -4
  88. package/dist/commands/pkg/browse/catalog.js +2 -4
  89. package/dist/commands/pkg/browse/doc-view.js +17 -11
  90. package/dist/commands/pkg/browse/model.d.ts +7 -9
  91. package/dist/commands/sys/migrate.d.ts +1 -0
  92. package/dist/commands/sys/migrate.js +106 -0
  93. package/dist/commands/sys/sync-deps.js +5 -10
  94. package/dist/commands/sys/sync-project-guidance.js +36 -16
  95. package/dist/commands/sys/sync-skills.js +8 -4
  96. package/dist/commands/sys.js +3 -2
  97. package/dist/core/__tests__/canvas-inbox-watcher.test.js +1 -1
  98. package/dist/core/__tests__/helpers/broker-clients.d.ts +3 -1
  99. package/dist/core/__tests__/helpers/broker-clients.js +7 -2
  100. package/dist/core/__tests__/inline-memory-refs.test.js +8 -5
  101. package/dist/core/__tests__/memory-resolver-precedence.test.js +5 -4
  102. package/dist/core/__tests__/nested-store-discovery.test.js +1 -3
  103. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +3 -3
  104. package/dist/core/__tests__/on-read-dedup-resume.test.js +12 -12
  105. package/dist/core/__tests__/on-read-nested-store.test.js +23 -20
  106. package/dist/core/__tests__/seam/broker-attach-multiclient.test.js +9 -0
  107. package/dist/core/__tests__/serial/broker-snapshot-history.test.js +52 -1
  108. package/dist/core/__tests__/serial/flagship-lifecycle.test.js +1 -1
  109. package/dist/core/canvas/db.d.ts +3 -1
  110. package/dist/core/canvas/db.js +12 -2
  111. package/dist/core/memory/history.d.ts +4 -1
  112. package/dist/core/memory/history.js +1 -0
  113. package/dist/core/memory/inline-ref-inventory.d.ts +5 -4
  114. package/dist/core/memory/inline-ref-inventory.js +23 -19
  115. package/dist/core/memory-resolver.d.ts +4 -4
  116. package/dist/core/memory-resolver.js +16 -43
  117. package/dist/core/runtime/bearings.d.ts +4 -3
  118. package/dist/core/runtime/bearings.js +4 -4
  119. package/dist/core/runtime/broker/fault-retry.js +2 -7
  120. package/dist/core/runtime/broker/frame-dispatch.js +1 -1
  121. package/dist/core/runtime/broker/tool-groups.js +7 -6
  122. package/dist/core/runtime/broker-extension-render.d.ts +3 -2
  123. package/dist/core/runtime/broker-extension-render.js +3 -3
  124. package/dist/core/runtime/broker.js +4 -4
  125. package/dist/core/runtime/kickoff.js +4 -10
  126. package/dist/core/runtime/memory.js +2 -3
  127. package/dist/core/runtime/node-read.js +7 -1
  128. package/dist/core/runtime/stop-guard.js +3 -3
  129. package/dist/core/substrate/index.d.ts +7 -4
  130. package/dist/core/substrate/index.js +6 -4
  131. package/dist/core/substrate/injected-store.d.ts +24 -12
  132. package/dist/core/substrate/injected-store.js +80 -33
  133. package/dist/core/substrate/listings.d.ts +21 -0
  134. package/dist/core/substrate/listings.js +88 -0
  135. package/dist/core/substrate/on-read-node.d.ts +5 -5
  136. package/dist/core/substrate/on-read-node.js +4 -5
  137. package/dist/core/substrate/on-read.d.ts +25 -4
  138. package/dist/core/substrate/on-read.js +81 -102
  139. package/dist/core/substrate/render-node.d.ts +5 -2
  140. package/dist/core/substrate/render-node.js +5 -3
  141. package/dist/core/substrate/render.d.ts +9 -8
  142. package/dist/core/substrate/render.js +104 -96
  143. package/dist/core/substrate/schema.d.ts +34 -18
  144. package/dist/core/substrate/schema.js +75 -32
  145. package/dist/core/substrate/surface-match.d.ts +32 -0
  146. package/dist/core/substrate/surface-match.js +179 -0
  147. package/dist/daemon/api/handlers/nodes.js +9 -0
  148. package/dist/daemon/review/comment-notify.js +1 -8
  149. package/dist/daemon/review/deliver.js +1 -1
  150. package/dist/daemon/review/finish.js +1 -1
  151. package/dist/migrations/001-surfaces-frontmatter.d.ts +2 -0
  152. package/dist/migrations/001-surfaces-frontmatter.js +276 -0
  153. package/dist/migrations/convergent.d.ts +31 -0
  154. package/dist/migrations/convergent.js +71 -0
  155. package/dist/migrations/registry.d.ts +2 -0
  156. package/dist/migrations/registry.js +19 -0
  157. package/dist/migrations/types.d.ts +40 -0
  158. package/dist/migrations/types.js +11 -0
  159. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +1 -1
  160. package/dist/pi-extensions/canvas-context-intro.d.ts +2 -1
  161. package/dist/pi-extensions/canvas-doc-substrate.d.ts +2 -1
  162. package/dist/pi-extensions/canvas-doc-substrate.js +57 -34
  163. package/dist/pi-extensions/canvas-review-boundary.js +1 -1
  164. package/dist/shared/__tests__/generated-context-grammar.test.js +19 -22
  165. package/dist/shared/generated-context.d.ts +0 -18
  166. package/dist/shared/generated-context.js +16 -120
  167. package/dist/shared/tool-groups.d.ts +1 -0
  168. package/dist/shared/tool-groups.js +7 -0
  169. package/package.json +1 -1
  170. package/runtime.lock.json +2 -2
  171. package/dist/core/substrate/ceiling.d.ts +0 -17
  172. package/dist/core/substrate/ceiling.js +0 -67
@@ -1,32 +1,63 @@
1
- // injected-store.ts — durable per-node dedup set for on-read doc injection.
1
+ // injected-store.ts — durable per-node dedup map for substrate doc delivery.
2
2
  //
3
- // The on-read substrate hook (canvas-doc-substrate.ts → renderOnReadDocs) dedups
4
- // so a given doc surfaces at most once per conversation. That set USED to live
5
- // only in the pi process heap, cleared on session_start. But a node's logical
6
- // session — the .jsonl transcript — spans MULTIPLE pi processes: a dormancy →
7
- // revive(resume) cycle exits the old process and launches a fresh `pi --session`
8
- // that REUSES the same transcript. The fresh process started with an empty set,
9
- // so any doc already injected before dormancy got injected AGAIN on the next
10
- // read — the "fires a second time per session" bug.
3
+ // Delivery channels (boot render seeding, workspace-open, on-read, memory-read
4
+ // listings and events, command events) dedup so a given doc surfaces at most
5
+ // once per conversation AT A GIVEN DISCLOSURE. The map keys on realpath and
6
+ // carries the highest rung rank delivered so far; higher rungs pierce lower —
7
+ // a listing line (preview rank) must not block a later explicit content
8
+ // delivery, while a content delivery silences everything after it. Directory
9
+ // listings participate under the directory's own realpath (a directory never
10
+ // collides with a file's realpath).
11
11
  //
12
- // This module persists the set to `nodes/<id>/injected-docs.json` so the resumed
13
- // process rehydrates it and skips docs already present in the transcript. The
14
- // one launch path that begins a FRESH transcript (reviveNode with resume=false,
15
- // in runtime/revive.ts) calls clearInjectedDocs(), so a new conversation starts
16
- // with an empty set.
12
+ // The map USED to live only in the pi process heap, cleared on session_start.
13
+ // But a node's logical session — the .jsonl transcript — spans MULTIPLE pi
14
+ // processes: a dormancy → revive(resume) cycle exits the old process and
15
+ // launches a fresh `pi --session` that REUSES the same transcript. This module
16
+ // persists the map to `nodes/<id>/injected-docs.json` so the resumed process
17
+ // rehydrates it and skips docs already present in the transcript. The one
18
+ // launch path that begins a FRESH transcript (reviveNode with resume=false, in
19
+ // runtime/revive.ts) calls clearInjectedDocs(), so a new conversation starts
20
+ // empty. CLI processes (the `memory read` leaf) load/save the same file.
17
21
  //
18
- // All ops are best-effort: a failed read/write degrades to a possible re-inject,
19
- // never a crash — a dedup miss must never break a read or a revive.
22
+ // Disk format: `{"v":2,"docs":{"<realpath>":<rank 1|2|3>}}` (rank =
23
+ // rungRank of the delivered rung). A legacy v1 string-array rehydrates at
24
+ // content rank — its entries were recorded under never-re-deliver semantics.
25
+ //
26
+ // All ops are best-effort: a failed read/write degrades to a possible
27
+ // re-inject, never a crash — a dedup miss must never break a read or a revive.
20
28
  import { readFileSync, writeFileSync, rmSync } from 'node:fs';
21
29
  import { injectedDocsPath } from '../canvas/paths.js';
22
- /** Per-process cache behind `sharedInjectedDocs`: one Set instance per node,
30
+ import { rungRank } from './schema.js';
31
+ /** Already delivered at `rung` or above? — the skip test every channel runs
32
+ * before rendering a doc (or a listing line, at preview) again. */
33
+ export function deliveredAtOrAbove(seen, realpath, rung) {
34
+ const have = seen.get(realpath);
35
+ return have !== undefined && have >= rungRank(rung);
36
+ }
37
+ /** Record a delivery at `rung`, keeping the highest rank seen. */
38
+ export function recordDelivery(seen, realpath, rung) {
39
+ const rank = rungRank(rung);
40
+ const have = seen.get(realpath);
41
+ if (have === undefined || have < rank)
42
+ seen.set(realpath, rank);
43
+ }
44
+ /** Max-merge `source` into `target` — used to fold an out-of-process delivery
45
+ * (the daemon's warm-spare bearings push) into a live process's shared map. */
46
+ export function mergeInjectedDocs(target, source) {
47
+ for (const [real, rank] of source) {
48
+ const have = target.get(real);
49
+ if (have === undefined || have < rank)
50
+ target.set(real, rank);
51
+ }
52
+ }
53
+ /** Per-process cache behind `sharedInjectedDocs`: one Map instance per node,
23
54
  * shared by every extension in the pi process (context-intro's workspace-open
24
55
  * render and doc-substrate's on-read hook), so a doc delivered by one hook is
25
- * already in the set the other dedups against. */
56
+ * already in the map the other dedups against. */
26
57
  const cache = new Map();
27
- /** The process-shared dedup set for a node: loaded from disk once, then the
28
- * SAME Set instance on every call, so all hooks in this pi process grow and
29
- * consult one set. Daemon-side deliveries (a warm-spare claim) write the disk
58
+ /** The process-shared dedup map for a node: loaded from disk once, then the
59
+ * SAME Map instance on every call, so all hooks in this pi process grow and
60
+ * consult one map. Daemon-side deliveries (a warm-spare claim) write the disk
30
61
  * file from another process; consumers that must see those merge the plain
31
62
  * `loadInjectedDocs` result in before rendering. */
32
63
  export function sharedInjectedDocs(nodeId) {
@@ -37,31 +68,47 @@ export function sharedInjectedDocs(nodeId) {
37
68
  }
38
69
  return seen;
39
70
  }
40
- /** Rehydrate a node's on-read dedup set from disk. Returns an empty set when the
41
- * file is absent, unreadable, or malformed (a fresh transcript, or a node that
42
- * has not yet surfaced any on-read doc). */
71
+ const CONTENT_RANK = rungRank('content');
72
+ /** Rehydrate a node's dedup map from disk. Returns an empty map when the file
73
+ * is absent, unreadable, or malformed (a fresh transcript, or a node that has
74
+ * not yet surfaced any doc). */
43
75
  export function loadInjectedDocs(nodeId) {
44
76
  try {
45
- const arr = JSON.parse(readFileSync(injectedDocsPath(nodeId), 'utf8'));
46
- if (!Array.isArray(arr))
47
- return new Set();
48
- return new Set(arr.filter((x) => typeof x === 'string'));
77
+ const raw = JSON.parse(readFileSync(injectedDocsPath(nodeId), 'utf8'));
78
+ const out = new Map();
79
+ if (Array.isArray(raw)) {
80
+ // legacy v1: bare realpath array, never-re-deliver semantics
81
+ for (const x of raw)
82
+ if (typeof x === 'string')
83
+ out.set(x, CONTENT_RANK);
84
+ return out;
85
+ }
86
+ if (raw !== null && typeof raw === 'object') {
87
+ const docs = raw.docs;
88
+ if (docs !== null && typeof docs === 'object' && !Array.isArray(docs)) {
89
+ for (const [real, rank] of Object.entries(docs)) {
90
+ if (typeof rank === 'number' && rank >= 1 && rank <= CONTENT_RANK)
91
+ out.set(real, rank);
92
+ }
93
+ }
94
+ }
95
+ return out;
49
96
  }
50
97
  catch {
51
- return new Set();
98
+ return new Map();
52
99
  }
53
100
  }
54
- /** Persist a node's on-read dedup set. Called after each read that surfaced a
55
- * new doc, so the grown set survives a later dormancy. */
101
+ /** Persist a node's dedup map. Called after each delivery that grew it, so
102
+ * the grown map survives a later dormancy. */
56
103
  export function saveInjectedDocs(nodeId, seen) {
57
104
  try {
58
- writeFileSync(injectedDocsPath(nodeId), JSON.stringify([...seen]));
105
+ writeFileSync(injectedDocsPath(nodeId), JSON.stringify({ v: 2, docs: Object.fromEntries(seen) }));
59
106
  }
60
107
  catch {
61
108
  // best-effort — a failed persist only risks a re-inject, never a crash
62
109
  }
63
110
  }
64
- /** Drop a node's persisted dedup set. Called by the launch paths that start a
111
+ /** Drop a node's persisted dedup map. Called by the launch paths that start a
65
112
  * FRESH transcript, so the new conversation surfaces docs from scratch. */
66
113
  export function clearInjectedDocs(nodeId) {
67
114
  cache.delete(nodeId);
@@ -0,0 +1,21 @@
1
+ import type { MemoryDoc } from '../memory-resolver.js';
2
+ import { type InjectedDocs } from './injected-store.js';
3
+ /** Dedup key for a whole directory's listing. Realpath keys start with `/`,
4
+ * so the scheme prefix never collides with a doc entry. */
5
+ export declare function dirDedupKey(dir: string): string;
6
+ /** First-occurrence-wins name map over a precedence-ordered corpus: the doc
7
+ * that renders a name's line is the doc a read of that name would return. */
8
+ export declare function docsByName(corpus: readonly MemoryDoc[]): Map<string, MemoryDoc>;
9
+ /** Is `name` a directory in the merged name tree — i.e. does any doc live
10
+ * beneath it? The corpus root ('') is never a directory here: store roots
11
+ * merge into the whole catalog, which `memory list` owns. */
12
+ export declare function isDirName(byName: ReadonlyMap<string, MemoryDoc>, name: string): boolean;
13
+ /** The dirs a doc read discloses: the doc's own directory, then each ancestor
14
+ * walking up, the root excluded. `a/b/c` → `['a/b', 'a']`. */
15
+ export declare function ancestorDirsOf(name: string): string[];
16
+ /** Render one directory's listing lines: `[[name]]: <when-and-why>` per
17
+ * member doc (bare `[[name]]` when the doc carries no routing line), a bare
18
+ * `[[name]]` per subdirectory. `unlisted` members are omitted always;
19
+ * `filterDelivered` additionally drops members already delivered at preview
20
+ * rank or higher. Rendered members record at preview rank into `seen`. */
21
+ export declare function renderDirListing(byName: ReadonlyMap<string, MemoryDoc>, dir: string, seen: InjectedDocs | null, filterDelivered: boolean): string[];
@@ -0,0 +1,88 @@
1
+ // listings.ts — system-owned directory listings over the merged name tree.
2
+ //
3
+ // Reading a doc discloses where it sits: `ls` semantics over the corpus
4
+ // namespace (directories merge across stores), consulting no frontmatter
5
+ // except `unlisted`. A listing renders one when-and-why line per member doc
6
+ // and one bare name per subdirectory — never recursive contents, never gates.
7
+ //
8
+ // Two render modes share one function. An EXPLICIT directory read (`crtr
9
+ // memory read <dir>`) is a deliberate act, so every line renders (unlisted
10
+ // excluded) — mirroring how an explicit doc read always returns content. The
11
+ // AUTOMATIC neighborhood listings a doc read prepends are unsolicited, so
12
+ // each member line dedup-filters against the transcript set and whole
13
+ // directories dedup under a synthetic `dir://<name>` key (a name-tree
14
+ // directory spans stores, so no single physical realpath identifies it).
15
+ // Either mode records rendered member docs at preview rank — a listing line
16
+ // IS a preview-rank render — when a dedup map is supplied.
17
+ import { realpathOrSelf } from '../fs-utils.js';
18
+ import { parseSubstrateDoc, previewLine } from './schema.js';
19
+ import { deliveredAtOrAbove, recordDelivery } from './injected-store.js';
20
+ /** Dedup key for a whole directory's listing. Realpath keys start with `/`,
21
+ * so the scheme prefix never collides with a doc entry. */
22
+ export function dirDedupKey(dir) {
23
+ return `dir://${dir}`;
24
+ }
25
+ /** First-occurrence-wins name map over a precedence-ordered corpus: the doc
26
+ * that renders a name's line is the doc a read of that name would return. */
27
+ export function docsByName(corpus) {
28
+ const byName = new Map();
29
+ for (const d of corpus)
30
+ if (!byName.has(d.name))
31
+ byName.set(d.name, d);
32
+ return byName;
33
+ }
34
+ /** Is `name` a directory in the merged name tree — i.e. does any doc live
35
+ * beneath it? The corpus root ('') is never a directory here: store roots
36
+ * merge into the whole catalog, which `memory list` owns. */
37
+ export function isDirName(byName, name) {
38
+ if (name === '')
39
+ return false;
40
+ const prefix = name + '/';
41
+ for (const n of byName.keys())
42
+ if (n.startsWith(prefix))
43
+ return true;
44
+ return false;
45
+ }
46
+ /** The dirs a doc read discloses: the doc's own directory, then each ancestor
47
+ * walking up, the root excluded. `a/b/c` → `['a/b', 'a']`. */
48
+ export function ancestorDirsOf(name) {
49
+ const dirs = [];
50
+ const segs = name.split('/');
51
+ for (let i = segs.length - 1; i >= 1; i--)
52
+ dirs.push(segs.slice(0, i).join('/'));
53
+ return dirs;
54
+ }
55
+ /** Render one directory's listing lines: `[[name]]: <when-and-why>` per
56
+ * member doc (bare `[[name]]` when the doc carries no routing line), a bare
57
+ * `[[name]]` per subdirectory. `unlisted` members are omitted always;
58
+ * `filterDelivered` additionally drops members already delivered at preview
59
+ * rank or higher. Rendered members record at preview rank into `seen`. */
60
+ export function renderDirListing(byName, dir, seen, filterDelivered) {
61
+ const prefix = dir + '/';
62
+ const memberLines = new Map();
63
+ const subdirs = new Set();
64
+ for (const [name, doc] of byName) {
65
+ if (!name.startsWith(prefix))
66
+ continue;
67
+ const rest = name.slice(prefix.length);
68
+ const slash = rest.indexOf('/');
69
+ if (slash >= 0) {
70
+ subdirs.add(rest.slice(0, slash));
71
+ continue;
72
+ }
73
+ const sub = parseSubstrateDoc(doc);
74
+ if (sub?.unlisted)
75
+ continue;
76
+ const real = realpathOrSelf(doc.path);
77
+ if (filterDelivered && seen !== null && deliveredAtOrAbove(seen, real, 'preview'))
78
+ continue;
79
+ const line = sub === null ? '' : previewLine(sub);
80
+ memberLines.set(rest, line === '' ? `[[${name}]]` : `[[${name}]]: ${line}`);
81
+ if (seen !== null)
82
+ recordDelivery(seen, real, 'preview');
83
+ }
84
+ return [
85
+ ...[...memberLines.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([, l]) => l),
86
+ ...[...subdirs].sort((a, b) => a.localeCompare(b)).map((s) => `[[${prefix}${s}]]`),
87
+ ];
88
+ }
@@ -1,9 +1,9 @@
1
+ import type { InjectedDocs } from './injected-store.js';
1
2
  /** Surface docs matched by a successful read tool call. An unknown node id or
2
3
  * an unreadable canvas renders nothing. */
3
- export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: Set<string>): string;
4
+ export declare function renderOnReadDocs(nodeId: string, readFilePath: string, seen?: InjectedDocs): string;
4
5
  /** Surface workspace-wide docs during first-message assembly, using the node's
5
- * recorded cwd and selected profile. `.` is a reserved applies-to target
6
- * meaning “when this project store is mounted by cwd/profile” (and, on the
7
- * read event, “a file under the store's owning dir was read”). `seen` is the
6
+ * recorded cwd and selected profile: docs in mounted project stores carrying
7
+ * a `workspace-open` surfaces entry deliver here. `seen` is the
8
8
  * transcript-scoped dedup set shared with the on-read hook. */
9
- export declare function renderWorkspaceOpenDocs(nodeId: string, seen?: Set<string>): string;
9
+ export declare function renderWorkspaceOpenDocs(nodeId: string, seen?: InjectedDocs): string;
@@ -8,7 +8,7 @@ import { assembleNodeSubject } from './subject.js';
8
8
  import { renderOnReadDocsForSubject, renderWorkspaceOpenDocsForSubject } from './on-read.js';
9
9
  /** Surface docs matched by a successful read tool call. An unknown node id or
10
10
  * an unreadable canvas renders nothing. */
11
- export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
11
+ export function renderOnReadDocs(nodeId, readFilePath, seen = new Map()) {
12
12
  let subject;
13
13
  try {
14
14
  subject = assembleNodeSubject(nodeId);
@@ -21,11 +21,10 @@ export function renderOnReadDocs(nodeId, readFilePath, seen = new Set()) {
21
21
  return renderOnReadDocsForSubject(subject, readFilePath, seen);
22
22
  }
23
23
  /** Surface workspace-wide docs during first-message assembly, using the node's
24
- * recorded cwd and selected profile. `.` is a reserved applies-to target
25
- * meaning “when this project store is mounted by cwd/profile” (and, on the
26
- * read event, “a file under the store's owning dir was read”). `seen` is the
24
+ * recorded cwd and selected profile: docs in mounted project stores carrying
25
+ * a `workspace-open` surfaces entry deliver here. `seen` is the
27
26
  * transcript-scoped dedup set shared with the on-read hook. */
28
- export function renderWorkspaceOpenDocs(nodeId, seen = new Set()) {
27
+ export function renderWorkspaceOpenDocs(nodeId, seen = new Map()) {
29
28
  try {
30
29
  const subject = assembleNodeSubject(nodeId);
31
30
  const node = getNode(nodeId);
@@ -1,9 +1,30 @@
1
+ import { type InjectedDocs } from './injected-store.js';
2
+ import { type Rung, type SubstrateDoc } from './schema.js';
1
3
  import type { NodeConfigSubject } from './subject-fields.js';
2
- /** Surface docs matched by a successful read tool call using the caller-supplied
3
- * daemon snapshot subject. */
4
- export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, seen?: Set<string>): string;
4
+ /** Render candidates at their matched rungs, gate-checked and deduped against
5
+ * the transcript set (higher rungs pierce lower; a delivery records at its
6
+ * rendered rung). A `null` subject means no node identity is available for
7
+ * gate evaluation: gated docs are skipped, ungated docs still deliver. */
8
+ export declare function renderCandidateBlocks(subject: NodeConfigSubject | null, candidates: Array<{
9
+ doc: SubstrateDoc;
10
+ rung: Rung;
11
+ }>, seen: InjectedDocs): string[];
12
+ /** Surface docs whose `read` entries match a successfully read file, using
13
+ * the caller-supplied daemon snapshot subject. */
14
+ export declare function renderOnReadDocsForSubject(subject: NodeConfigSubject, readFilePath: string, seen?: InjectedDocs): string;
5
15
  /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
6
16
  * lookup. `seen` is the transcript-scoped dedup set shared with the on-read
7
17
  * hook, so a front door delivered here never re-delivers on a later read
8
18
  * beneath its store. */
9
- export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null, seen?: Set<string>): string;
19
+ export declare function renderWorkspaceOpenDocsForSubject(subject: NodeConfigSubject, cwd: string, profileId: string | null, seen?: InjectedDocs): string;
20
+ /** Inner `<memory>` blocks for the memory-read event: docs whose
21
+ * `memory-read` entries match the doc a `crtr memory read` just resolved
22
+ * (known by `targetNames`; `excludeRealpath` keeps the resolved doc from
23
+ * delivering itself). Returns unwrapped blocks — the read leaf composes one
24
+ * `<auto-loaded-context>` envelope from these plus the directory listings. */
25
+ export declare function memoryReadDocBlocks(subject: NodeConfigSubject | null, excludeRealpath: string, targetNames: string[], seen: InjectedDocs): string[];
26
+ /** Surface docs whose `command` entries match an executed bash command
27
+ * string. The corpus is the resolved cwd/profile set only — a command has no
28
+ * file to walk enclosing stores from. `seen` is the same transcript-scoped
29
+ * dedup set as the other channels. */
30
+ export declare function renderOnCommandDocsForSubject(subject: NodeConfigSubject, command: string, seen?: InjectedDocs): string;
@@ -1,45 +1,41 @@
1
- // on-read.ts — the pure file-context delivery functions for the document
2
- // substrate. Both take a caller-supplied subject; the canvas-db wrappers that
3
- // resolve one from a node id live in on-read-node.ts, so a consumer that
4
- // already holds a subject imports this module WITHOUT pulling subject.js's
5
- // canvas-db reach (the same split as render.ts / render-node.ts).
1
+ // on-read.ts — the pure event-delivery renderers for the document substrate.
2
+ // Each takes a caller-supplied subject; the canvas-db wrappers that resolve
3
+ // one from a node id live in on-read-node.ts, so a consumer that already
4
+ // holds a subject imports this module WITHOUT pulling subject.js's canvas-db
5
+ // reach (the same split as render.ts / render-node.ts).
6
6
  //
7
- // A memory with file-read visibility participates only through an explicit
8
- // `applies-to` path glob (authoring lint requires one). Two events use that one
9
- // routing mechanism:
7
+ // A doc participates through its `surfaces` entries (surface-match.ts owns
8
+ // the per-entry matchers; this module folds them per doc and renders):
10
9
  //
11
- // • A successful `read` tool call evaluates the read file against every
12
- // visible substrate doc. Project stores encountered between that file and
13
- // the filesystem root join the resolved cwd/profile corpus, so a nested
14
- // workspace can contribute guidance exactly when a file beneath it is read.
15
- // On this event the reserved `applies-to: "."` target means "the read file
16
- // is under the doc's store's owning directory" — a nested store's front
17
- // door surfaces on the first read beneath it, delivery driven purely by
18
- // filesystem truth.
19
- // • Opening a workspace evaluates the reserved `applies-to: "."` target for
20
- // every project store mounted by the cwd and selected profile. This happens
21
- // while the first-message bearings are assembled, so workspace-wide context
22
- // arrives before the task rather than waiting for an arbitrary first file.
23
- // Both hooks share one transcript-scoped dedup set, so a doc delivered at
24
- // workspace open never re-delivers on a later matching read.
10
+ // • `read` — a successful read tool call evaluates the read file against
11
+ // every doc's `read` entries: path globs vs the absolute path and
12
+ // basename, `./`-anchored globs vs the path relative to the doc's store's
13
+ // owning repo dir (project stores only), `match-frontmatter` vs the read
14
+ // file's own YAML. Project stores encountered between the file and the
15
+ // filesystem root join the resolved cwd/profile corpus, so a nested
16
+ // workspace contributes guidance exactly when a file beneath it is read.
17
+ // • `workspace-open` — opening a workspace delivers docs carrying a
18
+ // `workspace-open` entry from every project store mounted by the cwd and
19
+ // selected profile, while the first-message bearings are assembled — so
20
+ // workspace-wide context arrives before the task rather than waiting for
21
+ // an arbitrary first file.
22
+ // • `command` — a `bash` tool call evaluates the executed command string
23
+ // against every doc's `command` entries (string globs, `*` crossing `/`).
25
24
  //
26
- // `read-when` remains an additional OR trigger over a read markdown file's own
27
- // frontmatter. It does not replace `applies-to`: every non-none file-read rung
28
- // still declares its path boundary explicitly.
29
- //
30
- // Every candidate renders at its own fileReadVisibility rung. Explicit routing
31
- // is the whole on-read model, so directory INDEX ceilings remain a boot-catalog
32
- // concern and do not cap a deliberately matched document.
25
+ // All channels share one transcript-scoped (realpath → rung rank) dedup set,
26
+ // so a doc delivered by one never re-delivers at the same or lower rung
27
+ // through another. Each candidate renders at its matched rung — the highest
28
+ // `at` over its matching entries for the event.
33
29
  import { homedir } from 'node:os';
34
- import { basename, dirname, isAbsolute, matchesGlob, parse, relative, sep } from 'node:path';
30
+ import { dirname, parse, relative, sep } from 'node:path';
35
31
  import { CRTR_DIR_NAME } from '../../types.js';
36
32
  import { pathExists, readText, walkFiles } from '../fs-utils.js';
37
33
  import { parseFrontmatterGeneric } from '../frontmatter.js';
38
- import { evalCondition } from '../predicate.js';
39
34
  import { listAllMemoryDocs, listProjectMemoryDocs } from '../memory-resolver.js';
40
35
  import { userScopeRoot } from '../scope.js';
41
- import { displayName } from './ceiling.js';
42
36
  import { gatePasses } from './gate.js';
37
+ import { commandDeliveryRung, memoryReadDeliveryRung, owningRootOf, readDeliveryRung, workspaceOpenRung } from './surface-match.js';
38
+ import { deliveredAtOrAbove, recordDelivery } from './injected-store.js';
43
39
  import { normalizeDocName, parseSubstrateDoc, parseSubstrateFrontmatter, previewLine, resolveDocName } from './schema.js';
44
40
  import { cachedSubstrateDocs } from './session-cache.js';
45
41
  import { realpathOrSelf } from '../fs-utils.js';
@@ -75,13 +71,6 @@ function disp(p) {
75
71
  function isJunkAncestor(dir) {
76
72
  return dir.split(sep).some((segment) => JUNK_DIRS.has(segment));
77
73
  }
78
- function owningRootOf(doc) {
79
- const parts = doc.path.split(sep);
80
- const idx = parts.lastIndexOf(CRTR_DIR_NAME);
81
- if (idx <= 0)
82
- return null;
83
- return parts.slice(0, idx).join(sep) || sep;
84
- }
85
74
  function loadProjectDoc(file, memoryDir) {
86
75
  try {
87
76
  const raw = relative(memoryDir, file)
@@ -168,31 +157,6 @@ function dedupeByPhysicalPath(docs) {
168
157
  }
169
158
  return out;
170
159
  }
171
- function globMatches(glob, absReadFile, owningRoot) {
172
- const targets = [absReadFile, basename(absReadFile)];
173
- if (owningRoot !== null)
174
- targets.push(relative(owningRoot, absReadFile));
175
- return targets.some((target) => {
176
- try {
177
- return matchesGlob(target, glob);
178
- }
179
- catch {
180
- return false;
181
- }
182
- });
183
- }
184
- /** True when the read file sits beneath the doc's store's owning directory —
185
- * the read-event meaning of the reserved `applies-to: "."` target. Callers
186
- * additionally guard on `doc.scope === 'project'`: a profile store's path
187
- * computes an owning root of `~`, and user/builtin/node docs likewise have no
188
- * project-owning dir, so `.` outside a project store must never match. */
189
- function underOwningRoot(doc, absReadFile) {
190
- const root = owningRootOf(doc);
191
- if (root === null)
192
- return false;
193
- const rel = relative(realpathOrSelf(root), absReadFile);
194
- return rel !== '' && !rel.startsWith('..') && !isAbsolute(rel);
195
- }
196
160
  function readFileFrontmatter(absReadFile) {
197
161
  if (!/\.(md|mdx|markdown)$/i.test(absReadFile))
198
162
  return {};
@@ -203,24 +167,7 @@ function readFileFrontmatter(absReadFile) {
203
167
  return {};
204
168
  }
205
169
  }
206
- function matchesReadEvent(doc, absReadFile, readFrontmatter) {
207
- const pathMatch = doc.appliesTo?.some((glob) => glob.trim() === '.'
208
- ? doc.scope === 'project' && underOwningRoot(doc, absReadFile)
209
- : globMatches(glob, absReadFile, owningRootOf(doc))) === true;
210
- const frontmatterMatch = doc.readWhen !== undefined &&
211
- Object.keys(readFrontmatter).length > 0 &&
212
- evalCondition(doc.readWhen, readFrontmatter);
213
- return pathMatch || frontmatterMatch;
214
- }
215
- function envelopeName(doc) {
216
- const displayed = displayName(doc.name);
217
- if (displayed !== '')
218
- return displayed;
219
- const root = owningRootOf(doc);
220
- return root === null ? doc.name : basename(root);
221
- }
222
- function renderDocEnvelope(doc) {
223
- const rung = doc.fileReadVisibility;
170
+ function renderDocEnvelope(doc, rung) {
224
171
  if (rung === 'none')
225
172
  return null;
226
173
  let body = '';
@@ -228,47 +175,56 @@ function renderDocEnvelope(doc) {
228
175
  body = doc.body.trim();
229
176
  else if (rung === 'preview')
230
177
  body = previewLine(doc);
231
- const attrs = `kind="${attr(doc.kind)}" name="${attr(envelopeName(doc))}" src="${attr(disp(doc.path))}"`;
178
+ const attrs = `kind="${attr(doc.kind)}" name="${attr(doc.name)}" src="${attr(disp(doc.path))}"`;
232
179
  return body === '' ? `<memory ${attrs} />` : `<memory ${attrs}>\n${body}\n</memory>`;
233
180
  }
234
- function renderCandidates(subject, docs, seen) {
181
+ /** Render candidates at their matched rungs, gate-checked and deduped against
182
+ * the transcript set (higher rungs pierce lower; a delivery records at its
183
+ * rendered rung). A `null` subject means no node identity is available for
184
+ * gate evaluation: gated docs are skipped, ungated docs still deliver. */
185
+ export function renderCandidateBlocks(subject, candidates, seen) {
235
186
  const rendered = [];
236
- for (const doc of docs) {
187
+ for (const { doc, rung } of candidates) {
237
188
  const real = realpathOrSelf(doc.path);
238
- if (seen.has(real))
189
+ if (deliveredAtOrAbove(seen, real, rung))
239
190
  continue;
240
191
  try {
241
- if (!gatePasses(doc, subject))
192
+ if (subject === null ? doc.gate !== undefined : !gatePasses(doc, subject))
242
193
  continue;
243
- const block = renderDocEnvelope(doc);
194
+ const block = renderDocEnvelope(doc, rung);
244
195
  if (block === null)
245
196
  continue;
246
- seen.add(real);
197
+ recordDelivery(seen, real, rung);
247
198
  rendered.push(block);
248
199
  }
249
200
  catch {
250
201
  continue;
251
202
  }
252
203
  }
204
+ return rendered;
205
+ }
206
+ function renderCandidates(subject, candidates, seen) {
207
+ const rendered = renderCandidateBlocks(subject, candidates, seen);
253
208
  return rendered.length === 0
254
209
  ? ''
255
210
  : `<auto-loaded-context>\n${rendered.join('\n')}\n</auto-loaded-context>`;
256
211
  }
257
- /** Surface docs matched by a successful read tool call using the caller-supplied
258
- * daemon snapshot subject. */
259
- export function renderOnReadDocsForSubject(subject, readFilePath, seen = new Set()) {
212
+ /** Surface docs whose `read` entries match a successfully read file, using
213
+ * the caller-supplied daemon snapshot subject. */
214
+ export function renderOnReadDocsForSubject(subject, readFilePath, seen = new Map()) {
260
215
  const absReadFile = realpathOrSelf(readFilePath);
261
216
  const readFrontmatter = readFileFrontmatter(absReadFile);
262
- const docs = dedupeByPhysicalPath([...enclosingProjectDocs(absReadFile), ...resolvedDocs()])
217
+ const candidates = dedupeByPhysicalPath([...enclosingProjectDocs(absReadFile), ...resolvedDocs()])
263
218
  .filter((doc) => realpathOrSelf(doc.path) !== absReadFile)
264
- .filter((doc) => matchesReadEvent(doc, absReadFile, readFrontmatter));
265
- return renderCandidates(subject, docs, seen);
219
+ .map((doc) => ({ doc, rung: readDeliveryRung(doc, absReadFile, readFrontmatter) }))
220
+ .filter(({ rung }) => rung !== 'none');
221
+ return renderCandidates(subject, candidates, seen);
266
222
  }
267
223
  /** Workspace boot docs using the snapshot's cwd/profile instead of a canvas
268
224
  * lookup. `seen` is the transcript-scoped dedup set shared with the on-read
269
225
  * hook, so a front door delivered here never re-delivers on a later read
270
226
  * beneath its store. */
271
- export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, seen = new Set()) {
227
+ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, seen = new Map()) {
272
228
  let docs;
273
229
  try {
274
230
  docs = listProjectMemoryDocs(cwd, profileId)
@@ -278,13 +234,36 @@ export function renderWorkspaceOpenDocsForSubject(subject, cwd, profileId, seen
278
234
  catch {
279
235
  return '';
280
236
  }
281
- docs = dedupeByPhysicalPath(docs)
282
- .filter((doc) => doc.appliesTo?.some((glob) => glob.trim() === '.') === true)
237
+ const candidates = dedupeByPhysicalPath(docs)
238
+ .map((doc) => ({ doc, rung: workspaceOpenRung(doc) }))
239
+ .filter(({ rung }) => rung !== 'none')
283
240
  .sort((a, b) => {
284
- const aRoot = owningRootOf(a) ?? '';
285
- const bRoot = owningRootOf(b) ?? '';
241
+ const aRoot = owningRootOf(a.doc) ?? '';
242
+ const bRoot = owningRootOf(b.doc) ?? '';
286
243
  const depth = aRoot.split(sep).filter(Boolean).length - bRoot.split(sep).filter(Boolean).length;
287
- return depth || aRoot.localeCompare(bRoot) || a.path.localeCompare(b.path);
244
+ return depth || aRoot.localeCompare(bRoot) || a.doc.path.localeCompare(b.doc.path);
288
245
  });
289
- return renderCandidates(subject, docs, seen);
246
+ return renderCandidates(subject, candidates, seen);
247
+ }
248
+ /** Inner `<memory>` blocks for the memory-read event: docs whose
249
+ * `memory-read` entries match the doc a `crtr memory read` just resolved
250
+ * (known by `targetNames`; `excludeRealpath` keeps the resolved doc from
251
+ * delivering itself). Returns unwrapped blocks — the read leaf composes one
252
+ * `<auto-loaded-context>` envelope from these plus the directory listings. */
253
+ export function memoryReadDocBlocks(subject, excludeRealpath, targetNames, seen) {
254
+ const candidates = dedupeByPhysicalPath(resolvedDocs())
255
+ .filter((doc) => realpathOrSelf(doc.path) !== excludeRealpath)
256
+ .map((doc) => ({ doc, rung: memoryReadDeliveryRung(doc, targetNames) }))
257
+ .filter(({ rung }) => rung !== 'none');
258
+ return renderCandidateBlocks(subject, candidates, seen);
259
+ }
260
+ /** Surface docs whose `command` entries match an executed bash command
261
+ * string. The corpus is the resolved cwd/profile set only — a command has no
262
+ * file to walk enclosing stores from. `seen` is the same transcript-scoped
263
+ * dedup set as the other channels. */
264
+ export function renderOnCommandDocsForSubject(subject, command, seen = new Map()) {
265
+ const candidates = dedupeByPhysicalPath(resolvedDocs())
266
+ .map((doc) => ({ doc, rung: commandDeliveryRung(doc, command) }))
267
+ .filter(({ rung }) => rung !== 'none');
268
+ return renderCandidates(subject, candidates, seen);
290
269
  }
@@ -1,3 +1,4 @@
1
+ import type { InjectedDocs } from './injected-store.js';
1
2
  /** The system-prompt `<memory kind="preference">` block for `nodeId`: assemble
2
3
  * the node's subject from the canvas-db, render the pure block, then swap the
3
4
  * literal `$CRTR_CONTEXT_DIR` env-var token for the node's real absolute path
@@ -7,5 +8,7 @@
7
8
  export declare function renderPreferencesSection(nodeId: string): string;
8
9
  /** The first-message `<memory kind="knowledge">` block for `nodeId`: assemble
9
10
  * the node's subject from the canvas-db and render the consultable catalog.
10
- * Returns '' for an unknown id or when nothing is eligible. */
11
- export declare function renderKnowledgeBlock(nodeId: string): string;
11
+ * Returns '' for an unknown id or when nothing is eligible. `seen` (optional,
12
+ * write-only) receives each rendered doc at its displayed rung — boot
13
+ * delivery seeding for the transcript dedup map. */
14
+ export declare function renderKnowledgeBlock(nodeId: string, seen?: InjectedDocs): string;