@north-light/crouter 0.3.227 → 0.3.228

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 (286) hide show
  1. package/dist/api/client.d.ts +5 -5
  2. package/dist/api/client.js +3 -3
  3. package/dist/api/dto/broker-ops.d.ts +8 -0
  4. package/dist/api/dto/memory.d.ts +8 -7
  5. package/dist/api/dto/memory.js +2 -2
  6. package/dist/builtin-memory/05-kinds/design/00-base.md +3 -4
  7. package/dist/builtin-memory/05-kinds/design/01-orchestrator.md +4 -3
  8. package/dist/builtin-memory/05-kinds/design/design-contract.md +3 -6
  9. package/dist/builtin-memory/05-kinds/plan/00-base.md +3 -4
  10. package/dist/builtin-memory/05-kinds/plan/01-orchestrator.md +4 -5
  11. package/dist/builtin-memory/05-kinds/plan/plan-contract.md +5 -13
  12. package/dist/builtin-memory/design/guide.md +38 -16
  13. package/dist/builtin-memory/design/roadmap.md +4 -4
  14. package/dist/builtin-memory/insights/init.md +5 -5
  15. package/dist/builtin-memory/internal/INDEX.md +1 -1
  16. package/dist/builtin-memory/internal/examples/INDEX.md +1 -1
  17. package/dist/builtin-memory/internal/memory-loading.md +6 -6
  18. package/dist/builtin-memory/internal/plugins.md +1 -1
  19. package/dist/builtin-memory/plan/guide.md +53 -0
  20. package/dist/builtin-memory/plan/roadmap.md +10 -8
  21. package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/memory-slash-commands.ts +50 -37
  22. package/dist/clients/attach/__tests__/pane-tag-successor.test.js +56 -0
  23. package/dist/clients/attach/__tests__/profile-files.test.js +21 -0
  24. package/dist/clients/attach/chrome/inbox-strip.js +11 -7
  25. package/dist/clients/attach/session/pane-tag.d.ts +28 -2
  26. package/dist/clients/attach/session/pane-tag.js +48 -22
  27. package/dist/clients/attach/session/profile-files.d.ts +6 -0
  28. package/dist/clients/attach/session/profile-files.js +44 -11
  29. package/dist/clients/attach/slash/dispatch.js +4 -7
  30. package/dist/clients/attach/viewer.js +588 -596
  31. package/dist/commands/__tests__/surface-reload-target.test.js +21 -0
  32. package/dist/commands/api-client.d.ts +3 -7
  33. package/dist/commands/api-client.js +7 -12
  34. package/dist/commands/memory/__tests__/command-selector-and-mutation-guards.test.js +292 -0
  35. package/dist/commands/memory/__tests__/repository-root-lint.test.js +146 -0
  36. package/dist/commands/memory/delete.js +51 -24
  37. package/dist/commands/memory/edit.js +50 -10
  38. package/dist/commands/memory/find.js +105 -56
  39. package/dist/commands/memory/history.js +51 -31
  40. package/dist/commands/memory/lint.d.ts +0 -9
  41. package/dist/commands/memory/lint.js +78 -322
  42. package/dist/commands/memory/list.d.ts +12 -4
  43. package/dist/commands/memory/list.js +64 -25
  44. package/dist/commands/memory/move.js +103 -73
  45. package/dist/commands/memory/origin.js +35 -5
  46. package/dist/commands/memory/read.d.ts +4 -0
  47. package/dist/commands/memory/read.js +132 -80
  48. package/dist/commands/memory/shared.d.ts +115 -25
  49. package/dist/commands/memory/shared.js +331 -74
  50. package/dist/commands/memory/write.js +72 -21
  51. package/dist/commands/memory.js +2 -2
  52. package/dist/commands/node/lifecycle.js +2 -2
  53. package/dist/commands/pkg/market-manage.js +25 -24
  54. package/dist/commands/pkg/plugin-manage.d.ts +22 -6
  55. package/dist/commands/pkg/plugin-manage.js +118 -25
  56. package/dist/commands/pkg/shared.d.ts +8 -0
  57. package/dist/commands/pkg/shared.js +23 -20
  58. package/dist/commands/surface-reload.d.ts +5 -0
  59. package/dist/commands/surface-reload.js +9 -1
  60. package/dist/commands/sys/__tests__/migrate.test.js +1140 -22
  61. package/dist/commands/sys/__tests__/sync-project-guidance.test.js +218 -0
  62. package/dist/commands/sys/migrate.js +126 -140
  63. package/dist/commands/sys/panels/profiles-panel.d.ts +66 -0
  64. package/dist/commands/sys/panels/profiles-panel.js +599 -0
  65. package/dist/commands/sys/settings-shell.d.ts +4 -2
  66. package/dist/commands/sys/settings-shell.js +35 -4
  67. package/dist/commands/sys/settings.js +21 -5
  68. package/dist/commands/sys/sync-deps.d.ts +2 -0
  69. package/dist/commands/sys/sync-deps.js +26 -15
  70. package/dist/commands/sys/sync-project-guidance.js +228 -145
  71. package/dist/commands/sys/sync-skills.js +28 -17
  72. package/dist/commands/sys/update.js +11 -3
  73. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +15 -2
  74. package/dist/core/__tests__/config-change-delta.test.js +106 -0
  75. package/dist/core/__tests__/context-intro.test.js +13 -9
  76. package/dist/core/__tests__/daemon-boot.test.js +22 -16
  77. package/dist/core/__tests__/fixtures/memory-slash-live-probe.js +5 -1
  78. package/dist/core/__tests__/human-deliver.test.js +19 -0
  79. package/dist/core/__tests__/inline-memory-refs.test.js +50 -46
  80. package/dist/core/__tests__/{serial → integration}/broker-fork-seam.test.js +1 -1
  81. package/dist/core/__tests__/{serial → integration}/broker-sdk-wiring.test.js +1 -1
  82. package/dist/core/__tests__/{serial → integration}/broker-snapshot-history.test.js +1 -1
  83. package/dist/core/__tests__/{serial → integration}/command-plugins.test.js +94 -3
  84. package/dist/core/__tests__/{serial → integration}/deferred-no-wake.test.js +1 -1
  85. package/dist/core/__tests__/{serial → integration}/flagship-lifecycle.test.js +13 -27
  86. package/dist/core/__tests__/{serial → integration}/host-teardown-process-group.test.js +1 -1
  87. package/dist/core/__tests__/{serial → integration}/human-deliver-e2e.test.js +1 -1
  88. package/dist/core/__tests__/{live-mutation-verbs.test.js → integration/live-mutation-verbs.test.js} +22 -33
  89. package/dist/core/__tests__/{serial → integration}/live-mutation.test.js +21 -41
  90. package/dist/core/__tests__/integration/refresh-stall-recycle.test.d.ts +1 -0
  91. package/dist/core/__tests__/{serial → integration}/refresh-stall-recycle.test.js +1 -1
  92. package/dist/core/__tests__/integration/revive.test.d.ts +1 -0
  93. package/dist/core/__tests__/{serial → integration}/revive.test.js +38 -19
  94. package/dist/core/__tests__/integration/spawn-root.test.d.ts +1 -0
  95. package/dist/core/__tests__/{serial → integration}/spawn-root.test.js +104 -1
  96. package/dist/core/__tests__/integration/subscription-delivery.test.d.ts +1 -0
  97. package/dist/core/__tests__/{serial → integration}/subscription-delivery.test.js +1 -1
  98. package/dist/core/__tests__/integration/tmux-surface.test.d.ts +1 -0
  99. package/dist/core/__tests__/{serial → integration}/tmux-surface.test.js +1 -1
  100. package/dist/core/__tests__/integration/worktree-land.test.d.ts +1 -0
  101. package/dist/core/__tests__/integration/worktree-land.test.js +400 -0
  102. package/dist/core/__tests__/integration/worktree-reap.test.d.ts +1 -0
  103. package/dist/core/__tests__/{serial/worktree.test.js → integration/worktree-reap.test.js} +6 -338
  104. package/dist/core/__tests__/kickoff.test.js +16 -5
  105. package/dist/core/__tests__/memory-resolver-precedence.test.js +122 -91
  106. package/dist/core/__tests__/nested-store-discovery.test.js +5 -3
  107. package/dist/core/__tests__/on-read-crouter-home-fence.test.js +2 -1
  108. package/dist/core/__tests__/on-read-dedup-resume.test.js +39 -27
  109. package/dist/core/__tests__/on-read-nested-store.test.js +19 -13
  110. package/dist/core/__tests__/profile-project-memory-delivery.test.js +138 -44
  111. package/dist/core/__tests__/repository-association.test.d.ts +1 -0
  112. package/dist/core/__tests__/repository-association.test.js +153 -0
  113. package/dist/core/__tests__/repository-root-identity.test.d.ts +1 -0
  114. package/dist/core/__tests__/repository-root-identity.test.js +219 -0
  115. package/dist/core/__tests__/seam/memory-slash-node-relative-inventory.test.js +27 -11
  116. package/dist/core/__tests__/warm-claim-preference-snapshot.test.d.ts +1 -0
  117. package/dist/core/__tests__/warm-claim-preference-snapshot.test.js +40 -0
  118. package/dist/core/canvas/canvas.d.ts +3 -0
  119. package/dist/core/canvas/canvas.js +30 -10
  120. package/dist/core/canvas/db.js +28 -1
  121. package/dist/core/canvas/paths.d.ts +4 -6
  122. package/dist/core/canvas/paths.js +8 -6
  123. package/dist/core/canvas/render-source.js +2 -2
  124. package/dist/core/canvas/types.d.ts +9 -13
  125. package/dist/core/exclusive-lock.d.ts +2 -0
  126. package/dist/core/exclusive-lock.js +21 -0
  127. package/dist/core/git.d.ts +0 -1
  128. package/dist/core/git.js +0 -3
  129. package/dist/core/human/__tests__/integration/inbox-core.test.d.ts +1 -0
  130. package/dist/core/human/feedback-companion.js +3 -0
  131. package/dist/core/human/scan.js +8 -1
  132. package/dist/core/keybindings/catalog.d.ts +2 -2
  133. package/dist/core/keybindings/catalog.js +2 -1
  134. package/dist/core/memory/doc-link-grammar.js +2 -2
  135. package/dist/core/memory/history.d.ts +24 -0
  136. package/dist/core/memory/history.js +66 -1
  137. package/dist/core/memory/identity.d.ts +65 -0
  138. package/dist/core/memory/identity.js +185 -0
  139. package/dist/core/memory/inline-ref-guidance.d.ts +1 -1
  140. package/dist/core/memory/inline-ref-guidance.js +1 -1
  141. package/dist/core/memory/inline-ref-inventory.d.ts +3 -10
  142. package/dist/core/memory/inline-ref-inventory.js +30 -62
  143. package/dist/core/memory/lint.d.ts +129 -0
  144. package/dist/core/memory/lint.js +517 -0
  145. package/dist/core/memory/project-namespace.d.ts +31 -0
  146. package/dist/core/memory/project-namespace.js +85 -0
  147. package/dist/core/memory/repository-association.d.ts +31 -0
  148. package/dist/core/memory/repository-association.js +129 -0
  149. package/dist/core/memory/tree.d.ts +39 -0
  150. package/dist/core/memory/tree.js +93 -0
  151. package/dist/core/memory-resolver.d.ts +193 -90
  152. package/dist/core/memory-resolver.js +461 -375
  153. package/dist/core/nested-stores.js +6 -13
  154. package/dist/core/profiles/select.d.ts +4 -1
  155. package/dist/core/profiles/select.js +108 -48
  156. package/dist/core/review/__tests__/capture-origin.test.js +2 -2
  157. package/dist/core/review/__tests__/stage-identity.test.js +2 -2
  158. package/dist/core/review/companion.js +11 -2
  159. package/dist/core/runtime/bearings.d.ts +2 -2
  160. package/dist/core/runtime/bearings.js +3 -3
  161. package/dist/core/runtime/broker/daemon-ops.d.ts +2 -2
  162. package/dist/core/runtime/broker/rebind.js +5 -0
  163. package/dist/core/runtime/broker-extension-render.d.ts +7 -3
  164. package/dist/core/runtime/broker-extension-render.js +10 -5
  165. package/dist/core/runtime/broker-persona-guidance.d.ts +19 -5
  166. package/dist/core/runtime/broker-persona-guidance.js +122 -27
  167. package/dist/core/runtime/deliver-live.d.ts +16 -4
  168. package/dist/core/runtime/deliver-live.js +29 -15
  169. package/dist/core/runtime/kickoff.d.ts +3 -3
  170. package/dist/core/runtime/kickoff.js +8 -16
  171. package/dist/core/runtime/lifecycle.js +2 -3
  172. package/dist/core/runtime/nodes.d.ts +3 -4
  173. package/dist/core/runtime/nodes.js +3 -4
  174. package/dist/core/runtime/persona.d.ts +8 -12
  175. package/dist/core/runtime/persona.js +24 -96
  176. package/dist/core/runtime/promote.d.ts +2 -2
  177. package/dist/core/runtime/promote.js +8 -21
  178. package/dist/core/runtime/revive.js +20 -17
  179. package/dist/core/runtime/spawn.js +18 -7
  180. package/dist/core/runtime/tmux-bindings.js +2 -3
  181. package/dist/core/runtime/warm-pool.js +3 -4
  182. package/dist/core/scope.js +2 -0
  183. package/dist/core/self-update.d.ts +0 -2
  184. package/dist/core/self-update.js +2 -35
  185. package/dist/core/substrate/__tests__/surface-match-memory-read.test.d.ts +1 -0
  186. package/dist/core/substrate/__tests__/surface-match-memory-read.test.js +28 -0
  187. package/dist/core/substrate/index.d.ts +2 -2
  188. package/dist/core/substrate/index.js +1 -1
  189. package/dist/core/substrate/injected-store.d.ts +43 -27
  190. package/dist/core/substrate/injected-store.js +208 -104
  191. package/dist/core/substrate/listings.d.ts +19 -12
  192. package/dist/core/substrate/listings.js +75 -52
  193. package/dist/core/substrate/on-read-node.d.ts +4 -7
  194. package/dist/core/substrate/on-read-node.js +6 -8
  195. package/dist/core/substrate/on-read.d.ts +22 -25
  196. package/dist/core/substrate/on-read.js +103 -147
  197. package/dist/core/substrate/render-node.d.ts +4 -7
  198. package/dist/core/substrate/render-node.js +5 -7
  199. package/dist/core/substrate/render.d.ts +21 -3
  200. package/dist/core/substrate/render.js +291 -223
  201. package/dist/core/substrate/schema.d.ts +1 -13
  202. package/dist/core/substrate/schema.js +5 -40
  203. package/dist/core/substrate/session-cache.d.ts +14 -4
  204. package/dist/core/substrate/session-cache.js +40 -22
  205. package/dist/core/substrate/surface-match.d.ts +9 -7
  206. package/dist/core/substrate/surface-match.js +26 -25
  207. package/dist/daemon/__tests__/helpers/source-daemon.d.ts +30 -0
  208. package/dist/daemon/__tests__/helpers/source-daemon.js +174 -0
  209. package/dist/daemon/__tests__/integration/migration-startup.test.d.ts +1 -0
  210. package/dist/daemon/__tests__/integration/migration-startup.test.js +97 -0
  211. package/dist/daemon/api/__tests__/bridge-heartbeat.test.js +21 -5
  212. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  213. package/dist/daemon/api/handlers/memory.js +2 -0
  214. package/dist/daemon/crtrd.js +2 -0
  215. package/dist/daemon/human/finish.js +8 -1
  216. package/dist/daemon/manage.d.ts +0 -1
  217. package/dist/daemon/manage.js +7 -16
  218. package/dist/daemon/startup-policy.d.ts +1 -0
  219. package/dist/daemon/startup-policy.js +1 -0
  220. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  221. package/dist/migrations/002-profile-project-memory.js +1 -0
  222. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  223. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  224. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  225. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  226. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  227. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  228. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  229. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  230. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  231. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  232. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation.test.js +149 -0
  234. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  236. package/dist/migrations/activation.d.ts +16 -0
  237. package/dist/migrations/activation.js +78 -0
  238. package/dist/migrations/convergent.d.ts +14 -3
  239. package/dist/migrations/convergent.js +21 -10
  240. package/dist/migrations/corpus.d.ts +78 -0
  241. package/dist/migrations/corpus.js +497 -0
  242. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  243. package/dist/migrations/frontmatter-splice.js +176 -0
  244. package/dist/migrations/registry.d.ts +6 -1
  245. package/dist/migrations/registry.js +7 -2
  246. package/dist/migrations/runner.d.ts +41 -0
  247. package/dist/migrations/runner.js +81 -0
  248. package/dist/migrations/types.d.ts +148 -9
  249. package/dist/migrations/types.js +9 -2
  250. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  251. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  252. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  253. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  254. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  255. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  256. package/dist/pi-extensions/canvas-stophook.js +6 -13
  257. package/dist/shared/generated-context.d.ts +0 -3
  258. package/dist/shared/generated-context.js +0 -57
  259. package/dist/shared/tool-groups.js +2 -3
  260. package/package.json +5 -4
  261. package/runtime.lock.json +2 -2
  262. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  263. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  264. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  265. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  266. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  267. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  268. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  269. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  272. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  273. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  275. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  276. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  285. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  286. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -1,25 +1,27 @@
1
1
  ---
2
2
  kind: knowledge
3
- when-and-why-to-read: When choosing between a flat plan and a decomposed one, or synthesizing part-plans into an index, this knowledge should be read so a planning effort splits only where a domain seam pays for the synthesis it costs.
3
+ when-and-why-to-read: When choosing between a flat plan and a decomposed one, or synthesizing part-plans into an index, this knowledge should be read so a planning effort splits only where independent execution repays the synthesis it costs.
4
4
  short-form: Use when deciding whether a planning effort splits into part-plans, and how to synthesize them into one index.
5
5
  gate: {kind: plan}
6
6
  rationale: >-
7
- Carries plan shape and decomposition only. The general planning contract — scope discipline, task quality, plan review — lives in the plan kind layer so every plan node loads it whether or not the effort ever needs a roadmap; do not pull generic planning guidance back in here.
7
+ Carries plan shape and decomposition only. The implementation-unit contract lives in [[plan/guide]] so every planner can use it whether or not the effort ever needs a roadmap; do not pull generic planning guidance back in here.
8
8
  surfaces:
9
9
  - on: boot
10
10
  at: preview
11
11
  ---
12
12
 
13
- # Plan Shapes and the Decomposition Decision
13
+ # Plan shapes and the decomposition decision
14
14
 
15
- Every planning effort produces either a flat plan or a decomposed plan (index + part-plans). Choose decomposition for worthwhile parallel planning, not raw size: a flat plan can span many yields, while part-plans add delegation and synthesis cost that independent slices must repay.
15
+ Every planning effort produces either one flat plan or an index with part-plans. Choose decomposition for worthwhile parallel planning, not raw size: a flat plan can span many yields, while part-plans add delegation and synthesis cost that independent slices must repay.
16
16
 
17
17
  ## Choosing a shape
18
18
 
19
- **Use a flat plan** when the work is a single coherent domain and can be written at consistent task granularity in one plan. A flat plan has an overview, ordered phases, and a verification section. No sub-plans. One file.
19
+ Use a **flat plan** when one coherent owner can map the work at consistent unit granularity. It follows [[plan/guide]] in one file.
20
20
 
21
- **Use a decomposed plan** when settled boundaries expose independent planning slices that can proceed concurrently and the effort is large enough that parallel work materially improves intelligence, productivity, or elapsed time after synthesis cost. Produce an index plan (the navigable master) and delegate each slice to a `plan`-kind child node, giving it the relevant spec, explicit scope, and place in the dependency graph. A slice goes to a `plan` sub-orchestrator (`crtr node new --kind plan --mode orchestrator`) only when its own work passes the same parallelism threshold; a long sequential slice goes to a base child that can yield. The index plan is the synthesis artifact — it lists all sub-plans by path, defines phases and dependencies, and contains a task table the implementation orchestrator can execute directly. Detail lives in sub-plans; the master is not allowed to carry it.
21
+ Use a **decomposed plan** when settled dependency edges expose slices that can be planned independently, their edit ownership does not overlap, and the effort is large enough that parallel work materially improves intelligence, productivity, or elapsed time. A frontend/backend distinction alone is not a decomposition trigger when both sides still share an unsettled seam or edit surface.
22
22
 
23
- **The decomposition trigger is domain boundary, not size alone.** Three backend files and three frontend files are two domains even if the total count is modest — plan them separately and synthesize, because the integration seam is where bugs live and one agent reading both halves won't catch them as cleanly as two agents each going deep.
23
+ Produce a navigable index and delegate each slice to a `plan`-kind child with the approved spec and design, explicit scope, dependencies, and ownership boundary. A slice goes to a `plan` sub-orchestrator only when it independently passes the same parallelism threshold; a long sequential slice stays with a base child across yields.
24
24
 
25
- After collecting part-plans from children, synthesize before declaring done: resolve file ownership conflicts (two sub-plans naming the same file means you decide the sequence), align naming across all parts, fill integration gaps at domain boundaries, and ensure the task table in the index accurately reflects dependencies exposed only by reading all sub-plans together.
25
+ Each part-plan follows [[plan/guide]]. The index links every part-plan and carries a lane table with each plan, its outcome, dependencies, ownership boundary, and proof gate. It also maps every acceptance criterion to the responsible part-plan or a final cross-lane runtime gate. Unit detail stays in the part-plans.
26
+
27
+ After the part-plans land, synthesize the index before declaring done. Resolve file and symbol ownership conflicts, align names and phase gates, fill integration gaps, confirm the dependency graph permits every claimed parallel lane, and prove that part-plan and cross-lane gates cover the full approved outcome. Do not duplicate part-plan units in the index.
@@ -1,12 +1,20 @@
1
1
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import { listMemoryDocs } from "../../../commands/memory/list.js";
3
2
  import { createMemoryDocSnapshot, readMemoryDocContent } from "../../../commands/memory/read.js";
3
+ import { parseSubstrateDoc } from "../../../core/substrate/schema.js";
4
4
  import {
5
5
  expandDeterministicCommand,
6
6
  type DeterministicCommandExpansion,
7
7
  } from "../../../core/runtime/command-expansion.js";
8
8
  import { expandShellBlocks, hasShellBlocks, DEFAULT_SHELL_TIMEOUT_MS } from "../../../core/runtime/shell-expansion.js";
9
9
  import type { ChatCommandMetadata } from "../../../core/runtime/command-surface.js";
10
+ import {
11
+ exposureTarget,
12
+ loadContextExposureState,
13
+ mergeContextExposureState,
14
+ registerDocumentExposure,
15
+ saveContextExposureState,
16
+ sharedContextExposureState,
17
+ } from "../../../core/substrate/injected-store.js";
10
18
  import { piShellRunner } from "./pi-shell-runner.js";
11
19
 
12
20
  // ---------------------------------------------------------------------------
@@ -16,57 +24,50 @@ import { piShellRunner } from "./pi-shell-runner.js";
16
24
  // the doc body (plus the user's free-text args) as a normal turn instead of
17
25
  // requiring a separate skill-invocation mechanism.
18
26
  //
19
- // Discovery uses the in-process `memory list` inventory and its source
20
- // snapshot, which already resolve the substrate's scope precedence (project
21
- // stack > profile > user > builtin). This file does not walk memory/
22
- // directories or reimplement scope resolution. `list`'s items are
23
- // scope-precedence sorted (see `scopeRank` in crouter's memory/shared.ts), so
24
- // first-occurrence-wins per doc name reproduces the resolver's identity
25
- // precedence for free. The inventory carries parsed `slash` flags and resolved
26
- // paths for command bodies.
27
+ // Discovery uses one in-process target snapshot for both listing and body
28
+ // resolution. The snapshot already resolves exact canonical identities in
29
+ // source precedence; this file does not walk memory directories or reimplement
30
+ // scope resolution.
27
31
  // ---------------------------------------------------------------------------
28
32
 
29
33
  interface MemoryListItem {
30
34
  name: string;
31
- path: string;
32
35
  short_form: string;
33
36
  slash: boolean;
34
37
  }
35
38
 
36
39
  export interface SlashDoc {
37
- /** Path-derived doc name, e.g. "research-alpha" or "taste/foo" — what
38
- * `crtr memory read` resolves. */
40
+ /** Canonical name resolved by `crtr memory read`. */
39
41
  name: string;
40
- /** `name` with `/` -> `:` — the registered command name (a flat name is
41
- * unchanged). */
42
+ /** Canonical `/` segments rendered as ordinary `:` command segments. */
42
43
  commandName: string;
43
44
  /** The memory document's short-form description, shown in the slash menu. */
44
45
  description: string;
45
46
  }
46
47
 
47
- // `memory list` already chose each winning document. Keep its canonical body
48
- // for disclosure classification and its path for the existing execution read.
48
+ // Keep each canonical winner's body and path for disclosure classification and
49
+ // the existing execution read.
49
50
  const resolvedMemoryDocs = new Map<string, { path: string; canonicalBody: string }>();
50
51
 
51
52
  async function defaultListMemoryDocs(): Promise<MemoryListItem[]> {
52
53
  const snapshot = createMemoryDocSnapshot();
53
- const items = listMemoryDocs(undefined, undefined, true, snapshot.docs).map((doc) => ({
54
- name: doc.name,
55
- path: doc.path,
56
- short_form: doc.shortForm,
57
- slash: doc.slash,
58
- }));
59
-
60
- // Slash eligibility follows the list identity, while the body follows the
61
- // separate `memory read <name>` resolver contract (including direct-path
62
- // precedence). Resolve all eligible names against one source snapshot.
54
+ const items: MemoryListItem[] = [];
63
55
  const seen = new Set<string>();
64
- const names = items.flatMap((item) => {
65
- if (seen.has(item.name)) return [];
66
- seen.add(item.name);
67
- return item.slash === true ? [item.name] : [];
68
- });
69
- const resolved = snapshot.resolve(names);
56
+ for (const memoryDoc of snapshot.docs) {
57
+ if (seen.has(memoryDoc.name)) continue;
58
+ seen.add(memoryDoc.name);
59
+ const doc = parseSubstrateDoc(memoryDoc);
60
+ if (doc === null) continue;
61
+ items.push({
62
+ name: memoryDoc.name,
63
+ short_form: doc.shortForm,
64
+ slash: doc.slash,
65
+ });
66
+ }
67
+
68
+ // Resolve eligible canonical names against the same target snapshot that
69
+ // supplied the list rows.
70
+ const resolved = snapshot.resolve(items.filter((item) => item.slash).map((item) => item.name));
70
71
  resolvedMemoryDocs.clear();
71
72
  for (const [name, doc] of resolved) {
72
73
  resolvedMemoryDocs.set(name, { path: doc.path, canonicalBody: doc.body });
@@ -74,27 +75,36 @@ async function defaultListMemoryDocs(): Promise<MemoryListItem[]> {
74
75
  return items;
75
76
  }
76
77
 
77
- function defaultReadMemoryDoc(name: string): { canonicalBody: string; executionBody: string } {
78
+ function defaultReadMemoryDoc(name: string): { path: string; canonicalBody: string; executionBody: string } {
78
79
  const resolved = resolvedMemoryDocs.get(name);
79
80
  if (resolved === undefined) throw new Error(`memory slash document was not resolved: ${name}`);
80
81
  return {
82
+ path: resolved.path,
81
83
  canonicalBody: resolved.canonicalBody,
82
84
  executionBody: readMemoryDocContent(resolved.path),
83
85
  };
84
86
  }
85
87
 
88
+ function registerSlashExposure(path: string, body: string): void {
89
+ const nodeId = process.env["CRTR_NODE_ID"];
90
+ if (nodeId === undefined || nodeId === "") return;
91
+ const state = sharedContextExposureState(nodeId);
92
+ mergeContextExposureState(state, loadContextExposureState(nodeId));
93
+ registerDocumentExposure(exposureTarget(state, "transcript"), path, body, "content");
94
+ saveContextExposureState(nodeId, state);
95
+ }
96
+
86
97
  function hasNodeRelativeExpansion(body: string): boolean {
87
98
  return body.includes("$CRTR_CONTEXT_DIR") || body.includes("$CRTR_NODE_ID");
88
99
  }
89
100
 
90
- /** `/`-joined path-derived doc name -> the registered slash-command name. */
101
+ /** Canonical name -> the registered slash-command name. */
91
102
  export function slashCommandName(docName: string): string {
92
103
  return docName.split("/").join(":");
93
104
  }
94
105
 
95
- /** Every `slash: true` memory doc, scope-precedence-deduped by name (see
96
- * module header). Directory INDEX entries never register as commands. A doc
97
- * without `slash` (or `slash: false`) is silently excluded. */
106
+ /** Every `slash: true` canonical winner, scope-precedence-deduped by name. A
107
+ * doc without `slash` (or `slash: false`) is silently excluded. */
98
108
  export async function discoverSlashDocs(): Promise<SlashDoc[]> {
99
109
  let items: MemoryListItem[];
100
110
  try {
@@ -118,6 +128,7 @@ export default async function (pi: ExtensionAPI) {
118
128
  const docs = await discoverSlashDocs();
119
129
  for (const doc of docs) {
120
130
  let expansion: DeterministicCommandExpansion | undefined;
131
+ let exposure: { path: string; body: string } | undefined;
121
132
  let discloseExpansion = false;
122
133
  let loadError: unknown;
123
134
  try {
@@ -127,6 +138,7 @@ export default async function (pi: ExtensionAPI) {
127
138
  commandName: doc.commandName,
128
139
  body: body.executionBody,
129
140
  };
141
+ exposure = { path: body.path, body: body.canonicalBody };
130
142
  discloseExpansion = !hasNodeRelativeExpansion(body.canonicalBody);
131
143
  } catch (err) {
132
144
  loadError = err;
@@ -174,6 +186,7 @@ export default async function (pi: ExtensionAPI) {
174
186
  },
175
187
  { triggerTurn: true },
176
188
  );
189
+ if (exposure !== undefined) registerSlashExposure(exposure.path, exposure.body);
177
190
  },
178
191
  } as Parameters<ExtensionAPI["registerCommand"]>[1] & { expansion?: DeterministicCommandExpansion } & ChatCommandMetadata);
179
192
  }
@@ -0,0 +1,56 @@
1
+ // Regression: a viewer pane lost `@crtr_node` and `@crtr_attach` while a live
2
+ // viewer was running in it, so `surface reload` and viewer focus refused the
3
+ // pane (`no_viewer`, exit 2) until it was re-tagged by hand.
4
+ //
5
+ // tmux `respawn-pane -k` SIGHUPs the outgoing viewer and starts the replacement
6
+ // ~50ms later without waiting for it. Measured on this machine: SIGHUP at t+0,
7
+ // successor running at t+49ms. An outgoing viewer whose event loop was busy
8
+ // when the signal landed therefore ran its teardown AFTER the successor had
9
+ // claimed the pane, and cleared the successor's tags.
10
+ // Run with: node --conditions=crtr-src --import tsx/esm --test 'src/clients/attach/__tests__/pane-tag-successor.test.ts'
11
+ import { test } from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { createPaneTag } from '../session/pane-tag.js';
14
+ const PANE = '%474';
15
+ const NODE = '3zl47w7d-mt0chixr-cd2fc29b';
16
+ function fakeTmux() {
17
+ const options = new Map();
18
+ let stampCounter = 1_000;
19
+ const io = (pid) => ({
20
+ readOption: (_pane, name) => options.get(name) ?? '',
21
+ writeOption: (_pane, name, value) => { options.set(name, value); },
22
+ stampAsync: () => { options.set('@crtr_viewer_at', String(++stampCounter)); },
23
+ currentPane: () => PANE,
24
+ ownerId: () => pid,
25
+ });
26
+ return { options, io };
27
+ }
28
+ test('a late teardown does not clear the tags of the viewer that replaced it', () => {
29
+ const { options, io } = fakeTmux();
30
+ const outgoing = createPaneTag(false, NODE, io('4001'));
31
+ outgoing.set();
32
+ // The replacement claims the pane while the outgoing viewer is still busy.
33
+ const successor = createPaneTag(false, NODE, io('4002'));
34
+ successor.set();
35
+ outgoing.clear();
36
+ assert.equal(options.get('@crtr_node'), NODE, 'live viewer kept its node tag');
37
+ assert.equal(options.get('@crtr_attach'), '1', 'live viewer kept its attach tag');
38
+ assert.notEqual(options.get('@crtr_viewer_at'), '', 'live viewer kept its stamp');
39
+ });
40
+ test('an ordinary teardown still clears the tags it owns', () => {
41
+ const { options, io } = fakeTmux();
42
+ const viewer = createPaneTag(false, NODE, io('4001'));
43
+ viewer.set();
44
+ viewer.clear();
45
+ assert.equal(options.get('@crtr_node'), '');
46
+ assert.equal(options.get('@crtr_attach'), '');
47
+ assert.equal(options.get('@crtr_viewer_at'), '');
48
+ });
49
+ test('a focus refresh does not cost a viewer its own claim', () => {
50
+ const { options, io } = fakeTmux();
51
+ const viewer = createPaneTag(false, NODE, io('4001'));
52
+ viewer.set();
53
+ viewer.stamp(); // focus refresh
54
+ viewer.clear();
55
+ assert.equal(options.get('@crtr_node'), '', 'the pane it still owns is cleared');
56
+ });
@@ -0,0 +1,21 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import { relatedNodeDirs } from '../session/profile-files.js';
4
+ test('relatedNodeDirs includes a node family but not unrelated nodes', () => {
5
+ const nodes = [
6
+ { node_id: 'root', cwd: '/work/root', parent: null },
7
+ { node_id: 'parent', cwd: '/work/parent', parent: 'root' },
8
+ { node_id: 'current', cwd: '/work/current', parent: 'parent' },
9
+ { node_id: 'child', cwd: '/work/child', parent: 'current' },
10
+ { node_id: 'grandchild', cwd: '/work/grandchild', parent: 'child' },
11
+ { node_id: 'sibling', cwd: '/work/sibling', parent: 'parent' },
12
+ { node_id: 'unrelated', cwd: '/work/unrelated', parent: null },
13
+ ];
14
+ assert.deepEqual(relatedNodeDirs(nodes, 'current'), [
15
+ '/work/root',
16
+ '/work/parent',
17
+ '/work/current',
18
+ '/work/child',
19
+ '/work/grandchild',
20
+ ]);
21
+ });
@@ -91,16 +91,20 @@ export function createInboxStrip(s, hooks) {
91
91
  // Same pending merge the inbox surface does — inbox-projected tickets plus
92
92
  // reply-bearing no-inbox pages — but for every subtree node, then scoped to
93
93
  // tickets whose origin is in the subtree.
94
+ // ONE page-history walk for the whole subtree, filtered by origin here.
95
+ // Calling `scanPageHistory(id)` per node re-walked every ticket directory
96
+ // once per subtree node — the same O(all tickets) scan repeated N times for
97
+ // a result that is just the union over subtree origins.
94
98
  const merged = new Map();
95
99
  for (const item of scanInbox())
96
100
  merged.set(item.dir, item);
97
- for (const id of nodes.keys()) {
98
- for (const page of scanPageHistory(id)) {
99
- if (page.state !== 'pending' || !page.awaitsResponse || page.inbox)
100
- continue;
101
- if (!merged.has(page.dir))
102
- merged.set(page.dir, page);
103
- }
101
+ for (const page of scanPageHistory()) {
102
+ if (page.source.nodeId === undefined || !nodes.has(page.source.nodeId))
103
+ continue;
104
+ if (page.state !== 'pending' || !page.awaitsResponse || page.inbox)
105
+ continue;
106
+ if (!merged.has(page.dir))
107
+ merged.set(page.dir, page);
104
108
  }
105
109
  tickets = [...merged.values()]
106
110
  .filter((t) => t.source.nodeId !== undefined && nodes.has(t.source.nodeId))
@@ -5,13 +5,39 @@
5
5
  * Split into a claim + a clear because ordering matters: a remote relay's "no
6
6
  * node"/"no running broker" close can still throw AFTER the socket is up, so
7
7
  * the tag is claimed only past that checkpoint — while `clear()` must be safe
8
- * to call from a teardown that runs before it was ever claimed. */
9
- export declare function createPaneTag(remote: boolean, nodeId: string): {
8
+ * to call from a teardown that runs before it was ever claimed.
9
+ *
10
+ * `clear()` also has to survive a SUCCESSOR already owning the pane. tmux
11
+ * `respawn-pane -k` only SIGHUPs the old viewer — it does not wait for it, and
12
+ * the replacement is running ~50ms later. A departing viewer whose event loop
13
+ * was busy when the signal arrived therefore runs its teardown after the
14
+ * replacement has claimed the pane, and an unconditional clear then wipes the
15
+ * LIVE viewer's identity: `nodeInPane()` returns nothing, so `surface reload`
16
+ * and viewer focus refuse the pane until someone re-tags it by hand. So the
17
+ * clear is conditional on still owning the pane, decided by `@crtr_viewer_pid`:
18
+ * a pane carrying someone else's pid has a successor, and its tags are not
19
+ * ours to remove. Skipping leaves at worst a stale tag on a dead pane, which
20
+ * `liveViewerPanes` and `surface reload` already validate against the live
21
+ * process; wiping a live viewer's tag has no such defense. */
22
+ /** The tmux reads and writes the tag lifecycle makes, injectable so the
23
+ * successor race is testable without a live tmux server. */
24
+ export interface PaneTagIo {
25
+ readOption: (pane: string, name: string) => string | undefined;
26
+ writeOption: (pane: string, name: string, value: string) => void;
27
+ stampAsync: (pane: string) => void;
28
+ currentPane: () => string | undefined;
29
+ /** This viewer process's identity — the pid, unique per viewer by construction. */
30
+ ownerId: () => string;
31
+ }
32
+ export declare function createPaneTag(remote: boolean, nodeId: string, io?: PaneTagIo): {
10
33
  set: () => void;
11
34
  stamp: () => void;
12
35
  clear: () => void;
13
36
  pane: () => string | undefined;
14
37
  };
38
+ /** Which viewer process owns this pane's tags. Read only by `clear()`, so a
39
+ * departing viewer never removes a successor's identity. */
40
+ export declare const VIEWER_OWNER_OPTION = "@crtr_viewer_pid";
15
41
  export declare function tagAttachPane(remote: boolean, pane: string | undefined, nodeId: string, setOpt?: (pane: string, name: string, value: string) => void): void;
16
42
  export declare function untagAttachPane(remote: boolean, pane: string | undefined, setOpt?: (pane: string, name: string, value: string) => void): void;
17
43
  export declare function tagAttachViewerPane(pane: string | undefined, setOpt?: (pane: string, name: string, value: string) => void): void;
@@ -5,50 +5,76 @@
5
5
  // local id) from that pane. `@crtr_attach` carries no identity: every local or
6
6
  // remote attach viewer sets it so tmux can conditionally forward opted-in root
7
7
  // gestures into whichever attach pane currently has focus.
8
- import { currentTmux, setPaneOption, stampViewerPaneAsync, VIEWER_STAMP_OPTION, } from '../../../core/runtime/placement-tmux.js';
9
- /** The tag's lifecycle for one viewer: `set()` once the startup checkpoint has
10
- * cleared, `clear()` on teardown so a stray tag can't outlive the pane.
11
- * `pane()` is the id teardown reports as this viewer's home.
12
- *
13
- * Split into a claim + a clear because ordering matters: a remote relay's "no
14
- * node"/"no running broker" close can still throw AFTER the socket is up, so
15
- * the tag is claimed only past that checkpoint — while `clear()` must be safe
16
- * to call from a teardown that runs before it was ever claimed. */
17
- export function createPaneTag(remote, nodeId) {
8
+ import { currentTmux, getPaneOption, setPaneOption, stampViewerPaneAsync, VIEWER_STAMP_OPTION, } from '../../../core/runtime/placement-tmux.js';
9
+ const realPaneTagIo = {
10
+ readOption: getPaneOption,
11
+ writeOption: (pane, name, value) => { setPaneOption(pane, name, value); },
12
+ stampAsync: stampViewerPaneAsync,
13
+ currentPane: () => process.env['TMUX_PANE'] ?? currentTmux()?.pane,
14
+ ownerId: () => String(process.pid),
15
+ };
16
+ export function createPaneTag(remote, nodeId, io = realPaneTagIo) {
18
17
  let pane;
18
+ const ownerPid = io.ownerId();
19
+ /** False once another viewer's pid is on the pane. An unclaimed pane (no pid
20
+ * recorded — a viewer that never reached `set()`, or one from a generation
21
+ * that predates this option) keeps the old unconditional behavior. */
22
+ const stillOwnsPane = () => {
23
+ if (pane === undefined || pane === '')
24
+ return true;
25
+ const claimed = io.readOption(pane, VIEWER_OWNER_OPTION);
26
+ if (claimed === undefined || claimed === '')
27
+ return true;
28
+ return claimed === ownerPid;
29
+ };
19
30
  return {
20
31
  set: () => {
21
- pane = process.env['TMUX_PANE'] ?? currentTmux()?.pane;
22
- tagAttachPane(remote, pane, nodeId);
23
- tagAttachViewerPane(pane);
24
- stampAttachPane(remote, pane);
32
+ pane = io.currentPane();
33
+ claimAttachPane(pane, ownerPid, io.writeOption);
34
+ tagAttachPane(remote, pane, nodeId, io.writeOption);
35
+ tagAttachViewerPane(pane, io.writeOption);
36
+ stampAttachPane(remote, pane, io.writeOption);
25
37
  },
26
38
  stamp: () => {
27
39
  if (remote || pane === undefined || pane === '')
28
40
  return;
29
- stampViewerPaneAsync(pane);
41
+ io.stampAsync(pane);
30
42
  },
31
43
  clear: () => {
32
- untagAttachPane(remote, pane);
33
- untagAttachViewerPane(pane);
34
- unstampAttachPane(remote, pane);
44
+ if (!stillOwnsPane())
45
+ return;
46
+ untagAttachPane(remote, pane, io.writeOption);
47
+ untagAttachViewerPane(pane, io.writeOption);
48
+ unstampAttachPane(remote, pane, io.writeOption);
49
+ claimAttachPane(pane, '', io.writeOption);
35
50
  },
36
51
  pane: () => pane,
37
52
  };
38
53
  }
39
- function stampAttachPane(remote, pane) {
54
+ /** Which viewer process owns this pane's tags. Read only by `clear()`, so a
55
+ * departing viewer never removes a successor's identity. */
56
+ export const VIEWER_OWNER_OPTION = '@crtr_viewer_pid';
57
+ function claimAttachPane(pane, ownerPid, setOpt) {
58
+ if (pane === undefined || pane === '')
59
+ return;
60
+ try {
61
+ setOpt(pane, VIEWER_OWNER_OPTION, ownerPid);
62
+ }
63
+ catch { /* best-effort */ }
64
+ }
65
+ function stampAttachPane(remote, pane, setOpt) {
40
66
  if (remote || pane === undefined || pane === '')
41
67
  return;
42
68
  try {
43
- setPaneOption(pane, VIEWER_STAMP_OPTION, String(Date.now()));
69
+ setOpt(pane, VIEWER_STAMP_OPTION, String(Date.now()));
44
70
  }
45
71
  catch { /* best-effort */ }
46
72
  }
47
- function unstampAttachPane(remote, pane) {
73
+ function unstampAttachPane(remote, pane, setOpt = setPaneOption) {
48
74
  if (remote || pane === undefined || pane === '')
49
75
  return;
50
76
  try {
51
- setPaneOption(pane, VIEWER_STAMP_OPTION, '');
77
+ setOpt(pane, VIEWER_STAMP_OPTION, '');
52
78
  }
53
79
  catch { /* best-effort */ }
54
80
  }
@@ -1,8 +1,14 @@
1
+ import type { NodeRow } from '../../../core/canvas/types.js';
1
2
  interface ProfileFileSearchOptions {
2
3
  cwd: string;
4
+ nodeDirs: readonly string[];
3
5
  profileId: string | null;
4
6
  fdPath: string | null;
5
7
  onNotice: (message: string) => void;
6
8
  }
9
+ /** Return the working directories for this node, its complete parent chain,
10
+ * and its full descendant subtree. The `parent` hierarchy, rather than the
11
+ * subscription graph, is the user-visible child/parent relationship. */
12
+ export declare function relatedNodeDirs(nodes: readonly Pick<NodeRow, 'node_id' | 'cwd' | 'parent'>[], nodeId: string): string[];
7
13
  export declare function launchProfileFileSearch(opts: ProfileFileSearchOptions): void;
8
14
  export {};
@@ -1,11 +1,11 @@
1
- // session/profile-files.ts — fuzzy file search across a node's local profile.
1
+ // session/profile-files.ts — fuzzy file search across a node's working family and local profile.
2
2
  //
3
- // The popup searches the current workspace, every project named by the node's
4
- // profile manifest, and the profile's own memory store. Ancestor
5
- // `.crouter/memory/` stores are added with ignore rules disabled, so project
6
- // memory remains findable even when a parent ignore file hides `.crouter/`.
7
- // fzf owns selection and nvim owns viewing; the attach viewer only resolves the
8
- // relevant roots and wires the two popups.
3
+ // The popup searches the current workspace, every ancestor and descendant
4
+ // node's workspace, every project named by the node's profile manifest, and the
5
+ // profile's own memory store. Ancestor `.crouter/memory/` stores are added with
6
+ // ignore rules disabled, so project memory remains findable even when a parent
7
+ // ignore file hides `.crouter/`. fzf owns matching and selection and nvim owns
8
+ // viewing; the attach viewer resolves the related nodes and wires the two popups.
9
9
  import { constants, accessSync, existsSync, mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
10
10
  import { tmpdir } from 'node:os';
11
11
  import { delimiter, dirname, join, resolve, sep } from 'node:path';
@@ -13,6 +13,31 @@ import { execFile, spawnSync } from 'node:child_process';
13
13
  import { loadProfileManifest, profileMemoryDir } from '../../../core/profiles/manifest.js';
14
14
  import { shellQuote } from '../../../core/runtime/placement-tmux.js';
15
15
  import { surfaceTmuxStyleArgs } from '../../../core/runtime/surface-bg.js';
16
+ /** Return the working directories for this node, its complete parent chain,
17
+ * and its full descendant subtree. The `parent` hierarchy, rather than the
18
+ * subscription graph, is the user-visible child/parent relationship. */
19
+ export function relatedNodeDirs(nodes, nodeId) {
20
+ const byId = new Map(nodes.map((node) => [node.node_id, node]));
21
+ const included = new Set([nodeId]);
22
+ for (let current = byId.get(nodeId); current?.parent !== null && current?.parent !== undefined;) {
23
+ const parentId = current.parent;
24
+ if (included.has(parentId))
25
+ break;
26
+ included.add(parentId);
27
+ current = byId.get(parentId);
28
+ }
29
+ const pending = [nodeId];
30
+ while (pending.length > 0) {
31
+ const parentId = pending.pop();
32
+ for (const child of nodes) {
33
+ if (child.parent !== parentId || included.has(child.node_id))
34
+ continue;
35
+ included.add(child.node_id);
36
+ pending.push(child.node_id);
37
+ }
38
+ }
39
+ return nodes.filter((node) => included.has(node.node_id)).map((node) => node.cwd);
40
+ }
16
41
  function isDirectory(path) {
17
42
  try {
18
43
  return statSync(path).isDirectory();
@@ -93,6 +118,9 @@ function ancestorMemoryDirs(pointers) {
93
118
  }
94
119
  return dedupeExistingDirs(found);
95
120
  }
121
+ /** Columns fzf keeps to the right of the matched substring when it scrolls a
122
+ * long path into view. Wide enough to show the rest of the filename. */
123
+ const HSCROLL_OFF = 20;
96
124
  function selectionPath(path) {
97
125
  try {
98
126
  const bytes = readFileSync(path);
@@ -122,7 +150,7 @@ export function launchProfileFileSearch(opts) {
122
150
  return;
123
151
  }
124
152
  const profile = profileSearchRoots(opts.profileId);
125
- const pointers = dedupeExistingDirs([opts.cwd, ...profile.projects]);
153
+ const pointers = dedupeExistingDirs([opts.cwd, ...opts.nodeDirs, ...profile.projects]);
126
154
  if (pointers.length === 0) {
127
155
  opts.onNotice('No profile file-search directories exist');
128
156
  return;
@@ -134,11 +162,16 @@ export function launchProfileFileSearch(opts) {
134
162
  ]);
135
163
  const tempDir = mkdtempSync(join(tmpdir(), 'crtr-profile-files-'));
136
164
  const selected = join(tempDir, 'selection');
165
+ const candidates = join(tempDir, 'candidates');
137
166
  const scans = [
138
- ...roots.map((root) => `${shellQuote(fd)} --type f --hidden --exclude .git --exclude ${shellQuote('**/.crouter/memory/**')} --print0 . ${shellQuote(root)}`),
139
- ...memoryDirs.map((memory) => `${shellQuote(fd)} --type f --hidden --exclude .git --no-ignore --print0 . ${shellQuote(memory)}`),
167
+ ...roots.map((root) => `${shellQuote(fd)} --type f --hidden --exclude .git --exclude ${shellQuote('**/.crouter/memory/**')} . ${shellQuote(root)}`),
168
+ ...memoryDirs.map((memory) => `${shellQuote(fd)} --type f --hidden --exclude .git --no-ignore . ${shellQuote(memory)}`),
140
169
  ];
141
- const command = `{ ${scans.map((scan) => `${scan} &`).join(' ')} wait; } | ${shellQuote(fzf)} --read0 --print0 --preview ${shellQuote('head -100 -- {}')} > ${shellQuote(selected)}`;
170
+ // Every line is the whole path and nothing else, so fzf matches, ranks, and
171
+ // horizontally scrolls against the same text — a hidden field would be
172
+ // unsearchable, and a display slice would silently narrow the query.
173
+ const yankPath = 'printf \'%s\' {} | { if command -v pbcopy >/dev/null 2>&1; then pbcopy; elif command -v wl-copy >/dev/null 2>&1; then wl-copy; elif command -v xclip >/dev/null 2>&1; then xclip -selection clipboard; else cat >/dev/null; fi; }; printf \'%s\' {} | tmux load-buffer -w -';
174
+ const command = `{ ${scans.map((scan) => `${scan} &`).join(' ')} wait; } > ${shellQuote(candidates)} && ${shellQuote(fzf)} --print0 --scheme=path --hscroll-off=${HSCROLL_OFF} --header ${shellQuote('fuzzy path • ctrl-y yank file path')} --bind ${shellQuote(`ctrl-y:execute-silent(${yankPath})`)} --preview ${shellQuote('head -100 -- {}')} < ${shellQuote(candidates)} > ${shellQuote(selected)}`;
142
175
  try {
143
176
  execFile('tmux', ['display-popup', '-E', '-T', ' profile files ', '-w', '80%', '-h', '60%', ...surfaceTmuxStyleArgs(), command], () => {
144
177
  const file = selectionPath(selected);
@@ -396,11 +396,8 @@ function dispatchCanvasCommand(name, arg, ctx) {
396
396
  return true;
397
397
  }
398
398
  }
399
- /** `/promote [kind]` — the live promote path (the broker is headless, so promotion
400
- * has no in-engine command). Shells `crtr node promote --json` for
401
- * this node (out-of-process), notifies the result, then triggers a turn so the
402
- * persona injector steers in the new-role guidance at the turn boundary — exactly
403
- * as the node would by running the command itself. */
399
+ /** `/promote [kind]` shells the out-of-process mutation, reports it, then
400
+ * triggers a turn whose provider gate appends the pending transition context. */
404
401
  function promoteNode(arg, ctx) {
405
402
  const nodeId = nodeIdFor(ctx).trim();
406
403
  if (nodeId === '') {
@@ -446,8 +443,8 @@ function promoteNode(arg, ctx) {
446
443
  }
447
444
  const rmPath = (result.roadmap_path ?? '').trim();
448
445
  ctx.notify(`Promoted to ${result.kind ?? 'orchestrator'} orchestrator — authoring roadmap${rmPath !== '' ? ` (${rmPath})` : ''}.`);
449
- // Trigger a turn so the persona injector (canvas-stophook turn_end) fires and
450
- // delivers the new-role guidance — the viewer's only turn-trigger is `prompt`.
446
+ // Trigger a turn so the pending transition lands before the promoted
447
+ // node's response; the viewer's only turn trigger is `prompt`.
451
448
  ctx.send({
452
449
  type: 'prompt',
453
450
  text: 'You have just been promoted to an orchestrator. Your new-role guidance is arriving — read it, author your roadmap, and start delegating.',