@crouton-kit/crouter 0.3.18 → 0.3.20

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 (289) hide show
  1. package/dist/build-root.js +6 -0
  2. package/dist/builtin-memory/crouter-development/marketplaces.md +164 -0
  3. package/dist/builtin-memory/crouter-development/personas/base-prompt.md +58 -0
  4. package/dist/builtin-memory/crouter-development/personas/orchestrator-prompt.md +60 -0
  5. package/dist/builtin-memory/crouter-development/personas.md +117 -0
  6. package/dist/builtin-memory/crouter-development/plugins.md +163 -0
  7. package/dist/builtin-memory/design.md +60 -0
  8. package/dist/builtin-memory/development.md +116 -0
  9. package/dist/builtin-memory/planning.md +66 -0
  10. package/dist/builtin-memory/spec.md +99 -0
  11. package/dist/builtin-personas/design/PERSONA.md +1 -0
  12. package/dist/builtin-personas/design/orchestrator.md +2 -1
  13. package/dist/builtin-personas/developer/PERSONA.md +1 -0
  14. package/dist/builtin-personas/developer/orchestrator.md +2 -1
  15. package/dist/builtin-personas/explore/PERSONA.md +1 -0
  16. package/dist/builtin-personas/explore/orchestrator.md +4 -0
  17. package/dist/builtin-personas/general/PERSONA.md +1 -0
  18. package/dist/builtin-personas/general/orchestrator.md +4 -0
  19. package/dist/builtin-personas/lifecycle/resident.md +1 -1
  20. package/dist/builtin-personas/lifecycle/terminal.md +5 -2
  21. package/dist/builtin-personas/orchestration-kernel.md +15 -24
  22. package/dist/builtin-personas/plan/PERSONA.md +1 -0
  23. package/dist/builtin-personas/plan/orchestrator.md +2 -1
  24. package/dist/builtin-personas/plan/reviewers/architecture-fit/PERSONA.md +1 -0
  25. package/dist/builtin-personas/plan/reviewers/code-smells/PERSONA.md +1 -0
  26. package/dist/builtin-personas/plan/reviewers/pattern-consistency/PERSONA.md +1 -0
  27. package/dist/builtin-personas/plan/reviewers/requirements-coverage/PERSONA.md +1 -0
  28. package/dist/builtin-personas/plan/reviewers/security/PERSONA.md +1 -0
  29. package/dist/builtin-personas/review/PERSONA.md +1 -0
  30. package/dist/builtin-personas/review/orchestrator.md +4 -0
  31. package/dist/builtin-personas/runtime-base.md +6 -3
  32. package/dist/builtin-personas/spec/PERSONA.md +1 -0
  33. package/dist/builtin-personas/spec/orchestrator.md +2 -1
  34. package/dist/builtin-personas/waiting.md +8 -0
  35. package/dist/builtin-skills/skills/crouter-development/personas/orchestrator-prompt/SKILL.md +1 -1
  36. package/dist/builtin-skills/skills/crouter-development/plugins/SKILL.md +2 -2
  37. package/dist/builtin-views/_lib/states.mjs +161 -0
  38. package/dist/builtin-views/canvas/client.mjs +303 -0
  39. package/dist/builtin-views/canvas/view.mjs +576 -0
  40. package/dist/builtin-views/git-pr/client.mjs +440 -0
  41. package/dist/builtin-views/git-pr/view.mjs +675 -0
  42. package/dist/builtin-views/inbox/_lib/render.mjs +177 -0
  43. package/dist/builtin-views/inbox/sources/gmail.mjs +965 -0
  44. package/dist/builtin-views/inbox/sources/linkedin.mjs +427 -0
  45. package/dist/builtin-views/inbox/view.mjs +889 -0
  46. package/dist/builtin-views/linkedin/client.mjs +610 -0
  47. package/dist/builtin-views/linkedin/view.mjs +1171 -0
  48. package/dist/clients/attach/attach-cmd.d.ts +2 -0
  49. package/dist/clients/attach/attach-cmd.js +354 -0
  50. package/dist/clients/attach/chat-view.d.ts +77 -0
  51. package/dist/clients/attach/chat-view.js +450 -0
  52. package/dist/clients/attach/clipboard-image.d.ts +16 -0
  53. package/dist/clients/attach/clipboard-image.js +113 -0
  54. package/dist/clients/attach/config-load.d.ts +31 -0
  55. package/dist/clients/attach/config-load.js +113 -0
  56. package/dist/clients/attach/extension-dialogs.d.ts +29 -0
  57. package/dist/clients/attach/extension-dialogs.js +101 -0
  58. package/dist/clients/attach/input-controller.d.ts +54 -0
  59. package/dist/clients/attach/input-controller.js +204 -0
  60. package/dist/clients/attach/slash-commands.d.ts +36 -0
  61. package/dist/clients/attach/slash-commands.js +200 -0
  62. package/dist/clients/attach/view-socket.d.ts +48 -0
  63. package/dist/clients/attach/view-socket.js +126 -0
  64. package/dist/commands/attention.js +3 -3
  65. package/dist/commands/canvas-prune.js +1 -1
  66. package/dist/commands/daemon.js +3 -2
  67. package/dist/commands/human/prompts.js +1 -1
  68. package/dist/commands/human/queue.js +54 -8
  69. package/dist/commands/human/shared.d.ts +2 -0
  70. package/dist/commands/memory/find.d.ts +1 -0
  71. package/dist/commands/memory/find.js +180 -0
  72. package/dist/commands/memory/lint.d.ts +1 -0
  73. package/dist/commands/memory/lint.js +140 -0
  74. package/dist/commands/memory/list.d.ts +1 -0
  75. package/dist/commands/memory/list.js +79 -0
  76. package/dist/commands/memory/read.js +103 -0
  77. package/dist/commands/memory/shared.d.ts +30 -0
  78. package/dist/commands/memory/shared.js +122 -0
  79. package/dist/commands/memory/write.d.ts +1 -0
  80. package/dist/commands/memory/write.js +85 -0
  81. package/dist/commands/memory.d.ts +2 -0
  82. package/dist/commands/memory.js +27 -0
  83. package/dist/commands/node.d.ts +3 -2
  84. package/dist/commands/node.js +610 -61
  85. package/dist/commands/pkg/market-manage.js +1 -1
  86. package/dist/commands/push.js +6 -6
  87. package/dist/commands/revive.js +1 -1
  88. package/dist/commands/skill/author.js +1 -1
  89. package/dist/commands/skill/shared.d.ts +1 -8
  90. package/dist/commands/skill/shared.js +2 -55
  91. package/dist/commands/skill.js +9 -14
  92. package/dist/commands/sys/doctor.js +1 -1
  93. package/dist/commands/sys/update.js +1 -1
  94. package/dist/commands/view-cycle.d.ts +2 -0
  95. package/dist/commands/view-cycle.js +125 -0
  96. package/dist/commands/view-list.d.ts +2 -0
  97. package/dist/commands/view-list.js +66 -0
  98. package/dist/commands/view-new.d.ts +2 -0
  99. package/dist/commands/view-new.js +70 -0
  100. package/dist/commands/view-pick.d.ts +2 -0
  101. package/dist/commands/view-pick.js +119 -0
  102. package/dist/commands/view-run.d.ts +2 -0
  103. package/dist/commands/view-run.js +191 -0
  104. package/dist/commands/view.d.ts +2 -0
  105. package/dist/commands/view.js +29 -0
  106. package/dist/core/__tests__/broker-attach-limits.test.d.ts +1 -0
  107. package/dist/core/__tests__/broker-attach-limits.test.js +157 -0
  108. package/dist/core/__tests__/broker-attach-stream.test.d.ts +1 -0
  109. package/dist/core/__tests__/broker-attach-stream.test.js +125 -0
  110. package/dist/core/__tests__/broker-crash-teardown.test.d.ts +1 -0
  111. package/dist/core/__tests__/broker-crash-teardown.test.js +116 -0
  112. package/dist/core/__tests__/broker-dialogs.test.d.ts +1 -0
  113. package/dist/core/__tests__/broker-dialogs.test.js +126 -0
  114. package/dist/core/__tests__/broker-dormant-wake.test.d.ts +1 -0
  115. package/dist/core/__tests__/broker-dormant-wake.test.js +51 -0
  116. package/dist/core/__tests__/broker-lifecycle.test.d.ts +1 -0
  117. package/dist/core/__tests__/broker-lifecycle.test.js +87 -0
  118. package/dist/core/__tests__/broker-sdk-wiring.test.d.ts +1 -0
  119. package/dist/core/__tests__/broker-sdk-wiring.test.js +166 -0
  120. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.d.ts +1 -0
  121. package/dist/core/__tests__/canvas-inbox-watcher-hold.test.js +102 -0
  122. package/dist/core/__tests__/canvas-inbox-watcher.test.js +2 -36
  123. package/dist/core/__tests__/cascade-close.test.js +12 -2
  124. package/dist/core/__tests__/child-death-wake.test.d.ts +1 -0
  125. package/dist/core/__tests__/child-death-wake.test.js +245 -0
  126. package/dist/core/__tests__/context-intro.test.js +76 -62
  127. package/dist/core/__tests__/daemon-boot.test.js +14 -5
  128. package/dist/core/__tests__/daemon-liveness.test.js +34 -9
  129. package/dist/core/__tests__/detach-focus.test.d.ts +1 -0
  130. package/dist/core/__tests__/detach-focus.test.js +206 -0
  131. package/dist/core/__tests__/draw-style.test.d.ts +1 -0
  132. package/dist/core/__tests__/draw-style.test.js +258 -0
  133. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.d.ts +2 -0
  134. package/dist/core/__tests__/fixtures/c3-custom-provider-ext.js +18 -0
  135. package/dist/core/__tests__/fixtures/fake-engine.d.ts +138 -0
  136. package/dist/core/__tests__/fixtures/fake-engine.js +614 -0
  137. package/dist/core/__tests__/fixtures/fake-pi-host.js +1 -0
  138. package/dist/core/__tests__/flagship-lifecycle.test.js +7 -1
  139. package/dist/core/__tests__/frame-decoder-perf.test.d.ts +1 -0
  140. package/dist/core/__tests__/frame-decoder-perf.test.js +198 -0
  141. package/dist/core/__tests__/helpers/broker-clients.d.ts +43 -0
  142. package/dist/core/__tests__/helpers/broker-clients.js +178 -0
  143. package/dist/core/__tests__/helpers/harness.d.ts +9 -0
  144. package/dist/core/__tests__/helpers/harness.js +111 -1
  145. package/dist/core/__tests__/home-session.test.js +41 -8
  146. package/dist/core/__tests__/human-new-window-regression.test.d.ts +1 -0
  147. package/dist/core/__tests__/human-new-window-regression.test.js +101 -0
  148. package/dist/core/__tests__/kickoff.test.js +37 -3
  149. package/dist/core/__tests__/live-mutation-verbs.test.d.ts +1 -0
  150. package/dist/core/__tests__/live-mutation-verbs.test.js +175 -0
  151. package/dist/core/__tests__/live-mutation.test.js +6 -132
  152. package/dist/core/__tests__/memory.test.js +23 -115
  153. package/dist/core/__tests__/placement-focus.test.js +5 -0
  154. package/dist/core/__tests__/placement-teardown.test.js +54 -11
  155. package/dist/core/__tests__/relaunch.test.js +4 -3
  156. package/dist/core/__tests__/review-render-pane-regression.test.d.ts +1 -0
  157. package/dist/core/__tests__/review-render-pane-regression.test.js +133 -0
  158. package/dist/core/__tests__/spawn-root.test.js +10 -0
  159. package/dist/core/__tests__/spike-harness.test.js +1 -0
  160. package/dist/core/__tests__/wake-bearings.test.d.ts +1 -0
  161. package/dist/core/__tests__/wake-bearings.test.js +156 -0
  162. package/dist/core/__tests__/wake-origin.test.d.ts +1 -0
  163. package/dist/core/__tests__/wake-origin.test.js +110 -0
  164. package/dist/core/bootstrap.js +1 -1
  165. package/dist/core/canvas/browse/__tests__/render.test.js +1 -0
  166. package/dist/core/canvas/browse/app.js +24 -2
  167. package/dist/core/canvas/browse/model.d.ts +38 -2
  168. package/dist/core/canvas/browse/model.js +134 -10
  169. package/dist/core/canvas/browse/render.d.ts +6 -12
  170. package/dist/core/canvas/browse/render.js +72 -104
  171. package/dist/core/canvas/canvas.js +16 -23
  172. package/dist/core/canvas/db.js +46 -0
  173. package/dist/core/canvas/index.d.ts +1 -0
  174. package/dist/core/canvas/index.js +1 -0
  175. package/dist/core/canvas/pid.d.ts +4 -0
  176. package/dist/core/canvas/pid.js +23 -0
  177. package/dist/core/canvas/render.d.ts +11 -2
  178. package/dist/core/canvas/render.js +69 -0
  179. package/dist/core/canvas/types.d.ts +91 -3
  180. package/dist/core/canvas/wakeups.d.ts +76 -0
  181. package/dist/core/canvas/wakeups.js +185 -0
  182. package/dist/core/config.js +4 -1
  183. package/dist/core/frontmatter.js +37 -124
  184. package/dist/core/memory-resolver.d.ts +49 -0
  185. package/dist/core/memory-resolver.js +141 -0
  186. package/dist/core/personas/loader.d.ts +7 -0
  187. package/dist/core/personas/loader.js +36 -5
  188. package/dist/core/personas/resolve.js +13 -3
  189. package/dist/core/predicate.d.ts +63 -0
  190. package/dist/core/predicate.js +189 -0
  191. package/dist/core/resolver.js +26 -5
  192. package/dist/core/runtime/bearings.d.ts +53 -12
  193. package/dist/core/runtime/bearings.js +132 -59
  194. package/dist/core/runtime/broker-cli.d.ts +1 -0
  195. package/dist/core/runtime/broker-cli.js +46 -0
  196. package/dist/core/runtime/broker-protocol.d.ts +332 -0
  197. package/dist/core/runtime/broker-protocol.js +153 -0
  198. package/dist/core/runtime/broker-sdk.d.ts +48 -0
  199. package/dist/core/runtime/broker-sdk.js +72 -0
  200. package/dist/core/runtime/broker.d.ts +55 -0
  201. package/dist/core/runtime/broker.js +1128 -0
  202. package/dist/core/runtime/close.js +35 -6
  203. package/dist/core/runtime/host.d.ts +53 -0
  204. package/dist/core/runtime/host.js +186 -0
  205. package/dist/core/runtime/kickoff.d.ts +2 -1
  206. package/dist/core/runtime/kickoff.js +91 -5
  207. package/dist/core/runtime/launch.d.ts +54 -4
  208. package/dist/core/runtime/launch.js +85 -5
  209. package/dist/core/runtime/lifecycle.js +23 -6
  210. package/dist/core/runtime/memory.d.ts +2 -42
  211. package/dist/core/runtime/memory.js +11 -162
  212. package/dist/core/runtime/nodes.d.ts +37 -0
  213. package/dist/core/runtime/nodes.js +60 -1
  214. package/dist/core/runtime/persona.js +21 -11
  215. package/dist/core/runtime/pi-vendored.d.ts +18 -0
  216. package/dist/core/runtime/pi-vendored.js +49 -0
  217. package/dist/core/runtime/placement.d.ts +32 -14
  218. package/dist/core/runtime/placement.js +206 -52
  219. package/dist/core/runtime/promote.d.ts +0 -6
  220. package/dist/core/runtime/promote.js +4 -12
  221. package/dist/core/runtime/{demote.d.ts → recycle.d.ts} +5 -5
  222. package/dist/core/runtime/{demote.js → recycle.js} +27 -11
  223. package/dist/core/runtime/reset.js +11 -8
  224. package/dist/core/runtime/revive.d.ts +2 -0
  225. package/dist/core/runtime/revive.js +34 -34
  226. package/dist/core/runtime/spawn.d.ts +23 -0
  227. package/dist/core/runtime/spawn.js +77 -23
  228. package/dist/core/runtime/stop-guard.d.ts +1 -1
  229. package/dist/core/runtime/stop-guard.js +6 -1
  230. package/dist/core/runtime/tmux-chrome.d.ts +1 -1
  231. package/dist/core/runtime/tmux-chrome.js +1 -1
  232. package/dist/core/runtime/tmux.d.ts +28 -0
  233. package/dist/core/runtime/tmux.js +80 -6
  234. package/dist/core/scope.d.ts +11 -0
  235. package/dist/core/scope.js +39 -0
  236. package/dist/core/spawn.d.ts +5 -0
  237. package/dist/core/spawn.js +20 -2
  238. package/dist/core/substrate/gate.d.ts +13 -0
  239. package/dist/core/substrate/gate.js +21 -0
  240. package/dist/core/substrate/index.d.ts +7 -0
  241. package/dist/core/substrate/index.js +18 -0
  242. package/dist/core/substrate/on-read.d.ts +14 -0
  243. package/dist/core/substrate/on-read.js +292 -0
  244. package/dist/core/substrate/render.d.ts +25 -0
  245. package/dist/core/substrate/render.js +256 -0
  246. package/dist/core/substrate/schema.d.ts +76 -0
  247. package/dist/core/substrate/schema.js +124 -0
  248. package/dist/core/substrate/session-cache.d.ts +30 -0
  249. package/dist/core/substrate/session-cache.js +77 -0
  250. package/dist/core/substrate/subject.d.ts +41 -0
  251. package/dist/core/substrate/subject.js +54 -0
  252. package/dist/core/tui/contract.d.ts +83 -0
  253. package/dist/core/tui/contract.js +8 -0
  254. package/dist/core/tui/draw.d.ts +96 -0
  255. package/dist/core/tui/draw.js +339 -0
  256. package/dist/core/tui/host.d.ts +29 -0
  257. package/dist/core/tui/host.js +379 -0
  258. package/dist/core/tui/loader.d.ts +16 -0
  259. package/dist/core/tui/loader.js +94 -0
  260. package/dist/core/{canvas/browse → tui}/terminal.js +7 -6
  261. package/dist/core/wake.d.ts +86 -0
  262. package/dist/core/wake.js +308 -0
  263. package/dist/daemon/crtrd.d.ts +29 -4
  264. package/dist/daemon/crtrd.js +662 -46
  265. package/dist/pi-extensions/__tests__/canvas-context-intro.test.d.ts +1 -0
  266. package/dist/pi-extensions/__tests__/canvas-context-intro.test.js +171 -0
  267. package/dist/pi-extensions/__tests__/canvas-stophook-agentend.test.js +19 -12
  268. package/dist/pi-extensions/canvas-commands.d.ts +3 -0
  269. package/dist/pi-extensions/canvas-commands.js +10 -0
  270. package/dist/pi-extensions/canvas-context-intro.d.ts +17 -0
  271. package/dist/pi-extensions/canvas-context-intro.js +55 -15
  272. package/dist/pi-extensions/canvas-doc-substrate.d.ts +44 -0
  273. package/dist/pi-extensions/canvas-doc-substrate.js +112 -0
  274. package/dist/pi-extensions/canvas-nav.d.ts +3 -0
  275. package/dist/pi-extensions/canvas-nav.js +87 -23
  276. package/dist/pi-extensions/canvas-stophook.js +17 -8
  277. package/dist/pi-extensions/canvas-view.d.ts +21 -0
  278. package/dist/pi-extensions/canvas-view.js +75 -0
  279. package/dist/prompts/skill.js +19 -26
  280. package/dist/prompts/view.d.ts +7 -0
  281. package/dist/prompts/view.js +101 -0
  282. package/dist/types.d.ts +4 -0
  283. package/dist/types.js +1 -0
  284. package/package.json +9 -4
  285. package/dist/commands/skill/find.d.ts +0 -4
  286. package/dist/commands/skill/find.js +0 -257
  287. package/dist/commands/skill/read.js +0 -91
  288. /package/dist/commands/{skill → memory}/read.d.ts +0 -0
  289. /package/dist/core/{canvas/browse → tui}/terminal.d.ts +0 -0
@@ -0,0 +1,614 @@
1
+ // fake-engine.ts — a deterministic in-process stand-in for the pi SDK engine,
2
+ // the SDK analog of fixtures/fake-pi-host.ts.
3
+ //
4
+ // This is NOT an LLM and NOT a mock of the canvas extensions. It occupies the
5
+ // pi SDK's place INSIDE the REAL broker (src/core/runtime/broker.ts): the broker
6
+ // loads it through the `CRTR_BROKER_ENGINE` seam (broker-sdk.ts `loadBrokerEngine`),
7
+ // then drives it exactly as it drives the real `@earendil-works/pi-coding-agent`
8
+ // engine — SessionManager.open/create, createAgentSession, session.bindExtensions,
9
+ // session.subscribe, session.prompt. We load the REAL `-e` canvas extensions (the
10
+ // modules the runtime put in the node's broker-launch.json) and fire REAL
11
+ // lifecycle events under harness control, so the real hooks (canvas-stophook,
12
+ // canvas-inbox-watcher) and the real broker drive real canvas state — across the
13
+ // detached broker process boundary, with NOTHING in the broker mocked.
14
+ //
15
+ // It is run UNDER tsx in the broker process (the harness puts `--import tsx/esm`
16
+ // in the broker's NODE_OPTIONS), so the `.ts` extension paths and this `.ts`
17
+ // module both resolve. By the time the broker calls our SessionManager/
18
+ // createAgentSession, piInvocationToSdkConfig has merged inv.env into
19
+ // process.env, so CRTR_NODE_ID / CRTR_HOME are set — that is how we find the
20
+ // node dir.
21
+ //
22
+ // Proof / control surface — IDENTICAL filenames to fake-pi-host.ts, so the
23
+ // EXISTING harness observers (awaitBoot, awaitWake, eventCount, bootCount,
24
+ // injected) work for broker nodes UNCHANGED:
25
+ // <nodeDir>/fake-pi.boot.json — argv + env + loaded exts (latest boot)
26
+ // <nodeDir>/fake-pi.boots.jsonl — append-only, one line per boot (broker pid)
27
+ // <nodeDir>/fake-pi.events.jsonl — append-only, one line per fired event
28
+ // <nodeDir>/fake-pi.injected.jsonl — every sendUserMessage (inbox-watcher wake)
29
+ // <nodeDir>/fake-pi.error — any boot/import failure
30
+ // <nodeDir>/fake-pi.cmd — harness writes ONE JSON command (turn|stop|dialog)
31
+ // <nodeDir>/fake-pi.dialog.jsonl — one line per resolved unattended dialog (§5.4)
32
+ import { writeFileSync, readFileSync, existsSync, mkdirSync, appendFileSync, unlinkSync, } from 'node:fs';
33
+ import { join } from 'node:path';
34
+ import { randomUUID } from 'node:crypto';
35
+ import { DefaultResourceLoader, getAgentDir } from '@earendil-works/pi-coding-agent';
36
+ /** Test seam (M-1 regression): a FRESH-start kickoff prompt carrying this token
37
+ * makes the fake engine THROW inside bindExtensions BEFORE it fires
38
+ * session_start — the exact failure mode that records no pid and no session, so
39
+ * the broker exits(1) and the daemon must surface a boot failure rather than
40
+ * strand the node. Carried on the prompt (not a global env) so it is per-spawn. */
41
+ export const FAIL_BEFORE_SESSION_START = '__FAIL_BEFORE_SESSION_START__';
42
+ // ---------------------------------------------------------------------------
43
+ // node dir / proof helpers
44
+ // ---------------------------------------------------------------------------
45
+ function nodeDirFromEnv() {
46
+ const home = (process.env['CRTR_HOME'] ?? '').trim();
47
+ const nodeId = (process.env['CRTR_NODE_ID'] ?? '').trim();
48
+ if (home === '' || nodeId === '') {
49
+ throw new Error('[fake-engine] CRTR_HOME / CRTR_NODE_ID missing in env');
50
+ }
51
+ const dir = join(home, 'nodes', nodeId);
52
+ mkdirSync(dir, { recursive: true });
53
+ return dir;
54
+ }
55
+ function recordError(dir, msg) {
56
+ try {
57
+ appendFileSync(join(dir, 'fake-pi.error'), msg + '\n');
58
+ }
59
+ catch {
60
+ /* best effort */
61
+ }
62
+ try {
63
+ process.stderr.write('[fake-engine] ' + msg + '\n');
64
+ }
65
+ catch {
66
+ /* ignore */
67
+ }
68
+ }
69
+ // ---------------------------------------------------------------------------
70
+ // SessionManager — the broker calls .open(path) (resume) or .create(cwd) (fresh)
71
+ // and hands the result to createAgentSession. sessionId/sessionFile round-trip
72
+ // through it, so a resume (`--session <pi_session_file>`) re-opens the SAME
73
+ // .jsonl the fresh start created — the load-bearing identity for revive.
74
+ // ---------------------------------------------------------------------------
75
+ export class SessionManager {
76
+ sessionId;
77
+ sessionFile;
78
+ resumed;
79
+ constructor(sessionId, sessionFile, resumed) {
80
+ this.sessionId = sessionId;
81
+ this.sessionFile = sessionFile;
82
+ this.resumed = resumed;
83
+ }
84
+ /** Fresh start: a brand-new session whose .jsonl lives under the node dir
85
+ * (created empty if missing, like fake-pi-host). */
86
+ static create(_cwd) {
87
+ const dir = nodeDirFromEnv();
88
+ const sessionFile = join(dir, 'fake-session.jsonl');
89
+ if (!existsSync(sessionFile))
90
+ writeFileSync(sessionFile, '');
91
+ return new SessionManager(`fake-sess-${randomUUID()}`, sessionFile, false);
92
+ }
93
+ /** Resume: round-trip the .jsonl PATH the revive passed via `--session`. */
94
+ static open(path) {
95
+ return new SessionManager(`fake-sess-resume-${randomUUID()}`, path, true);
96
+ }
97
+ getSessionId() {
98
+ return this.sessionId;
99
+ }
100
+ getSessionFile() {
101
+ return this.sessionFile;
102
+ }
103
+ }
104
+ // ---------------------------------------------------------------------------
105
+ // AgentSessionEvent stream synthesis (T8 / G1, G3, G4, G8). The fake emits a
106
+ // REALISTIC streaming assistant turn through session.subscribe — the SAME channel
107
+ // the real pi engine feeds the broker's fan-out — so the frames a `crtr attach`
108
+ // client receives are byte-for-byte real AgentSessionEvents. The EMITTED event
109
+ // shapes are typed against the real `AgentSessionEvent` union from pi (whose full
110
+ // 17 variants are message_start/update/end, tool_execution_start/update/end,
111
+ // agent_start/end, turn_start/end, queue_update, compaction_start/end, auto_retry_
112
+ // start/end, session_info_changed, thinking_level_changed) — emit() takes that
113
+ // union, so a drift in the shape of any variant emitTurn() constructs (the
114
+ // streaming subset: the message_*, tool_execution_*, agent/turn start+end events)
115
+ // fails the build, not a test.
116
+ // ---------------------------------------------------------------------------
117
+ /** A minimal but fully-typed AssistantMessage. `pad` bytes of filler text let a
118
+ * test size a single frame to multiple MiB (G8 backpressure flood). */
119
+ function assistantMessage(text, pad = 0) {
120
+ return {
121
+ role: 'assistant',
122
+ content: [{ type: 'text', text: pad > 0 ? `${text} ${'x'.repeat(pad)}` : text }],
123
+ api: 'anthropic-messages',
124
+ provider: 'anthropic',
125
+ model: 'fake-sonnet',
126
+ usage: {
127
+ input: 10,
128
+ output: 5,
129
+ cacheRead: 0,
130
+ cacheWrite: 0,
131
+ totalTokens: 15,
132
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
133
+ },
134
+ stopReason: 'stop',
135
+ timestamp: Date.now(),
136
+ };
137
+ }
138
+ /** A streaming text-delta event (the `assistantMessageEvent` carried by a
139
+ * message_update). Kept small (the big payload rides in message_update.message). */
140
+ function textDelta(partial, delta) {
141
+ return { type: 'text_delta', contentIndex: 0, delta, partial };
142
+ }
143
+ class FakeSession {
144
+ sm;
145
+ loader;
146
+ dir;
147
+ extensions = [];
148
+ injected = [];
149
+ uiContext;
150
+ shutdownHandler;
151
+ streaming = false;
152
+ disposed = false;
153
+ eventSeq = 0;
154
+ timer;
155
+ // The broker's fan-out subscribers (broker.ts `session.subscribe(...)`) and the
156
+ // accumulating message history the catch-up snapshot serializes (G3).
157
+ listeners = new Set();
158
+ messageLog = [];
159
+ constructor(sm, loader) {
160
+ this.sm = sm;
161
+ this.loader = loader;
162
+ this.dir = nodeDirFromEnv();
163
+ }
164
+ // --- broker-read getters. buildSnapshot (on a client hello) reads messages/
165
+ // stats/model: T8 clients attach and assert on welcome.snapshot, so messages
166
+ // returns the accrued history; stats/model stay cheap stubs. ---------------
167
+ get messages() {
168
+ return this.messageLog;
169
+ }
170
+ get sessionId() {
171
+ return this.sm.getSessionId();
172
+ }
173
+ get sessionFile() {
174
+ return this.sm.getSessionFile();
175
+ }
176
+ get model() {
177
+ return undefined;
178
+ }
179
+ get isStreaming() {
180
+ return this.streaming;
181
+ }
182
+ getSessionStats() {
183
+ return { messages: 0, tokens: 0 };
184
+ }
185
+ // --- naming is cosmetic (broker calls it in a try/catch) ---------------------
186
+ setSessionName(_name) {
187
+ /* no-op recording stub */
188
+ }
189
+ // --- controller-drive stubs (no controller connects in these tests) ----------
190
+ prompt(text, _options) {
191
+ // The broker calls this for BOTH the fresh-start kickoff AND a controller's
192
+ // `prompt` frame (G1/G4). Either way emit a realistic streaming turn on the
193
+ // subscribe channel; the broker fans it out to every attached viewer VERBATIM.
194
+ // This is the SUBSCRIBE channel only — it does NOT run the canvas stophook
195
+ // (that fires via the separate extension-handler channel in dispatch()/fire()),
196
+ // so it never tears the broker down. The broker fire-and-forgets the promise.
197
+ return this.emitTurn(text);
198
+ }
199
+ steer(_text) {
200
+ return Promise.resolve();
201
+ }
202
+ followUp(_text) {
203
+ return Promise.resolve();
204
+ }
205
+ abort() {
206
+ return Promise.resolve();
207
+ }
208
+ // --- the single engine event stream the broker fans out (broker.ts subscribes
209
+ // here and broadcasts each event VERBATIM to every attached viewer) --------
210
+ subscribe(listener) {
211
+ const l = listener;
212
+ this.listeners.add(l);
213
+ return () => {
214
+ this.listeners.delete(l);
215
+ };
216
+ }
217
+ /** Fan one typed AgentSessionEvent out to every broker subscriber. A throwing
218
+ * listener is recorded, never propagated (mirrors the broker's m7 try/catch). */
219
+ emit(event) {
220
+ for (const l of this.listeners) {
221
+ try {
222
+ l(event);
223
+ }
224
+ catch (e) {
225
+ recordError(this.dir, `subscribe listener threw: ${String(e)}`);
226
+ }
227
+ }
228
+ }
229
+ /** Emit a realistic streaming assistant turn on the subscribe channel:
230
+ * agent_start → turn_start → message_start → message_update×N →
231
+ * tool_execution_start/update/end → message_end → turn_end → agent_end. Drives
232
+ * G1 (controller prompt relay), G3 (produce-while-detached, accrues messageLog),
233
+ * and G8 (a fast event stream — `updates`/`padBytes` size the flood that sheds a
234
+ * stalled viewer at the broker HWM). A per-update setImmediate yield lets a
235
+ * fast viewer drain between frames so only the stalled one trips the HWM. */
236
+ async emitTurn(text, opts = {}) {
237
+ if (this.disposed)
238
+ return;
239
+ const updates = opts.updates ?? 3;
240
+ const pad = opts.padBytes ?? 0;
241
+ const withTool = opts.tool ?? true;
242
+ const small = assistantMessage(text);
243
+ // A normal turn yields with setImmediate (fast). A sized flood (G8, pad>0) is
244
+ // PACED with a small real delay so a fast (reading) viewer always drains each
245
+ // frame and stays under the HWM, while a stalled (non-reading) viewer's backlog
246
+ // still climbs monotonically past the 32 MiB byte cap and is shed. Without the
247
+ // pace the burst outruns the reader's event loop and sheds the fast viewer too.
248
+ const yieldTick = () => pad > 0 ? new Promise((r) => setTimeout(r, 3)) : new Promise((r) => setImmediate(r));
249
+ this.streaming = true;
250
+ this.emit({ type: 'agent_start' });
251
+ this.emit({ type: 'turn_start' });
252
+ this.emit({ type: 'message_start', message: small });
253
+ // F2 (message_update coalescing): the broker now coalesces message_update
254
+ // relays (latest-wins, ~75 ms window), so a padded flood riding message_update
255
+ // can no longer climb a stalled viewer's backlog — the coalescer would shed
256
+ // the flood before the HWM does. A sized flood (pad>0, G8) therefore rides
257
+ // tool_execution_update frames, which the broker relays VERBATIM (only
258
+ // message_update is coalesced — tool output streams remain the realistic
259
+ // unbounded-backlog vector M1 guards against). The flood gets its own
260
+ // tool_execution_start so the frame sequence stays well-formed.
261
+ const floodToolCallId = pad > 0 ? `flood-${++this.eventSeq}` : undefined;
262
+ if (floodToolCallId !== undefined) {
263
+ this.emit({
264
+ type: 'tool_execution_start',
265
+ toolCallId: floodToolCallId,
266
+ toolName: 'read',
267
+ args: { path: 'flood.txt' },
268
+ });
269
+ }
270
+ for (let i = 0; i < updates; i++) {
271
+ if (this.disposed)
272
+ return;
273
+ this.emit({
274
+ type: 'message_update',
275
+ message: assistantMessage(`${text} [${i + 1}/${updates}]`),
276
+ assistantMessageEvent: textDelta(small, `chunk ${i + 1}`),
277
+ });
278
+ if (floodToolCallId !== undefined) {
279
+ // The big payload (G8) rides the partialResult; ~padBytes per frame.
280
+ this.emit({
281
+ type: 'tool_execution_update',
282
+ toolCallId: floodToolCallId,
283
+ toolName: 'read',
284
+ args: { path: 'flood.txt' },
285
+ partialResult: { chunk: 'x'.repeat(pad) },
286
+ });
287
+ }
288
+ await yieldTick();
289
+ }
290
+ if (floodToolCallId !== undefined) {
291
+ this.emit({
292
+ type: 'tool_execution_end',
293
+ toolCallId: floodToolCallId,
294
+ toolName: 'read',
295
+ result: { ok: true },
296
+ isError: false,
297
+ });
298
+ }
299
+ if (withTool) {
300
+ const toolCallId = `call-${++this.eventSeq}`;
301
+ this.emit({ type: 'tool_execution_start', toolCallId, toolName: 'read', args: { path: 'README.md' } });
302
+ this.emit({
303
+ type: 'tool_execution_update',
304
+ toolCallId,
305
+ toolName: 'read',
306
+ args: { path: 'README.md' },
307
+ partialResult: { bytes: 42 },
308
+ });
309
+ this.emit({ type: 'tool_execution_end', toolCallId, toolName: 'read', result: { ok: true }, isError: false });
310
+ }
311
+ if (this.disposed)
312
+ return;
313
+ const finalMsg = assistantMessage(text);
314
+ this.emit({ type: 'message_end', message: finalMsg });
315
+ this.emit({ type: 'turn_end', message: finalMsg, toolResults: [] });
316
+ this.messageLog.push(finalMsg);
317
+ this.streaming = false;
318
+ // agent_end on the SUBSCRIBE channel is a relayed frame ONLY (willRetry=false);
319
+ // it never runs the stophook, so the broker stays alive (G2/G3/G4/G8).
320
+ this.emit({ type: 'agent_end', messages: this.messageLog.slice(), willRetry: false });
321
+ }
322
+ dispose() {
323
+ this.disposed = true;
324
+ if (this.timer !== undefined)
325
+ clearInterval(this.timer);
326
+ }
327
+ // --- bindExtensions: drive the loader's ALREADY-loaded extensions. The REAL
328
+ // DefaultResourceLoader.reload() (broker.ts, BEFORE createAgentSession) has
329
+ // already imported the `-e` canvas extensions via jiti and called each
330
+ // default(pi) against the SDK's `notInitialized` runtime — registering their
331
+ // handlers but leaving every action method (pi.sendUserMessage, …) a stub
332
+ // that THROWS during load. We must NOT re-import them: a native import() is a
333
+ // jiti-independent SECOND module instance, so the inbox-watcher's module-
334
+ // level timer would run TWICE — and the reload() copy (bound to the throwing
335
+ // runtime) wins the race for the shared on-disk inbox cursor, consuming the
336
+ // entry and never delivering. Instead we replicate the minimal slice of
337
+ // agent-session's bindCore: swap the shared runtime's action methods IN PLACE
338
+ // so the real handlers' pi.sendUserMessage delivers into our proof files,
339
+ // then emit session_start into their handler maps so the REAL canvas-stophook
340
+ // records pi_session_id/file + recordPid(process.pid=broker pid). The broker's
341
+ // shutdownHandler is wired into ctx.shutdown so the stophook's done/idle-
342
+ // release/refresh branches exit the broker. -------------------------------
343
+ async bindExtensions(opts) {
344
+ this.uiContext = opts.uiContext;
345
+ this.shutdownHandler = opts.shutdownHandler;
346
+ const { argv, extPaths, label, prompt } = readLaunch(this.dir);
347
+ const resuming = this.sm.resumed;
348
+ // M-1 regression seam: simulate a broker that throws BEFORE session_start
349
+ // (no pid, no session ever recorded). Only a fresh start carries the kickoff
350
+ // prompt; the throw propagates out of bindExtensions → runBroker rejects →
351
+ // broker-cli's fatal catch logs to job/broker.log + exit(1).
352
+ if (!resuming && (prompt ?? '').includes(FAIL_BEFORE_SESSION_START)) {
353
+ throw new Error('[fake-engine] simulated pre-session_start boot failure');
354
+ }
355
+ // bindCore (minimal): adopt the loader's extensions + their shared runtime, and
356
+ // replace the throwing notInitialized action methods with working ones. The
357
+ // handlers themselves stay the REAL ones the SDK registered during reload().
358
+ const loaded = [];
359
+ const failedExt = [];
360
+ try {
361
+ const result = this.loader?.getExtensions();
362
+ this.extensions = result?.extensions ?? [];
363
+ for (const e of this.extensions)
364
+ loaded.push(e.path);
365
+ for (const err of result?.errors ?? []) {
366
+ failedExt.push(String(err?.error ?? err));
367
+ }
368
+ const rt = result?.runtime;
369
+ if (rt !== undefined) {
370
+ rt.assertActive = () => { };
371
+ rt.sendUserMessage = (content, options) => this.recordInjected({ content, deliverAs: options?.deliverAs });
372
+ rt.sendMessage = (message, options) => this.recordInjected({ content: JSON.stringify(message), deliverAs: options?.deliverAs });
373
+ rt.setSessionName = () => { };
374
+ }
375
+ else {
376
+ recordError(this.dir, 'bindExtensions: resourceLoader.getExtensions() returned no runtime');
377
+ }
378
+ }
379
+ catch (e) {
380
+ recordError(this.dir, `bindExtensions getExtensions/bindCore failed: ${String(e)}`);
381
+ }
382
+ const ctx = this.buildCtx();
383
+ // session_start: the real boot-confirm hook captures session id/file + the
384
+ // broker's pid (recordPid(process.pid)) and clears any pending refresh.
385
+ await this.fire('session_start', { reason: resuming ? 'resume' : 'startup' }, ctx);
386
+ // The boot proof the harness asserts on (same shape as fake-pi-host's boot).
387
+ const env = process.env;
388
+ const boot = {
389
+ pid: process.pid, // the broker pid (recorded as pi_pid by the stophook)
390
+ nodeId: (env['CRTR_NODE_ID'] ?? '').trim(),
391
+ home: (env['CRTR_HOME'] ?? '').trim(),
392
+ rawArgv: argv,
393
+ extPaths,
394
+ loaded,
395
+ failedExt,
396
+ sessionId: this.sm.getSessionId(),
397
+ sessionFile: this.sm.getSessionFile(),
398
+ resuming,
399
+ label: label ?? null,
400
+ prompt: prompt ?? null,
401
+ env: {
402
+ CRTR_NODE_ID: env['CRTR_NODE_ID'] ?? null,
403
+ CRTR_KIND: env['CRTR_KIND'] ?? null,
404
+ CRTR_MODE: env['CRTR_MODE'] ?? null,
405
+ CRTR_LIFECYCLE: env['CRTR_LIFECYCLE'] ?? null,
406
+ CRTR_NODE_CWD: env['CRTR_NODE_CWD'] ?? null,
407
+ CRTR_HOME: env['CRTR_HOME'] ?? null,
408
+ CRTR_PARENT_NODE_ID: env['CRTR_PARENT_NODE_ID'] ?? null,
409
+ CRTR_ROOT_SESSION: env['CRTR_ROOT_SESSION'] ?? null,
410
+ CRTR_SUBTREE: env['CRTR_SUBTREE'] ?? null,
411
+ CRTR_FRONT_DOOR: env['CRTR_FRONT_DOOR'] ?? null,
412
+ },
413
+ injectedDuringBoot: this.injected.slice(),
414
+ };
415
+ writeFileSync(join(this.dir, 'fake-pi.boot.json'), JSON.stringify(boot, null, 2));
416
+ try {
417
+ appendFileSync(join(this.dir, 'fake-pi.boots.jsonl'), JSON.stringify(boot) + '\n');
418
+ }
419
+ catch {
420
+ /* best effort */
421
+ }
422
+ // Control loop: poll for one harness command at a time (turn | stop | dialog).
423
+ this.timer = setInterval(() => {
424
+ void this.step().catch((e) => recordError(this.dir, `step: ${String(e)}`));
425
+ }, 100);
426
+ if (typeof this.timer.unref === 'function')
427
+ this.timer.unref();
428
+ }
429
+ // --- record an injected user message (the inbox-watcher wake proof). Called by
430
+ // the runtime's swapped-in sendUserMessage/sendMessage. ---------------------
431
+ recordInjected(rec) {
432
+ this.injected.push(rec);
433
+ try {
434
+ appendFileSync(join(this.dir, 'fake-pi.injected.jsonl'), JSON.stringify({ ...rec, ts: Date.now() }) + '\n');
435
+ }
436
+ catch {
437
+ /* best effort */
438
+ }
439
+ }
440
+ // --- the ctx passed to fired handlers (union across both focus extensions) ---
441
+ buildCtx() {
442
+ return {
443
+ sessionManager: {
444
+ getSessionId: () => this.sm.getSessionId(),
445
+ getSessionFile: () => this.sm.getSessionFile(),
446
+ },
447
+ getContextUsage: () => ({ tokens: 1000 }),
448
+ // Wired to the broker's shutdownHandler: the stophook calls ctx.shutdown()
449
+ // in exactly its done / idle-release / refresh branches → disposeAndExit.
450
+ shutdown: () => {
451
+ this.shutdownHandler?.();
452
+ },
453
+ isIdle: () => !this.streaming,
454
+ abort: () => { },
455
+ };
456
+ }
457
+ // Append a durable record of every fired event BEFORE its handlers run — the
458
+ // harness's robust "the broker received my command and is dispatching it"
459
+ // signal, surviving a handler that exits the process (agent_end → ctx.shutdown
460
+ // → broker disposeAndExit → process.exit).
461
+ recordEvent(event, ev) {
462
+ try {
463
+ const reason = ev?.reason;
464
+ appendFileSync(join(this.dir, 'fake-pi.events.jsonl'), JSON.stringify({ seq: ++this.eventSeq, event, reason: reason ?? null, ts: Date.now() }) + '\n');
465
+ }
466
+ catch {
467
+ /* best effort */
468
+ }
469
+ }
470
+ async fire(event, ev, ctx) {
471
+ this.recordEvent(event, ev);
472
+ // Dispatch into the loader's REAL extension handler maps (the same handlers
473
+ // the SDK's reload() registered as each extension called pi.on(event, …)).
474
+ for (const ext of this.extensions) {
475
+ for (const h of ext.handlers.get(event) ?? []) {
476
+ try {
477
+ await h(ev, ctx);
478
+ }
479
+ catch (e) {
480
+ recordError(this.dir, `handler ${event} threw: ${String(e)}`);
481
+ }
482
+ }
483
+ }
484
+ }
485
+ // --- the command channel: one JSON command at a time, atomically renamed in --
486
+ async step() {
487
+ if (this.disposed)
488
+ return;
489
+ const cmdFile = join(this.dir, 'fake-pi.cmd');
490
+ if (!existsSync(cmdFile))
491
+ return;
492
+ let cmd = null;
493
+ try {
494
+ cmd = JSON.parse(readFileSync(cmdFile, 'utf8'));
495
+ }
496
+ catch {
497
+ cmd = null;
498
+ }
499
+ try {
500
+ unlinkSync(cmdFile);
501
+ }
502
+ catch {
503
+ /* ignore */
504
+ }
505
+ if (cmd)
506
+ await this.dispatch(cmd);
507
+ }
508
+ async dispatch(cmd) {
509
+ const ctx = this.buildCtx();
510
+ switch (cmd.cmd) {
511
+ case 'stream':
512
+ // Drive a subscribe-channel streaming turn WITHOUT a controller (G3:
513
+ // produce-while-detached) or with sized frames (G8: a fast event stream
514
+ // that sheds a stalled viewer at the HWM). Pure fan-out — no stophook, no
515
+ // exit — so the broker stays alive.
516
+ await this.emitTurn(cmd.text ?? 'streamed turn', {
517
+ updates: cmd.updates,
518
+ padBytes: cmd.padBytes,
519
+ tool: cmd.tool,
520
+ });
521
+ break;
522
+ case 'turn':
523
+ this.streaming = true;
524
+ await this.fire('agent_start', {}, ctx);
525
+ this.streaming = false;
526
+ await this.fire('turn_end', { message: { role: 'assistant', usage: { input: 10, output: 5 }, model: 'fake' } }, ctx);
527
+ await this.fire('agent_end', { messages: [{ role: 'assistant', stopReason: 'stop', content: [{ type: 'text', text: cmd.text ?? '' }] }] }, ctx);
528
+ break;
529
+ case 'stop':
530
+ this.streaming = false;
531
+ await this.fire('agent_end', {
532
+ messages: [
533
+ { role: 'assistant', stopReason: cmd.reason ?? 'stop', content: [{ type: 'text', text: cmd.text ?? '' }] },
534
+ ],
535
+ }, ctx);
536
+ break;
537
+ case 'dialog':
538
+ // Raise a blocking confirm() through the broker's REAL uiContext. The path
539
+ // taken depends on who is attached when it raises:
540
+ // • ZERO viewers (C2 / G6a): makeBrokerUiContext resolves the default
541
+ // (false) IMMEDIATELY — noOp fallback, no timer, no wait, no deadlock.
542
+ // • a controller attached (G5): forwarded as an extension_ui_request the
543
+ // controller answers via extension_ui_response — the engine proceeds.
544
+ // • a controller attached but silent (G6b): resolves on the broker-side
545
+ // timeout. Pass a SHORT explicit per-dialog `timeout` so that path is
546
+ // fast and deterministic (never the broker's 120s default).
547
+ await this.runDialog(cmd.timeout ?? 0);
548
+ break;
549
+ default:
550
+ recordError(this.dir, `unknown cmd: ${String(cmd.cmd)}`);
551
+ }
552
+ }
553
+ async runDialog(timeout) {
554
+ const start = Date.now();
555
+ try {
556
+ const resolved = await Promise.resolve(this.uiContext?.confirm?.('headless-dialog', 'forward-progress check', { timeout }));
557
+ appendFileSync(join(this.dir, 'fake-pi.dialog.jsonl'), JSON.stringify({ resolved: resolved ?? null, ms: Date.now() - start, ts: Date.now() }) + '\n');
558
+ }
559
+ catch (e) {
560
+ recordError(this.dir, `dialog: ${String(e)}`);
561
+ }
562
+ }
563
+ }
564
+ export async function createAgentSessionServices(options) {
565
+ const loader = new DefaultResourceLoader({
566
+ cwd: options.cwd,
567
+ agentDir: options.agentDir ?? getAgentDir(),
568
+ additionalExtensionPaths: options.resourceLoaderOptions?.additionalExtensionPaths,
569
+ appendSystemPrompt: options.resourceLoaderOptions?.appendSystemPrompt,
570
+ });
571
+ await loader.reload();
572
+ return {
573
+ resourceLoader: loader,
574
+ modelRegistry: { find: () => undefined },
575
+ };
576
+ }
577
+ export async function createAgentSessionFromServices(options) {
578
+ return { session: new FakeSession(options.sessionManager, options.services.resourceLoader) };
579
+ }
580
+ // ---------------------------------------------------------------------------
581
+ // VERSION — set to the SDK's published version so assertEngineVersion (which
582
+ // only logs, never throws) emits no spurious mismatch warning.
583
+ // ---------------------------------------------------------------------------
584
+ export const VERSION = '0.78.1';
585
+ // ---------------------------------------------------------------------------
586
+ // Parse the broker-launch.json recipe the host serialized (the PiInvocation
587
+ // buildPiArgv produced) for the proof shape — the same `-e`/`-n`/positional
588
+ // vocabulary fake-pi-host parses off its argv.
589
+ // ---------------------------------------------------------------------------
590
+ function readLaunch(dir) {
591
+ let argv = [];
592
+ try {
593
+ const inv = JSON.parse(readFileSync(join(dir, 'broker-launch.json'), 'utf8'));
594
+ argv = Array.isArray(inv.argv) ? inv.argv : [];
595
+ }
596
+ catch {
597
+ /* no launch file — leave argv empty */
598
+ }
599
+ const extPaths = [];
600
+ let label;
601
+ let prompt;
602
+ for (let i = 0; i < argv.length; i++) {
603
+ const a = argv[i];
604
+ if (a === '-e')
605
+ extPaths.push(argv[++i]);
606
+ else if (a === '-n')
607
+ label = argv[++i];
608
+ else if (a === '--session' || a === '--model' || a === '--tools' || a === '--append-system-prompt' || a === '--fork')
609
+ i++;
610
+ else if (!a.startsWith('-'))
611
+ prompt = a; // positional kickoff (fresh start only)
612
+ }
613
+ return { argv, extPaths, label, prompt };
614
+ }
@@ -212,6 +212,7 @@ const boot = {
212
212
  CRTR_HOME: env['CRTR_HOME'] ?? null,
213
213
  CRTR_PARENT_NODE_ID: env['CRTR_PARENT_NODE_ID'] ?? null,
214
214
  CRTR_ROOT_SESSION: env['CRTR_ROOT_SESSION'] ?? null,
215
+ CRTR_SUBTREE: env['CRTR_SUBTREE'] ?? null,
215
216
  CRTR_FRONT_DOOR: env['CRTR_FRONT_DOOR'] ?? null,
216
217
  },
217
218
  injectedDuringBoot: injected.slice(),
@@ -36,7 +36,13 @@
36
36
  // pane-alive but pi-dead) — unit: daemon-liveness.test.ts "idle-release +
37
37
  // live (frozen) pane …"; the grace-window double-spawn guard around it is
38
38
  // exercised faithfully in grace-clock.test.ts.
39
- // • node lifecycle --detach (A3: orphaned-focus-row hazard) — untested.
39
+ // • node lifecycle --detach (A3: orphaned-focus-row hazard) — faithful E2E:
40
+ // detach-focus.test.ts (the real verb on a FOCUSED live node → terminal +
41
+ // pane relocated to the backstage + focus row CLOSED).
42
+ // • focused-finish → manager-TAKEOVER (handFocusToManager swap) — NOT harness-
43
+ // reachable: the harness root is a paneless never-booted row, so the done-
44
+ // branch only ever hits the paneless-manager false guard (closeFocusToShell),
45
+ // never a live/dormant-idle-release takeover. Unit: placement-teardown.test.ts.
40
46
  // • node msg / focus / cycle wake of a dormant node (A7) — untested faithfully.
41
47
  // These are intentional boundaries, not oversights.
42
48
  import { test } from 'node:test';
@@ -0,0 +1 @@
1
+ export {};