@north-light/crouter 0.3.226 → 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 (288) 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 +37 -9
  212. package/dist/daemon/api/bridge.d.ts +35 -4
  213. package/dist/daemon/api/bridge.js +50 -8
  214. package/dist/daemon/api/handlers/broker-ops.js +21 -16
  215. package/dist/daemon/api/handlers/memory.js +2 -0
  216. package/dist/daemon/crtrd.js +2 -0
  217. package/dist/daemon/human/finish.js +8 -1
  218. package/dist/daemon/manage.d.ts +0 -1
  219. package/dist/daemon/manage.js +7 -16
  220. package/dist/daemon/startup-policy.d.ts +1 -0
  221. package/dist/daemon/startup-policy.js +1 -0
  222. package/dist/migrations/001-surfaces-frontmatter.js +21 -109
  223. package/dist/migrations/002-profile-project-memory.js +1 -0
  224. package/dist/migrations/003-repository-root-memory-identity/front-door.d.ts +26 -0
  225. package/dist/migrations/003-repository-root-memory-identity/front-door.js +231 -0
  226. package/dist/migrations/003-repository-root-memory-identity/index.d.ts +2 -0
  227. package/dist/migrations/003-repository-root-memory-identity/index.js +513 -0
  228. package/dist/migrations/003-repository-root-memory-identity/references.d.ts +95 -0
  229. package/dist/migrations/003-repository-root-memory-identity/references.js +469 -0
  230. package/dist/migrations/003-repository-root-memory-identity/repository-facts.d.ts +39 -0
  231. package/dist/migrations/003-repository-root-memory-identity/repository-facts.js +349 -0
  232. package/dist/migrations/__tests__/activation-concurrency.test.d.ts +1 -0
  233. package/dist/migrations/__tests__/activation-concurrency.test.js +145 -0
  234. package/dist/migrations/__tests__/activation.test.d.ts +1 -0
  235. package/dist/migrations/__tests__/activation.test.js +149 -0
  236. package/dist/migrations/__tests__/deletion-and-root-declaration.test.d.ts +1 -0
  237. package/dist/migrations/__tests__/deletion-and-root-declaration.test.js +149 -0
  238. package/dist/migrations/activation.d.ts +16 -0
  239. package/dist/migrations/activation.js +78 -0
  240. package/dist/migrations/convergent.d.ts +14 -3
  241. package/dist/migrations/convergent.js +21 -10
  242. package/dist/migrations/corpus.d.ts +78 -0
  243. package/dist/migrations/corpus.js +497 -0
  244. package/dist/migrations/frontmatter-splice.d.ts +15 -0
  245. package/dist/migrations/frontmatter-splice.js +176 -0
  246. package/dist/migrations/registry.d.ts +6 -1
  247. package/dist/migrations/registry.js +7 -2
  248. package/dist/migrations/runner.d.ts +41 -0
  249. package/dist/migrations/runner.js +81 -0
  250. package/dist/migrations/types.d.ts +148 -9
  251. package/dist/migrations/types.js +9 -2
  252. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +225 -17
  253. package/dist/pi-extensions/__tests__/canvas-goal-capture-envelope.test.js +11 -3
  254. package/dist/pi-extensions/canvas-context-intro.d.ts +3 -5
  255. package/dist/pi-extensions/canvas-context-intro.js +50 -46
  256. package/dist/pi-extensions/canvas-doc-substrate.d.ts +1 -8
  257. package/dist/pi-extensions/canvas-doc-substrate.js +55 -122
  258. package/dist/pi-extensions/canvas-stophook.js +6 -13
  259. package/dist/shared/generated-context.d.ts +0 -3
  260. package/dist/shared/generated-context.js +0 -57
  261. package/dist/shared/tool-groups.js +2 -3
  262. package/package.json +5 -4
  263. package/runtime.lock.json +2 -2
  264. /package/dist/api/__tests__/{serial → integration}/client.test.d.ts +0 -0
  265. /package/dist/api/__tests__/{serial → integration}/client.test.js +0 -0
  266. /package/dist/builtin-pi-packages/pi-crtr-extensions/extensions/__tests__/{serial → integration}/provider-rotation.test.ts +0 -0
  267. /package/dist/clients/{inbox/__tests__/serial/inbox-controller.test.d.ts → attach/__tests__/pane-tag-successor.test.d.ts} +0 -0
  268. /package/dist/clients/{inbox/__tests__/serial/mount-panel.test.d.ts → attach/__tests__/profile-files.test.d.ts} +0 -0
  269. /package/dist/{core/__tests__/live-mutation-verbs.test.d.ts → clients/inbox/__tests__/integration/inbox-controller.test.d.ts} +0 -0
  270. /package/dist/clients/inbox/__tests__/{serial → integration}/inbox-controller.test.js +0 -0
  271. /package/dist/{core/__tests__/serial/broker-fork-seam.test.d.ts → clients/inbox/__tests__/integration/mount-panel.test.d.ts} +0 -0
  272. /package/dist/clients/inbox/__tests__/{serial → integration}/mount-panel.test.js +0 -0
  273. /package/dist/{core/__tests__/serial/broker-sdk-wiring.test.d.ts → commands/__tests__/surface-reload-target.test.d.ts} +0 -0
  274. /package/dist/{core/__tests__/serial/broker-snapshot-history.test.d.ts → commands/memory/__tests__/command-selector-and-mutation-guards.test.d.ts} +0 -0
  275. /package/dist/{core/__tests__/serial/command-plugins.test.d.ts → commands/memory/__tests__/repository-root-lint.test.d.ts} +0 -0
  276. /package/dist/{core/__tests__/serial/deferred-no-wake.test.d.ts → commands/sys/__tests__/sync-project-guidance.test.d.ts} +0 -0
  277. /package/dist/core/__tests__/{serial/flagship-lifecycle.test.d.ts → config-change-delta.test.d.ts} +0 -0
  278. /package/dist/core/__tests__/{serial/host-teardown-process-group.test.d.ts → integration/broker-fork-seam.test.d.ts} +0 -0
  279. /package/dist/core/__tests__/{serial/human-deliver-e2e.test.d.ts → integration/broker-sdk-wiring.test.d.ts} +0 -0
  280. /package/dist/core/__tests__/{serial/live-mutation.test.d.ts → integration/broker-snapshot-history.test.d.ts} +0 -0
  281. /package/dist/core/__tests__/{serial/refresh-stall-recycle.test.d.ts → integration/command-plugins.test.d.ts} +0 -0
  282. /package/dist/core/__tests__/{serial/revive.test.d.ts → integration/deferred-no-wake.test.d.ts} +0 -0
  283. /package/dist/core/__tests__/{serial/spawn-root.test.d.ts → integration/flagship-lifecycle.test.d.ts} +0 -0
  284. /package/dist/core/__tests__/{serial/subscription-delivery.test.d.ts → integration/host-teardown-process-group.test.d.ts} +0 -0
  285. /package/dist/core/__tests__/{serial/tmux-surface.test.d.ts → integration/human-deliver-e2e.test.d.ts} +0 -0
  286. /package/dist/core/__tests__/{serial/worktree.test.d.ts → integration/live-mutation-verbs.test.d.ts} +0 -0
  287. /package/dist/core/{human/__tests__/serial/inbox-core.test.d.ts → __tests__/integration/live-mutation.test.d.ts} +0 -0
  288. /package/dist/core/human/__tests__/{serial → integration}/inbox-core.test.js +0 -0
@@ -1,5 +1,5 @@
1
1
  // Contract-seam tests for the external command-plugin seam (Phase 1).
2
- // Run with: npm run test:serial
2
+ // Run with: npm run test:integration
3
3
  //
4
4
  // All tests run against an ISOLATED home (HOME → a fresh temp dir, no ambient
5
5
  // user plugins) and pass an explicit no-`.crouter` startDir, so discovery is
@@ -23,7 +23,7 @@
23
23
  // mount composition without executing the fixture binary.
24
24
  import { test, describe, before, after, beforeEach, afterEach } from 'node:test';
25
25
  import assert from 'node:assert/strict';
26
- import { mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync, existsSync } from 'node:fs';
26
+ import { mkdtempSync, mkdirSync, writeFileSync, rmSync, chmodSync, existsSync, lstatSync, realpathSync } from 'node:fs';
27
27
  import { readFileSync } from 'node:fs';
28
28
  import { execFileSync } from 'node:child_process';
29
29
  import { tmpdir } from 'node:os';
@@ -40,6 +40,7 @@ import { CrtrError } from '../../errors.js';
40
40
  import { resetScopeCache } from '../../scope.js';
41
41
  import { listInstalledPluginsInRoot } from '../../resolver.js';
42
42
  import { pluginShow } from '../../../commands/pkg/plugin-inspect.js';
43
+ import { pluginDisable, pluginEnable, pluginInstall } from '../../../commands/pkg/plugin-manage.js';
43
44
  // The set of reserved core names discovery must fail closed against. We don't
44
45
  // import the real SUBTREE_NAMES (build-root.ts pulls the whole tree); a small
45
46
  // explicit set is enough for the collision contract and keeps the test cheap.
@@ -371,7 +372,7 @@ describe('protocol integration', () => {
371
372
  // 4b. True end-to-end CLI invocation (argv → parseArgv → adapter → renderer)
372
373
  // ---------------------------------------------------------------------------
373
374
  describe('end-to-end CLI invocation', () => {
374
- // dist/cli.js is up 4 from src/core/__tests__/serial/. Requires a prior
375
+ // dist/cli.js is up 4 from src/core/__tests__/integration/. Requires a prior
375
376
  // `npm run build` (the harness/CI builds before tests). Drives the WHOLE seam: the
376
377
  // declared positional `app-id` must arrive as `appId` in the request, and
377
378
  // the rendered + --json outputs come through the real dispatcher.
@@ -663,3 +664,93 @@ process.stdout.write(JSON.stringify({ protocolVersion: 1, ok: true, result: { ap
663
664
  assertNotExecuted();
664
665
  });
665
666
  });
667
+ // ---------------------------------------------------------------------------
668
+ // Memory-corpus gate across the plugin lifecycle — install, and enable of a
669
+ // package installed while disabled, both converge the candidate's memory store
670
+ // onto exact canonical identity BEFORE it can mount. A local-path install stays
671
+ // a live link, so the caller's own directory is the corpus that gets rewritten;
672
+ // a corpus that cannot converge writes nothing and changes no config. Update
673
+ // runs the same gate through the same helper, so it needs no cases of its own.
674
+ // ---------------------------------------------------------------------------
675
+ describe('memory corpus gate across the plugin lifecycle', () => {
676
+ let realCwd;
677
+ beforeEach(() => {
678
+ realCwd = process.cwd();
679
+ process.chdir(emptyStart); // no .crouter ancestor → the real repo is never a scope
680
+ resetScopeCache();
681
+ });
682
+ afterEach(() => {
683
+ process.chdir(realCwd);
684
+ resetScopeCache();
685
+ });
686
+ function writeDoc(path, frontmatter, body) {
687
+ mkdirSync(dirname(path), { recursive: true });
688
+ writeFileSync(path, `---\n${frontmatter}\nwhen-and-why-to-read: When the fixture runs, this knowledge should be read because it proves the gate.\n---\n\n${body}\n`);
689
+ }
690
+ function pluginSource(name) {
691
+ const root = join(mintDir('crtr-plugin-src-'), name);
692
+ mkdirSync(join(root, '.crouter-plugin'), { recursive: true });
693
+ writeFileSync(join(root, '.crouter-plugin', 'plugin.json'), JSON.stringify({ name, version: '0.1.0', description: 'fixture' }));
694
+ return root;
695
+ }
696
+ function userConfig() {
697
+ const path = join(userRoot, 'config.json');
698
+ return existsSync(path) ? JSON.parse(readFileSync(path, 'utf8')) : { plugins: {} };
699
+ }
700
+ test('a local install migrates the caller-owned linked corpus in place and reports it', async () => {
701
+ const source = pluginSource('memfix');
702
+ writeDoc(join(source, 'memory', 'topics', 'INDEX.md'), 'kind: knowledge\nname: topics/INDEX', 'Topic overview.');
703
+ writeDoc(join(source, 'memory', 'overview.md'), 'kind: knowledge', 'Depth lives in [[memfix/topics/INDEX]].');
704
+ // Non-document files the substrate never loads as documents: the count
705
+ // below must not include them even though lint YAML-checks them too.
706
+ writeDoc(join(source, 'memory', 'SKILL.md'), 'kind: knowledge\nname: unrelated-skill', 'Skill body.');
707
+ writeDoc(join(source, 'memory', 'MEMORY.md'), 'kind: knowledge', 'Index body.');
708
+ resetScopeCache();
709
+ const out = await pluginInstall.run({ installRef: source, scope: 'user' });
710
+ const memory = out['memory'];
711
+ assert.deepEqual(memory.migrated, ['overview.md', 'topics/INDEX.md'], 'both phases rewrote a document');
712
+ assert.equal(memory.linkedSource, realpathSync(join(source, 'memory')), 'the report names the caller-owned directory it rewrote');
713
+ assert.equal(memory.docs, 2);
714
+ assert.equal(lstatSync(join(userRoot, 'plugins', 'memfix')).isSymbolicLink(), true, 'the install stays a live link');
715
+ // The rewrite landed in the caller's source, not in a copy.
716
+ assert.match(readFileSync(join(source, 'memory', 'overview.md'), 'utf8'), /\[\[memfix\/topics\]\]/);
717
+ assert.doesNotMatch(readFileSync(join(source, 'memory', 'topics', 'INDEX.md'), 'utf8'), /^name:/m);
718
+ assert.equal(userConfig().plugins['memfix']?.enabled, true);
719
+ });
720
+ test('a corpus that cannot converge blocks the install, writes nothing, and registers nothing', async () => {
721
+ const source = pluginSource('blocked');
722
+ writeDoc(join(source, 'memory', 'dup-a.md'), 'kind: knowledge\nname: dup', 'First claimant.');
723
+ writeDoc(join(source, 'memory', 'dup-b.md'), 'kind: knowledge\nname: dup', 'Second claimant.');
724
+ const before = ['dup-a.md', 'dup-b.md'].map((f) => readFileSync(join(source, 'memory', f), 'utf8'));
725
+ resetScopeCache();
726
+ await assert.rejects(pluginInstall.run({ installRef: source, scope: 'user' }), (error) => /cannot be migrated to exact canonical identity/.test(error.message) && /dup/.test(error.message));
727
+ assert.equal(existsSync(join(userRoot, 'plugins', 'blocked')), false, 'a blocked candidate installs nothing');
728
+ assert.equal(userConfig().plugins['blocked'], undefined, 'config is unchanged');
729
+ assert.deepEqual(['dup-a.md', 'dup-b.md'].map((f) => readFileSync(join(source, 'memory', f), 'utf8')), before, 'a blocked plan writes nothing');
730
+ });
731
+ test('enable migrates a package installed while disabled before it flips config', async () => {
732
+ const source = pluginSource('lifecycle');
733
+ writeDoc(join(source, 'memory', 'ready.md'), 'kind: knowledge', 'Already canonical.');
734
+ resetScopeCache();
735
+ await pluginInstall.run({ installRef: source, scope: 'user' });
736
+ resetScopeCache();
737
+ await pluginDisable.run({ name: 'lifecycle' });
738
+ resetScopeCache();
739
+ // The package gains an old-format document while it sits disabled.
740
+ writeDoc(join(source, 'memory', 'guide', 'INDEX.md'), 'kind: knowledge\nname: guide/INDEX', 'Guide.');
741
+ const enabled = await pluginEnable.run({ name: 'lifecycle' });
742
+ assert.equal(enabled['enabled'], true);
743
+ const memory = enabled['memory'];
744
+ assert.deepEqual(memory.migrated, ['guide/INDEX.md']);
745
+ assert.equal(memory.linkedSource, realpathSync(join(source, 'memory')));
746
+ assert.doesNotMatch(readFileSync(join(source, 'memory', 'guide', 'INDEX.md'), 'utf8'), /^name:/m);
747
+ assert.equal(userConfig().plugins['lifecycle']?.enabled, true);
748
+ resetScopeCache();
749
+ await pluginDisable.run({ name: 'lifecycle' });
750
+ resetScopeCache();
751
+ writeDoc(join(source, 'memory', 'dup-a.md'), 'kind: knowledge\nname: dup', 'First claimant.');
752
+ writeDoc(join(source, 'memory', 'dup-b.md'), 'kind: knowledge\nname: dup', 'Second claimant.');
753
+ await assert.rejects(pluginEnable.run({ name: 'lifecycle' }), /cannot be migrated to exact canonical identity/);
754
+ assert.equal(userConfig().plugins['lifecycle']?.enabled, false, 'a refused corpus leaves the plugin disabled');
755
+ });
756
+ });
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // REGRESSION LOCK — gh issue #341. `--tier deferred` is documented as the ONE
4
4
  // tier that never wakes an idle node ("read on its next natural cycle"), but
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // HEADLESS RETARGET (foundation-spec §C.9 + §E). The flagship end-to-end
4
4
  // lifecycle scenario, driven entirely at the MODEL layer against FABRICATED
@@ -29,20 +29,11 @@
29
29
  // "B revived" asserts go RED. If a push wrongly fanned past its direct
30
30
  // subscriber, the "A inbox empty" one-hop asserts go RED.
31
31
  //
32
- // STOPHOOK CAVEAT — split (option b). The original leaned on the REAL stophook
33
- // committing idle-release / the stall reprompt / the persona steer INSIDE a live
34
- // broker at agent_end/turn_end. Fabrication can set the post-stop row but cannot
35
- // run that hook. So for each such point this test fabricates the post-stop MODEL
36
- // state (or asserts the PURE decision the hook enacts — evaluateStop /
37
- // personaDrift) and asserts the downstream lifecycle/daemon CONSEQUENCE, leaving
38
- // the hook-commit proof to the already-pure siblings:
39
- // • idle-release commit at agent_end → canvas-stophook-agentend.test.ts
40
- // ('natural stop while awaiting a live worker → idle-release …').
41
- // • stall reprompt at agent_end → canvas-stophook-agentend.test.ts
42
- // ('stalled leaf … is still reprompted').
43
- // • persona-drift steer + ack commit at turn_end → persona.test.ts
44
- // ('personaDrift detects base→orchestrator after promote, then clears on
45
- // commit'). Here we assert only the PENDING drift the injector would deliver.
32
+ // HOST-HOOK CAVEAT — fabrication cannot run agent_end or the broker's
33
+ // pre-provider persona gate. This test enacts or asserts model state only;
34
+ // canvas-stophook-agentend.test.ts pins idle release and stall reprompt, while
35
+ // canvas-context-intro.test.ts pins persona transition delivery and ack. Here,
36
+ // personaDrift proves the promoted node remains pending.
46
37
  //
47
38
  // 2×2 coverage (mode {base,orchestrator} × lifecycle {terminal,resident}):
48
39
  // • base×resident — A's fabricated row shape at S1 (a spawnRoot
@@ -114,7 +105,7 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
114
105
  status: 'active', pi_pid: deadPid(), pi_session_id: 'sess-B',
115
106
  });
116
107
  subscribe(A, B, true); // the load-bearing spine seed: A subscribes_to B (active).
117
- commitPersonaAck(B, { mode: 'base', lifecycle: 'terminal' }); // born acked to its persona
108
+ commitPersonaAck(B, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }); // born acked to its persona
118
109
  {
119
110
  const b = h.node(B);
120
111
  assert.equal(b.host_kind, 'broker', 'B broker-hosted (paneless)');
@@ -211,20 +202,15 @@ test('flagship: full node lifecycle — spawn, dormancy, one-hop wake, promote,
211
202
  assert.equal(b.intent ?? null, null, 'B intent untouched by promote');
212
203
  }
213
204
  // ===================================================================
214
- // S9 — The next turn's hooks, asserted as the PURE decisions they enact:
215
- // (a) turn_end would inject the base→orchestrator persona-drift steer —
216
- // personaDrift(B) reports that PENDING drift (the guidance the
217
- // injector delivers). The actual delivery + ack commit is locked
218
- // by persona.test.ts.
219
- // (b) agent_end would stall (orchestrator, no live sub — C done, D not
220
- // yet spawned — no final) → evaluateStop → 'stalled'/STALL_REPROMPT.
221
- // The actual reprompt injection is locked by canvas-stophook-agentend.
205
+ // S9 — The next provider request must first deliver the pending persona
206
+ // transition. Independently, agent_end would stall this orchestrator
207
+ // because it has no live subscription and has not pushed a final.
222
208
  // ===================================================================
223
209
  {
224
210
  const drift = personaDrift(B);
225
- assert.ok(drift !== null, 'turn_end would inject a persona-drift steer (drift pending)');
226
- assert.deepEqual(drift?.from, { mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
227
- assert.deepEqual(drift?.to, { mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
211
+ assert.ok(drift !== null, 'the provider gate has a pending persona transition');
212
+ assert.deepEqual(drift?.from, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
213
+ assert.deepEqual(drift?.to, { kind: 'developer', mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
228
214
  assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the steer carries the base→orchestrator guidance');
229
215
  const stop = evaluateStop(B, { pushedFinal: false, requestedHumanReply: false });
230
216
  assert.equal(stop.action, 'reprompt', 'orchestrator B with no live sub, no final → reprompt');
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // Regression cover for crouton-labs/crouter#98: a canceled/wedged child node
4
4
  // ORPHANS its subprocess tree (bash → test runner → spawned app) because
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // Drive a terminal ticket through the public crtrd ticket route. Terminal
4
4
  // delivery is daemon-owned; no subprocess handler or delivery receipt exists.
@@ -16,9 +16,7 @@
16
16
  // promote, no roadmap seeded, no persona_ack drift — it just sets
17
17
  // intent=refresh. `node yield --promote` is the explicit opt-in that
18
18
  // rides the promote() path (mode→orchestrator + roadmap seeded), and
19
- // because its agent_end goes straight to the refresh-revive with NO
20
- // preceding turn_end, the persona-drift steer that promotion would have
21
- // queued is never delivered (ack left PENDING).
19
+ // leaves persona_ack pending until the fresh-context baseline persists.
22
20
  // (2) WHY MODEL-LEVEL, NOT PANE/WINDOW — demote writes lifecycle on the row
23
21
  // (setLifecycle→updateNode); recycle's FINISH+MINT half is pushFinal +
24
22
  // spawnNode (pure model) — only its pane-respawn is tmux (a deliberately-
@@ -33,22 +31,17 @@
33
31
  // or seed a roadmap, or if it committed the ack itself, the A4 'yield
34
32
  // --promote' asserts go RED.
35
33
  //
36
- // STOPHOOK CAVEAT — split (option b). The original drove a LIVE fake-pi so the
37
- // REAL stophook enacted recycle's viewer teardown and A4's refresh-revive ack-drain
38
- // at agent_end. Fabrication cannot run those hooks. So: recycle is driven through
39
- // the real recycleNode (its FINISH+MINT model half runs fully; the pane respawn
40
- // is the only tmux artifact, intentionally skipped → recycled:false). For A4 the
41
- // PURE pre-stop facts are asserted (mode preserved on a plain yield; mode flip +
42
- // roadmap seeded + ack PENDING on `--promote`); the ack-commit-at-refresh-drain
43
- // is locked by persona.test.ts ('… then clears on commit'), and the steer-bypass
44
- // follows because no turn_end ever fires on the yield path.
34
+ // HOST-HOOK CAVEAT — fabrication cannot run the replacement context intro.
35
+ // recycleNode still exercises its finish-and-mint model path; these A4 cases pin
36
+ // the pre-refresh state. canvas-context-intro.test.ts proves the fresh baseline
37
+ // persists before persona_ack advances.
45
38
  import { test } from 'node:test';
46
39
  import assert from 'node:assert/strict';
47
- import { createHeadlessHarness } from './helpers/harness.js';
48
- import { subscribe } from '../canvas/canvas.js';
49
- import { recycleNode } from '../runtime/recycle.js';
50
- import { commitPersonaAck, personaDrift } from '../runtime/persona.js';
51
- import { hasRoadmap } from '../runtime/roadmap.js';
40
+ import { createHeadlessHarness } from '../helpers/harness.js';
41
+ import { subscribe } from '../../canvas/canvas.js';
42
+ import { recycleNode } from '../../runtime/recycle.js';
43
+ import { commitPersonaAck, personaDrift } from '../../runtime/persona.js';
44
+ import { hasRoadmap } from '../../runtime/roadmap.js';
52
45
  /** Normalize the two persona axes off a NodeMeta for deepEqual. */
53
46
  function persona(m) {
54
47
  return { mode: m.mode, lifecycle: m.lifecycle };
@@ -117,7 +110,7 @@ test('node lifecycle demote flips lifecycle→terminal IN PLACE; node lifecycle
117
110
  {
118
111
  const fresh = h.node(res.newRoot);
119
112
  assert.deepEqual(persona(fresh), { mode: 'base', lifecycle: 'resident' }, 'recycled root is born base×resident (general)');
120
- assert.deepEqual(fresh.persona_ack, { mode: 'base', lifecycle: 'resident' }, 'fresh root born acked base×resident');
113
+ assert.deepEqual(fresh.persona_ack, { kind: 'general', mode: 'base', lifecycle: 'resident' }, 'fresh root born acked general×base×resident');
121
114
  assert.equal(fresh.parent ?? null, null, 'the fresh root is a root (no parent)');
122
115
  }
123
116
  }
@@ -129,9 +122,8 @@ test('node lifecycle demote flips lifecycle→terminal IN PLACE; node lifecycle
129
122
  // (b) A4 BOUNDARY — plain yield is mode-preserving; `yield --promote` is the
130
123
  // explicit one-step promotion + refresh. A plain yield sets intent=refresh
131
124
  // only, leaves the base persona acknowledged, and does NOT seed a roadmap.
132
- // With `--promote`, the mode flips and a roadmap is seeded; that promotion
133
- // still leaves its persona drift pending, because agent_end goes STRAIGHT to
134
- // refresh-revive with no preceding turn_end to deliver the steer.
125
+ // With `--promote`, the mode flips and a roadmap is seeded; persona drift
126
+ // remains pending until the replacement context persists its baseline.
135
127
  // ===========================================================================
136
128
  test('A4: a plain yield preserves base mode and does not seed a roadmap',
137
129
  // Full-CLI subprocess spawn under load — see the sibling test's budget note.
@@ -141,7 +133,7 @@ test('A4: a plain yield preserves base mode and does not seed a roadmap',
141
133
  const A = h.spawnRoot('resident root');
142
134
  const B = h.fabricateBrokerNode({ parent: A, kind: 'developer', mode: 'base', lifecycle: 'terminal', status: 'active' });
143
135
  subscribe(A, B, true);
144
- commitPersonaAck(B, { mode: 'base', lifecycle: 'terminal' });
136
+ commitPersonaAck(B, { kind: 'developer', mode: 'base', lifecycle: 'terminal' });
145
137
  assert.equal(hasRoadmap(B), false, 'B has no roadmap before a plain yield');
146
138
  const y = h.cli(B, ['node', 'yield', 'refresh against the original goal']);
147
139
  assert.equal(y.code, 0, `node yield exit 0\n${y.stderr}`);
@@ -149,10 +141,10 @@ test('A4: a plain yield preserves base mode and does not seed a roadmap',
149
141
  const b = h.node(B);
150
142
  assert.equal(b.mode, 'base', 'plain yield preserves base mode');
151
143
  assert.equal(b.intent, 'refresh', 'intent=refresh set by the yield');
152
- assert.deepEqual(b.persona_ack, { mode: 'base', lifecycle: 'terminal' }, 'plain yield leaves the acknowledged base persona unchanged');
144
+ assert.deepEqual(b.persona_ack, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }, 'plain yield leaves the acknowledged base persona unchanged');
153
145
  }
154
146
  assert.equal(hasRoadmap(B), false, 'plain yield does not seed a roadmap');
155
- assert.equal(personaDrift(B), null, 'plain yield leaves no persona drift to steer');
147
+ assert.equal(personaDrift(B), null, 'plain yield leaves no persona transition pending');
156
148
  }
157
149
  finally {
158
150
  await h.dispose();
@@ -166,7 +158,7 @@ test('A4: yield --promote flips base→orchestrator, seeds a roadmap, and leaves
166
158
  const A = h.spawnRoot('resident root');
167
159
  const B = h.fabricateBrokerNode({ parent: A, kind: 'developer', mode: 'base', lifecycle: 'terminal', status: 'active' });
168
160
  subscribe(A, B, true);
169
- commitPersonaAck(B, { mode: 'base', lifecycle: 'terminal' });
161
+ commitPersonaAck(B, { kind: 'developer', mode: 'base', lifecycle: 'terminal' });
170
162
  assert.equal(hasRoadmap(B), false, 'B has no roadmap before yield --promote');
171
163
  const y = h.cli(B, ['node', 'yield', '--promote', 'refresh as an orchestrator']);
172
164
  assert.equal(y.code, 0, `node yield --promote exit 0\n${y.stderr}`);
@@ -174,19 +166,16 @@ test('A4: yield --promote flips base→orchestrator, seeds a roadmap, and leaves
174
166
  const b = h.node(B);
175
167
  assert.equal(b.mode, 'orchestrator', 'yield --promote flips base→orchestrator');
176
168
  assert.equal(b.intent, 'refresh', 'intent=refresh set by the yield');
177
- assert.deepEqual(b.persona_ack, { mode: 'base', lifecycle: 'terminal' }, 'ack STILL base — promote/yield never commits it; only an injector or the refresh drain does');
169
+ assert.deepEqual(b.persona_ack, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }, 'ack stays base until provider-gated delivery or the fresh-context baseline completes');
178
170
  }
179
171
  assert.equal(hasRoadmap(B), true, 'yield --promote seeds a roadmap');
180
- // THE A4 PENDING-DRIFT LOCK — the base→orchestrator guidance is left
181
- // PENDING (personaDrift reports it) and is the ONLY thing the (bypassed)
182
- // turn_end injector would have delivered as a steer. The ack's silent
183
- // advance at the refresh drain — NOT via a steer — is locked by
184
- // persona.test.ts; the steer is lost because no turn_end fires on this path.
172
+ // The refresh kickoff carries this framing into the replacement context;
173
+ // persona_ack stays pending until that context's snapshots persist.
185
174
  {
186
175
  const drift = personaDrift(B);
187
176
  assert.ok(drift !== null, 'the orchestration guidance is PENDING after yield --promote (drift detected)');
188
- assert.deepEqual(drift?.to, { mode: 'orchestrator', lifecycle: 'terminal' }, 'pending drift → orchestrator×terminal');
189
- assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the pending (lost) steer carries the orchestration guidance');
177
+ assert.deepEqual(drift?.to, { kind: 'developer', mode: 'orchestrator', lifecycle: 'terminal' }, 'pending drift → orchestrator×terminal');
178
+ assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the pending fresh-revive framing carries the orchestration guidance');
190
179
  }
191
180
  }
192
181
  finally {
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // HEADLESS RETARGET (foundation-spec §C.12 + §E). AXIS: LIVE MUTATION of the
4
4
  // 2×2 state vector (mode {base,orchestrator} × lifecycle {terminal,resident})
@@ -12,8 +12,7 @@
12
12
  // (1) BUG LOCKED — (a) a live lifecycle flip changes the idle-release decision:
13
13
  // a resident node is NEVER forced dormant (suppressed), a terminal one with
14
14
  // an active live sub IS legitimately awaiting (restored). (b) a live promote
15
- // flips mode but leaves persona_ack PENDING for the injector — it never
16
- // commits the ack itself.
15
+ // flips mode but leaves persona_ack pending for provider-gated delivery.
17
16
  // (2) WHY MODEL-LEVEL, NOT PANE/WINDOW — the flip writes lifecycle/mode on the
18
17
  // row (updateNode) and the idle-release call is decided by the PURE
19
18
  // evaluateStop (stop-guard.ts), which keys on lifecycle + active live subs,
@@ -28,16 +27,10 @@
28
27
  // orchestrator PENDING; if promote wrongly committed the ack, drift reads
29
28
  // null → the pending-drift assert goes RED.
30
29
  //
31
- // STOPHOOK CAVEAT — split (option b). The original drove a LIVE fake-pi turn so
32
- // the REAL stophook enacted the idle-release at agent_end and committed
33
- // persona_ack at turn_end. Fabrication cannot run those hooks. So this test
34
- // asserts the PURE decisions the hooks enact (evaluateStop / personaDrift) and
35
- // leaves the hook-commit proof to the already-pure siblings:
36
- // • idle-release / resident-suppression commit at agent_end → canvas-stophook-
37
- // agentend.test.ts ('natural stop while awaiting a live worker → idle-release'
38
- // and '§5.1.7 resident attended … nothing happens').
39
- // • persona_ack recompose at turn_end → persona.test.ts ('personaDrift detects
40
- // base→orchestrator after promote, then clears on commit').
30
+ // HOST-HOOK CAVEAT — fabrication cannot run agent_end or the broker's
31
+ // pre-provider persona gate. This test asserts evaluateStop and personaDrift;
32
+ // canvas-stophook-agentend.test.ts pins idle-release behavior, and
33
+ // canvas-context-intro.test.ts pins transition delivery and guarded ack.
41
34
  import { test } from 'node:test';
42
35
  import assert from 'node:assert/strict';
43
36
  import { createHeadlessHarness } from '../helpers/harness.js';
@@ -132,15 +125,11 @@ test('live lifecycle flip: terminal→resident suppresses idle-release; resident
132
125
  }
133
126
  });
134
127
  // ===========================================================================
135
- // (b) MODE FLIP — promote: base → orchestrator on a LIVE node. The flip itself
136
- // does NOT commit the persona ack; the turn_end injector recomposes (commits
137
- // the ack + delivers the steer) on the next turn. The NEW assertions here
138
- // are that promote leaves the ack PENDING (not committed) and personaDrift
139
- // reports the exact base→orchestrator transition the injector would deliver —
140
- // the model precondition of the recompose (whose commit is locked by
141
- // persona.test.ts).
128
+ // (b) MODE FLIP — promote: base → orchestrator on a LIVE node. Promotion leaves
129
+ // persona_ack pending; the next provider request appends and persists the
130
+ // transition before a guarded ack. This test pins that model precondition.
142
131
  // ===========================================================================
143
- test('live mode flip: promote base→orchestrator leaves persona_ack PENDING for the turn_end injector',
132
+ test('live mode flip: promote base→orchestrator leaves persona_ack pending for provider-gated delivery',
144
133
  // Full-CLI subprocess spawn under load — see the sibling test's budget note.
145
134
  { timeout: 60_000 }, async () => {
146
135
  const h = await createHeadlessHarness({ sessionPrefix: 'crtr-live-mode' });
@@ -150,16 +139,14 @@ test('live mode flip: promote base→orchestrator leaves persona_ack PENDING for
150
139
  subscribe(A, B, true);
151
140
  // Born acked to its own persona (mirroring a real birth) → no spurious
152
141
  // drift before the promote (invariant 11).
153
- commitPersonaAck(B, { mode: 'base', lifecycle: 'terminal' });
142
+ commitPersonaAck(B, { kind: 'developer', mode: 'base', lifecycle: 'terminal' });
154
143
  {
155
144
  const b = h.node(B);
156
145
  assert.deepEqual(persona(b), { mode: 'base', lifecycle: 'terminal' }, 'B born base×terminal');
157
- assert.deepEqual(b.persona_ack, { mode: 'base', lifecycle: 'terminal' }, 'persona_ack born equal to the initial persona (invariant 11)');
146
+ assert.deepEqual(b.persona_ack, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }, 'persona_ack born equal to the initial persona (invariant 11)');
158
147
  assert.equal(personaDrift(B), null, 'no drift before the promote');
159
148
  }
160
- // PROMOTE (live): mode→orchestrator, lifecycle UNCHANGED, status/intent
161
- // untouched (no transition). Crucially promote does NOT commit the ack —
162
- // the drift is left PENDING for the injector.
149
+ // PROMOTE changes mode only and leaves the transition pending.
163
150
  {
164
151
  const res = h.cli(B, ['node', 'promote', '--kind', 'developer']);
165
152
  assert.equal(res.code, 0, `promote exit 0\n${res.stderr}`);
@@ -168,27 +155,20 @@ test('live mode flip: promote base→orchestrator leaves persona_ack PENDING for
168
155
  assert.equal(b.lifecycle, 'terminal', 'lifecycle UNCHANGED (promote is mode-only)');
169
156
  assert.equal(b.status, 'active', 'status untouched by promote');
170
157
  assert.equal(b.intent ?? null, null, 'intent untouched by promote');
171
- assert.deepEqual(b.persona_ack, { mode: 'base', lifecycle: 'terminal' }, 'promote does NOT commit the ack — drift left PENDING for the injector');
158
+ assert.deepEqual(b.persona_ack, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }, 'promote leaves the previous persona acknowledged');
172
159
  }
173
- // THE PENDING-DRIFT LOCK — personaDrift(B) reports the exact base→
174
- // orchestrator transition the turn_end injector would deliver as a steer
175
- // (and then commit). NON-VACUOUS: if promote wrongly committed the ack,
176
- // personaDrift reads null and the asserts below go RED. The actual delivery
177
- // + commit (and its idempotence on a second turn) is locked by
178
- // persona.test.ts ('… then clears on commit').
160
+ // If promotion committed the ack itself, personaDrift would be null and
161
+ // these assertions would fail. Provider delivery is pinned separately.
179
162
  {
180
163
  const drift = personaDrift(B);
181
164
  assert.ok(drift !== null, 'a recompose is pending after the live promote');
182
- assert.deepEqual(drift?.from, { mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
183
- assert.deepEqual(drift?.to, { mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
165
+ assert.deepEqual(drift?.from, { kind: 'developer', mode: 'base', lifecycle: 'terminal' }, 'drift.from = the acked base×terminal');
166
+ assert.deepEqual(drift?.to, { kind: 'developer', mode: 'orchestrator', lifecycle: 'terminal' }, 'drift.to = the new orchestrator persona');
184
167
  assert.match(drift?.guidance ?? '', /ORCHESTRATOR/i, 'the steer carries the base→orchestrator orchestration guidance');
185
168
  }
186
- // And the recompose IS the injector's job, committed at turn_end — modeled
187
- // here by commitPersonaAck (what the injector calls), proving drift then
188
- // clears (idempotent). The firing of that commit inside the live stophook
189
- // is locked by persona.test.ts.
190
- commitPersonaAck(B, { mode: 'orchestrator', lifecycle: 'terminal' });
191
- assert.deepEqual(h.node(B).persona_ack, { mode: 'orchestrator', lifecycle: 'terminal' }, 'persona_ack recomposed to the new persona');
169
+ // Model a successful delivery ack and prove the detector clears.
170
+ commitPersonaAck(B, { kind: 'developer', mode: 'orchestrator', lifecycle: 'terminal' });
171
+ assert.deepEqual(h.node(B).persona_ack, { kind: 'developer', mode: 'orchestrator', lifecycle: 'terminal' }, 'persona_ack recomposed to the new persona');
192
172
  assert.equal(personaDrift(B), null, 'no fresh drift after the ack commit (idempotent recompose)');
193
173
  }
194
174
  finally {
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // BUG-REGRESSION (broker-universal-host cut, design §H refresh-authority; U7
4
4
  // report mq8gq8we-4cadd8e6; stuck-refresh, parent mq7s5o93). A `node yield` /
@@ -0,0 +1 @@
1
+ export {};
@@ -1,4 +1,4 @@
1
- // Run with: npm run test:serial
1
+ // Run with: npm run test:integration
2
2
  //
3
3
  // HEADLESS RETARGET (foundation-spec §C.13 + §E). Covers the session-picker fix
4
4
  // (resume by ABSOLUTE session-file path, cwd-immune) and the double-revive guard
@@ -28,11 +28,11 @@
28
28
  // goes RED.
29
29
  import { test, before, after, beforeEach } from 'node:test';
30
30
  import assert from 'node:assert/strict';
31
- import { mkdtempSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
31
+ import { mkdtempSync, realpathSync, rmSync, writeFileSync, mkdirSync } from 'node:fs';
32
32
  import { tmpdir } from 'node:os';
33
33
  import { join } from 'node:path';
34
34
  import { spawn, spawnSync } from 'node:child_process';
35
- import { createNode, getNode, subscribe, updateNode } from '../../canvas/canvas.js';
35
+ import { createNode, deleteNode, getNode, subscribe, updateNode } from '../../canvas/canvas.js';
36
36
  import { armCron, hasPendingCancelOnWakeCron } from '../../canvas/crons.js';
37
37
  import { readInboxSince } from '../../feed/inbox.js';
38
38
  import { closeDb } from '../../canvas/db.js';
@@ -46,6 +46,7 @@ import { InputError } from '../../io.js';
46
46
  import { createHarness } from '../helpers/harness.js';
47
47
  import { bindTestFleet } from '../helpers/fleet.js';
48
48
  import { resetFleetForTests } from '../../runtime/fleet.js';
49
+ import { emptyContextExposureState, exposureTarget, initializeForkContextExposure, loadContextExposureState, registerExposure, saveContextExposureState, } from '../../substrate/injected-store.js';
49
50
  let home;
50
51
  function node(id, over = {}) {
51
52
  return {
@@ -80,7 +81,10 @@ function disposableLivePid() {
80
81
  return c.pid;
81
82
  }
82
83
  before(() => {
83
- home = mkdtempSync(join(tmpdir(), 'crtr-revive-'));
84
+ // realpath, not the raw mkdtemp path: on macOS `tmpdir()` is the /var symlink
85
+ // to /private/var, and the runtime resolves a fork's durable source file, so a
86
+ // raw temp path would compare unequal to the very path the runtime replays.
87
+ home = realpathSync(mkdtempSync(join(tmpdir(), 'crtr-revive-')));
84
88
  process.env['CRTR_HOME'] = home;
85
89
  });
86
90
  beforeEach(() => {
@@ -345,7 +349,7 @@ test('reviveNode fresh-fallback clears the stale session identity (Major-3: no p
345
349
  await h.dispose();
346
350
  }
347
351
  });
348
- test('an unbound ordinary fork replays its immutable source with and without its goal', () => {
352
+ test('unbound forks replay one durable source file for node-id, path, and UUID operands', () => {
349
353
  const originalLaunch = headlessBrokerHost.launch;
350
354
  const tf = bindTestFleet();
351
355
  const forkFile = join(home, 'ordinary-fork-source.jsonl');
@@ -353,26 +357,41 @@ test('an unbound ordinary fork replays its immutable source with and without its
353
357
  try {
354
358
  mkdirSync(home, { recursive: true });
355
359
  writeFileSync(forkFile, '{}\n', 'utf8');
360
+ createNode(node('ordinary-fork-source', { cwd: home, pi_session_file: forkFile }));
361
+ const sourceExposure = emptyContextExposureState();
362
+ registerExposure(exposureTarget(sourceExposure, 'system'), 'source-system-only', 'content');
363
+ registerExposure(exposureTarget(sourceExposure, 'transcript'), 'source-transcript', 'content');
364
+ saveContextExposureState('ordinary-fork-source', sourceExposure);
356
365
  headlessBrokerHost.launch = (nodeId, inv) => {
357
366
  launches.push([...inv.argv]);
358
367
  const handle = { pid: disposableLivePid(), exited: new Promise(() => { }) };
359
368
  tf.fleet.register(nodeId, handle);
360
369
  return handle;
361
370
  };
362
- createNode(node('ordinary-fork-promptless', { cwd: home, fork_from: forkFile }));
363
- reviveNode('ordinary-fork-promptless', { resume: true });
364
- const promptless = launches[0];
365
- const promptlessFork = promptless.indexOf('--fork');
366
- assert.equal(promptless[promptlessFork + 1], forkFile, 'an unbound ordinary fork uses its immutable source');
367
- assert.deepEqual(promptless, buildPiArgv(getNode('ordinary-fork-promptless'), { forkFrom: forkFile }).argv, 'a promptless fork has no trailing positional prompt');
368
- createNode(node('ordinary-fork-goal', { cwd: home, fork_from: forkFile }));
369
- writeGoal('ordinary-fork-goal', 'continue the forked task');
370
- const goal = readGoal('ordinary-fork-goal');
371
- reviveNode('ordinary-fork-goal', { resume: false });
372
- const goalCarrying = launches[1];
373
- const goalFork = goalCarrying.indexOf('--fork');
374
- assert.equal(goalCarrying[goalFork + 1], forkFile, 'an unbound ordinary fork uses its immutable source regardless of requested resume mode');
375
- assert.equal(goalCarrying.at(-1), goal, 'a goal-carrying fork preserves its exact goal as the trailing prompt');
371
+ const operands = ['ordinary-fork-source', forkFile, '019e8ce3-322e-7000-8000-000000000001'];
372
+ for (const [index, operand] of operands.entries()) {
373
+ const destination = `ordinary-fork-${index}`;
374
+ createNode(node(destination, {
375
+ cwd: home,
376
+ fork_from: operand,
377
+ fork_source_file: forkFile,
378
+ }));
379
+ initializeForkContextExposure(destination, 'ordinary-fork-source');
380
+ if (index === 1)
381
+ writeGoal(destination, 'continue the forked task');
382
+ }
383
+ deleteNode('ordinary-fork-source');
384
+ for (let index = 0; index < operands.length; index++) {
385
+ const destination = `ordinary-fork-${index}`;
386
+ reviveNode(destination, { resume: index !== 1 });
387
+ const argv = launches[index];
388
+ const fork = argv.indexOf('--fork');
389
+ assert.equal(argv[fork + 1], forkFile, `${operands[index]} retries from the durable resolved file`);
390
+ const exposure = loadContextExposureState(destination);
391
+ assert.deepEqual(exposure.exposures.get('source-transcript'), { transcript: 3 });
392
+ assert.equal(exposure.exposures.has('source-system-only'), false, 'forks never inherit source system exposure');
393
+ }
394
+ assert.equal(launches[1].at(-1), readGoal('ordinary-fork-1'), 'a goal-carrying fork preserves its exact goal');
376
395
  }
377
396
  finally {
378
397
  headlessBrokerHost.launch = originalLaunch;