pi-daddy 0.22.0 → 0.23.0

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 (362) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/contracts/daily-view/v1/README.md +114 -0
  3. package/contracts/daily-view/v1/fixtures/view.json +525 -0
  4. package/contracts/daily-view/v1/fixtures/view.txt +75 -0
  5. package/contracts/daily-view/v1/fixtures/work.jsonl +17 -0
  6. package/contracts/daily-view/v1/p03/README.md +26 -0
  7. package/contracts/daily-view/v1/p03/fixtures/retained-executions.json +241 -0
  8. package/contracts/daily-view/v1/p03/projection.schema.json +194 -0
  9. package/contracts/daily-view/v1/p03/provenance.json +21 -0
  10. package/contracts/dashboard-host/v1/README.md +176 -0
  11. package/contracts/debrief/v1/README.md +146 -0
  12. package/contracts/debrief/v1/intervention/README.md +32 -0
  13. package/contracts/debrief/v1/intervention/fixtures/artifact-3ee11136beb864f518252e5024dae9197421fbf97b6e981d3243c782b5c44bd0.txt +1 -0
  14. package/contracts/debrief/v1/intervention/fixtures/artifact-58905d872dd3c54a9705f67b05fa82edbad7af6476ae8993faf1a7b9fadfb905.txt +1 -0
  15. package/contracts/debrief/v1/intervention/fixtures/assessment.json +29 -0
  16. package/contracts/debrief/v1/intervention/fixtures/blind-view.json +20 -0
  17. package/contracts/debrief/v1/intervention/fixtures/choice.json +7 -0
  18. package/contracts/debrief/v1/intervention/fixtures/manifest.json +57 -0
  19. package/contracts/debrief/v1/intervention/fixtures/reveal.json +39 -0
  20. package/contracts/debrief/v1/provenance.json +89 -0
  21. package/contracts/debrief/v1/work-capture/README.md +38 -0
  22. package/contracts/debrief/v1/work-capture/fixtures/batch.json +17 -0
  23. package/contracts/debrief/v1/work-capture/fixtures/cases.json +184 -0
  24. package/contracts/debrief/v1/work-capture/fixtures/review.json +184 -0
  25. package/contracts/debrief/v1/work-capture/review-request.schema.json +41 -0
  26. package/contracts/debrief/v1/work-capture/work-case.schema.json +190 -0
  27. package/contracts/debrief/v2/README.md +71 -0
  28. package/contracts/debrief/v2/input-pin.json +66 -0
  29. package/contracts/debrief/v2/intervention-upstream.md +40 -0
  30. package/contracts/debrief/v2/work-signals-upstream.md +22 -0
  31. package/contracts/dispatch-control/v1/README.md +109 -0
  32. package/contracts/dispatch-control/v1/request.schema.json +19 -0
  33. package/contracts/effect-profile/v1/README.md +124 -0
  34. package/contracts/execution-retention/v1/README.md +109 -0
  35. package/contracts/execution-retention/v2/README.md +148 -0
  36. package/contracts/execution-retention/v2/fixtures/interrupted-native-bytes.json +91 -0
  37. package/contracts/execution-retention/v2/fixtures/missing-native-session.json +91 -0
  38. package/contracts/execution-retention/v2/fixtures/native-file-unknown-branch.json +91 -0
  39. package/contracts/execution-retention/v2/fixtures/native-id-without-bytes.json +91 -0
  40. package/contracts/execution-retention/v2/fixtures/native-live-branch.json +90 -0
  41. package/contracts/execution-retention/v2/fixtures/replaced-native-session.json +92 -0
  42. package/contracts/execution-retention/v2/fixtures/result-82c620cdfeb8ca6b858cf653d7f5e2eedca5315d6244216d60e8b36f03223d6a.bin +1 -0
  43. package/contracts/execution-retention/v2/fixtures/session-2ec1f134bc739984434f7fca689e1c3d1c39fce2dcadae709e9b6faedfcc4ac7.bin +3 -0
  44. package/contracts/execution-retention/v2/fixtures/session-9bdb4bd16a901da31580acf056bda525225259e15ec409e0410480109011772f.bin +3 -0
  45. package/contracts/execution-retention/v2/manifest.schema.json +858 -0
  46. package/contracts/experiment/v1/README.md +164 -0
  47. package/contracts/factory-order/v1/README.md +151 -0
  48. package/contracts/factory-order/v1/adoption-pin.json +8 -0
  49. package/contracts/intent-control/v1/README.md +135 -0
  50. package/contracts/intent-control/v1/request.schema.json +1096 -0
  51. package/contracts/intent-control/v2/README.md +34 -0
  52. package/contracts/intent-control/v2/request.schema.json +1070 -0
  53. package/contracts/ledger/v4/README.md +241 -0
  54. package/contracts/ledger/v4/fixtures/layout-options.json +428 -0
  55. package/contracts/ledger/v4/fixtures/work-acceptance.json +57 -0
  56. package/contracts/ledger/v4/fixtures/work-occurrence.json +44 -0
  57. package/contracts/ledger/v4/fixtures/work-revision.json +26 -0
  58. package/contracts/ledger/v4/fixtures/work-snapshot.json +73 -0
  59. package/contracts/ledger/v4/ledger-event.schema.json +162 -0
  60. package/contracts/ordinary-control/v1/README.md +59 -0
  61. package/contracts/producer-ipc/v1/README.md +137 -0
  62. package/dist/check-runner.d.ts +5 -0
  63. package/dist/check-runner.d.ts.map +1 -1
  64. package/dist/check-runner.js +16 -2
  65. package/dist/check-runner.js.map +1 -1
  66. package/dist/control-journal.d.ts +20 -0
  67. package/dist/control-journal.d.ts.map +1 -0
  68. package/dist/control-journal.js +73 -0
  69. package/dist/control-journal.js.map +1 -0
  70. package/dist/daily-view-input.d.ts +44 -0
  71. package/dist/daily-view-input.d.ts.map +1 -0
  72. package/dist/daily-view-input.js +68 -0
  73. package/dist/daily-view-input.js.map +1 -0
  74. package/dist/daily-view-render.d.ts +4 -0
  75. package/dist/daily-view-render.d.ts.map +1 -0
  76. package/dist/daily-view-render.js +41 -0
  77. package/dist/daily-view-render.js.map +1 -0
  78. package/dist/daily-view.d.ts +74 -0
  79. package/dist/daily-view.d.ts.map +1 -0
  80. package/dist/daily-view.js +170 -0
  81. package/dist/daily-view.js.map +1 -0
  82. package/dist/dashboard-cli.d.ts +26 -1
  83. package/dist/dashboard-cli.d.ts.map +1 -1
  84. package/dist/dashboard-cli.js +121 -3
  85. package/dist/dashboard-cli.js.map +1 -1
  86. package/dist/dashboard-harness.d.ts +19 -0
  87. package/dist/dashboard-harness.d.ts.map +1 -0
  88. package/dist/dashboard-harness.js +75 -0
  89. package/dist/dashboard-harness.js.map +1 -0
  90. package/dist/dashboard-herdr.d.ts.map +1 -1
  91. package/dist/dashboard-herdr.js +7 -2
  92. package/dist/dashboard-herdr.js.map +1 -1
  93. package/dist/dashboard-host-contract.d.ts +111 -0
  94. package/dist/dashboard-host-contract.d.ts.map +1 -0
  95. package/dist/dashboard-host-contract.js +14 -0
  96. package/dist/dashboard-host-contract.js.map +1 -0
  97. package/dist/dashboard-host-transport.d.ts +15 -0
  98. package/dist/dashboard-host-transport.d.ts.map +1 -0
  99. package/dist/dashboard-host-transport.js +122 -0
  100. package/dist/dashboard-host-transport.js.map +1 -0
  101. package/dist/dashboard-host.d.ts +149 -0
  102. package/dist/dashboard-host.d.ts.map +1 -0
  103. package/dist/dashboard-host.js +329 -0
  104. package/dist/dashboard-host.js.map +1 -0
  105. package/dist/dashboard-observation.d.ts +28 -0
  106. package/dist/dashboard-observation.d.ts.map +1 -0
  107. package/dist/dashboard-observation.js +86 -0
  108. package/dist/dashboard-observation.js.map +1 -0
  109. package/dist/debrief-contract.d.ts +65 -0
  110. package/dist/debrief-contract.d.ts.map +1 -0
  111. package/dist/debrief-contract.js +151 -0
  112. package/dist/debrief-contract.js.map +1 -0
  113. package/dist/debrief-fixture.d.ts +16 -0
  114. package/dist/debrief-fixture.d.ts.map +1 -0
  115. package/dist/debrief-fixture.js +41 -0
  116. package/dist/debrief-fixture.js.map +1 -0
  117. package/dist/debrief-host.d.ts +59 -0
  118. package/dist/debrief-host.d.ts.map +1 -0
  119. package/dist/debrief-host.js +49 -0
  120. package/dist/debrief-host.js.map +1 -0
  121. package/dist/debrief-render.d.ts +5 -0
  122. package/dist/debrief-render.d.ts.map +1 -0
  123. package/dist/debrief-render.js +65 -0
  124. package/dist/debrief-render.js.map +1 -0
  125. package/dist/debrief.d.ts +90 -0
  126. package/dist/debrief.d.ts.map +1 -0
  127. package/dist/debrief.js +263 -0
  128. package/dist/debrief.js.map +1 -0
  129. package/dist/delegate-types.d.ts +2 -0
  130. package/dist/delegate-types.d.ts.map +1 -1
  131. package/dist/delegate-types.js.map +1 -1
  132. package/dist/delegate.d.ts.map +1 -1
  133. package/dist/delegate.js +1 -0
  134. package/dist/delegate.js.map +1 -1
  135. package/dist/dispatch-control.d.ts +45 -0
  136. package/dist/dispatch-control.d.ts.map +1 -0
  137. package/dist/dispatch-control.js +76 -0
  138. package/dist/dispatch-control.js.map +1 -0
  139. package/dist/effect-profile-runtime.d.ts +40 -0
  140. package/dist/effect-profile-runtime.d.ts.map +1 -0
  141. package/dist/effect-profile-runtime.js +51 -0
  142. package/dist/effect-profile-runtime.js.map +1 -0
  143. package/dist/effect-profile.d.ts +40 -0
  144. package/dist/effect-profile.d.ts.map +1 -0
  145. package/dist/effect-profile.js +152 -0
  146. package/dist/effect-profile.js.map +1 -0
  147. package/dist/execution-retention.d.ts +90 -0
  148. package/dist/execution-retention.d.ts.map +1 -0
  149. package/dist/execution-retention.js +247 -0
  150. package/dist/execution-retention.js.map +1 -0
  151. package/dist/experiment-contract.d.ts +56 -0
  152. package/dist/experiment-contract.d.ts.map +1 -0
  153. package/dist/experiment-contract.js +80 -0
  154. package/dist/experiment-contract.js.map +1 -0
  155. package/dist/experiment-state.d.ts +22 -0
  156. package/dist/experiment-state.d.ts.map +1 -0
  157. package/dist/experiment-state.js +79 -0
  158. package/dist/experiment-state.js.map +1 -0
  159. package/dist/experiment-store.d.ts +28 -0
  160. package/dist/experiment-store.d.ts.map +1 -0
  161. package/dist/experiment-store.js +130 -0
  162. package/dist/experiment-store.js.map +1 -0
  163. package/dist/experiment.d.ts +65 -0
  164. package/dist/experiment.d.ts.map +1 -0
  165. package/dist/experiment.js +357 -0
  166. package/dist/experiment.js.map +1 -0
  167. package/dist/factory-contract.d.ts +79 -0
  168. package/dist/factory-contract.d.ts.map +1 -0
  169. package/dist/factory-contract.js +89 -0
  170. package/dist/factory-contract.js.map +1 -0
  171. package/dist/factory-migration.d.ts +20 -0
  172. package/dist/factory-migration.d.ts.map +1 -0
  173. package/dist/factory-migration.js +55 -0
  174. package/dist/factory-migration.js.map +1 -0
  175. package/dist/factory-order.d.ts +848 -0
  176. package/dist/factory-order.d.ts.map +1 -0
  177. package/dist/factory-order.js +83 -0
  178. package/dist/factory-order.js.map +1 -0
  179. package/dist/factory-registry.d.ts +121 -0
  180. package/dist/factory-registry.d.ts.map +1 -0
  181. package/dist/factory-registry.js +133 -0
  182. package/dist/factory-registry.js.map +1 -0
  183. package/dist/file-lock.d.ts +8 -2
  184. package/dist/file-lock.d.ts.map +1 -1
  185. package/dist/file-lock.js +78 -19
  186. package/dist/file-lock.js.map +1 -1
  187. package/dist/herdr-poll.d.ts +8 -0
  188. package/dist/herdr-poll.d.ts.map +1 -1
  189. package/dist/herdr-poll.js +23 -0
  190. package/dist/herdr-poll.js.map +1 -1
  191. package/dist/index.d.ts +20 -0
  192. package/dist/index.d.ts.map +1 -1
  193. package/dist/index.js +20 -0
  194. package/dist/index.js.map +1 -1
  195. package/dist/intent-application.d.ts +17 -0
  196. package/dist/intent-application.d.ts.map +1 -0
  197. package/dist/intent-application.js +113 -0
  198. package/dist/intent-application.js.map +1 -0
  199. package/dist/intent-control.d.ts +201 -0
  200. package/dist/intent-control.d.ts.map +1 -0
  201. package/dist/intent-control.js +110 -0
  202. package/dist/intent-control.js.map +1 -0
  203. package/dist/intent-scheduling.d.ts +9 -0
  204. package/dist/intent-scheduling.d.ts.map +1 -0
  205. package/dist/intent-scheduling.js +15 -0
  206. package/dist/intent-scheduling.js.map +1 -0
  207. package/dist/ledger-append.d.ts +15 -0
  208. package/dist/ledger-append.d.ts.map +1 -0
  209. package/dist/ledger-append.js +94 -0
  210. package/dist/ledger-append.js.map +1 -0
  211. package/dist/ledger.d.ts +11 -0
  212. package/dist/ledger.d.ts.map +1 -1
  213. package/dist/ledger.js +3 -7
  214. package/dist/ledger.js.map +1 -1
  215. package/dist/native-session-target.d.ts +9 -0
  216. package/dist/native-session-target.d.ts.map +1 -0
  217. package/dist/native-session-target.js +29 -0
  218. package/dist/native-session-target.js.map +1 -0
  219. package/dist/native-session.d.ts +59 -0
  220. package/dist/native-session.d.ts.map +1 -0
  221. package/dist/native-session.js +154 -0
  222. package/dist/native-session.js.map +1 -0
  223. package/dist/order-schedule.d.ts +37 -0
  224. package/dist/order-schedule.d.ts.map +1 -0
  225. package/dist/order-schedule.js +81 -0
  226. package/dist/order-schedule.js.map +1 -0
  227. package/dist/ordinary-children.d.ts +52 -0
  228. package/dist/ordinary-children.d.ts.map +1 -0
  229. package/dist/ordinary-children.js +93 -0
  230. package/dist/ordinary-children.js.map +1 -0
  231. package/dist/producer-ipc-contract.d.ts +29 -0
  232. package/dist/producer-ipc-contract.d.ts.map +1 -0
  233. package/dist/producer-ipc-contract.js +29 -0
  234. package/dist/producer-ipc-contract.js.map +1 -0
  235. package/dist/producer-ipc.d.ts +55 -0
  236. package/dist/producer-ipc.d.ts.map +1 -0
  237. package/dist/producer-ipc.js +143 -0
  238. package/dist/producer-ipc.js.map +1 -0
  239. package/dist/resource-budget.d.ts +334 -0
  240. package/dist/resource-budget.d.ts.map +1 -0
  241. package/dist/resource-budget.js +415 -0
  242. package/dist/resource-budget.js.map +1 -0
  243. package/dist/retention-contract.d.ts +73 -0
  244. package/dist/retention-contract.d.ts.map +1 -0
  245. package/dist/retention-contract.js +104 -0
  246. package/dist/retention-contract.js.map +1 -0
  247. package/dist/retention-json.d.ts +3 -0
  248. package/dist/retention-json.d.ts.map +1 -0
  249. package/dist/retention-json.js +32 -0
  250. package/dist/retention-json.js.map +1 -0
  251. package/dist/run-child.d.ts +4 -0
  252. package/dist/run-child.d.ts.map +1 -1
  253. package/dist/run-child.js +16 -2
  254. package/dist/run-child.js.map +1 -1
  255. package/dist/run-herdr.d.ts +6 -0
  256. package/dist/run-herdr.d.ts.map +1 -1
  257. package/dist/run-herdr.js +19 -6
  258. package/dist/run-herdr.js.map +1 -1
  259. package/dist/vendor/adoption.d.ts +129 -0
  260. package/dist/vendor/adoption.d.ts.map +1 -0
  261. package/dist/vendor/adoption.js +102 -0
  262. package/dist/vendor/adoption.js.map +1 -0
  263. package/dist/work-ledger-destination.d.ts +20 -0
  264. package/dist/work-ledger-destination.d.ts.map +1 -0
  265. package/dist/work-ledger-destination.js +131 -0
  266. package/dist/work-ledger-destination.js.map +1 -0
  267. package/dist/work-ledger-json.d.ts +25 -0
  268. package/dist/work-ledger-json.d.ts.map +1 -0
  269. package/dist/work-ledger-json.js +267 -0
  270. package/dist/work-ledger-json.js.map +1 -0
  271. package/dist/work-ledger-occurrences.d.ts +227 -0
  272. package/dist/work-ledger-occurrences.d.ts.map +1 -0
  273. package/dist/work-ledger-occurrences.js +115 -0
  274. package/dist/work-ledger-occurrences.js.map +1 -0
  275. package/dist/work-ledger-projection.d.ts +295 -0
  276. package/dist/work-ledger-projection.d.ts.map +1 -0
  277. package/dist/work-ledger-projection.js +237 -0
  278. package/dist/work-ledger-projection.js.map +1 -0
  279. package/dist/work-ledger-snapshot.d.ts +29 -0
  280. package/dist/work-ledger-snapshot.d.ts.map +1 -0
  281. package/dist/work-ledger-snapshot.js +267 -0
  282. package/dist/work-ledger-snapshot.js.map +1 -0
  283. package/dist/work-ledger-types.d.ts +261 -0
  284. package/dist/work-ledger-types.d.ts.map +1 -0
  285. package/dist/work-ledger-types.js +13 -0
  286. package/dist/work-ledger-types.js.map +1 -0
  287. package/dist/work-ledger-validation.d.ts +14 -0
  288. package/dist/work-ledger-validation.d.ts.map +1 -0
  289. package/dist/work-ledger-validation.js +226 -0
  290. package/dist/work-ledger-validation.js.map +1 -0
  291. package/dist/work-ledger.d.ts +43 -0
  292. package/dist/work-ledger.d.ts.map +1 -0
  293. package/dist/work-ledger.js +225 -0
  294. package/dist/work-ledger.js.map +1 -0
  295. package/extensions/delegate-chain.ts +1 -0
  296. package/extensions/delegation-native.ts +12 -0
  297. package/extensions/delegation.ts +4 -3
  298. package/extensions/execute-child.ts +34 -4
  299. package/extensions/grants.ts +2 -0
  300. package/extensions/ordinary-runtime.ts +14 -0
  301. package/extensions/run-delegation.ts +7 -7
  302. package/extensions/session.ts +3 -1
  303. package/package.json +78 -4
  304. package/src/check-runner.ts +20 -2
  305. package/src/control-journal.ts +26 -0
  306. package/src/daily-view-input.ts +59 -0
  307. package/src/daily-view-render.ts +44 -0
  308. package/src/daily-view.ts +152 -0
  309. package/src/dashboard-cli.ts +103 -4
  310. package/src/dashboard-harness.ts +28 -0
  311. package/src/dashboard-herdr.ts +7 -2
  312. package/src/dashboard-host-contract.ts +42 -0
  313. package/src/dashboard-host-transport.ts +38 -0
  314. package/src/dashboard-host.ts +168 -0
  315. package/src/dashboard-observation.ts +44 -0
  316. package/src/debrief-contract.ts +127 -0
  317. package/src/debrief-fixture.ts +29 -0
  318. package/src/debrief-host.ts +53 -0
  319. package/src/debrief-render.ts +49 -0
  320. package/src/debrief.ts +198 -0
  321. package/src/delegate-types.ts +2 -0
  322. package/src/delegate.ts +1 -0
  323. package/src/dispatch-control.ts +81 -0
  324. package/src/effect-profile-runtime.ts +46 -0
  325. package/src/effect-profile.ts +129 -0
  326. package/src/execution-retention.ts +230 -0
  327. package/src/experiment-contract.ts +66 -0
  328. package/src/experiment-state.ts +44 -0
  329. package/src/experiment-store.ts +90 -0
  330. package/src/experiment.ts +231 -0
  331. package/src/factory-contract.ts +47 -0
  332. package/src/factory-migration.ts +29 -0
  333. package/src/factory-order.ts +56 -0
  334. package/src/factory-registry.ts +60 -0
  335. package/src/file-lock.ts +47 -11
  336. package/src/herdr-poll.ts +18 -0
  337. package/src/index.ts +32 -0
  338. package/src/intent-application.ts +90 -0
  339. package/src/intent-control.ts +107 -0
  340. package/src/intent-scheduling.ts +14 -0
  341. package/src/ledger-append.ts +72 -0
  342. package/src/ledger.ts +3 -9
  343. package/src/native-session-target.ts +23 -0
  344. package/src/native-session.ts +139 -0
  345. package/src/order-schedule.ts +42 -0
  346. package/src/ordinary-children.ts +59 -0
  347. package/src/producer-ipc-contract.ts +26 -0
  348. package/src/producer-ipc.ts +107 -0
  349. package/src/resource-budget.ts +349 -0
  350. package/src/retention-contract.ts +84 -0
  351. package/src/retention-json.ts +20 -0
  352. package/src/run-child.ts +13 -2
  353. package/src/run-herdr.ts +11 -7
  354. package/src/vendor/adoption.ts +109 -0
  355. package/src/work-ledger-destination.ts +113 -0
  356. package/src/work-ledger-json.ts +228 -0
  357. package/src/work-ledger-occurrences.ts +99 -0
  358. package/src/work-ledger-projection.ts +262 -0
  359. package/src/work-ledger-snapshot.ts +246 -0
  360. package/src/work-ledger-types.ts +229 -0
  361. package/src/work-ledger-validation.ts +174 -0
  362. package/src/work-ledger.ts +186 -0
@@ -1,4 +1,5 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
+ import { beginExecutionRetention, retentionConfigurationDigest, type RetentionStatus } from "./execution-retention.ts";
2
3
  import { chmod, mkdtemp, readFile, realpath, rm, stat, writeFile } from "node:fs/promises";
3
4
  import { tmpdir } from "node:os";
4
5
  import { basename, isAbsolute, join } from "node:path";
@@ -128,11 +129,15 @@ export async function runNamedCheck(input: {
128
129
  signal?: AbortSignal;
129
130
  leaseDir?: string;
130
131
  ledgerPath?: string;
132
+ /** Explicit host archive boundary; defaults to PI_GRANTS_EXECUTION_ARCHIVE, never correlation. */
133
+ retentionDirectory?: string;
134
+ toolCallId?: string;
131
135
  }): Promise<{
132
136
  output: string;
133
137
  exitCode: number | null;
134
138
  signal: NodeJS.Signals | null;
135
139
  receipt: CheckReceipt;
140
+ retention: RetentionStatus;
136
141
  }> {
137
142
  if (input.registry?.version !== 1 || !input.registry.checks || typeof input.registry.checks !== "object") {
138
143
  throw new GovernanceRefusal(refusal("CHECK_CONFIGURATION_INVALID", "check registry must contain {version:1, checks:{...}}"));
@@ -159,11 +164,17 @@ export async function runNamedCheck(input: {
159
164
  const executionId = newExecutionId();
160
165
  const parentExecutionId = null;
161
166
  const correlation = normaliseCorrelation(input.correlation);
167
+ const retention = beginExecutionRetention({ executionId, parentExecutionId, childId: ownerId,
168
+ toolCallId: input.toolCallId ?? null, executor: "check", taskDigest: null, definitionDigest: null,
169
+ configurationDigest: retentionConfigurationDigest({ executable, argv: definition.argv, access,
170
+ timeoutMs: definition.timeout_ms ?? null, maxOutputBytes: definition.max_output_bytes ?? null }),
171
+ workspaceId: input.workspace.workspaceId }, input.retentionDirectory);
162
172
  let lease: Awaited<ReturnType<typeof acquireWorkspaceLease>> | undefined;
163
173
  let stagedDir: string | undefined;
164
174
  /** Failures of best-effort RECORDS, reported alongside whatever actually happened — never instead of it. */
165
175
  const notes: string[] = [];
166
176
  let releaseReason = "failed";
177
+ let observedOutcome = { code: null as number | null, signal: null as string | null, timedOut: false, aborted: false, truncated: false };
167
178
  return runWithFinalizers(async () => {
168
179
  try {
169
180
  try {
@@ -228,8 +239,11 @@ export async function runNamedCheck(input: {
228
239
  command: "setpriv", args: ["--pdeathsig", "KILL", "--", stagedExecutable, ...definition.argv],
229
240
  env: buildCheckEnvironment(definition, input.inheritedEnv), cwd: input.workspace.root,
230
241
  signal: checkSignal, timeoutMs: definition.timeout_ms, maxOutputBytes: definition.max_output_bytes,
231
- onSpawn: (pid) => lease!.attachProcess(pid),
242
+ onSpawn: (pid) => { lease!.attachProcess(pid); retention.native({ pid }); },
243
+ onObservation: (stream, bytes) => retention.capture(stream, bytes),
232
244
  });
245
+ observedOutcome = { code: result.code, signal: result.signal ?? null, timedOut: result.timedOut, aborted: result.aborted, truncated: result.truncated };
246
+ retention.capture("result", Buffer.from(result.text), true);
233
247
  const ended = new Date();
234
248
  releaseReason = input.signal?.aborted ? "cancelled" : result.timedOut ? "timeout" : result.aborted ? "failed" : "completed";
235
249
  // Every throw below re-stamps this. Without that, a check refused for CHECK_IDENTITY_MISMATCH still
@@ -285,6 +299,7 @@ export async function runNamedCheck(input: {
285
299
  head_sha: before.headSha, tree_sha: before.treeSha, ...(correlation ? { correlation } : {}),
286
300
  };
287
301
  const receipt: CheckReceipt = { receipt_id: receiptId(body), ...body };
302
+ retention.capture("checkReceipt", Buffer.from(JSON.stringify(receipt) + "\n"), true);
288
303
  if (input.ledgerPath) {
289
304
  await appendLedgerEvent(
290
305
  { path: input.ledgerPath, strict: true },
@@ -295,8 +310,11 @@ export async function runNamedCheck(input: {
295
310
  }),
296
311
  );
297
312
  }
298
- return { output: result.text, exitCode: result.code, signal: result.signal ?? null, receipt };
313
+ retention.finish({ code: result.code, signal: result.signal ?? null, timedOut: result.timedOut,
314
+ aborted: result.aborted, truncated: result.truncated, failed: result.code !== 0 });
315
+ return { output: result.text, exitCode: result.code, signal: result.signal ?? null, receipt, retention: retention.status() };
299
316
  } catch (error) {
317
+ retention.finish({ ...observedOutcome, aborted: observedOutcome.aborted || Boolean(input.signal?.aborted), failed: true });
300
318
  // The run did not complete, so the release must not be recorded as if it had. Without this the
301
319
  // `finally` wrote `released / completed` as the LAST lease event for a refused check, so the trail
302
320
  // still read like a clean run and `verifyLedger` still counted a clean release — the R-114 fix put a
@@ -0,0 +1,26 @@
1
+ import { constants } from "node:fs";
2
+ import { mkdir, open, lstat } from "node:fs/promises";
3
+ import { dirname, join, resolve } from "node:path";
4
+ import { withFileLock } from "./file-lock.ts";
5
+ import { runWithFinalizers } from "./finalization.ts";
6
+ import { ownedDirectory, readExperimentFile } from "./experiment-store.ts";
7
+ import { cloneExperiment, closed, experimentHash } from "./experiment-contract.ts";
8
+ import { freezeWork, parseWorkJson } from "./work-ledger-json.ts";
9
+ export interface ControlBinding<T> {version:"control-journal-v1";directory:string;device:string;inode:string;journalDevice:string;journalInode:string;initial:T}
10
+ export async function createControlJournal<T>(directory:string,input:T):Promise<ControlBinding<T>>{
11
+ const initial=cloneExperiment(input);if(typeof directory!=="string"||directory!==resolve(directory)||directory.length>1024||directory.split("/").includes(".pi"))throw new Error("explicit control directory required");
12
+ await ownedDirectory(dirname(directory));await mkdir(directory,{mode:0o700});const root=await ownedDirectory(directory),f=await open(join(directory,"control.jsonl"),"wx",0o600);
13
+ const b=await runWithFinalizers(async()=>{const s=await f.stat({bigint:true});const b={version:"control-journal-v1" as const,directory,device:String(root.dev),inode:String(root.ino),journalDevice:String(s.dev),journalInode:String(s.ino),initial};await f.writeFile(JSON.stringify(b)+"\n");await f.sync();return b;},[{label:"control creation close failed",run:()=>f.close()}]);
14
+ const d=await open(directory,constants.O_RDONLY|constants.O_DIRECTORY);await runWithFinalizers(()=>d.sync(),[{label:"control directory close failed",run:()=>d.close()}]);return freezeWork(b) as unknown as ControlBinding<T>;
15
+ }
16
+ export function controlJournal<T>(input:ControlBinding<T>){
17
+ const b=freezeWork(cloneExperiment(input)) as unknown as ControlBinding<T>;closed(b,["version","directory","device","inode","journalDevice","journalInode","initial"]);
18
+ if(b.version!=="control-journal-v1"||b.directory!==resolve(b.directory)||b.directory.split("/").includes(".pi")||[b.device,b.inode,b.journalDevice,b.journalInode].some(v=>!/^\d+$/.test(v)))throw new Error("invalid control binding");
19
+ const path=join(b.directory,"control.jsonl");
20
+ const check=async()=>{const root=await ownedDirectory(b.directory),file=await lstat(path,{bigint:true});if(String(root.dev)!==b.device||String(root.ino)!==b.inode||String(file.dev)!==b.journalDevice||String(file.ino)!==b.journalInode)throw new Error("control identity changed");};
21
+ const read=async()=>{await check();const bytes=await readExperimentFile(path,512*1024);await check();const text=new TextDecoder("utf8",{fatal:true}).decode(bytes);if(!text.endsWith("\n"))throw new Error("torn control journal");const lines=text.slice(0,-1).split("\n");if(lines.length>257||experimentHash(parseWorkJson(lines[0]))!==experimentHash(b))throw new Error("control header/limit mismatch");let previous=experimentHash(b);const events:Record<string,unknown>[]=[];
22
+ for(const line of lines.slice(1)){const r=parseWorkJson(line) as Record<string,unknown>;closed(r,["sequence","previous","event"]);if(r.sequence!==events.length||r.previous!==previous||!r.event||typeof r.event!=="object"||Array.isArray(r.event))throw new Error("invalid control sequence");previous=experimentHash(r);events.push(r.event as Record<string,unknown>);}return{events,previous,size:bytes.length};};
23
+ return {binding:b,read,async transaction<R>(fn:(events:Record<string,unknown>[],append:(e:unknown)=>Promise<void>)=>Promise<R>){await check();return withFileLock(path,"factory policy",async()=>{const state=await read();return fn(state.events,async event=>{if(state.events.length>=256)throw new Error("control event budget exhausted");const record={sequence:state.events.length,previous:state.previous,event:cloneExperiment(event)},data=Buffer.from(JSON.stringify(record)+"\n");if(state.size+data.length>512*1024||data.length>65536)throw new Error("control byte budget exhausted");await check();const f=await open(path,constants.O_WRONLY|constants.O_APPEND|constants.O_NOFOLLOW|constants.O_NONBLOCK);
24
+ await runWithFinalizers(async()=>{const s=await f.stat({bigint:true});if(!s.isFile()||s.nlink!==1n||String(s.dev)!==b.journalDevice||String(s.ino)!==b.journalInode||s.size!==BigInt(state.size)||(s.mode&0o077n)!==0n)throw new Error("control journal changed under lock");await f.writeFile(data);await f.sync();},[{label:"control append close failed",run:()=>f.close()}]);await check();state.size+=data.length;state.previous=experimentHash(record);state.events.push(cloneExperiment(event) as Record<string,unknown>);
25
+ });},{staleRecovery:"disabled"});}};
26
+ }
@@ -0,0 +1,59 @@
1
+ import { constants } from "node:fs";
2
+ import { open, lstat } from "node:fs/promises";
3
+ import { createRequire } from "node:module";
4
+ import { createHash } from "node:crypto";
5
+ import { Compile } from "typebox/compile";
6
+ import { parseRetentionJson } from "./retention-json.ts";
7
+ import { runWithFinalizers } from "./finalization.ts";
8
+
9
+ export const ARCHIVE_PROJECTION_VERSION = "execution-archive-projection-v1" as const;
10
+ export const DAILY_INPUT_LIMIT = 4 * 1024 * 1024;
11
+ export interface ArchiveExecution {
12
+ executionId: string; parentExecutionIds: (string | null)[]; retainedSessionIds: string[]; activeBranch: null;
13
+ toolCallIds: (string | null)[]; archiveIds: string[]; runtime: "running" | "terminal" | "conflict";
14
+ outcome: { code: number | null; signal: string | null; timedOut: boolean; aborted: boolean; truncated: boolean; failed: boolean } | null;
15
+ sourceReferences: string[]; issues: string[]; coverage: "partial"; acceptance: "not-assessed";
16
+ }
17
+ export interface ArchiveProjection { version: typeof ARCHIVE_PROJECTION_VERSION; executions: ArchiveExecution[]; acceptance: "not-assessed" }
18
+ const schema = createRequire(import.meta.url)("../contracts/daily-view/v1/p03/projection.schema.json");
19
+ const validator = Compile(schema);
20
+ export const byteHash = (bytes: Uint8Array | string): string => createHash("sha256").update(bytes).digest("hex");
21
+ export function freezeDaily<T>(value: T): T {
22
+ if (value && typeof value === "object") { Object.values(value).forEach(freezeDaily); Object.freeze(value); }
23
+ return value;
24
+ }
25
+ /** Exact vendored P03 shape, plus no duplicate execution identity or contradictory terminal shape.
26
+ * Only the producer's emitted integer-token subset is supported; no lossy fractional rounding. */
27
+ export function parseArchiveProjection(text: string): ArchiveProjection {
28
+ const value = parseRetentionJson(text, DAILY_INPUT_LIMIT);
29
+ for (const match of text.matchAll(/"(?:[^"\\]|\\[\s\S])*"|[^\s{}\[\],:]+/g)) {
30
+ if (/^-?\d/.test(match[0]) && !/^-?(0|[1-9]\d*)$/.test(match[0])) throw new TypeError("unsupported archive numeric token");
31
+ }
32
+ if (!validator.Check(value)) throw new TypeError("unsupported archive projection shape/version");
33
+ const projection = value as ArchiveProjection, ids = new Set<string>();
34
+ for (const e of projection.executions) {
35
+ if (ids.has(e.executionId) || (e.runtime !== "terminal" && e.outcome !== null)) throw new TypeError("ambiguous archive execution");
36
+ ids.add(e.executionId);
37
+ }
38
+ projection.executions.sort((a, b) => a.executionId < b.executionId ? -1 : a.executionId > b.executionId ? 1 : 0);
39
+ return freezeDaily(projection);
40
+ }
41
+ export type SnapshotRead = { status: "read"; bytes: Buffer; sha256: string } | { status: "missing" | "error" | "unconfigured"; reason: string };
42
+ /** Caller-authorized exact file only. No directories, symlink following, watching or mtime freshness claim. */
43
+ export async function readDailySnapshot(path: string | undefined, limit = DAILY_INPUT_LIMIT): Promise<SnapshotRead> {
44
+ if (!path) return { status: "unconfigured", reason: "not-configured" };
45
+ try {
46
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
47
+ return await runWithFinalizers(async (): Promise<SnapshotRead> => {
48
+ const before = await file.stat({ bigint: true });
49
+ if (!before.isFile() || before.size > BigInt(limit)) return { status: "error", reason: "not-regular-or-over-limit" };
50
+ const bytes = Buffer.alloc(Number(before.size)); let offset = 0;
51
+ while (offset < bytes.length) { const n = (await file.read(bytes, offset, bytes.length - offset, offset)).bytesRead;
52
+ if (!n) return { status: "error", reason: "changed-during-read" }; offset += n; }
53
+ const after = await file.stat({ bigint: true }), located = await lstat(path, { bigint: true });
54
+ if (before.size !== after.size || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs ||
55
+ after.dev !== located.dev || after.ino !== located.ino) return { status: "error", reason: "changed-during-read" };
56
+ return { status: "read", bytes, sha256: byteHash(bytes) };
57
+ }, [{ label: "snapshot close failed", run: () => file.close() }]);
58
+ } catch (error) { return { status: (error as { code?: string }).code === "ENOENT" ? "missing" : "error", reason: "snapshot-unavailable" }; }
59
+ }
@@ -0,0 +1,44 @@
1
+ import type { DailyView } from "./daily-view.ts";
2
+ const clean = (value: unknown) => String(value ?? "unknown").replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
3
+ /** Text-only, accessible labels. No command links, control actions, model calls or terminal input. */
4
+ export function renderDailyView(view: DailyView, width = 100): string {
5
+ const lines = ["PI-DADDY — READ-ONLY DAILY VIEW", view.narrative,
6
+ `Scope: ${view.scope ? `${view.scope.id} revision ${view.scope.revision} digest ${view.scope.digest}` : "UNRESOLVED — select an exact P01 snapshot"}`,
7
+ `Acceptance authority: ${view.authority}; coverage: PARTIAL; freshness: SNAPSHOT / UNKNOWN`,
8
+ `Models: NOT CONSULTED; connectivity UNKNOWN. Continuity: ${view.continuity}${view.scopeChanged ? "; SCOPE CHANGED" : ""}`,
9
+ `Inputs: work ${view.sources.work}; archive ${view.sources.archive}`, "", "INTENDED OBLIGATIONS"];
10
+ if (!view.obligations.length) lines.push("EMPTY / UNRESOLVED: no scoped obligations available; not proof of no intended work.");
11
+ const attempts = new Map(view.attempts.map(a => [a.executionId, a]));
12
+ for (const o of view.obligations) {
13
+ lines.push(`${o.obligation.id}@${o.obligation.revision} | ${o.acceptance.toUpperCase()} | ${o.activity.toUpperCase()}`,
14
+ ` intent ${o.intent.id}@${o.intent.revision}; policy ${o.policy.id}@${o.policy.revision}`,
15
+ ` artifact ${o.artifactCoverage}; evidence ${o.evidenceCoverage}; P01 matched receipts: ${o.receiptIds.join(", ") || "none"}`,
16
+ ` obstacle: ${o.obstacles.join(", ") || (o.acceptance === "accepted-under-supplied-authority" ? "none under supplied snapshot authority" : "acceptance not established")}`);
17
+ for (const id of o.attempts) {
18
+ const a = attempts.get(id)!;
19
+ lines.push(` attempt ${id} (logical ${a.logicalChildId ?? "unknown"}) | P01 runtime ${a.intentRuntime} | P03 ${a.archive?.runtime ?? "unavailable"}`);
20
+ }
21
+ for (const ref of o.claimReferences) lines.push(` claim source ${ref}`);
22
+ }
23
+ lines.push("", "EXECUTION SNAPSHOTS — NOT ACCEPTANCE");
24
+ if (!view.attempts.length) lines.push("EMPTY: no execution attempts in these snapshots.");
25
+ for (const a of view.attempts) {
26
+ const e = a.archive;
27
+ lines.push(`${a.executionId} | logical ${a.logicalChildId ?? "unknown"} | P01 ${a.intentRuntime} | P03 ${e?.runtime ?? "unavailable"}`);
28
+ if (e) {
29
+ lines.push(` producer-reported exit ${e.outcome?.code ?? "unknown"}; failed control/runtime ${e.outcome?.failed ?? "unknown"}; aborted ${e.outcome?.aborted ?? "unknown"}`,
30
+ ` parents ${e.parentExecutionIds.map(clean).join(", ") || "unknown"}; calls ${e.toolCallIds.map(clean).join(", ") || "unknown"}`,
31
+ ` reported retained sessions ${e.retainedSessionIds.join(", ") || "none"}; ACTIVE BRANCH UNKNOWN`,
32
+ " check receipt detail/bytes not supplied by P03 v1; never inferred from exit status");
33
+ }
34
+ lines.push(` obstacle/coverage: ${a.issues.join(", ") || "partial snapshot; freshness unknown"}`);
35
+ for (const r of a.sourceAvailability) lines.push(` source ${r.sha256} | ${r.state}`);
36
+ }
37
+ lines.push("", `Coverage gaps: ${view.issues.join("; ") || "partial snapshots; live freshness unavailable"}`,
38
+ `Work source SHA256: ${view.sources.workSha256 ?? "missing"}`, `Archive projection SHA256: ${view.sources.archiveSha256 ?? "missing"}`,
39
+ "P06: fixed digest profile only; arbitrary shell/model effects and aggregate CPU/memory/PID/money caps unsupported.",
40
+ "No steering, dispatch, acceptance, session edit or recovery action is performed by this view.");
41
+ const bounded = lines.slice(0, 300); if (lines.length > 300) bounded.push(`${lines.length - 300} additional lines omitted; full rows remain in the read-model.`);
42
+ const columns = Number.isFinite(width) ? Math.max(20, Math.min(240, Math.floor(width))) : 100;
43
+ return bounded.map(line => { const text = [...clean(line)]; return text.length > columns ? text.slice(0, columns - 3).join("") + "..." : text.join(""); }).join("\n");
44
+ }
@@ -0,0 +1,152 @@
1
+ import { createHash } from "node:crypto";
2
+ import { projectWorkLedger, parseWorkLedgerText, type WorkProjectionContext, type WorkFrozen, type RevisionRef } from "./work-ledger.ts";
3
+ import { canonicalWorkJson, copyWorkJson } from "./work-ledger-json.ts";
4
+ import { readDailySnapshot, parseArchiveProjection, freezeDaily, type ArchiveExecution } from "./daily-view-input.ts";
5
+
6
+ export const DAILY_VIEW_VERSION = "pi-daddy-daily-view-v1" as const;
7
+ export interface DailyViewOptions {
8
+ archiveProjectionPath?: string; workLedgerPath?: string;
9
+ /** Explicit retained adapter bytes; never authority. Mutually exclusive with the corresponding path. */
10
+ archiveBytes?: Uint8Array; workBytes?: Uint8Array;
11
+ sourceManifestBytes?: Readonly<Record<string, Uint8Array>>;
12
+ /** Genuine independently selected host authority only. Never populated from P03 or a context file. */
13
+ workContext?: WorkFrozen<WorkProjectionContext>;
14
+ /** Policy-authorized exact paths keyed by expected raw-manifest SHA256. No producer paths followed. */
15
+ sourceManifestFiles?: Readonly<Record<string, string>>;
16
+ }
17
+ export interface DailyAttempt {
18
+ executionId: string; logicalChildId: string | null; intentRuntime: string;
19
+ archive: ArchiveExecution | null; sourceAvailability: Array<{ sha256: string; state: "available" | "missing" | "mismatch" | "error" }>;
20
+ issues: string[]; activeBranch: null;
21
+ }
22
+ export interface DailyObligation {
23
+ key: string; intent: RevisionRef; obligation: RevisionRef; policy: RevisionRef;
24
+ acceptance: "accepted-under-supplied-authority" | "unaccepted" | "unresolved";
25
+ artifactCoverage: string; evidenceCoverage: string; receiptIds: string[]; claimReferences: string[];
26
+ attempts: string[]; activity: "unstarted" | "attempts-attached"; obstacles: string[];
27
+ }
28
+ export interface DailyView {
29
+ version: typeof DAILY_VIEW_VERSION; readOnly: true; freshness: "snapshot-unknown";
30
+ model: "not-consulted-connectivity-unknown"; continuity: "initial-snapshot" | "resnapshot" | "reconnected-gap";
31
+ scopeChanged: boolean; scope: RevisionRef | null; selectedSnapshot: WorkProjectionContext["selectedSnapshot"];
32
+ scopeState: string; progress: { accepted: number; total: number } | null;
33
+ authority: "supplied-host-context" | "unavailable" | "stale-for-selection"; coverage: "partial";
34
+ sources: { archive: string; work: string; archiveSha256: string | null; workSha256: string | null };
35
+ obligations: DailyObligation[]; attempts: DailyAttempt[]; issues: string[]; narrative: string;
36
+ }
37
+ const refKey = (r: WorkFrozen<RevisionRef>) => `${r.kind}:${r.id}:${r.revision}:${r.digest}`;
38
+ const sorted = (values: readonly string[]) => [...new Set(values)].sort();
39
+ function contextCopy(value: DailyViewOptions["workContext"]): WorkProjectionContext {
40
+ const copied = JSON.parse(canonicalWorkJson(copyWorkJson(value ?? { selectedSnapshot: null, authority: null }))) as WorkProjectionContext;
41
+ if (projectWorkLedger("", copied).errors.some(e => e.code === "WORK_CONTEXT_INVALID")) throw new TypeError("invalid independent P01 context");
42
+ return copied;
43
+ }
44
+ const readers = new WeakSet<object>();
45
+ /** Only actual factory readers may carry continuity state through the dashboard; not arbitrary callbacks. */
46
+ export function isDailyViewReader(value: unknown): value is ReturnType<typeof createDailyViewReader> {
47
+ return typeof value === "function" && readers.has(value);
48
+ }
49
+ /** Each read starts from complete bounded snapshots. The tracker holds only gap/scope metadata, not authority. */
50
+ export function createDailyViewReader() {
51
+ let seen = false, gap = false, lastSelection = "";
52
+ const read = async (options: DailyViewOptions): Promise<DailyView> => {
53
+ try {
54
+ const archivePath = options.archiveProjectionPath, workPath = options.workLedgerPath;
55
+ const context = contextCopy(options.workContext);
56
+ const files = Object.fromEntries(Object.entries(options.sourceManifestFiles ?? {}));
57
+ if (Object.keys(files).length > 128 || Object.entries(files).some(([key, value]) => !/^[a-f0-9]{64}$/.test(key) || typeof value !== "string")) throw new TypeError("bounded explicit source file map required");
58
+ const snapshot = (bytes: Uint8Array | undefined, path: string | undefined, limit: number) => {
59
+ if (bytes === undefined) return readDailySnapshot(path, limit);
60
+ if (path !== undefined || !(bytes instanceof Uint8Array) || bytes.length > limit) throw new TypeError("bounded exclusive retained snapshot required");
61
+ const copy=Buffer.from(bytes); return { status: "read" as const, bytes: copy, sha256: createHash("sha256").update(copy).digest("hex") };
62
+ };
63
+ const entries=Object.entries(options.sourceManifestBytes??{});if(entries.length>128)throw Error("source byte map bound");
64
+ const manifestBytes=Object.fromEntries(entries.map(([key,bytes])=>{
65
+ if(!/^[a-f0-9]{64}$/.test(key)||!(bytes instanceof Uint8Array)||bytes.length>65536)throw Error("bounded source bytes required");return [key,Buffer.from(bytes)];
66
+ }));
67
+ const [archiveRead, workRead] = await Promise.all([snapshot(options.archiveBytes,archivePath, 2*1024*1024), snapshot(options.workBytes,workPath, 16 * 1024 * 1024)]);
68
+ let archiveStatus: string = archiveRead.status, workStatus: string = workRead.status;
69
+ let archive: ArchiveExecution[] = [], work = projectWorkLedger("", context), workText = "";
70
+ const issues: string[] = [];
71
+ if (archiveRead.status === "read") {
72
+ try { archive = parseArchiveProjection(new TextDecoder("utf-8", { fatal: true }).decode(archiveRead.bytes)).executions; }
73
+ catch { archiveStatus = "error"; issues.push("archive-invalid-or-unsupported-version"); }
74
+ }
75
+ if (workRead.status === "read") {
76
+ try { workText = new TextDecoder("utf-8", { fatal: true }).decode(workRead.bytes); work = projectWorkLedger(workText, context); }
77
+ catch { workStatus = "error"; issues.push("work-input-invalid"); }
78
+ if (work.errors.length) { workStatus = "error"; issues.push("work-projection-errors"); }
79
+ }
80
+ const selection = JSON.stringify(context.selectedSnapshot), scopeChanged = seen && selection !== lastSelection;
81
+ const unavailable = archiveStatus !== "read" || ((Boolean(workPath) || options.workBytes !== undefined) && workStatus !== "read");
82
+ if (unavailable) gap = true;
83
+ const continuity = seen ? gap && !unavailable ? "reconnected-gap" : "resnapshot" : "initial-snapshot";
84
+ seen = true; lastSelection = selection;
85
+ if (scopeChanged) issues.push("scope-selection-changed; prior acceptance not carried forward");
86
+ if (gap) issues.push("observation-gap; resnapshot is not continuity");
87
+ if (archiveStatus !== "read") issues.push(`archive-${archiveStatus}`);
88
+ if (workStatus !== "read") issues.push(`work-${workStatus}`);
89
+ // Authority snapshot and selected work snapshot inhabit different identity namespaces.
90
+ // Applicability comes from P01's exact claim/selection/receipt validation, never ID equality.
91
+ const stale = !work.claims.some(c => c.matchedReceiptIds.length) && work.claims.some(c =>
92
+ c.applicability === "superseded" || c.problems.some(p => p.code === "RECEIPT_MISMATCH"));
93
+ const authority: DailyView["authority"] = !context.authority ? "unavailable" : stale ? "stale-for-selection" : "supplied-host-context";
94
+ if (authority !== "supplied-host-context") issues.push(`host-authority-${authority}`);
95
+ const events = workText && work.scopeState === "valid" ? parseWorkLedgerText(workText).events : [];
96
+ const selected = events.find(e => e.event === "work_snapshot" && e.eventId === context.selectedSnapshot?.event.eventId && e.digest === context.selectedSnapshot.event.digest);
97
+ const scope = selected?.event === "work_snapshot" ? selected.payload.snapshot.scope : null;
98
+ const available = new Map<string, DailyAttempt["sourceAvailability"][number]["state"]>();
99
+ const requested = sorted(archive.flatMap(e => e.sourceReferences));
100
+ for (const sha256 of requested) {
101
+ if (manifestBytes[sha256]) { available.set(sha256,createHash("sha256").update(manifestBytes[sha256]).digest("hex")===sha256?"available":"mismatch"); continue; }
102
+ if (!files[sha256]) { available.set(sha256, "missing"); continue; }
103
+ const source = await readDailySnapshot(files[sha256], 64 * 1024);
104
+ available.set(sha256, source.status === "read" ? source.sha256 === sha256 ? "available" : "mismatch" : source.status === "error" ? "error" : "missing");
105
+ }
106
+ const attempts = new Map<string, DailyAttempt>();
107
+ for (const attempt of work.runtime?.attempts ?? []) attempts.set(attempt.executionId, {
108
+ executionId: attempt.executionId, logicalChildId: attempt.childId, intentRuntime: attempt.state, archive: null,
109
+ sourceAvailability: [], issues: attempt.problems.map(p => `P01:${p.code}`), activeBranch: null,
110
+ });
111
+ for (const entry of archive) {
112
+ const a = attempts.get(entry.executionId) ?? { executionId: entry.executionId, logicalChildId: null, intentRuntime: "unknown",
113
+ archive: null, sourceAvailability: [], issues: [], activeBranch: null } as DailyAttempt;
114
+ a.archive = entry; a.issues.push(...entry.issues.map(i => `P03:${i}`));
115
+ a.sourceAvailability = entry.sourceReferences.map(sha256 => ({ sha256, state: available.get(sha256) ?? "missing" }));
116
+ if (!entry.sourceReferences.length) a.issues.push("source-hashes-absent");
117
+ if (a.sourceAvailability.some(r => r.state !== "available")) a.issues.push("source-bytes-unavailable-or-mismatched");
118
+ const intent = work.runtime?.attempts.find(r => r.executionId === entry.executionId);
119
+ if (intent && (entry.parentExecutionIds.length !== 1 || entry.parentExecutionIds[0] !== intent.parentExecutionId)) a.issues.push("P01-P03-parent-disagreement");
120
+ if (intent && intent.state === "running" && entry.runtime === "terminal") a.issues.push("P01-P03-runtime-disagreement");
121
+ a.issues = sorted(a.issues); attempts.set(a.executionId, a);
122
+ }
123
+ const obligations: DailyObligation[] = work.obligations.map(o => {
124
+ const attached = (work.runtime?.attempts ?? []).filter(a => a.bindings.some(b => scope && refKey(b.scope) === refKey(scope) && refKey(b.obligation) === refKey(o.binding.obligation)));
125
+ return {
126
+ key: canonicalWorkJson(copyWorkJson(o.binding)), intent: o.binding.intent, obligation: o.binding.obligation, policy: o.binding.policy,
127
+ acceptance: authority === "supplied-host-context" ? o.acceptance : "unresolved", artifactCoverage: o.artifactCoverage.state,
128
+ evidenceCoverage: o.evidenceCoverage.state,
129
+ receiptIds: sorted(work.claims.filter(c => o.claims.some(r => r.eventId === c.claim.eventId && r.digest === c.claim.digest)).flatMap(c => [...c.matchedReceiptIds])),
130
+ claimReferences: o.claims.map(c => `${c.eventId}:${c.digest}`).sort(), attempts: attached.map(a => a.executionId).sort(),
131
+ activity: attached.length ? "attempts-attached" : "unstarted", obstacles: sorted(o.problems.map(p => `P01:${p.code}`)),
132
+ };
133
+ });
134
+ obligations.sort((a, b) => {
135
+ const left = `${a.obligation.id}:${a.key}`, right = `${b.obligation.id}:${b.key}`;
136
+ return left < right ? -1 : left > right ? 1 : 0;
137
+ });
138
+ const progress = authority === "supplied-host-context" && workStatus === "read" ? work.progress : null;
139
+ issues.push(...work.problems.map(p => `P01:${p.code}`));
140
+ const view: DailyView = { version: DAILY_VIEW_VERSION, readOnly: true, freshness: "snapshot-unknown", model: "not-consulted-connectivity-unknown",
141
+ continuity, scopeChanged, scope, selectedSnapshot: context.selectedSnapshot, scopeState: work.scopeState,
142
+ progress, authority, coverage: "partial",
143
+ sources: { archive: archiveStatus, work: workStatus, archiveSha256: archiveRead.status === "read" ? archiveRead.sha256 : null, workSha256: workRead.status === "read" ? workRead.sha256 : null },
144
+ obligations, attempts: [...attempts.values()].sort((a, b) => a.executionId < b.executionId ? -1 : a.executionId > b.executionId ? 1 : 0), issues: sorted(issues),
145
+ narrative: `${obligations.length} scoped obligation(s); ${progress ? `${progress.accepted}/${progress.total} accepted under supplied P01 authority` : "acceptance unresolved"}; ${attempts.size} distinct attempt(s). Runtime exits do not establish acceptance. Snapshot freshness unknown.`,
146
+ };
147
+ return freezeDaily(view);
148
+ } catch (error) { gap = true; seen = true; throw error; }
149
+ };
150
+ readers.add(read); return read;
151
+ }
152
+ export const readDailyView = (options: DailyViewOptions): Promise<DailyView> => createDailyViewReader()(options);
@@ -1,10 +1,33 @@
1
1
  #!/usr/bin/env node
2
+ import { connectDashboardHost, isDashboardConnection, ENV_DASHBOARD_HOST_SOCKET, type DashboardConnection } from "./dashboard-host-transport.ts";
3
+ import { dashboardHostRequest, type DashboardHostRequest } from "./dashboard-host.ts";
4
+ export { ENV_DASHBOARD_HOST_SOCKET } from "./dashboard-host-transport.ts";
2
5
  import { realpathSync } from "node:fs";
3
6
  import { readFile } from "node:fs/promises";
4
7
  import { resolve } from "node:path";
5
8
  import { pathToFileURL } from "node:url";
6
9
  import { parseDashboardLedger } from "./dashboard-projection.ts";
7
10
  import { renderDashboard } from "./dashboard-render.ts";
11
+ import { createDailyViewReader, isDailyViewReader, readDailyView, type DailyViewOptions } from "./daily-view.ts";
12
+ import { renderDailyView } from "./daily-view-render.ts";
13
+ import { projectWorkLedger, type WorkProjectionContext } from "./work-ledger.ts";
14
+ import { parseRetentionJson } from "./retention-json.ts";
15
+ import { createInterface } from "node:readline";
16
+ import { isDebriefPresenter, type DebriefPresenter } from "./debrief.ts";
17
+ import { renderDebrief, debriefAction } from "./debrief-render.ts";
18
+ import { createFixtureDebrief } from "./debrief-fixture.ts";
19
+ export const ENV_DEBRIEF_FIXTURE = "PI_DADDY_DEBRIEF_FIXTURE";
20
+ export { debriefAction as dashboardDebriefAction } from "./debrief-render.ts";
21
+
22
+ export const ENV_DAILY_ARCHIVE = "PI_DADDY_ARCHIVE_PROJECTION";
23
+ export const ENV_DAILY_WORK = "PI_DADDY_WORK_LEDGER";
24
+ export const ENV_DAILY_SELECTION = "PI_DADDY_WORK_SNAPSHOT";
25
+ function dailySelection(text: string | undefined): WorkProjectionContext["selectedSnapshot"] {
26
+ if (!text) return null;
27
+ const selection = parseRetentionJson(text) as WorkProjectionContext["selectedSnapshot"];
28
+ if (projectWorkLedger("", { selectedSnapshot: selection, authority: null }).errors.some(e => e.code === "WORK_CONTEXT_INVALID")) throw new TypeError("invalid P01 snapshot selection");
29
+ return selection;
30
+ }
8
31
 
9
32
  export const DASHBOARD_PROTOCOL_VERSION = 1 as const;
10
33
  export const ENV_DASHBOARD_LEDGER = "PI_DADDY_LEDGER";
@@ -20,6 +43,16 @@ export interface DashboardFrameOptions {
20
43
  width?: number;
21
44
  details?: boolean;
22
45
  now?: Date;
46
+ dailyView?: DailyViewOptions;
47
+ dailyReader?: ReturnType<typeof createDailyViewReader>;
48
+ dailyJson?: boolean;
49
+ debrief?: DebriefPresenter;
50
+ debriefJson?: boolean;
51
+ connected?: DashboardConnection;
52
+ }
53
+ export async function dashboardHostAction(host:DashboardConnection,line:string){
54
+ if(!isDashboardConnection(host)||Buffer.byteLength(line)>60000)throw Error("genuine bounded dashboard connection required");
55
+ return host.action(dashboardHostRequest(parseRetentionJson(line,60000) as DashboardHostRequest));
23
56
  }
24
57
 
25
58
  function shellQuote(value: string): string {
@@ -57,6 +90,29 @@ export async function dashboardFrame(options: DashboardFrameOptions): Promise<st
57
90
  if (options.protocol !== undefined && options.protocol !== DASHBOARD_PROTOCOL_VERSION) {
58
91
  return incompatibleFrame(options.protocol);
59
92
  }
93
+ if(options.connected){
94
+ if(!isDashboardConnection(options.connected))throw Error("original dashboard connection required");
95
+ let view:Awaited<ReturnType<DashboardConnection["frame"]>>;
96
+ try{view=await options.connected.frame();}catch{return "PI-DADDY — CONNECTED HOST UNAVAILABLE\nNo prior acceptance, presentation or continuity substituted. No action retried.";}
97
+ if(options.dailyJson||options.debriefJson)return JSON.stringify(view);
98
+ const source=view.source as {daily?:Parameters<typeof renderDailyView>[0]}|null;
99
+ return ["PI-DADDY — CONNECTED HOST (snapshot; identity/acceptance not authenticated)",source?.daily?renderDailyView(source.daily,options.width):"Source unavailable; no previous acceptance substituted.",
100
+ view.debrief?renderDebrief(view.debrief as Parameters<typeof renderDebrief>[0],options.width):"Debrief deferred/closed. No implicit presentation or steering.",
101
+ `Attention reserved ${view.attention.attentionUsed}/5 (not proof of delivery). Tip ${view.tip}.`,"Explicit exact approved JSON requests only; refresh never acts."].join("\n\n");
102
+ }
103
+ if (options.debrief || options.debriefJson) {
104
+ if (!isDebriefPresenter(options.debrief)) return "PI-DADDY — DEBRIEF UNAVAILABLE: genuine presenter/host missing";
105
+ const view = options.debrief.view(); return options.debriefJson ? JSON.stringify(view) : renderDebrief(view, options.width);
106
+ }
107
+ if (options.dailyView) {
108
+ try {
109
+ if (options.dailyReader !== undefined && !isDailyViewReader(options.dailyReader)) throw new TypeError("untrusted daily reader callback");
110
+ const view = await (options.dailyReader ?? readDailyView)(options.dailyView);
111
+ return options.dailyJson ? JSON.stringify(view) : renderDailyView(view, options.width);
112
+ } catch {
113
+ return "PI-DADDY — DAILY VIEW UNAVAILABLE\nInvalid or unavailable read-only input. No previous acceptance or continuity was substituted.";
114
+ }
115
+ }
60
116
  if (!options.ledgerPath) return setupFrame(options.cwd);
61
117
 
62
118
  const ledgerPath = resolve(options.cwd, options.ledgerPath);
@@ -90,25 +146,43 @@ interface CliOptions {
90
146
  details: boolean;
91
147
  color: boolean;
92
148
  ledgerPath?: string;
149
+ archivePath?: string;
150
+ workPath?: string;
151
+ selection?: string;
152
+ dailyJson: boolean;
153
+ debriefFixture: boolean;
154
+ debriefJson: boolean;
155
+ hostSocket?: string;
93
156
  }
94
157
 
95
158
  function parseArgs(argv: string[]): CliOptions {
96
159
  let once = false;
97
160
  let details = false;
98
161
  let color = process.stdout.isTTY;
99
- let ledgerPath: string | undefined;
162
+ let ledgerPath: string | undefined, archivePath: string | undefined, workPath: string | undefined, selection: string | undefined;
163
+ let dailyJson = false, debriefFixture = false, debriefJson = false;let hostSocket:string|undefined;
100
164
  for (let index = 0; index < argv.length; index += 1) {
101
165
  const arg = argv[index];
102
166
  if (arg === "--once") once = true;
103
167
  else if (arg === "--details") details = true;
104
168
  else if (arg === "--no-color") color = false;
105
169
  else if (arg === "--ledger") ledgerPath = argv[++index];
170
+ else if (arg === "--daily-json") dailyJson = true;
171
+ else if (arg === "--debrief-fixture") debriefFixture = true;
172
+ else if (arg === "--debrief-json") debriefJson = true;
173
+ else if(arg === "--host-socket"){hostSocket=argv[++index];if(!hostSocket||hostSocket.startsWith("--"))throw Error("explicit host socket required");}
174
+ else if (["--archive-projection", "--work-ledger", "--work-snapshot"].includes(arg)) {
175
+ const value = argv[++index]; if (!value || value.startsWith("--")) throw new Error("missing daily view argument");
176
+ if (arg === "--archive-projection") archivePath = value;
177
+ else if (arg === "--work-ledger") workPath = value;
178
+ else selection = value;
179
+ }
106
180
  else throw new Error(`unknown dashboard argument ${JSON.stringify(arg)}`);
107
181
  }
108
- return { once, details, color, ledgerPath };
182
+ return { once, details, color, ledgerPath, archivePath, workPath, selection, dailyJson, debriefFixture, debriefJson, hostSocket };
109
183
  }
110
184
 
111
- export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.ProcessEnv = process.env): Promise<void> {
185
+ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.ProcessEnv = process.env, host: { debrief?: DebriefPresenter; connected?:DashboardConnection } = {}): Promise<void> {
112
186
  const cli = parseArgs(argv);
113
187
  const cwd = process.cwd();
114
188
  const ledgerPath = cli.ledgerPath ?? (env[ENV_DASHBOARD_LEDGER]?.trim() || undefined);
@@ -117,6 +191,22 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
117
191
  const key = env[ENV_DASHBOARD_KEY]?.trim();
118
192
  process.title = `pi-daddy-dashboard${key ? `:${key.slice(0, 12)}` : ""}`;
119
193
 
194
+ const archivePath = cli.archivePath ?? env[ENV_DAILY_ARCHIVE];
195
+ const workPath = cli.workPath ?? env[ENV_DAILY_WORK];
196
+ const selectedSnapshot = dailySelection(cli.selection ?? env[ENV_DAILY_SELECTION]);
197
+ const dailyView = archivePath || workPath || selectedSnapshot || cli.dailyJson ? {
198
+ archiveProjectionPath: archivePath ? resolve(cwd, archivePath) : undefined,
199
+ workLedgerPath: workPath ? resolve(cwd, workPath) : undefined,
200
+ workContext: { selectedSnapshot, authority: null },
201
+ } : undefined;
202
+ const socket=cli.hostSocket??env[ENV_DASHBOARD_HOST_SOCKET];const connected=host.connected??(socket?connectDashboardHost(socket):undefined);
203
+ if(connected&&(host.debrief||cli.debriefFixture||env[ENV_DEBRIEF_FIXTURE]==="1"))throw Error("connected host and separate debrief are exclusive");
204
+ const debrief = host.debrief ?? (cli.debriefFixture || env[ENV_DEBRIEF_FIXTURE] === "1" ? await createFixtureDebrief() : undefined);
205
+ if (debrief) {
206
+ if (!isDebriefPresenter(debrief)) throw new Error("invalid debrief presenter");
207
+ if (!host.debrief) await debrief.open({ mode: "manual", userPresent: true }); // Explicit CLI fixture only; never override a host deferral.
208
+ }
209
+ const dailyReader = createDailyViewReader();
120
210
  let previous = "";
121
211
  const draw = async (clear: boolean): Promise<void> => {
122
212
  const frame = await dashboardFrame({
@@ -126,6 +216,7 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
126
216
  color: cli.color,
127
217
  width: process.stdout.columns || 80,
128
218
  details: cli.details,
219
+ dailyView, dailyReader, dailyJson: cli.dailyJson, debrief, debriefJson: cli.debriefJson, connected,
129
220
  });
130
221
  if (frame === previous && !clear) return;
131
222
  previous = frame;
@@ -143,11 +234,19 @@ export async function runDashboard(argv = process.argv.slice(2), env: NodeJS.Pro
143
234
  drawing = true;
144
235
  void draw(true).finally(() => { drawing = false; });
145
236
  };
237
+ const input = debrief || connected ? createInterface({ input: process.stdin, terminal: false }) : null;
238
+ let acting = false;
239
+ input?.on("line", line => {
240
+ if (acting) return;
241
+ acting = true;
242
+ void (connected?dashboardHostAction(connected,line):debriefAction(debrief!, line)).catch(() => { process.stderr.write("Debrief action refused or acknowledgement unknown; inspect/reconcile explicitly.\n"); })
243
+ .finally(() => { acting = false; redraw(); });
244
+ });
146
245
  const timer = setInterval(redraw, DASHBOARD_REFRESH_MS);
147
246
  process.on("SIGWINCH", redraw);
148
247
  await new Promise<void>((settle) => {
149
248
  const stop = () => {
150
- clearInterval(timer);
249
+ clearInterval(timer); input?.close();
151
250
  process.off("SIGWINCH", redraw);
152
251
  settle();
153
252
  };
@@ -0,0 +1,28 @@
1
+ import { constants } from "node:fs";
2
+ import { lstat,realpath,open,mkdir,mkdtemp,writeFile,symlink } from "node:fs/promises";
3
+ import { dirname,join,isAbsolute } from "node:path";
4
+ import { pathToFileURL } from "node:url";
5
+ import { dataDigest,byteDigest,detached,sha } from "./debrief-contract.ts";
6
+ import { DASHBOARD_HARNESS_PIN,type DashboardHarness } from "./dashboard-host-contract.ts";
7
+ export interface DashboardHarnessArtifact {version:"dashboard-harness-artifact-v1";sourceCommit:typeof DASHBOARD_HARNESS_PIN;files:Record<string,string>;typeboxRoot:string;typeboxPackageSha256:string}
8
+ const loaded=new WeakMap<object,string>();
9
+ export const loadedDashboardHarnessDigest=(h:unknown):string|null=>typeof h==="object"&&h!==null?loaded.get(h)??null:null;
10
+ async function privateDirectory(path:string){const stat=await lstat(path);if(!isAbsolute(path)||await realpath(path)!==path||!stat.isDirectory()||stat.mode&0o077||stat.uid!==process.getuid?.())throw Error("canonical private owned harness directory required");}
11
+ async function bytes(path:string){if(await realpath(path)!==path)throw Error("harness artifact alias");const fd=await open(path,constants.O_RDONLY|constants.O_NOFOLLOW|constants.O_NONBLOCK);try{const stat=await fd.stat();if(!stat.isFile()||stat.nlink!==1||stat.mode&0o022||stat.size>1048576)throw Error("bounded immutable artifact required");const value=Buffer.alloc(stat.size+1),n=await fd.read(value,0,value.length,0),after=await fd.stat();if(n.bytesRead!==stat.size||after.size!==stat.size||after.mtimeMs!==stat.mtimeMs||after.ctimeMs!==stat.ctimeMs)throw Error("artifact changed during read");return value.subarray(0,n.bytesRead);}finally{await fd.close();}}
12
+ /** Load approved real compiled artifacts into an exclusive private tree, avoiding stale module-cache paths.
13
+ * Byte identity is NOT human/module authentication or hostile-same-UID confinement. No build/install here.
14
+ * Typebox is the explicit existing peer; its package metadata is bound, not a whole peer-source attestation. */
15
+ export async function loadDashboardHarness(artifactRoot:string,input:DashboardHarnessArtifact,parent:string){
16
+ const manifest=detached(input);if(manifest.version!=="dashboard-harness-artifact-v1"||manifest.sourceCommit!==DASHBOARD_HARNESS_PIN||!sha(manifest.typeboxPackageSha256))throw Error("exact supported harness artifact required");
17
+ const files=Object.entries(manifest.files);if(!files.length||files.length>128||files.some(([p,h])=>!sha(h)||!/^([a-zA-Z0-9_-]+\/)*[a-zA-Z0-9_.-]+\.(js|json)$/.test(p)))throw Error("bounded closed artifact inventory required");
18
+ await privateDirectory(artifactRoot);await privateDirectory(parent);
19
+ if(await realpath(manifest.typeboxRoot)!==manifest.typeboxRoot||byteDigest(await bytes(join(manifest.typeboxRoot,"package.json")))!==manifest.typeboxPackageSha256)throw Error("retained typebox peer changed");
20
+ const content=new Map<string,Buffer>();let total=0;for(const[p,digest]of files){const b=await bytes(join(artifactRoot,p));total+=b.length;if(total>8*1024*1024||byteDigest(b)!==digest)throw Error("harness artifact digest mismatch");content.set(p,b);}
21
+ if(content.get("package.json")?.toString()!=='{"type":"module"}')throw Error("explicit ESM artifact root required");
22
+ const directory=await mkdtemp(join(parent,"loaded-harness-"));for(const[p,b]of content){await mkdir(dirname(join(directory,p)),{recursive:true,mode:0o700});await writeFile(join(directory,p),b,{flag:"wx",mode:0o400});}
23
+ await mkdir(join(directory,"node_modules"));await symlink(manifest.typeboxRoot,join(directory,"node_modules/typebox"));
24
+ const names=["learning-journal","trust-lifecycle","archive-policy","archive-observer","archive-checkpoint","archived-work","execution-retention-archive","work-case-review","blind-intervention","evidence-archive","work-signal-observation","work-signal-cases","work-case-archive"];
25
+ const api=Object.assign({},...await Promise.all([...names.map(n=>"packages/adapters/src/"+n+".js"),"core.js"].map(p=>{if(!content.has(p))throw Error("missing genuine harness entry");return import(pathToFileURL(join(directory,p)).href);}))) as DashboardHarness;
26
+ for(const[p,digest]of files)if(byteDigest(await bytes(join(directory,p)))!==digest)throw Error("loaded harness bytes changed");
27
+ Object.freeze(api);const digest=dataDigest(manifest);loaded.set(api,digest);return Object.freeze({api,artifactDigest:digest,directory,manifest});
28
+ }
@@ -6,7 +6,7 @@ import {
6
6
  ENV_DASHBOARD_KEY,
7
7
  ENV_DASHBOARD_LEDGER,
8
8
  ENV_DASHBOARD_PROTOCOL,
9
- DASHBOARD_PROTOCOL_VERSION,
9
+ DASHBOARD_PROTOCOL_VERSION, ENV_DAILY_ARCHIVE, ENV_DAILY_WORK, ENV_DAILY_SELECTION, ENV_DEBRIEF_FIXTURE, ENV_DASHBOARD_HOST_SOCKET,
10
10
  } from "./dashboard-cli.ts";
11
11
  import { parseReply, type HerdrExec } from "./herdr-cli.ts";
12
12
  import { withFileLock } from "./file-lock.ts";
@@ -263,8 +263,12 @@ export async function openOrReuseDashboard(input: DashboardOpenInput): Promise<D
263
263
  const exec = input.exec ?? dashboardHerdrExec;
264
264
  const ledgerPath = input.ledgerPath.trim() ? resolve(input.cwd, input.ledgerPath) : "";
265
265
  const cwd = resolve(input.cwd);
266
+ // Explicit operator inputs/owned host endpoint only, never an authority file or ambient snapshot.
267
+ // The endpoint keeps original controller handles in its owning process; it is not TUI attachment.
268
+ const dailyEnv = [ENV_DAILY_ARCHIVE, ENV_DAILY_WORK, ENV_DAILY_SELECTION, ENV_DEBRIEF_FIXTURE, ENV_DASHBOARD_HOST_SOCKET]
269
+ .flatMap(name => process.env[name] ? ["--env", `${name}=${process.env[name]}`] : []);
266
270
  const key = createHash("sha256")
267
- .update(`${input.host.workspaceId}\0${input.host.tabId}\0${ledgerPath}`, "utf8")
271
+ .update(`${input.host.workspaceId}\0${input.host.tabId}\0${ledgerPath}${dailyEnv.length ? JSON.stringify([cwd, ...dailyEnv]) : ""}`, "utf8")
268
272
  .digest("hex");
269
273
  await mkdir(dirname(input.statePath), { recursive: true });
270
274
 
@@ -307,6 +311,7 @@ export async function openOrReuseDashboard(input: DashboardOpenInput): Promise<D
307
311
  ...(ledgerPath ? ["--env", `${ENV_DASHBOARD_LEDGER}=${ledgerPath}`] : []),
308
312
  "--env", `${ENV_DASHBOARD_PROTOCOL}=${DASHBOARD_PROTOCOL_VERSION}`,
309
313
  "--env", `${ENV_DASHBOARD_KEY}=${key}`,
314
+ ...dailyEnv,
310
315
  "--no-focus",
311
316
  ];
312
317
  const opened = parseReply(await exec(args));