pi-daddy 0.22.0 → 0.24.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 (385) hide show
  1. package/CHANGELOG.md +40 -0
  2. package/README.md +12 -6
  3. package/contracts/daily-view/v1/README.md +114 -0
  4. package/contracts/daily-view/v1/fixtures/view.json +525 -0
  5. package/contracts/daily-view/v1/fixtures/view.txt +75 -0
  6. package/contracts/daily-view/v1/fixtures/work.jsonl +17 -0
  7. package/contracts/daily-view/v1/p03/README.md +26 -0
  8. package/contracts/daily-view/v1/p03/fixtures/retained-executions.json +241 -0
  9. package/contracts/daily-view/v1/p03/projection.schema.json +194 -0
  10. package/contracts/daily-view/v1/p03/provenance.json +21 -0
  11. package/contracts/dashboard-host/v1/README.md +176 -0
  12. package/contracts/debrief/v1/README.md +146 -0
  13. package/contracts/debrief/v1/intervention/README.md +32 -0
  14. package/contracts/debrief/v1/intervention/fixtures/artifact-3ee11136beb864f518252e5024dae9197421fbf97b6e981d3243c782b5c44bd0.txt +1 -0
  15. package/contracts/debrief/v1/intervention/fixtures/artifact-58905d872dd3c54a9705f67b05fa82edbad7af6476ae8993faf1a7b9fadfb905.txt +1 -0
  16. package/contracts/debrief/v1/intervention/fixtures/assessment.json +29 -0
  17. package/contracts/debrief/v1/intervention/fixtures/blind-view.json +20 -0
  18. package/contracts/debrief/v1/intervention/fixtures/choice.json +7 -0
  19. package/contracts/debrief/v1/intervention/fixtures/manifest.json +57 -0
  20. package/contracts/debrief/v1/intervention/fixtures/reveal.json +39 -0
  21. package/contracts/debrief/v1/provenance.json +89 -0
  22. package/contracts/debrief/v1/work-capture/README.md +38 -0
  23. package/contracts/debrief/v1/work-capture/fixtures/batch.json +17 -0
  24. package/contracts/debrief/v1/work-capture/fixtures/cases.json +184 -0
  25. package/contracts/debrief/v1/work-capture/fixtures/review.json +184 -0
  26. package/contracts/debrief/v1/work-capture/review-request.schema.json +41 -0
  27. package/contracts/debrief/v1/work-capture/work-case.schema.json +190 -0
  28. package/contracts/debrief/v2/README.md +71 -0
  29. package/contracts/debrief/v2/input-pin.json +66 -0
  30. package/contracts/debrief/v2/intervention-upstream.md +40 -0
  31. package/contracts/debrief/v2/work-signals-upstream.md +22 -0
  32. package/contracts/dispatch-control/v1/README.md +109 -0
  33. package/contracts/dispatch-control/v1/request.schema.json +19 -0
  34. package/contracts/effect-profile/v1/README.md +124 -0
  35. package/contracts/execution-retention/v1/README.md +109 -0
  36. package/contracts/execution-retention/v2/README.md +148 -0
  37. package/contracts/execution-retention/v2/fixtures/interrupted-native-bytes.json +91 -0
  38. package/contracts/execution-retention/v2/fixtures/missing-native-session.json +91 -0
  39. package/contracts/execution-retention/v2/fixtures/native-file-unknown-branch.json +91 -0
  40. package/contracts/execution-retention/v2/fixtures/native-id-without-bytes.json +91 -0
  41. package/contracts/execution-retention/v2/fixtures/native-live-branch.json +90 -0
  42. package/contracts/execution-retention/v2/fixtures/replaced-native-session.json +92 -0
  43. package/contracts/execution-retention/v2/fixtures/result-82c620cdfeb8ca6b858cf653d7f5e2eedca5315d6244216d60e8b36f03223d6a.bin +1 -0
  44. package/contracts/execution-retention/v2/fixtures/session-2ec1f134bc739984434f7fca689e1c3d1c39fce2dcadae709e9b6faedfcc4ac7.bin +3 -0
  45. package/contracts/execution-retention/v2/fixtures/session-9bdb4bd16a901da31580acf056bda525225259e15ec409e0410480109011772f.bin +3 -0
  46. package/contracts/execution-retention/v2/manifest.schema.json +858 -0
  47. package/contracts/experiment/v1/README.md +164 -0
  48. package/contracts/factory-order/v1/README.md +151 -0
  49. package/contracts/factory-order/v1/adoption-pin.json +8 -0
  50. package/contracts/intent-control/v1/README.md +135 -0
  51. package/contracts/intent-control/v1/request.schema.json +1096 -0
  52. package/contracts/intent-control/v2/README.md +34 -0
  53. package/contracts/intent-control/v2/request.schema.json +1070 -0
  54. package/contracts/ledger/v4/README.md +241 -0
  55. package/contracts/ledger/v4/fixtures/layout-options.json +428 -0
  56. package/contracts/ledger/v4/fixtures/work-acceptance.json +57 -0
  57. package/contracts/ledger/v4/fixtures/work-occurrence.json +44 -0
  58. package/contracts/ledger/v4/fixtures/work-revision.json +26 -0
  59. package/contracts/ledger/v4/fixtures/work-snapshot.json +73 -0
  60. package/contracts/ledger/v4/ledger-event.schema.json +162 -0
  61. package/contracts/ordinary-control/v1/README.md +59 -0
  62. package/contracts/producer-ipc/v1/README.md +137 -0
  63. package/dist/check-runner.d.ts +5 -0
  64. package/dist/check-runner.d.ts.map +1 -1
  65. package/dist/check-runner.js +16 -2
  66. package/dist/check-runner.js.map +1 -1
  67. package/dist/cli.d.ts +3 -1
  68. package/dist/cli.d.ts.map +1 -1
  69. package/dist/cli.js +49 -3
  70. package/dist/cli.js.map +1 -1
  71. package/dist/control-journal.d.ts +20 -0
  72. package/dist/control-journal.d.ts.map +1 -0
  73. package/dist/control-journal.js +73 -0
  74. package/dist/control-journal.js.map +1 -0
  75. package/dist/daily-dashboard-host.d.ts +99 -0
  76. package/dist/daily-dashboard-host.d.ts.map +1 -0
  77. package/dist/daily-dashboard-host.js +71 -0
  78. package/dist/daily-dashboard-host.js.map +1 -0
  79. package/dist/daily-view-input.d.ts +44 -0
  80. package/dist/daily-view-input.d.ts.map +1 -0
  81. package/dist/daily-view-input.js +68 -0
  82. package/dist/daily-view-input.js.map +1 -0
  83. package/dist/daily-view-render.d.ts +4 -0
  84. package/dist/daily-view-render.d.ts.map +1 -0
  85. package/dist/daily-view-render.js +41 -0
  86. package/dist/daily-view-render.js.map +1 -0
  87. package/dist/daily-view.d.ts +74 -0
  88. package/dist/daily-view.d.ts.map +1 -0
  89. package/dist/daily-view.js +170 -0
  90. package/dist/daily-view.js.map +1 -0
  91. package/dist/dashboard-cli.d.ts +26 -1
  92. package/dist/dashboard-cli.d.ts.map +1 -1
  93. package/dist/dashboard-cli.js +128 -3
  94. package/dist/dashboard-cli.js.map +1 -1
  95. package/dist/dashboard-harness.d.ts +22 -0
  96. package/dist/dashboard-harness.d.ts.map +1 -0
  97. package/dist/dashboard-harness.js +89 -0
  98. package/dist/dashboard-harness.js.map +1 -0
  99. package/dist/dashboard-herdr.d.ts.map +1 -1
  100. package/dist/dashboard-herdr.js +7 -2
  101. package/dist/dashboard-herdr.js.map +1 -1
  102. package/dist/dashboard-host-contract.d.ts +116 -0
  103. package/dist/dashboard-host-contract.d.ts.map +1 -0
  104. package/dist/dashboard-host-contract.js +14 -0
  105. package/dist/dashboard-host-contract.js.map +1 -0
  106. package/dist/dashboard-host-transport.d.ts +16 -0
  107. package/dist/dashboard-host-transport.d.ts.map +1 -0
  108. package/dist/dashboard-host-transport.js +124 -0
  109. package/dist/dashboard-host-transport.js.map +1 -0
  110. package/dist/dashboard-host.d.ts +203 -0
  111. package/dist/dashboard-host.d.ts.map +1 -0
  112. package/dist/dashboard-host.js +376 -0
  113. package/dist/dashboard-host.js.map +1 -0
  114. package/dist/dashboard-observation.d.ts +28 -0
  115. package/dist/dashboard-observation.d.ts.map +1 -0
  116. package/dist/dashboard-observation.js +86 -0
  117. package/dist/dashboard-observation.js.map +1 -0
  118. package/dist/debrief-contract.d.ts +65 -0
  119. package/dist/debrief-contract.d.ts.map +1 -0
  120. package/dist/debrief-contract.js +151 -0
  121. package/dist/debrief-contract.js.map +1 -0
  122. package/dist/debrief-fixture.d.ts +16 -0
  123. package/dist/debrief-fixture.d.ts.map +1 -0
  124. package/dist/debrief-fixture.js +41 -0
  125. package/dist/debrief-fixture.js.map +1 -0
  126. package/dist/debrief-host.d.ts +59 -0
  127. package/dist/debrief-host.d.ts.map +1 -0
  128. package/dist/debrief-host.js +49 -0
  129. package/dist/debrief-host.js.map +1 -0
  130. package/dist/debrief-render.d.ts +5 -0
  131. package/dist/debrief-render.d.ts.map +1 -0
  132. package/dist/debrief-render.js +65 -0
  133. package/dist/debrief-render.js.map +1 -0
  134. package/dist/debrief.d.ts +90 -0
  135. package/dist/debrief.d.ts.map +1 -0
  136. package/dist/debrief.js +263 -0
  137. package/dist/debrief.js.map +1 -0
  138. package/dist/delegate-types.d.ts +2 -0
  139. package/dist/delegate-types.d.ts.map +1 -1
  140. package/dist/delegate-types.js.map +1 -1
  141. package/dist/delegate.d.ts.map +1 -1
  142. package/dist/delegate.js +1 -0
  143. package/dist/delegate.js.map +1 -1
  144. package/dist/dispatch-control.d.ts +45 -0
  145. package/dist/dispatch-control.d.ts.map +1 -0
  146. package/dist/dispatch-control.js +76 -0
  147. package/dist/dispatch-control.js.map +1 -0
  148. package/dist/effect-profile-runtime.d.ts +40 -0
  149. package/dist/effect-profile-runtime.d.ts.map +1 -0
  150. package/dist/effect-profile-runtime.js +51 -0
  151. package/dist/effect-profile-runtime.js.map +1 -0
  152. package/dist/effect-profile.d.ts +40 -0
  153. package/dist/effect-profile.d.ts.map +1 -0
  154. package/dist/effect-profile.js +152 -0
  155. package/dist/effect-profile.js.map +1 -0
  156. package/dist/execution-retention.d.ts +90 -0
  157. package/dist/execution-retention.d.ts.map +1 -0
  158. package/dist/execution-retention.js +247 -0
  159. package/dist/execution-retention.js.map +1 -0
  160. package/dist/experiment-contract.d.ts +56 -0
  161. package/dist/experiment-contract.d.ts.map +1 -0
  162. package/dist/experiment-contract.js +80 -0
  163. package/dist/experiment-contract.js.map +1 -0
  164. package/dist/experiment-state.d.ts +22 -0
  165. package/dist/experiment-state.d.ts.map +1 -0
  166. package/dist/experiment-state.js +79 -0
  167. package/dist/experiment-state.js.map +1 -0
  168. package/dist/experiment-store.d.ts +30 -0
  169. package/dist/experiment-store.d.ts.map +1 -0
  170. package/dist/experiment-store.js +147 -0
  171. package/dist/experiment-store.js.map +1 -0
  172. package/dist/experiment.d.ts +65 -0
  173. package/dist/experiment.d.ts.map +1 -0
  174. package/dist/experiment.js +357 -0
  175. package/dist/experiment.js.map +1 -0
  176. package/dist/factory-contract.d.ts +79 -0
  177. package/dist/factory-contract.d.ts.map +1 -0
  178. package/dist/factory-contract.js +89 -0
  179. package/dist/factory-contract.js.map +1 -0
  180. package/dist/factory-migration.d.ts +20 -0
  181. package/dist/factory-migration.d.ts.map +1 -0
  182. package/dist/factory-migration.js +55 -0
  183. package/dist/factory-migration.js.map +1 -0
  184. package/dist/factory-order.d.ts +848 -0
  185. package/dist/factory-order.d.ts.map +1 -0
  186. package/dist/factory-order.js +83 -0
  187. package/dist/factory-order.js.map +1 -0
  188. package/dist/factory-registry.d.ts +121 -0
  189. package/dist/factory-registry.d.ts.map +1 -0
  190. package/dist/factory-registry.js +133 -0
  191. package/dist/factory-registry.js.map +1 -0
  192. package/dist/file-lock.d.ts +8 -2
  193. package/dist/file-lock.d.ts.map +1 -1
  194. package/dist/file-lock.js +78 -19
  195. package/dist/file-lock.js.map +1 -1
  196. package/dist/herdr-poll.d.ts +8 -0
  197. package/dist/herdr-poll.d.ts.map +1 -1
  198. package/dist/herdr-poll.js +23 -0
  199. package/dist/herdr-poll.js.map +1 -1
  200. package/dist/index.d.ts +20 -0
  201. package/dist/index.d.ts.map +1 -1
  202. package/dist/index.js +20 -0
  203. package/dist/index.js.map +1 -1
  204. package/dist/intent-application.d.ts +17 -0
  205. package/dist/intent-application.d.ts.map +1 -0
  206. package/dist/intent-application.js +113 -0
  207. package/dist/intent-application.js.map +1 -0
  208. package/dist/intent-control.d.ts +201 -0
  209. package/dist/intent-control.d.ts.map +1 -0
  210. package/dist/intent-control.js +110 -0
  211. package/dist/intent-control.js.map +1 -0
  212. package/dist/intent-scheduling.d.ts +9 -0
  213. package/dist/intent-scheduling.d.ts.map +1 -0
  214. package/dist/intent-scheduling.js +15 -0
  215. package/dist/intent-scheduling.js.map +1 -0
  216. package/dist/ledger-append.d.ts +15 -0
  217. package/dist/ledger-append.d.ts.map +1 -0
  218. package/dist/ledger-append.js +94 -0
  219. package/dist/ledger-append.js.map +1 -0
  220. package/dist/ledger.d.ts +11 -0
  221. package/dist/ledger.d.ts.map +1 -1
  222. package/dist/ledger.js +3 -7
  223. package/dist/ledger.js.map +1 -1
  224. package/dist/native-session-target.d.ts +9 -0
  225. package/dist/native-session-target.d.ts.map +1 -0
  226. package/dist/native-session-target.js +29 -0
  227. package/dist/native-session-target.js.map +1 -0
  228. package/dist/native-session.d.ts +59 -0
  229. package/dist/native-session.d.ts.map +1 -0
  230. package/dist/native-session.js +154 -0
  231. package/dist/native-session.js.map +1 -0
  232. package/dist/order-schedule.d.ts +37 -0
  233. package/dist/order-schedule.d.ts.map +1 -0
  234. package/dist/order-schedule.js +81 -0
  235. package/dist/order-schedule.js.map +1 -0
  236. package/dist/ordinary-children.d.ts +52 -0
  237. package/dist/ordinary-children.d.ts.map +1 -0
  238. package/dist/ordinary-children.js +93 -0
  239. package/dist/ordinary-children.js.map +1 -0
  240. package/dist/producer-ipc-contract.d.ts +29 -0
  241. package/dist/producer-ipc-contract.d.ts.map +1 -0
  242. package/dist/producer-ipc-contract.js +29 -0
  243. package/dist/producer-ipc-contract.js.map +1 -0
  244. package/dist/producer-ipc.d.ts +55 -0
  245. package/dist/producer-ipc.d.ts.map +1 -0
  246. package/dist/producer-ipc.js +143 -0
  247. package/dist/producer-ipc.js.map +1 -0
  248. package/dist/resource-budget.d.ts +334 -0
  249. package/dist/resource-budget.d.ts.map +1 -0
  250. package/dist/resource-budget.js +415 -0
  251. package/dist/resource-budget.js.map +1 -0
  252. package/dist/retention-contract.d.ts +73 -0
  253. package/dist/retention-contract.d.ts.map +1 -0
  254. package/dist/retention-contract.js +104 -0
  255. package/dist/retention-contract.js.map +1 -0
  256. package/dist/retention-json.d.ts +3 -0
  257. package/dist/retention-json.d.ts.map +1 -0
  258. package/dist/retention-json.js +32 -0
  259. package/dist/retention-json.js.map +1 -0
  260. package/dist/run-child.d.ts +4 -0
  261. package/dist/run-child.d.ts.map +1 -1
  262. package/dist/run-child.js +16 -2
  263. package/dist/run-child.js.map +1 -1
  264. package/dist/run-herdr.d.ts +6 -0
  265. package/dist/run-herdr.d.ts.map +1 -1
  266. package/dist/run-herdr.js +19 -6
  267. package/dist/run-herdr.js.map +1 -1
  268. package/dist/vendor/adoption.d.ts +129 -0
  269. package/dist/vendor/adoption.d.ts.map +1 -0
  270. package/dist/vendor/adoption.js +102 -0
  271. package/dist/vendor/adoption.js.map +1 -0
  272. package/dist/work-command.d.ts +30 -0
  273. package/dist/work-command.d.ts.map +1 -0
  274. package/dist/work-command.js +148 -0
  275. package/dist/work-command.js.map +1 -0
  276. package/dist/work-ledger-destination.d.ts +20 -0
  277. package/dist/work-ledger-destination.d.ts.map +1 -0
  278. package/dist/work-ledger-destination.js +131 -0
  279. package/dist/work-ledger-destination.js.map +1 -0
  280. package/dist/work-ledger-json.d.ts +25 -0
  281. package/dist/work-ledger-json.d.ts.map +1 -0
  282. package/dist/work-ledger-json.js +267 -0
  283. package/dist/work-ledger-json.js.map +1 -0
  284. package/dist/work-ledger-occurrences.d.ts +227 -0
  285. package/dist/work-ledger-occurrences.d.ts.map +1 -0
  286. package/dist/work-ledger-occurrences.js +115 -0
  287. package/dist/work-ledger-occurrences.js.map +1 -0
  288. package/dist/work-ledger-projection.d.ts +295 -0
  289. package/dist/work-ledger-projection.d.ts.map +1 -0
  290. package/dist/work-ledger-projection.js +237 -0
  291. package/dist/work-ledger-projection.js.map +1 -0
  292. package/dist/work-ledger-snapshot.d.ts +29 -0
  293. package/dist/work-ledger-snapshot.d.ts.map +1 -0
  294. package/dist/work-ledger-snapshot.js +267 -0
  295. package/dist/work-ledger-snapshot.js.map +1 -0
  296. package/dist/work-ledger-types.d.ts +261 -0
  297. package/dist/work-ledger-types.d.ts.map +1 -0
  298. package/dist/work-ledger-types.js +13 -0
  299. package/dist/work-ledger-types.js.map +1 -0
  300. package/dist/work-ledger-validation.d.ts +14 -0
  301. package/dist/work-ledger-validation.d.ts.map +1 -0
  302. package/dist/work-ledger-validation.js +226 -0
  303. package/dist/work-ledger-validation.js.map +1 -0
  304. package/dist/work-ledger.d.ts +43 -0
  305. package/dist/work-ledger.d.ts.map +1 -0
  306. package/dist/work-ledger.js +225 -0
  307. package/dist/work-ledger.js.map +1 -0
  308. package/extensions/daily-dashboard-session.ts +30 -0
  309. package/extensions/daily-work-session.ts +20 -0
  310. package/extensions/delegate-chain.ts +1 -0
  311. package/extensions/delegation-native.ts +12 -0
  312. package/extensions/delegation.ts +25 -11
  313. package/extensions/execute-child.ts +44 -5
  314. package/extensions/executor-session.ts +9 -0
  315. package/extensions/grants-command.ts +14 -15
  316. package/extensions/grants-connected-command.ts +19 -0
  317. package/extensions/grants.ts +26 -2
  318. package/extensions/ordinary-runtime.ts +14 -0
  319. package/extensions/primary-shadow.ts +11 -0
  320. package/extensions/run-delegation.ts +9 -7
  321. package/extensions/session.ts +10 -20
  322. package/extensions/work-runtime.ts +44 -0
  323. package/package.json +78 -4
  324. package/src/check-runner.ts +20 -2
  325. package/src/cli.ts +41 -4
  326. package/src/control-journal.ts +26 -0
  327. package/src/daily-dashboard-host.ts +49 -0
  328. package/src/daily-view-input.ts +59 -0
  329. package/src/daily-view-render.ts +44 -0
  330. package/src/daily-view.ts +152 -0
  331. package/src/dashboard-cli.ts +108 -4
  332. package/src/dashboard-harness.ts +37 -0
  333. package/src/dashboard-herdr.ts +7 -2
  334. package/src/dashboard-host-contract.ts +45 -0
  335. package/src/dashboard-host-transport.ts +40 -0
  336. package/src/dashboard-host.ts +191 -0
  337. package/src/dashboard-observation.ts +45 -0
  338. package/src/debrief-contract.ts +127 -0
  339. package/src/debrief-fixture.ts +29 -0
  340. package/src/debrief-host.ts +53 -0
  341. package/src/debrief-render.ts +49 -0
  342. package/src/debrief.ts +198 -0
  343. package/src/delegate-types.ts +2 -0
  344. package/src/delegate.ts +1 -0
  345. package/src/dispatch-control.ts +81 -0
  346. package/src/effect-profile-runtime.ts +46 -0
  347. package/src/effect-profile.ts +129 -0
  348. package/src/execution-retention.ts +230 -0
  349. package/src/experiment-contract.ts +66 -0
  350. package/src/experiment-state.ts +44 -0
  351. package/src/experiment-store.ts +98 -0
  352. package/src/experiment.ts +231 -0
  353. package/src/factory-contract.ts +47 -0
  354. package/src/factory-migration.ts +29 -0
  355. package/src/factory-order.ts +56 -0
  356. package/src/factory-registry.ts +60 -0
  357. package/src/file-lock.ts +47 -11
  358. package/src/herdr-poll.ts +18 -0
  359. package/src/index.ts +32 -0
  360. package/src/intent-application.ts +90 -0
  361. package/src/intent-control.ts +107 -0
  362. package/src/intent-scheduling.ts +14 -0
  363. package/src/ledger-append.ts +72 -0
  364. package/src/ledger.ts +3 -9
  365. package/src/native-session-target.ts +23 -0
  366. package/src/native-session.ts +139 -0
  367. package/src/order-schedule.ts +42 -0
  368. package/src/ordinary-children.ts +59 -0
  369. package/src/producer-ipc-contract.ts +26 -0
  370. package/src/producer-ipc.ts +107 -0
  371. package/src/resource-budget.ts +349 -0
  372. package/src/retention-contract.ts +84 -0
  373. package/src/retention-json.ts +20 -0
  374. package/src/run-child.ts +13 -2
  375. package/src/run-herdr.ts +11 -7
  376. package/src/vendor/adoption.ts +109 -0
  377. package/src/work-command.ts +179 -0
  378. package/src/work-ledger-destination.ts +113 -0
  379. package/src/work-ledger-json.ts +228 -0
  380. package/src/work-ledger-occurrences.ts +99 -0
  381. package/src/work-ledger-projection.ts +262 -0
  382. package/src/work-ledger-snapshot.ts +246 -0
  383. package/src/work-ledger-types.ts +229 -0
  384. package/src/work-ledger-validation.ts +174 -0
  385. package/src/work-ledger.ts +186 -0
package/src/herdr-poll.ts CHANGED
@@ -12,6 +12,7 @@
12
12
  */
13
13
 
14
14
  import { parseReply, type HerdrExec } from "./herdr-cli.ts";
15
+ import { herdrSessionReference } from "./native-session.ts";
15
16
 
16
17
  /**
17
18
  * What `waitForSettled` needs from a run request.
@@ -22,6 +23,8 @@ import { parseReply, type HerdrExec } from "./herdr-cli.ts";
22
23
  export interface PollTarget {
23
24
  /** herdr agent name. */
24
25
  name: string;
26
+ nativePaneId?: string;
27
+ onSessionReference?: (reference: NonNullable<ReturnType<typeof herdrSessionReference>>) => void;
25
28
  signal?: AbortSignal;
26
29
  /**
27
30
  * The pane's last few lines, re-reported on every poll — a SNAPSHOT, not a stream (ADR-0032).
@@ -31,6 +34,8 @@ export interface PollTarget {
31
34
  * `tailLines`.
32
35
  */
33
36
  onSnapshot?: (lines: string[]) => void;
37
+ /** Reuses already-requested pane bytes; never adds an observation-only RPC. */
38
+ onObservation?: (bytes: Uint8Array) => void;
34
39
  /** How many lines the display wants. Bounds the per-poll cost regardless of how big the pane is. */
35
40
  snapshotLines?: number;
36
41
  /** Poll cadence override. Exists so tests do not wait `POLL_INTERVAL_MS` per state transition. */
@@ -40,6 +45,17 @@ export interface PollTarget {
40
45
  /** Statuses herdr reports for a settled agent. `blocked` counts: it is waiting for a human, not working. */
41
46
  const TERMINAL = new Set(["idle", "done", "blocked"]);
42
47
 
48
+ /** The pre-prompt sequence from the native start reply; absent is unknown, never zero. */
49
+ export function observeHerdrSession(agent: unknown, pane: string | undefined, sink: PollTarget["onSessionReference"]): void {
50
+ if (!pane || !sink) return;
51
+ try { const reference = herdrSessionReference(agent, pane); if (reference) sink(reference); } catch { /* optional observation */ }
52
+ }
53
+
54
+ export function seqOf(result: Record<string, unknown> | undefined): number {
55
+ const agent = (result?.agent ?? {}) as { state_change_seq?: number };
56
+ return typeof agent.state_change_seq === "number" ? agent.state_change_seq : -1;
57
+ }
58
+
43
59
  /** How often to poll `agent get` while waiting for the child to settle. */
44
60
  export const POLL_INTERVAL_MS = 750;
45
61
  /** Lines of pane tail reported per poll. Matches the status block's own tail, so nothing is fetched unused. */
@@ -81,6 +97,7 @@ export async function waitForSettled(
81
97
  if (reply.error) return { spawnError: `herdr agent get failed: ${reply.error}` };
82
98
 
83
99
  const agent = (reply.result?.agent ?? reply.result ?? {}) as { agent_status?: string; state_change_seq?: number };
100
+ observeHerdrSession(agent, request.nativePaneId, request.onSessionReference);
84
101
  const status = agent.agent_status;
85
102
  const seq = typeof agent.state_change_seq === "number" ? agent.state_change_seq : -1;
86
103
 
@@ -89,6 +106,7 @@ export async function waitForSettled(
89
106
  // output shown. `readFailed` is passed so a failed read renders as such instead of silently freezing the
90
107
  // block on the previous frame — and, crucially, is never mistaken for the child's output.
91
108
  const read = await readPane(exec, request.name, maxOutputBytes);
109
+ if (!read.readFailed) { try { request.onObservation?.(Buffer.from(read.text)); } catch { /* observation only */ } }
92
110
  try {
93
111
  request.onSnapshot(read.readFailed ? ["[pane could not be read]"] : tailLines(read.text, keep));
94
112
  } catch {
package/src/index.ts CHANGED
@@ -34,7 +34,39 @@ export {
34
34
  type WorkflowFactState,
35
35
  } from "./ledger.ts";
36
36
 
37
+ export { createDebriefPresenter, type DebriefPresenter, type DebriefHost, type DebriefFrame, type DebriefCheckpoint, type DebriefPersistence } from "./debrief.ts";
38
+ export { ordinaryChildrenFor, ordinaryCancellation, ordinaryCancellationDigest, isOrdinaryChildren, type OrdinaryChildren, type OrdinaryCancellation, type OrdinaryAuthority, type OrdinaryTarget } from "./ordinary-children.ts";
39
+ export { loadDashboardHarness, loadedDashboardHarnessDigest, type DashboardHarnessArtifact } from "./dashboard-harness.ts";
40
+ export { createDashboardHost, openDashboardHost, dashboardHostDigest, dashboardHostRequestDigest, dashboardSelectionDigest, type DashboardHost, type DashboardHostConfig, type DashboardHostRequest, type DashboardHostAuthority, type DashboardHarness, type DashboardHostOptions } from "./dashboard-host.ts";
41
+ export { serveDashboardHost, connectDashboardHost, ENV_DASHBOARD_HOST_SOCKET, type DashboardConnection } from "./dashboard-host-transport.ts";
42
+ export { createRetainedDebrief, retainDebriefBlind, openDebriefBlindPreview, type DebriefHarness, type CaseSelection, type DurableBlindBinding } from "./debrief-host.ts";
43
+ export { renderDebrief, debriefAction as dashboardDebriefAction } from "./debrief-render.ts";
44
+ export { type ReviewPort, type ReviewRequest, type BlindPort, type BlindChoice } from "./debrief-contract.ts";
45
+ export { readDailyView, createDailyViewReader, DAILY_VIEW_VERSION, type DailyView, type DailyViewOptions,
46
+ type DailyAttempt, type DailyObligation } from "./daily-view.ts";
47
+ export { renderDailyView } from "./daily-view-render.ts";
48
+ export { parseArchiveProjection, ARCHIVE_PROJECTION_VERSION } from "./daily-view-input.ts";
49
+ export { bindWorkIntent } from "./intent-application.ts";
50
+ export { intentRequestDigest, parseIntentRequest, type IntentRequest, type IntentSelection, type IntentPriority,
51
+ type IntentAdmission, type IntentSnapshot, type IntentReceipt, type WorkIntentBinding } from "./intent-control.ts";
52
+ export { dispatchRequestDigest, parseDispatchRequest, type DispatchRequest, type DispatchAuthority, type DispatchRecord, type DispatchSnapshot } from "./dispatch-control.ts";
53
+ export { createFactoryRegistry, openFactoryRegistry, createFactoryOrder, openFactoryOrder, migrateFactoryOrder,
54
+ factoryOrderDigest, parseFactoryOrder, fixedPolicyDigest, factoryDecisionDigest, activationRequestDigest, factoryMigrationDigest,
55
+ type FactoryRegistryBinding, type FactoryOrderCharter, type FactoryAuthority, type FactoryOrderView, type FixedPolicy, type ActivationRequest, type FactoryMigration } from "./factory-order.ts";
56
+ export { createExperiment, openExperiment, experimentCharterDigest, experimentBindingDigest, experimentCancellationDigest, parseExperimentCharter,
57
+ type ExperimentBinding, type ExperimentCharter, type ExperimentAuthority, type ExperimentCancellation, type ExperimentRun, type ExperimentView } from "./experiment.ts";
58
+ export { createResourceBudget, createDispatchBudget, createIntentBudget, createExperimentBudget, openResourceBudget, resourceBindingDigest, ResourceAdmissionError,
59
+ type BudgetBinding, type DispatchBudgetBinding, type IntentBudgetBinding, type ExperimentBudgetBinding, type GovernedBudgetBinding, type ResourceLimits, type AttemptDemand, type ResourcePermit, type BudgetSnapshot } from "./resource-budget.ts";
60
+ export { createProducerIpcHost, startProducerIpc, producerIpcBinding, producerIpcBindingDigest, producerIpcDemand, PRODUCER_IPC_LIMITS,
61
+ type ProducerIpcBinding, type ProducerIpcReferences, type ProducerIpcContext, type ProducerIpcHost, type ProducerIpcSnapshot, type ProducerIpcRun } from "./producer-ipc.ts";
62
+ export { prepareDigestProfile, runDigestProfile, DIGEST_PROFILE, EffectProfileUnavailableError,
63
+ type DigestProfile, type DigestAttempt } from "./effect-profile.ts";
37
64
  export { planSpawn, type SpawnPlan, type SpawnPlanInput } from "./spawn.ts";
65
+ export { beginExecutionRetention, drainExecutionRetention, ENV_EXECUTION_ARCHIVE, RETENTION_VERSION, retentionConfigurationDigest, verifyRetainedBytes,
66
+ type ExecutionRetentionManifest, type RetainedContent, type RetentionIdentity, type RetentionStatus,
67
+ type ExecutionRetention, buildExecutionRetentionManifest, parseExecutionRetentionManifest, RETENTION_SCHEMA,
68
+ ENV_NATIVE_SESSION_ROOT, readNativeSession, parseNativeSessionBytes,
69
+ type NativeSessionObservation, type NativeSessionManager } from "./execution-retention.ts";
38
70
 
39
71
  export {
40
72
  APPROVAL_TTL_DAYS,
@@ -0,0 +1,90 @@
1
+ import { lstat, realpath } from "node:fs/promises";
2
+ import { dirname, isAbsolute, resolve } from "node:path";
3
+ import { readDailySnapshot } from "./daily-view-input.ts";
4
+ import { workDestination } from "./work-ledger-destination.ts";
5
+ import { appendWorkLedgerEventOnce, buildWorkRevisionEvent, buildWorkSnapshotEvent, parseWorkLedgerText, type WorkFrozen, type WorkRevision } from "./work-ledger.ts";
6
+ import { resolveWorkSnapshotText } from "./work-ledger-snapshot.ts";
7
+ import { controlShape } from "./dispatch-control.ts";
8
+ import { freezeWork } from "./work-ledger-json.ts";
9
+ import { intentKey, intentSelection, intentPriorities, type WorkIntentBinding, type IntentRequest, type IntentState, type IntentSelection, type IntentPriority } from "./intent-control.ts";
10
+ const ref = (r: WorkFrozen<WorkRevision>) => ({ kind: r.kind, id: r.id, revision: r.revision, digest: r.digest });
11
+ const entity = (r: { kind: string; id: string } | null) => r ? `${r.kind}:${r.id}` : null;
12
+ export function workIntentBinding(input: WorkIntentBinding): WorkIntentBinding {
13
+ controlShape(input, ["version", "path", "device", "inode", "grantLedgerPath", "selection", "priorities"]);
14
+ if (input.version !== "1.0" || typeof input.path !== "string" || input.path.length > 1024 || !isAbsolute(input.path) || resolve(input.path) !== input.path || input.path.split("/").includes(".pi") ||
15
+ ![input.device, input.inode].every(x => typeof x === "string" && /^\d+$/.test(x)) || !(input.grantLedgerPath === null || typeof input.grantLedgerPath === "string" && input.grantLedgerPath.length <= 1024 && isAbsolute(input.grantLedgerPath))) throw new TypeError("invalid pinned work binding");
16
+ return JSON.parse(intentKey({ ...input, selection: intentSelection(input.selection), priorities: intentPriorities(input.priorities) }));
17
+ }
18
+ export async function readIntentWork(input: WorkFrozen<WorkIntentBinding>): Promise<string> {
19
+ const check = async () => {
20
+ const st = await lstat(input.path, { bigint: true });
21
+ if (!st.isFile() || st.nlink !== 1n || st.uid !== BigInt(process.getuid!()) || (st.mode & 0o077n) || String(st.dev) !== input.device || String(st.ino) !== input.inode || await realpath(input.path) !== input.path) throw new Error("work binding changed");
22
+ };
23
+ await check(); const bytes = await readDailySnapshot(input.path, 16 * 1024 * 1024); await check();
24
+ if (bytes.status !== "read") throw new Error("required work snapshot unavailable");
25
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes.bytes);
26
+ if (!text.endsWith("\n") || !parseWorkLedgerText(text).complete) throw new Error("incomplete work ledger; no repair");
27
+ return text;
28
+ }
29
+ export function resolveIntent(text: string, selection: WorkFrozen<IntentSelection>, priorities: WorkFrozen<IntentPriority[]>): ReturnType<typeof resolveWorkSnapshotText> {
30
+ const result = resolveWorkSnapshotText(text, selection);
31
+ if (result.scopeState !== "valid" || !result.scope || !result.snapshot || result.revisions.length > 64) throw new Error("unresolved authoritative work selection");
32
+ const wanted = result.snapshot.bindings.map(b => intentKey(b.obligation)).sort();
33
+ if (intentKey(wanted) !== intentKey(priorities.map(p => intentKey(p.obligation)).sort())) throw new Error("priority policy must name exactly the selected obligations");
34
+ return result;
35
+ }
36
+ /** Actual owned bytes establish the physical binding; caller supplies the exact initial selection. */
37
+ export async function bindWorkIntent(input: { path: string; grantLedgerPath: string | null; selection: IntentSelection; priorities: IntentPriority[] }): Promise<WorkFrozen<WorkIntentBinding>> {
38
+ controlShape(input, ["path", "grantLedgerPath", "selection", "priorities"]);
39
+ const selection = intentSelection(input.selection), priorities = intentPriorities(input.priorities), path = input.path, grantLedgerPath = input.grantLedgerPath;
40
+ if (typeof path !== "string" || !isAbsolute(path) || path.split("/").includes(".pi")) throw new TypeError("explicit work path required");
41
+ await workDestination(path, grantLedgerPath);
42
+ const parent = await lstat(dirname(path), { bigint: true });
43
+ if (!parent.isDirectory() || (parent.mode & 0o077n) || parent.uid !== BigInt(process.getuid!())) throw new Error("owner-private work parent required");
44
+ const st = await lstat(path, { bigint: true });
45
+ const binding = workIntentBinding({ version: "1.0", path, grantLedgerPath, device: String(st.dev), inode: String(st.ino), selection, priorities });
46
+ resolveIntent(await readIntentWork(binding), selection, priorities); return freezeWork(binding);
47
+ }
48
+ /** No authority callback: validate concrete P01 effects and prevent permission/owner/policy expansion. */
49
+ export async function validateIntentApplication(binding: WorkFrozen<WorkIntentBinding>, state: IntentState, request: WorkFrozen<IntentRequest>): Promise<void> {
50
+ const text = await readIntentWork(binding), current = resolveIntent(text, state.selection, state.priorities);
51
+ const existing = parseWorkLedgerText(text).events;
52
+ const missing = request.events.filter(e => {
53
+ const sameId = existing.filter(old => old.eventId === e.eventId);
54
+ if (sameId.some(old => old.digest !== e.digest)) throw new Error("proposed event identity conflicts with retained work");
55
+ return sameId.length === 0;
56
+ });
57
+ const candidate = text + missing.map(e => intentKey(e) + "\n").join("");
58
+ const next = resolveIntent(candidate, request.selection, request.priorities);
59
+ if (request.action === "reprioritize" && intentKey(request.selection) !== intentKey(state.selection)) throw new Error("priority is not scope selection");
60
+ if (request.action !== "reprioritize" && intentKey(request.selection) === intentKey(state.selection)) throw new Error("a different exact recorded selection is required");
61
+ const old = [current.scope!, ...current.revisions], after = [next.scope!, ...next.revisions];
62
+ if (intentKey(old.map(entity).sort()) !== intentKey(after.map(entity).sort())) throw new Error("entity expansion/removal is outside this bounded adapter");
63
+ for (const r of after) {
64
+ const prior = old.find(p => entity(p) === entity(r))!;
65
+ if (r.ownerId !== prior.ownerId || r.scopeId !== prior.scopeId || r.permittedEffects.some(e => !prior.permittedEffects.includes(e)) || intentKey(r.policy) !== intentKey(prior.policy) ||
66
+ entity(r.parent) !== entity(prior.parent) || intentKey(r.dependencies.map(entity).sort()) !== intentKey(prior.dependencies.map(entity).sort()) ||
67
+ r.kind === "policy" && r.digest !== prior.digest) throw new Error("owner, policy, topology or permission expansion refused");
68
+ if (["revise-scope", "revise-selection"].includes(request.action) && r.digest !== prior.digest && (r.revision !== prior.revision + 1 || intentKey(r.predecessor) !== intentKey(ref(prior)))) throw new Error("exact successor required");
69
+ }
70
+ if (request.action === "revise-selection" && !after.some(r => r.digest !== old.find(p => entity(p) === entity(r))!.digest)) throw new Error("at least one exact successor revision required");
71
+ if (request.action === "revise-scope" && (next.scope!.revision !== current.scope!.revision + 1 || intentKey(next.scope!.predecessor) !== intentKey(ref(current.scope!)))) throw new Error("scope revision is not dispatch revision");
72
+ for (const e of request.events) {
73
+ // Rebuild through the actual P01 builders, not an alternate event/hash encoder.
74
+ if (e.event === "work_revision") {
75
+ const { digest: _digest, ...revision } = e.payload.revision;
76
+ if (!after.some(r => intentKey(ref(r)) === intentKey(ref(e.payload.revision))) || intentKey(buildWorkRevisionEvent({ eventId: e.eventId, now: new Date(e.ts), revision: JSON.parse(intentKey(revision)) })) !== intentKey(e)) throw new Error("unselected or noncanonical revision proposal");
77
+ } else if (e.event === "work_snapshot") {
78
+ const { digest: _digest, ...snapshot } = e.payload.snapshot;
79
+ if (e.eventId !== request.selection.event.eventId || e.digest !== request.selection.event.digest || intentKey(buildWorkSnapshotEvent({ eventId: e.eventId, now: new Date(e.ts), snapshot: JSON.parse(intentKey(snapshot)) })) !== intentKey(e)) throw new Error("unselected snapshot proposal");
80
+ }
81
+ }
82
+ }
83
+ export async function applyIntentApplication(binding: WorkFrozen<WorkIntentBinding>, state: IntentState, request: WorkFrozen<IntentRequest>): Promise<void> {
84
+ await validateIntentApplication(binding, state, request);
85
+ for (const event of request.events) {
86
+ await readIntentWork(binding);
87
+ await appendWorkLedgerEventOnce({ path: binding.path, grantLedgerPath: binding.grantLedgerPath }, event);
88
+ }
89
+ resolveIntent(await readIntentWork(binding), request.selection, request.priorities);
90
+ }
@@ -0,0 +1,107 @@
1
+ import { createHash } from "node:crypto";
2
+ import { canonicalWorkJson, copyWorkJson, freezeWork, parseWorkJson } from "./work-ledger-json.ts";
3
+ import { parseWorkLedgerText, type WorkFrozen, type WorkProjectionContext, type RevisionRef, type WorkRevisionEvent, type WorkSnapshotEvent } from "./work-ledger.ts";
4
+ import { validateWorkSelection } from "./work-ledger-validation.ts";
5
+ import { controlShape } from "./dispatch-control.ts";
6
+ export type IntentSelection = NonNullable<WorkProjectionContext["selectedSnapshot"]>;
7
+ export interface IntentPriority { obligation: RevisionRef; rank: number }
8
+ export interface WorkIntentBinding {
9
+ version: "1.0"; path: string; device: string; inode: string; grantLedgerPath: string | null;
10
+ selection: IntentSelection; priorities: IntentPriority[];
11
+ }
12
+ export interface IntentRequest {
13
+ version: "intent-request-v1" | "intent-request-v2"; requestId: string; bindingDigest: string; expectedRevision: number;
14
+ expectedSelection: IntentSelection; action: "revise-scope" | "reprioritize" | "select-alternative" | "revise-selection";
15
+ events: (WorkRevisionEvent | WorkSnapshotEvent)[]; selection: IntentSelection; priorities: IntentPriority[];
16
+ }
17
+ export interface IntentReceipt {
18
+ requestId: string; digest: string; expectedRevision: number; expectedSelection: IntentSelection;
19
+ action: IntentRequest["action"]; selection: IntentSelection; priorities: IntentPriority[];
20
+ events: { eventId: string; digest: string }[];
21
+ decision: "approved" | "authority-unavailable" | "stale" | "busy";
22
+ application: "pending-or-unknown" | "applied" | "not-applied";
23
+ outcome: "pending-or-unknown" | "selection-applied" | "priority-applied" | "refused";
24
+ resultSelection: IntentSelection | null;
25
+ }
26
+ export interface IntentState { revision: number; selection: IntentSelection; priorities: IntentPriority[]; records: IntentReceipt[] }
27
+ export interface IntentAdmission { selection: IntentSelection; revision: number; obligation: RevisionRef }
28
+ export type IntentSnapshot = WorkFrozen<IntentState & { version: "intent-controller-snapshot-v1"; bindingDigest: string;
29
+ admission: "blocked-pending" | "intent-ready"; acceptance: "not-assessed"; freshness: "snapshot-unknown" }>;
30
+ export const intentKey = (value: unknown) => canonicalWorkJson(copyWorkJson(value));
31
+ const hash = (value: unknown) => createHash("sha256").update(intentKey(value)).digest("hex");
32
+ const id = (x: unknown) => typeof x === "string" && /^[a-zA-Z0-9:_-]{1,128}$/.test(x);
33
+ const hex = (x: unknown) => typeof x === "string" && /^[a-f0-9]{64}$/.test(x);
34
+ export function intentPriorities(input: unknown): IntentPriority[] {
35
+ const result = copyWorkJson(input) as unknown as IntentPriority[];
36
+ if (!Array.isArray(result) || result.length > 32) throw new TypeError("bounded explicit priorities required");
37
+ for (const p of result) {
38
+ controlShape(p, ["obligation", "rank"]); controlShape(p.obligation, ["kind", "id", "revision", "digest"]);
39
+ if (p.obligation.kind !== "obligation" || !id(p.obligation.id) || !hex(p.obligation.digest) || !Number.isSafeInteger(p.obligation.revision) || p.obligation.revision < 1 ||
40
+ !Number.isSafeInteger(p.rank) || p.rank < 0 || p.rank > 127) throw new TypeError("invalid priority");
41
+ }
42
+ if (new Set(result.map(p => intentKey(p.obligation))).size !== result.length) throw new TypeError("duplicate priority obligation");
43
+ return result.sort((a, b) => a.rank - b.rank || (intentKey(a.obligation) < intentKey(b.obligation) ? -1 : 1));
44
+ }
45
+ export function intentSelection(value: unknown): IntentSelection {
46
+ const copied = copyWorkJson(value); validateWorkSelection(copied);
47
+ if (copied === null) throw new TypeError("exact intent selection required");
48
+ return copied as unknown as IntentSelection;
49
+ }
50
+ export function intentRequest(input: IntentRequest): WorkFrozen<IntentRequest> {
51
+ const r = copyWorkJson(input) as unknown as IntentRequest;
52
+ controlShape(r, ["version", "requestId", "bindingDigest", "expectedRevision", "expectedSelection", "action", "events", "selection", "priorities"]);
53
+ if ((r.action === "revise-selection" ? r.version !== "intent-request-v2" : r.version !== "intent-request-v1") || !id(r.requestId) || !hex(r.bindingDigest) || !Number.isSafeInteger(r.expectedRevision) || r.expectedRevision < 0 || r.expectedRevision > 32 ||
54
+ !["revise-scope", "reprioritize", "select-alternative", "revise-selection"].includes(r.action) || !Array.isArray(r.events) || r.events.length > 16 || Buffer.byteLength(intentKey(r)) > 48 * 1024) throw new TypeError("invalid bounded intent request");
55
+ const parsed = parseWorkLedgerText(r.events.map(e => intentKey(e)).join("\n"));
56
+ if (!parsed.complete || parsed.events.some(e => !id(e.eventId) || !["work_revision", "work_snapshot"].includes(e.event)) || new Set(parsed.events.map(e => e.eventId)).size !== parsed.events.length ||
57
+ (!["revise-scope", "revise-selection"].includes(r.action) && r.events.length)) throw new TypeError("only strict revision/snapshot proposals may be appended");
58
+ return freezeWork({ ...r, expectedSelection: intentSelection(r.expectedSelection), selection: intentSelection(r.selection), priorities: intentPriorities(r.priorities) });
59
+ }
60
+ export const parseIntentRequest = (text: string) => intentRequest(parseWorkJson(text) as unknown as IntentRequest);
61
+ export const intentRequestDigest = (input: WorkFrozen<IntentRequest>) => hash(intentRequest(input as IntentRequest));
62
+ export function intentReceipt(input: WorkFrozen<IntentRequest>, decision: IntentReceipt["decision"]): IntentReceipt {
63
+ return JSON.parse(intentKey({ requestId: input.requestId, digest: intentRequestDigest(input), expectedRevision: input.expectedRevision,
64
+ expectedSelection: input.expectedSelection, action: input.action, selection: input.selection, priorities: input.priorities,
65
+ events: input.events.map(e => ({ eventId: e.eventId, digest: e.digest })), decision,
66
+ application: decision === "approved" ? "pending-or-unknown" : "not-applied",
67
+ outcome: decision === "approved" ? "pending-or-unknown" : "refused", resultSelection: null }));
68
+ }
69
+ export function matchesIntentReceipt(receipt: IntentReceipt, request: WorkFrozen<IntentRequest>): boolean {
70
+ const expected = intentReceipt(request, receipt.decision);
71
+ if (receipt.application === "applied") {
72
+ expected.application = "applied"; expected.resultSelection = JSON.parse(intentKey(request.selection));
73
+ expected.outcome = request.action === "reprioritize" ? "priority-applied" : "selection-applied";
74
+ }
75
+ return intentKey(receipt) === intentKey(expected);
76
+ }
77
+ export function intentDecision(state: IntentState, request: Pick<IntentRequest, "expectedRevision" | "expectedSelection">, authorized: boolean, dispatchBusy: boolean): IntentReceipt["decision"] {
78
+ if (!authorized) return "authority-unavailable";
79
+ if (request.expectedRevision !== state.revision || intentKey(request.expectedSelection) !== intentKey(state.selection)) return "stale";
80
+ if (dispatchBusy || state.records.some(r => r.application === "pending-or-unknown")) return "busy";
81
+ return "approved";
82
+ }
83
+ export function replayIntent(state: IntentState, event: Record<string, unknown>, active: number, dispatchBusy: boolean): void {
84
+ if (event.type === "intent-request") {
85
+ controlShape(event, ["type", "receipt"]);
86
+ const r = event.receipt as IntentReceipt;
87
+ controlShape(r, ["requestId", "digest", "expectedRevision", "expectedSelection", "action", "selection", "priorities", "events", "decision", "application", "outcome", "resultSelection"]);
88
+ intentSelection(r.expectedSelection); intentSelection(r.selection); intentPriorities(r.priorities);
89
+ if (!id(r.requestId) || !hex(r.digest) || !Number.isSafeInteger(r.expectedRevision) || r.expectedRevision < 0 || !["revise-scope", "reprioritize", "select-alternative", "revise-selection"].includes(r.action) ||
90
+ !Array.isArray(r.events) || r.events.length > 16 || state.records.length >= 32 || state.records.some(p => p.requestId === r.requestId) ||
91
+ intentDecision(state, r, r.decision !== "authority-unavailable", dispatchBusy) !== r.decision || r.application !== (r.decision === "approved" ? "pending-or-unknown" : "not-applied") ||
92
+ r.outcome !== (r.decision === "approved" ? "pending-or-unknown" : "refused") || r.resultSelection !== null) throw new Error("invalid intent decision sequence");
93
+ for (const ref of r.events) { controlShape(ref, ["eventId", "digest"]); if (!id(ref.eventId) || !hex(ref.digest)) throw new Error("invalid event reference"); }
94
+ state.records.push(r); if (r.decision === "approved") state.revision++;
95
+ } else {
96
+ controlShape(event, ["type", "requestId"]);
97
+ const r = state.records.find(r => r.requestId === event.requestId);
98
+ if (event.type !== "intent-apply" || !r || r.application !== "pending-or-unknown" || active || dispatchBusy) throw new Error("invalid intent application boundary");
99
+ state.selection = r.selection; state.priorities = r.priorities; r.application = "applied";
100
+ r.outcome = r.action === "reprioritize" ? "priority-applied" : "selection-applied"; r.resultSelection = r.selection;
101
+ }
102
+ }
103
+ export function intentSnapshot(state: IntentState, bindingDigest: string): IntentSnapshot {
104
+ return freezeWork({ version: "intent-controller-snapshot-v1", bindingDigest, ...state,
105
+ admission: state.records.some(r => r.application === "pending-or-unknown") ? "blocked-pending" : "intent-ready",
106
+ acceptance: "not-assessed", freshness: "snapshot-unknown" });
107
+ }
@@ -0,0 +1,14 @@
1
+ import { intentKey, intentSelection, intentPriorities, type IntentState, type IntentAdmission } from "./intent-control.ts";
2
+ import { controlShape } from "./dispatch-control.ts";
3
+ interface Charged { intent?: IntentAdmission }
4
+ export function nextIntent(state: IntentState, records: readonly Charged[]) {
5
+ return intentPriorities(state.priorities).find(p => !records.some(r => r.intent &&
6
+ intentKey(r.intent.selection) === intentKey(state.selection) && intentKey(r.intent.obligation) === intentKey(p.obligation)))?.obligation ?? null;
7
+ }
8
+ /** Explicit once-per-selection primary dispatch policy. This is not completion/acceptance accounting. */
9
+ export function intentAdmission(state: IntentState, records: readonly Charged[], input: IntentAdmission): IntentAdmission {
10
+ controlShape(input, ["selection", "revision", "obligation"]);
11
+ const selection = intentSelection(input.selection), obligation = intentPriorities([{ obligation: input.obligation, rank: 0 }])[0].obligation;
12
+ if (input.revision !== state.revision || intentKey(selection) !== intentKey(state.selection) || intentKey(obligation) !== intentKey(nextIntent(state, records))) throw new Error("stale selection/revision or out-of-priority dispatch");
13
+ return { selection, revision: input.revision, obligation };
14
+ }
@@ -0,0 +1,72 @@
1
+ import { appendFile, mkdir, open, type FileHandle } from "node:fs/promises";
2
+ import { constants } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ import { withFileLock } from "./file-lock.ts";
5
+ import { recheckWorkDestination, workFsError, type WorkDestination } from "./work-ledger-destination.ts";
6
+ import { WORK_TEXT_BYTES, WORK_RECORDS } from "./work-ledger-json.ts";
7
+ import { WorkInputError } from "./work-ledger-types.ts";
8
+ import { parseWorkLedgerText } from "./work-ledger.ts";
9
+
10
+ /** Bounded descriptor read, including one detection byte; never trust a pre-read size alone. */
11
+ export async function readWorkBytes(handle: FileHandle): Promise<Buffer> {
12
+ const chunks: Buffer[] = []; let total = 0;
13
+ for (;;) {
14
+ const chunk = Buffer.alloc(Math.min(64 * 1024, WORK_TEXT_BYTES + 1 - total));
15
+ const { bytesRead } = await handle.read(chunk, 0, chunk.length, total);
16
+ if (bytesRead === 0) return Buffer.concat(chunks, total);
17
+ total += bytesRead;
18
+ if (total > WORK_TEXT_BYTES) throw new WorkInputError("WORK_LIMIT_EXCEEDED");
19
+ chunks.push(chunk.subarray(0, bytesRead));
20
+ }
21
+ }
22
+ /** A secondary filesystem close failure must not replace a primary content/capacity refusal. */
23
+ export async function closeWorkHandle(handle: FileHandle, failed: boolean): Promise<void> {
24
+ try { await handle.close(); }
25
+ catch (error) { if (!failed || !workFsError(error)) throw error; }
26
+ }
27
+
28
+ /** Shared existing-producer persistence. Serialization and each caller's exception policy remain
29
+ * outside this function. The private destination facts select v4 protection, never ambient defaults. */
30
+ export async function appendLedgerLine(options: { readonly path: string }, line: string, destination?: WorkDestination, once?: { eventId: string; digest: string }): Promise<void> {
31
+ await mkdir(dirname(options.path), { recursive: true });
32
+ if (!destination) {
33
+ // Retain even legacy path-property evaluation timing across waits; only v4 owns detached inputs.
34
+ await withFileLock(options.path, "grant ledger", () => appendFile(options.path, line, { encoding: "utf8", flag: "a" }));
35
+ return;
36
+ }
37
+ const path = destination.path;
38
+ await recheckWorkDestination(destination);
39
+ await withFileLock(path, "work ledger", async () => {
40
+ await recheckWorkDestination(destination);
41
+ let handle: FileHandle;
42
+ try {
43
+ handle = await open(path, constants.O_RDWR | constants.O_APPEND | constants.O_CREAT | constants.O_NOFOLLOW | constants.O_NONBLOCK, 0o600);
44
+ } catch (error) {
45
+ // A newly proven alias/unsafe kind retains its classification rather than becoming generic I/O.
46
+ if (workFsError(error)) await recheckWorkDestination(destination);
47
+ throw error;
48
+ }
49
+ let failed = false;
50
+ try {
51
+ await recheckWorkDestination(destination, handle);
52
+ const bytes = await readWorkBytes(handle);
53
+ const ingestion = parseWorkLedgerText(bytes.toString("utf8"));
54
+ if (!ingestion.complete) throw new WorkInputError(ingestion.errors[0].code);
55
+ if (bytes.length && bytes[bytes.length - 1] !== 10) throw new WorkInputError("WORK_JSON_INVALID");
56
+ if (once) {
57
+ const sameId = ingestion.events.filter(e => e.eventId === once.eventId);
58
+ if (sameId.some(e => e.digest !== once.digest)) throw new WorkInputError("WORK_DIGEST_MISMATCH");
59
+ if (sameId.length) { await handle.sync(); return; } // Reconcile an earlier complete but unacknowledged append.
60
+ }
61
+ if (bytes.length + Buffer.byteLength(line, "utf8") > WORK_TEXT_BYTES || ingestion.events.length + 1 > WORK_RECORDS) {
62
+ throw new WorkInputError("WORK_LIMIT_EXCEEDED");
63
+ }
64
+ await recheckWorkDestination(destination, handle);
65
+ // Same descriptor, same non-expiring lock, O_APPEND. A failed write may leave partial bytes;
66
+ // preserve them and fail closed. Never truncate, repair or reserialize existing deliveries.
67
+ await handle.writeFile(line, "utf8");
68
+ if (once) await handle.sync(); // New application seam requires durable append before controller receipt.
69
+ } catch (error) { failed = true; throw error; }
70
+ finally { await closeWorkHandle(handle, failed); }
71
+ }, { staleRecovery: "disabled" });
72
+ }
package/src/ledger.ts CHANGED
@@ -19,9 +19,8 @@
19
19
  * "what was this child told to do?" is out of the ledger by decision, not by omission.
20
20
  */
21
21
 
22
- import { appendFile, mkdir, readFile } from "node:fs/promises";
23
- import { withFileLock } from "./file-lock.ts";
24
- import { dirname } from "node:path";
22
+ import { readFile } from "node:fs/promises";
23
+ import { appendLedgerLine } from "./ledger-append.ts";
25
24
  import type { Capability, ResolveResult } from "./resolve.ts";
26
25
  import type { ExecutorKind } from "./executor.ts";
27
26
  import type { DefinitionDigest } from "./definitions.ts";
@@ -314,15 +313,10 @@ export { verifyLedger, type LedgerReport } from "./ledger-report.ts";
314
313
  * — because a child running with granted capabilities and no audit line is what the ledger exists to
315
314
  * prevent. That is the opposite of what the approvals store does with the same lock, and deliberately so.
316
315
  */
317
- const withLedgerLock = <T>(path: string, write: () => Promise<T>): Promise<T> =>
318
- withFileLock(path, "grant ledger", write);
319
-
320
316
  export async function appendLedgerEvent(options: LedgerOptions, event: RuntimeLedgerEvent | GrantRecord): Promise<void> {
321
317
  const line = `${JSON.stringify(event)}\n`;
322
318
  try {
323
- await mkdir(dirname(options.path), { recursive: true });
324
- // O_APPEND alone is not enough once several processes write to one ledger — see `withLedgerLock`.
325
- await withLedgerLock(options.path, () => appendFile(options.path, line, { encoding: "utf8", flag: "a" }));
319
+ await appendLedgerLine(options, line);
326
320
  } catch (error) {
327
321
  if (options.strict ?? true) {
328
322
  throw new Error(`grant ledger write failed (failing closed): ${String(error)}`);
@@ -0,0 +1,23 @@
1
+ import { lstat, realpath, mkdir } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, parse, resolve } from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { ENV_NATIVE_SESSION_ROOT } from "./native-session.ts";
5
+ export interface NativeSessionHost { /** Explicit host opt-in captured once, not model-supplied. */ readonly nativeSessionRoot?: string }
6
+ export const nativeSessionRootFromEnv=(env:NodeJS.ProcessEnv)=>env[ENV_RETAIN_NATIVE_SESSIONS]==="1"?env[ENV_NATIVE_SESSION_ROOT]??"":undefined;
7
+ export const ENV_RETAIN_NATIVE_SESSIONS = "PI_GRANTS_RETAIN_NATIVE_SESSIONS";
8
+ /** Explicit ordinary-host opt-in only. Never read a destination from tool/model parameters.
9
+ * Allocate a fresh per-occurrence directory; no scan, existing-session takeover or transcript creation. */
10
+ export async function allocateNativeSessionTarget(root: string, executionId: string): Promise<string> {
11
+ if (!isAbsolute(root) || resolve(root) !== root || !/^[a-zA-Z0-9:_-]{1,128}$/.test(executionId)) throw new Error("explicit native root/execution identity required");
12
+ for (let p = root;; p = dirname(p)) {
13
+ const s = await lstat(p); if (!s.isDirectory() || s.isSymbolicLink()) throw new Error("native target ancestor changed");
14
+ if (p === parse(p).root) break;
15
+ }
16
+ const before = await lstat(root);
17
+ if (await realpath(root) !== root || before.mode & 0o077 || before.uid !== process.getuid?.()) throw new Error("private owned native target root required");
18
+ const directory = join(root, "execution-" + createHash("sha256").update(executionId).digest("hex"));
19
+ await mkdir(directory, { mode: 0o700 }); // EEXIST is a refusal, never permission to append an old occurrence.
20
+ const after = await lstat(root);
21
+ if (before.dev !== after.dev || before.ino !== after.ino || await realpath(directory) !== directory) throw new Error("native target root replaced");
22
+ return join(directory, "session.jsonl");
23
+ }
@@ -0,0 +1,139 @@
1
+ import { constants } from "node:fs";
2
+ import { open, lstat, realpath } from "node:fs/promises";
3
+ import { isAbsolute, relative, sep } from "node:path";
4
+ import { createHash } from "node:crypto";
5
+ import { parseRetentionJson } from "./retention-json.ts";
6
+
7
+ export const ENV_NATIVE_SESSION_ROOT = "PI_GRANTS_NATIVE_SESSION_ROOT";
8
+ export const MAX_NATIVE_SESSION_BYTES = 1024 * 1024;
9
+ export type NativeSessionSource = "herdr-id" | "herdr-path" | "pi-session-file" | "pi-session-manager";
10
+ export interface NativeSessionObservation {
11
+ source: NativeSessionSource | null;
12
+ status: "missing" | "verified" | "invalid" | "changed" | "truncated" | "unsupported";
13
+ sessionId: string | null;
14
+ sessionPath: string | null;
15
+ parentSessionPath: string | null;
16
+ branchLeafId: string | null;
17
+ branchState: "unknown" | "observed";
18
+ lastPersistedEntryId: string | null;
19
+ sha256: string | null;
20
+ reason: string | null;
21
+ }
22
+ export interface NativeSessionCapture { observation: NativeSessionObservation; bytes?: Buffer; fileIdentity?: { device: string; inode: string } }
23
+ /** Read-only methods of pi 0.84.2's public SessionManager; no runtime/session creation or model work. */
24
+ export interface NativeSessionManager {
25
+ getSessionFile(): string | undefined;
26
+ getSessionId(): string;
27
+ getLeafId(): string | null;
28
+ }
29
+ export const missingNativeSession = (): NativeSessionObservation => ({ source: null, status: "missing", sessionId: null,
30
+ sessionPath: null, parentSessionPath: null, branchLeafId: null, branchState: "unknown", lastPersistedEntryId: null,
31
+ sha256: null, reason: "native-session-unavailable" });
32
+ const id = (x: unknown): x is string => typeof x === "string" && /^[a-zA-Z0-9_-]{1,128}$/.test(x);
33
+ const uuid = (x: unknown): x is string => typeof x === "string" && /^[a-f0-9]{8}(?:-[a-f0-9]{4}){3}-[a-f0-9]{12}$/i.test(x);
34
+ const obj = (x: unknown): x is Record<string, unknown> => x !== null && typeof x === "object" && !Array.isArray(x);
35
+ const filePath = (x: unknown): x is string => typeof x === "string" && x.length <= 4096 && isAbsolute(x) && !x.includes("\0");
36
+
37
+ /** Parse the retained native v3 bytes, never load/migrate/rewrite a session via SessionManager.open(). */
38
+ export function parseNativeSessionBytes(bytes: Uint8Array, input: {
39
+ source: NativeSessionSource; path: string; expectedSessionId?: string; truncated?: boolean;
40
+ liveLeaf?: { sessionId: string; leafId: string | null };
41
+ }): NativeSessionCapture {
42
+ const observation = { ...missingNativeSession(), source: input.source, sessionPath: input.path };
43
+ const result: NativeSessionCapture = { observation };
44
+ const fail = (status: NativeSessionObservation["status"], reason: string) => {
45
+ observation.status = status; observation.reason = reason; observation.branchLeafId = null; observation.branchState = "unknown";
46
+ return result;
47
+ };
48
+ if (bytes.length > MAX_NATIVE_SESSION_BYTES) return fail("truncated", "native-session-size-limit");
49
+ const raw = Buffer.from(bytes), newline = raw.indexOf(10);
50
+ let header: unknown;
51
+ try { header = parseRetentionJson(new TextDecoder("utf-8", { fatal: true }).decode(raw.subarray(0, newline < 0 ? raw.length : newline)), MAX_NATIVE_SESSION_BYTES); }
52
+ catch { return fail("invalid", "native-session-header-invalid"); }
53
+ if (!obj(header) || header.type !== "session" || header.version !== 3 || !uuid(header.id) ||
54
+ !filePath(header.cwd) || typeof header.timestamp !== "string" || !Number.isFinite(Date.parse(header.timestamp)) ||
55
+ (header.parentSession !== undefined && !filePath(header.parentSession))) return fail("invalid", "native-session-header-invalid");
56
+ if (input.expectedSessionId !== undefined && header.id !== input.expectedSessionId) return fail("changed", "native-session-id-changed");
57
+ // Only validated session headers admit transcript bytes; arbitrary files are never archived here.
58
+ result.bytes = Buffer.from(bytes);
59
+ observation.sessionId = header.id;
60
+ observation.parentSessionPath = header.parentSession as string | undefined ?? null;
61
+ observation.sha256 = createHash("sha256").update(bytes).digest("hex");
62
+ if (input.truncated || raw.at(-1) !== 10) return fail("truncated", "native-session-incomplete-bytes");
63
+ let text: string;
64
+ try { text = new TextDecoder("utf-8", { fatal: true }).decode(raw); }
65
+ catch { return fail("invalid", "native-session-utf8-invalid"); }
66
+ const lines = text.split("\n");
67
+ const ids = new Set<string>();
68
+ if (lines.length > 10002) return fail("invalid", "native-session-entry-limit");
69
+ for (const line of lines.slice(1, -1)) {
70
+ let entry: unknown;
71
+ try { entry = parseRetentionJson(line, MAX_NATIVE_SESSION_BYTES); } catch { return fail("invalid", "native-session-entry-invalid"); }
72
+ if (!obj(entry) || typeof entry.type !== "string" || entry.type === "session" || !id(entry.id) || ids.has(entry.id) ||
73
+ !(entry.parentId === null || (id(entry.parentId) && ids.has(entry.parentId))) || typeof entry.timestamp !== "string" ||
74
+ !Number.isFinite(Date.parse(entry.timestamp))) return fail("invalid", "native-session-parent-link-invalid");
75
+ ids.add(entry.id); observation.lastPersistedEntryId = entry.id;
76
+ }
77
+ if (input.liveLeaf) {
78
+ if (input.liveLeaf.sessionId !== header.id || (input.liveLeaf.leafId !== null && !ids.has(input.liveLeaf.leafId))) {
79
+ return fail("changed", "native-session-live-leaf-mismatch");
80
+ }
81
+ observation.branchLeafId = input.liveLeaf.leafId; observation.branchState = "observed";
82
+ }
83
+ observation.status = "verified"; observation.reason = observation.branchState === "unknown" ? "active-branch-unknown" : null;
84
+ return result;
85
+ }
86
+
87
+ /** Private allowlisted files only, bounded non-following descriptor reads, no scan of session/auth dirs. */
88
+ export async function readNativeSession(input: {
89
+ path: string; source: NativeSessionSource; expectedSessionId?: string; allowedRoot?: string;
90
+ manager?: NativeSessionManager;
91
+ expectedFile?: { device: string; inode: string };
92
+ }): Promise<NativeSessionCapture> {
93
+ const gap = (status: NativeSessionObservation["status"], reason: string): NativeSessionCapture => ({ observation: {
94
+ ...missingNativeSession(), source: input.source, status, reason, sessionPath: filePath(input.path) ? input.path : null,
95
+ } });
96
+ const root = input.allowedRoot ?? process.env[ENV_NATIVE_SESSION_ROOT];
97
+ if (!filePath(input.path) || !input.path.endsWith(".jsonl") || !root || !filePath(root)) return gap("unsupported", "native-session-private-root-required");
98
+ const within = (base: string, candidate: string) => { const p = relative(base, candidate); return p !== "" && p !== ".." && !p.startsWith(`..${sep}`) && !isAbsolute(p); };
99
+ if (!within(root, input.path)) return gap("unsupported", "native-session-outside-private-root");
100
+ let handle: Awaited<ReturnType<typeof open>> | undefined;
101
+ try {
102
+ const canonicalRoot = await realpath(root), canonicalFile = await realpath(input.path);
103
+ const rootStat = await lstat(canonicalRoot);
104
+ if (!rootStat.isDirectory() || (rootStat.mode & 0o077) !== 0 || (typeof process.getuid === "function" && rootStat.uid !== process.getuid())) return gap("unsupported", "native-session-root-not-private");
105
+ if (!within(canonicalRoot, canonicalFile) || (await lstat(input.path)).isSymbolicLink()) return gap("unsupported", "native-session-path-alias");
106
+ handle = await open(input.path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
107
+ const before = await handle.stat();
108
+ if (!before.isFile() || before.nlink !== 1 || (typeof process.getuid === "function" && before.uid !== process.getuid())) {
109
+ return gap("unsupported", "native-session-not-private-regular-file");
110
+ }
111
+ const fileIdentity = { device: String(before.dev), inode: String(before.ino) };
112
+ if (input.expectedFile && (input.expectedFile.device !== fileIdentity.device || input.expectedFile.inode !== fileIdentity.inode)) return gap("changed", "native-session-file-replaced");
113
+ const live = input.manager ? { sessionId: input.manager.getSessionId(), leafId: input.manager.getLeafId(), path: input.manager.getSessionFile() } : undefined;
114
+ if (live && live.path !== input.path) return gap("changed", "native-session-manager-path-changed");
115
+ const bytes = Buffer.alloc(Math.min(before.size, MAX_NATIVE_SESSION_BYTES));
116
+ let offset = 0;
117
+ while (offset < bytes.length) { const read = await handle.read(bytes, offset, bytes.length - offset, offset); if (!read.bytesRead) break; offset += read.bytesRead; }
118
+ const after = await handle.stat(), location = await lstat(input.path);
119
+ if (before.dev !== location.dev || before.ino !== location.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs ||
120
+ before.ctimeMs !== after.ctimeMs || location.isSymbolicLink() || after.size !== location.size) return gap("changed", "native-session-file-changed");
121
+ if (live && (input.manager!.getSessionId() !== live.sessionId || input.manager!.getLeafId() !== live.leafId || input.manager!.getSessionFile() !== live.path)) {
122
+ return gap("changed", "native-session-live-state-changed");
123
+ }
124
+ return { ...parseNativeSessionBytes(bytes.subarray(0, offset), { ...input, liveLeaf: live,
125
+ truncated: before.size > MAX_NATIVE_SESSION_BYTES || offset !== before.size }), fileIdentity };
126
+ } catch (error) {
127
+ return gap((error as NodeJS.ErrnoException).code === "ENOENT" ? "missing" : "invalid", "native-session-read-failed");
128
+ } finally { await handle?.close().catch(() => undefined); }
129
+ }
130
+
131
+ /** Herdr 0.8.2 protocol 20 AgentInfo.agent_session; never scrape terminal text, PID or names. */
132
+ export function herdrSessionReference(value: unknown, expectedPane: string): { source: "herdr-id" | "herdr-path"; value: string } | null {
133
+ if (!obj(value) || value.pane_id !== expectedPane || value.agent !== "pi" || !obj(value.agent_session)) return null;
134
+ const s = value.agent_session;
135
+ if (s.agent !== "pi" || typeof s.source !== "string" || s.source.length > 128) return null;
136
+ if (s.kind === "id" && uuid(s.value)) return { source: "herdr-id", value: s.value };
137
+ if (s.kind === "path" && filePath(s.value)) return { source: "herdr-path", value: s.value };
138
+ return null;
139
+ }