gentle-pi 4.0.0 → 4.2.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 (425) hide show
  1. package/README.md +157 -18
  2. package/assets/agents/gentle-ai-explore.md +11 -4
  3. package/assets/agents/gentle-ai-verify.md +23 -4
  4. package/assets/agents/gentle-ai-worker.md +6 -6
  5. package/assets/install-wizard/index.html +47 -0
  6. package/assets/install-wizard/wizard.css +1279 -0
  7. package/assets/install-wizard/wizard.js +951 -0
  8. package/assets/orchestrator-delegation.md +70 -163
  9. package/assets/orchestrator-memory.md +11 -4
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-tracking.md +53 -0
  12. package/assets/orchestrator-verification.md +28 -0
  13. package/assets/orchestrator-writer.md +38 -0
  14. package/assets/orchestrator.md +29 -25
  15. package/assets/sounds/LICENSE.md +52 -0
  16. package/assets/sounds/attention.wav +0 -0
  17. package/assets/sounds/error.wav +0 -0
  18. package/assets/sounds/success.wav +0 -0
  19. package/bin/engram-guard +9 -0
  20. package/bin/gentle-shell-install.mjs +302 -0
  21. package/bin/gentle-shell.mjs +708 -22
  22. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  23. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  24. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  25. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  27. package/docs/auto-permission-answers.md +25 -0
  28. package/docs/auto-permission-evidence.md +39 -0
  29. package/docs/bundled-install.md +399 -0
  30. package/docs/delegated-verification.md +3 -5
  31. package/docs/gentle-agents-activity.md +513 -0
  32. package/docs/gentle-shell-uninstall.md +136 -0
  33. package/docs/gentle-shell.md +90 -33
  34. package/docs/install-wizard.md +2040 -0
  35. package/docs/native-authority-architecture.md +1 -1
  36. package/docs/permission-modes.md +127 -0
  37. package/docs/prompt-history.md +1 -1
  38. package/docs/readme-reference.md +310 -47
  39. package/docs/render-cache-performance.md +28 -0
  40. package/docs/session-profile-format.md +39 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/yolo-mode.md +11 -10
  44. package/extensions/ask-user-choice.ts +4 -0
  45. package/extensions/ask-user-question.ts +6 -1
  46. package/extensions/child-context.ts +11 -2
  47. package/extensions/child-safety.ts +4 -2
  48. package/extensions/codegraph-tools.ts +18 -5
  49. package/extensions/gentle-agents.ts +718 -87
  50. package/extensions/gentle-ai.ts +991 -263
  51. package/extensions/gentle-herdr-activity.ts +81 -0
  52. package/extensions/gentle-notifications.ts +104 -0
  53. package/extensions/gentle-shell.ts +271 -80
  54. package/extensions/gentle-todo.ts +123 -18
  55. package/extensions/history/index.ts +11 -6
  56. package/extensions/pi-pretty.ts +39 -3
  57. package/extensions/quiet-tools.ts +91 -21
  58. package/extensions/skill-registry.ts +175 -46
  59. package/extensions/startup-banner.ts +11 -2
  60. package/lib/agent-home.ts +10 -1
  61. package/lib/agent-model-resolution.ts +80 -0
  62. package/lib/agent-profiles.ts +13 -3
  63. package/lib/agents-completion-delivery.ts +11 -3
  64. package/lib/agents-messaging.ts +1 -1
  65. package/lib/agents-protocol.ts +40 -1
  66. package/lib/agents-runner.ts +90 -15
  67. package/lib/auto-permission-answers.ts +133 -0
  68. package/lib/auto-permission-classifier.ts +459 -0
  69. package/lib/auto-permission-evidence.ts +88 -0
  70. package/lib/auto-permission-memory.ts +69 -0
  71. package/lib/auto-permission-runtime.ts +484 -0
  72. package/lib/auto-permission-tool-context.ts +61 -0
  73. package/lib/background-jobs-tools.ts +202 -0
  74. package/lib/background-jobs.ts +253 -0
  75. package/lib/background-monitor.ts +154 -0
  76. package/lib/bounded-writer-admission.ts +83 -10
  77. package/lib/builtin-codemode-optout.ts +166 -0
  78. package/lib/card-content-policy.ts +85 -0
  79. package/lib/child-package-injection.ts +62 -0
  80. package/lib/codemode-renderer.ts +14 -4
  81. package/lib/command-palette-catalog.ts +3 -2
  82. package/lib/context-threshold.ts +26 -0
  83. package/lib/destructive-command-guard.ts +104 -28
  84. package/lib/gentle-ai-binary.ts +109 -4
  85. package/lib/gentle-shell-bundled.ts +535 -0
  86. package/lib/gentle-shell-launcher.ts +75 -24
  87. package/lib/gentle-shell-startup-flow.ts +54 -0
  88. package/lib/gentle-shell-startup-update.ts +92 -0
  89. package/lib/gentle-shell-uninstall.ts +840 -0
  90. package/lib/gentle-shell-update-panel.ts +231 -0
  91. package/lib/herdr-activity.ts +183 -0
  92. package/lib/inprocess-reviewer.ts +3 -0
  93. package/lib/jobs-sidebar-state.ts +20 -0
  94. package/lib/jobs-view.ts +161 -0
  95. package/lib/model-price-ratio.ts +83 -0
  96. package/lib/model-routing-authority.ts +6 -1
  97. package/lib/nan-provider.ts +33 -11
  98. package/lib/native-review-cli.ts +194 -73
  99. package/lib/notification-audio-native.ts +162 -0
  100. package/lib/notification-audio-windows.ts +399 -0
  101. package/lib/notification-audio.ts +332 -0
  102. package/lib/notification-customize.ts +241 -0
  103. package/lib/notification-events.ts +113 -0
  104. package/lib/notification-policy.ts +148 -0
  105. package/lib/notification-pulse-client.ts +367 -0
  106. package/lib/notification-pulse-protocol.ts +305 -0
  107. package/lib/notification-pulse-stream.ts +213 -0
  108. package/lib/notification-pulse-worker.ts +66 -0
  109. package/lib/notification-scheduler.ts +173 -0
  110. package/lib/notification-service.ts +202 -0
  111. package/lib/notification-ui.ts +87 -0
  112. package/lib/odd-phase.ts +1 -1
  113. package/lib/orchestrator-alias.ts +20 -0
  114. package/lib/orchestrator-catalog.ts +79 -0
  115. package/lib/orchestrator-consultation.ts +61 -0
  116. package/lib/orchestrator-discovery.ts +49 -0
  117. package/lib/orchestrator-helper-consent.ts +122 -0
  118. package/lib/orchestrator-helper.ts +167 -0
  119. package/lib/orchestrator-message-card.ts +105 -0
  120. package/lib/orchestrator-presence.ts +115 -14
  121. package/lib/orchestrator-scope.ts +80 -0
  122. package/lib/orchestrator-state.ts +107 -0
  123. package/lib/orchestrator-tool-card.ts +280 -0
  124. package/lib/orchestrator-work-search.ts +218 -0
  125. package/lib/orchestrator-work.ts +79 -0
  126. package/lib/permission-mode-policy.ts +59 -0
  127. package/lib/review-candidate-view-owner.ts +8 -6
  128. package/lib/review-candidate-view.ts +3 -2
  129. package/lib/review-compact-contract.ts +2 -2
  130. package/lib/review-host-relay.ts +7 -2
  131. package/lib/review-integration-v2.ts +100 -22
  132. package/lib/review-publication-gate.ts +1 -1
  133. package/lib/review-risk-assessment.ts +77 -51
  134. package/lib/review-sidebar-state.ts +2 -1
  135. package/lib/review-transaction.ts +72 -3
  136. package/lib/runtime-metrics-children.ts +1 -1
  137. package/lib/runtime-metrics-native.ts +3 -2
  138. package/lib/selection-engine.ts +22 -7
  139. package/lib/session-change-capture.ts +5 -4
  140. package/lib/session-changes.ts +27 -22
  141. package/lib/session-profile-binding.ts +83 -0
  142. package/lib/session-profile-freeze.ts +261 -0
  143. package/lib/session-profile-persistence.ts +187 -0
  144. package/lib/session-worktree-registry.ts +74 -8
  145. package/lib/shell-bar.ts +41 -11
  146. package/lib/shell-changes-view.ts +40 -13
  147. package/lib/shell-changes.ts +88 -71
  148. package/lib/shell-prompt.ts +6 -4
  149. package/lib/shell-sidebar-layout.ts +5 -1
  150. package/lib/shell-todo.ts +81 -26
  151. package/lib/shell-usage.ts +4 -3
  152. package/lib/status-timing-diagnostics.ts +201 -0
  153. package/lib/telemetry-trigger.ts +3 -1
  154. package/lib/vim-editor-adapter.ts +27 -5
  155. package/lib/visual-customization-policy.ts +20 -9
  156. package/lib/visual-customize-view.ts +171 -20
  157. package/lib/writer-surfaces.ts +162 -0
  158. package/lib/yolo-session-policy.ts +198 -121
  159. package/package.json +5 -5
  160. package/runtime/child-package-injection.mjs +63 -0
  161. package/runtime/gentle-ai-binary.mjs +109 -4
  162. package/runtime/gentle-shell-bundled.mjs +536 -0
  163. package/runtime/gentle-shell-launcher.mjs +74 -23
  164. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  165. package/runtime/gentle-shell-startup-update.mjs +93 -0
  166. package/runtime/gentle-shell-uninstall.mjs +841 -0
  167. package/runtime/gentle-shell-update-panel.mjs +232 -0
  168. package/runtime/native-review-cli.mjs +191 -70
  169. package/runtime/review-integration-v2.mjs +100 -22
  170. package/runtime/review-risk-assessment.mjs +77 -51
  171. package/runtime/status-timing-diagnostics.mjs +202 -0
  172. package/runtime/telemetry-trigger.mjs +3 -1
  173. package/scripts/bootstrap.cmd +255 -0
  174. package/scripts/bootstrap.sh +168 -0
  175. package/scripts/build-distribution.mjs +157 -0
  176. package/scripts/build-installer-bundles.mjs +136 -0
  177. package/scripts/build-runtime-modules.mjs +7 -0
  178. package/scripts/bundled-install.mjs +1308 -0
  179. package/scripts/bundled-wizard.mjs +399 -0
  180. package/scripts/check-types.mjs +4 -6
  181. package/scripts/gentle-ai-installer.mjs +79 -32
  182. package/scripts/install-gentle-ai.mjs +48 -1
  183. package/scripts/install-tui-mode-setting.mjs +10 -1
  184. package/scripts/install-wizard-preview.mjs +154 -0
  185. package/scripts/installer-downloads.mjs +648 -0
  186. package/scripts/installer-preflight.mjs +387 -0
  187. package/scripts/installer-probes.mjs +564 -0
  188. package/scripts/installer-runner.mjs +1102 -0
  189. package/scripts/installer-server.mjs +881 -0
  190. package/scripts/installer-windows-artifacts.json +15 -0
  191. package/scripts/installer-windows.mjs +752 -0
  192. package/scripts/main-channel.mjs +452 -0
  193. package/scripts/npm-pack-result.mjs +31 -0
  194. package/scripts/pi-update.mjs +223 -0
  195. package/scripts/render-cache-performance-worker.mjs +205 -0
  196. package/scripts/render-cache-performance.mjs +115 -0
  197. package/scripts/run-test-suite.mjs +22 -8
  198. package/scripts/test-installer-acceptance.sh +161 -0
  199. package/scripts/test-packed-runner.mjs +23 -5
  200. package/scripts/types-baseline.json +2 -92
  201. package/scripts/verify-package-files.mjs +82 -18
  202. package/skills/gentle-ai/SKILL.md +12 -11
  203. package/tests/agent-home.test.ts +56 -3
  204. package/tests/agent-model-resolution.test.ts +113 -0
  205. package/tests/agent-profiles.test.ts +12 -0
  206. package/tests/agent-risk-escalation.test.ts +123 -0
  207. package/tests/agents-completion-delivery.test.ts +17 -0
  208. package/tests/agents-protocol.test.ts +57 -0
  209. package/tests/agents-queries.test.ts +9 -9
  210. package/tests/agents-runner-process.test.ts +31 -0
  211. package/tests/agents-runner.test.ts +89 -4
  212. package/tests/append-system-prompt-route.test.ts +18 -1
  213. package/tests/auto-mode-human-answers.test.ts +68 -0
  214. package/tests/auto-mode-invariants.test.ts +276 -0
  215. package/tests/auto-mode-sdk.test.ts +84 -0
  216. package/tests/auto-mode.test.ts +49 -0
  217. package/tests/auto-permission-answers.test.ts +170 -0
  218. package/tests/auto-permission-classifier.test.ts +634 -0
  219. package/tests/auto-permission-compound.test.ts +24 -0
  220. package/tests/auto-permission-evidence.test.ts +88 -0
  221. package/tests/auto-permission-memory.test.ts +46 -0
  222. package/tests/auto-permission-public-regressions.test.ts +111 -0
  223. package/tests/auto-permission-runtime.test.ts +454 -0
  224. package/tests/autonomous-guard.test.ts +86 -16
  225. package/tests/background-cache-warming.test.ts +2 -1
  226. package/tests/background-jobs.test.ts +227 -0
  227. package/tests/background-monitor.test.ts +195 -0
  228. package/tests/background-subagents-default-mode.test.ts +28 -5
  229. package/tests/bounded-stop-contract.test.ts +85 -0
  230. package/tests/bounded-writer-admission.test.ts +52 -1
  231. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  232. package/tests/bridge-wake-rendering.test.ts +56 -0
  233. package/tests/build-distribution.test.ts +269 -0
  234. package/tests/builtin-codemode-optout.test.ts +242 -0
  235. package/tests/bundled-install.test.ts +1319 -0
  236. package/tests/bundled-wizard.test.ts +505 -0
  237. package/tests/card-content-policy.test.ts +68 -0
  238. package/tests/card-style-policy.test.ts +34 -2
  239. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  240. package/tests/child-package-entrypoints.test.ts +65 -0
  241. package/tests/child-package-injection.test.ts +74 -0
  242. package/tests/child-safety.test.ts +12 -0
  243. package/tests/child-web-package.test.ts +224 -0
  244. package/tests/codegraph-tools.test.ts +35 -0
  245. package/tests/codemode-rendering.test.ts +107 -2
  246. package/tests/command-palette.test.ts +5 -5
  247. package/tests/context-threshold.test.ts +31 -0
  248. package/tests/delegate-for-reason-contract.test.ts +327 -0
  249. package/tests/delegated-key-learnings-contract.test.ts +3 -2
  250. package/tests/devbinary/native-review-parity.devtest.ts +1 -1
  251. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  252. package/tests/engram-safety.test.ts +316 -0
  253. package/tests/generic-agent-tools.test.ts +16 -1
  254. package/tests/gentle-agents.test.ts +1895 -76
  255. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  256. package/tests/gentle-ai-binary.test.ts +19 -2
  257. package/tests/gentle-ai-child-guards.test.ts +202 -0
  258. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  259. package/tests/gentle-ai-installer.test.ts +548 -58
  260. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  261. package/tests/gentle-ai-renderer.test.ts +6 -6
  262. package/tests/gentle-ai.test.ts +999 -31
  263. package/tests/gentle-herdr-activity.test.ts +100 -0
  264. package/tests/gentle-notifications.test.ts +427 -0
  265. package/tests/gentle-shell-bin.test.ts +263 -19
  266. package/tests/gentle-shell-bundled.test.ts +1036 -0
  267. package/tests/gentle-shell-launcher.test.ts +251 -25
  268. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  269. package/tests/gentle-shell-startup-update.test.ts +85 -0
  270. package/tests/gentle-shell-uninstall.test.ts +978 -0
  271. package/tests/gentle-shell-update-panel.test.ts +158 -0
  272. package/tests/gentle-shell.test.ts +611 -100
  273. package/tests/gentle-todo.test.ts +317 -14
  274. package/tests/herdr-activity.test.ts +243 -0
  275. package/tests/history-agent-home.test.ts +81 -0
  276. package/tests/history-child-guard.test.ts +70 -0
  277. package/tests/inprocess-reviewer.test.ts +20 -0
  278. package/tests/install-gentle-ai-failure.test.ts +69 -0
  279. package/tests/install-tui-mode-setting.test.ts +15 -0
  280. package/tests/install-wizard.test.ts +1223 -0
  281. package/tests/installer-bundles.test.ts +122 -0
  282. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  283. package/tests/installer-preflight.test.ts +750 -0
  284. package/tests/installer-probes.test.ts +1106 -0
  285. package/tests/installer-runner.test.ts +2159 -0
  286. package/tests/installer-server.test.ts +1567 -0
  287. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  288. package/tests/jobs-view.test.ts +160 -0
  289. package/tests/main-channel-upgrade.test.ts +395 -0
  290. package/tests/main-channel.test.ts +344 -0
  291. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  292. package/tests/model-price-ratio.test.ts +116 -0
  293. package/tests/model-profiles-preservation.test.ts +153 -0
  294. package/tests/model-routing-package.test.ts +10 -0
  295. package/tests/nan-provider-runtime.test.ts +156 -0
  296. package/tests/nan-provider.test.ts +28 -18
  297. package/tests/native-binary-gate.test.ts +2 -0
  298. package/tests/native-review-capability-contract.test.ts +7 -1
  299. package/tests/native-review-cli.test.ts +236 -1
  300. package/tests/native-review-consent.test.ts +2 -2
  301. package/tests/native-review-parity-runtime.test.ts +1 -1
  302. package/tests/native-review-parity.test.ts +12 -48
  303. package/tests/native-risk-evidence-parity.test.ts +21 -0
  304. package/tests/no-self-pause-contract.test.ts +87 -0
  305. package/tests/notification-audio-native.test.ts +281 -0
  306. package/tests/notification-audio-windows.test.ts +581 -0
  307. package/tests/notification-audio.test.ts +548 -0
  308. package/tests/notification-customize.test.ts +655 -0
  309. package/tests/notification-events.test.ts +115 -0
  310. package/tests/notification-policy.test.ts +201 -0
  311. package/tests/notification-pulse-client.test.ts +356 -0
  312. package/tests/notification-pulse-protocol.test.ts +213 -0
  313. package/tests/notification-pulse-stream.test.ts +320 -0
  314. package/tests/notification-scheduler.test.ts +258 -0
  315. package/tests/notification-ui.test.ts +157 -0
  316. package/tests/npm-pack-result.test.ts +41 -0
  317. package/tests/odd-integration.test.ts +14 -5
  318. package/tests/odd-routing-canonical-ratchet.test.ts +53 -12
  319. package/tests/odd-routing-contract.test.ts +80 -23
  320. package/tests/odd-runtime-delegation-gate.test.ts +4 -1
  321. package/tests/orchestrator-budget.test.ts +29 -6
  322. package/tests/orchestrator-catalog.test.ts +146 -0
  323. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  324. package/tests/orchestrator-consultation.test.ts +111 -0
  325. package/tests/orchestrator-discovery.test.ts +241 -0
  326. package/tests/orchestrator-helper-consent.test.ts +171 -0
  327. package/tests/orchestrator-helper.test.ts +297 -0
  328. package/tests/orchestrator-message-card.test.ts +190 -0
  329. package/tests/orchestrator-presence.test.ts +23 -0
  330. package/tests/orchestrator-rdd-ownership.test.ts +6 -5
  331. package/tests/orchestrator-scope.test.ts +107 -0
  332. package/tests/orchestrator-state.test.ts +114 -0
  333. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  334. package/tests/orchestrator-tool-card.test.ts +398 -0
  335. package/tests/orchestrator-work-search.test.ts +318 -0
  336. package/tests/orchestrator-work.test.ts +147 -0
  337. package/tests/package-manifest.test.ts +105 -21
  338. package/tests/packed-runner-owned-path.test.ts +35 -1
  339. package/tests/parallel-reason-contract.test.ts +21 -0
  340. package/tests/parallel-writer-admission.test.ts +210 -0
  341. package/tests/permission-mode-policy.test.ts +194 -0
  342. package/tests/persona-single-channel.test.ts +4 -12
  343. package/tests/pi-pretty.test.ts +56 -0
  344. package/tests/pi-update-launcher.test.ts +87 -0
  345. package/tests/pi-update.test.ts +186 -0
  346. package/tests/provider-defect-handoff.test.ts +4 -3
  347. package/tests/quiet-tool-rendering.test.ts +160 -3
  348. package/tests/rdd-aware-verification-contract.test.ts +29 -41
  349. package/tests/rdd-status-line.test.ts +52 -0
  350. package/tests/render-cache-performance.test.ts +48 -0
  351. package/tests/review-agent-end-preflight.test.ts +62 -10
  352. package/tests/review-candidate-view.test.ts +101 -12
  353. package/tests/review-contract-prompt.test.ts +33 -4
  354. package/tests/review-controller-native-recovery.test.ts +327 -17
  355. package/tests/review-controller-native-routing.test.ts +255 -1
  356. package/tests/review-controller-workspace-root.test.ts +43 -2
  357. package/tests/review-controller.test.ts +2 -2
  358. package/tests/review-correction-lifecycle.test.ts +2 -1
  359. package/tests/review-gate.test.ts +7 -4
  360. package/tests/review-graph-schema.test.ts +2 -2
  361. package/tests/review-host-relay-routing.test.ts +5 -3
  362. package/tests/review-host-relay.test.ts +49 -2
  363. package/tests/review-integration-v2-forward.test.ts +21 -1
  364. package/tests/review-json-arguments.test.ts +167 -0
  365. package/tests/review-last-event-closure.test.ts +157 -8
  366. package/tests/review-ledger-contract.test.ts +5 -4
  367. package/tests/review-policy-judgment-day.test.ts +1 -1
  368. package/tests/review-policy-ordinary.test.ts +2 -2
  369. package/tests/review-relay-transport-agent.test.ts +66 -1
  370. package/tests/review-risk-assessment.test.ts +109 -33
  371. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  372. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  373. package/tests/review-test-fixtures.ts +7 -1
  374. package/tests/review-transaction.test.ts +18 -1
  375. package/tests/run-test-suite.test.ts +83 -5
  376. package/tests/runtime-harness.mjs +42 -15
  377. package/tests/runtime-metrics-extension.test.ts +1 -1
  378. package/tests/runtime-metrics-native.test.ts +31 -1
  379. package/tests/selection-engine.test.ts +65 -5
  380. package/tests/session-change-capture.test.ts +10 -10
  381. package/tests/session-changes-shell.test.ts +242 -48
  382. package/tests/session-changes.test.ts +17 -24
  383. package/tests/session-profile-binding.test.ts +163 -0
  384. package/tests/session-profile-freeze.test.ts +394 -0
  385. package/tests/session-profile-persistence.test.ts +334 -0
  386. package/tests/session-subject-routing-contract.test.ts +34 -0
  387. package/tests/session-worktree-registry.test.ts +57 -2
  388. package/tests/shell-bar.test.ts +40 -0
  389. package/tests/shell-changes-view.test.ts +85 -34
  390. package/tests/shell-changes.test.ts +79 -62
  391. package/tests/shell-render-cache-runtime.test.ts +69 -0
  392. package/tests/shell-render-cache.test.ts +129 -0
  393. package/tests/shell-sidebar-layout.test.ts +27 -2
  394. package/tests/shell-todo.test.ts +132 -11
  395. package/tests/shell-usage-view.test.ts +1 -1
  396. package/tests/shell-usage.test.ts +35 -0
  397. package/tests/short-check-routing-contract.test.ts +49 -0
  398. package/tests/skill-registry-watcher.test.ts +119 -0
  399. package/tests/skill-registry.test.ts +311 -5
  400. package/tests/startup-banner.test.ts +117 -3
  401. package/tests/startup-home-repo.test.ts +170 -0
  402. package/tests/status-timing-diagnostics.test.ts +283 -0
  403. package/tests/support/orchestrator-modules.ts +19 -0
  404. package/tests/task-size-routing-contract.test.ts +241 -0
  405. package/tests/telemetry-trigger.test.ts +27 -1
  406. package/tests/test-strength-contract.test.ts +124 -0
  407. package/tests/verify-agent-contract.test.ts +90 -0
  408. package/tests/verify-first-launch-contract.test.ts +139 -0
  409. package/tests/verify-package-files.test.ts +56 -0
  410. package/tests/vim-editor-adapter.test.ts +47 -21
  411. package/tests/visual-customization-policy.test.ts +32 -3
  412. package/tests/visual-customize-view.test.ts +288 -10
  413. package/tests/writer-edit-surface-scope.test.ts +59 -2
  414. package/tests/writer-surfaces.test.ts +199 -0
  415. package/tests/yolo-customize.test.ts +18 -3
  416. package/tests/yolo-mode-runtime.test.ts +37 -10
  417. package/tests/yolo-mode.test.ts +38 -5
  418. package/tests/yolo-session-policy.test.ts +1 -1
  419. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  420. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  421. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  422. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  423. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  424. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  425. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -2,6 +2,10 @@ import { createHash, randomUUID } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import { dirname, join, resolve } from "node:path";
4
4
  import { stripVTControlCharacters } from "node:util";
5
+ import { validRecordedScope, type RecordedScope } from "./orchestrator-scope.ts";
6
+ import { projectCatalog } from "./orchestrator-catalog.ts";
7
+ import { decodePublishedState, type PublishedState } from "./orchestrator-state.ts";
8
+ import { decodeTaskAliases, type TaskAliases } from "./orchestrator-alias.ts";
5
9
 
6
10
  // Same-profile OS-user trust boundary, not an authorization channel. POSIX modes
7
11
  // restrict newly created storage; Windows deployments must supply their own ACLs.
@@ -27,6 +31,19 @@ export interface Activity { tasks: { summary: ActivityInput["task"]; thread: {
27
31
  version: number; dropped: number; items: ObjectValue[];
28
32
  } }[] }
29
33
  export interface Target { sessionHash: string; incarnation: string }
34
+ export interface DiscoveryMetadata {
35
+ aliases?: TaskAliases;
36
+ state?: PublishedState;
37
+ scope?: RecordedScope;
38
+ activation: string;
39
+ workspace: string;
40
+ tasks: { id: string; label: string; status: string; workspace: string }[];
41
+ omitted: number;
42
+ }
43
+ export function sessionHash(sessionId: string) { return digest(sessionId); }
44
+ export function activationHash(peer: { sessionId: string; endpoint: string; createdAt: number }) {
45
+ return digest(JSON.stringify([peer.sessionId, peer.endpoint, peer.createdAt]));
46
+ }
30
47
  export interface Header extends Target {
31
48
  schema: 1; label: string; heartbeat: number; generation: number;
32
49
  counts: { running: number; queued: number; waiting: number; finished: number };
@@ -37,10 +54,11 @@ const object = (v: unknown): v is ObjectValue => !!v && typeof v === "object" &&
37
54
  const integer = (v: unknown) => Number.isSafeInteger(v) && (v as number) >= 0;
38
55
  const keys = (v: ObjectValue, names: string[]) => Object.keys(v).length === names.length && names.every((k) => Object.hasOwn(v, k));
39
56
  const digest = (data: string | Buffer) => createHash("sha256").update(data).digest("hex");
40
- function label(text: string) {
57
+ export function sanitizeDisplayLabel(text: string) {
41
58
  const clean = stripVTControlCharacters(text).replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
42
59
  return Array.from(clean).slice(0, 120).join("").trimEnd();
43
60
  }
61
+ const label = sanitizeDisplayLabel;
44
62
  const pick = (v: ObjectValue, names: string[]) => Object.fromEntries(names.map((k) => [k, v[k]]));
45
63
  const summaryTextKeys = ["id", "agent", "label", "status", "model"];
46
64
  const summaryKeys = [...summaryTextKeys, "createdAt", "startedAt", "endedAt", "lastActivityAt"];
@@ -78,6 +96,15 @@ function validTarget(value: Target) {
78
96
  return object(value) && typeof value.sessionHash === "string" && HASH.test(value.sessionHash)
79
97
  && typeof value.incarnation === "string" && UUID.test(value.incarnation);
80
98
  }
99
+ function validDiscovery(d: unknown): d is DiscoveryMetadata {
100
+ return object(d) && keys(d, ["activation", "workspace", "tasks", "omitted", ...["scope", "state", "aliases"].filter(k => Object.hasOwn(d, k))])
101
+ && typeof d.activation === "string" && HASH.test(d.activation)
102
+ && typeof d.workspace === "string" && d.workspace === label(d.workspace) && integer(d.omitted)
103
+ && Array.isArray(d.tasks) && d.tasks.length <= 8 && d.tasks.every((t: unknown) => object(t)
104
+ && keys(t, ["id", "label", "status", "workspace"])
105
+ && ["id", "label", "workspace"].every(k => typeof t[k] === "string" && t[k] === label(t[k]))
106
+ && STATUSES.includes(t.status));
107
+ }
81
108
  function validHeader(h: unknown): h is Header {
82
109
  if (!object(h) || !keys(h, ["schema", "sessionHash", "incarnation", "label", "heartbeat", "generation", "counts", "digest", "unavailable"])) return false;
83
110
  return validTarget(h as Header) && h.schema === 1 && typeof h.label === "string" && h.label === label(h.label)
@@ -86,7 +113,7 @@ function validHeader(h: unknown): h is Header {
86
113
  && ((h.unavailable === null && typeof h.digest === "string" && HASH.test(h.digest))
87
114
  || (h.unavailable === "activity-too-large" && h.digest === null));
88
115
  }
89
- function filename(target: Target, kind: "header" | "activity") {
116
+ function filename(target: Target, kind: "header" | "activity" | "discovery") {
90
117
  if (!validTarget(target)) throw new Error("malformed");
91
118
  return `${target.sessionHash}.${target.incarnation}.${kind}.json`;
92
119
  }
@@ -96,7 +123,7 @@ function directory(path: string, privateMode = false, owned = privateMode) {
96
123
  if (!stat.isDirectory() || stat.isSymbolicLink() || (owned && process.platform !== "win32"
97
124
  && ((stat.mode & (privateMode ? 0o077 : 0o022)) !== 0 || stat.uid !== process.getuid?.()))) throw new Error("unsafe-directory");
98
125
  }
99
- function rootFor(profile: string, create = false) {
126
+ export function rootFor(profile: string, create = false, catalog = false) {
100
127
  const absolute = resolve(profile);
101
128
  // Reject symlink ancestors too. The caller supplies an existing profile root.
102
129
  for (let path = absolute;; path = dirname(path)) {
@@ -104,7 +131,7 @@ function rootFor(profile: string, create = false) {
104
131
  if (dirname(path) === path) break;
105
132
  }
106
133
  const shared = join(absolute, "gentle-agents");
107
- const root = join(shared, "presence");
134
+ const root = join(shared, catalog ? "catalog" : "presence");
108
135
  // History may already own a 0755 shared root. Never change its permissions.
109
136
  for (const path of [shared, root]) {
110
137
  if (create) {
@@ -118,7 +145,7 @@ function rootFor(profile: string, create = false) {
118
145
  function regular(stat: fs.Stats) {
119
146
  if (!stat.isFile() || stat.nlink !== 1 || (process.platform !== "win32" && stat.uid !== process.getuid?.())) throw new Error("unsafe-file");
120
147
  }
121
- function boundedRead(path: string, limit: number) {
148
+ export function boundedRead(path: string, limit: number) {
122
149
  const before = fs.lstatSync(path);
123
150
  regular(before);
124
151
  if (before.size > limit) throw new Error("oversized");
@@ -236,11 +263,29 @@ export function readActivity(profile: string, selection: Header): { activity?: A
236
263
  } catch (error) { return { unavailable: reason(error) }; }
237
264
  }
238
265
 
266
+ /** Optional derived sidecar; failures never invalidate the schema-1 header. */
267
+ export function readDiscovery(profile: string, h: Header): DiscoveryMetadata | undefined {
268
+ try {
269
+ const value = JSON.parse(boundedRead(join(rootFor(profile), filename(h, "discovery")), HEADER_LIMIT).toString("utf8"));
270
+ if (!object(value) || !keys(value, ["schema", "sessionHash", "incarnation", "generation", "metadata"])
271
+ || value.schema !== 1 || value.sessionHash !== h.sessionHash || value.incarnation !== h.incarnation
272
+ || value.generation !== h.generation || !validDiscovery(value.metadata)) return undefined;
273
+ const { scope, state, aliases: rawAliases, ...legacy } = value.metadata;
274
+ const decoded = decodePublishedState(state);
275
+ const aliases = decodeTaskAliases(rawAliases);
276
+ return { ...legacy, ...(aliases ? { aliases } : {}), ...(validRecordedScope(scope) ? { scope } : {}), ...(decoded ? { state: decoded } : {}) };
277
+ } catch { return undefined; }
278
+ }
279
+
239
280
  export class PresencePublisher {
240
281
  readonly target: Readonly<Target>;
241
282
  private readonly profile: string;
242
283
  private readonly displayLabel: string;
284
+ private readonly labelSource?: () => string;
243
285
  private header!: Header;
286
+ private discovery?: DiscoveryMetadata;
287
+ private catalog?: ReturnType<typeof projectCatalog>;
288
+ private catalogTokens: string[] = [];
244
289
  private published = "";
245
290
  private pending = "";
246
291
  private timer?: ReturnType<typeof setTimeout>;
@@ -250,12 +295,13 @@ export class PresencePublisher {
250
295
  /** Timer I/O failures stop publication; consumers still apply the recent TTL. */
251
296
  error?: string;
252
297
 
253
- private constructor(options: { profile: string; sessionId: string; label: string }) {
298
+ private constructor(options: { profile: string; sessionId: string; label: string; labelSource?: () => string }) {
254
299
  this.profile = options.profile;
255
300
  this.displayLabel = label(options.label);
301
+ this.labelSource = options.labelSource;
256
302
  this.target = Object.freeze({ sessionHash: digest(options.sessionId), incarnation: randomUUID() });
257
303
  }
258
- static start(options: { profile: string; sessionId: string; label: string; activity: readonly ActivityInput[] }) {
304
+ static start(options: { profile: string; sessionId: string; label: string; labelSource?: () => string; activity: readonly ActivityInput[] }) {
259
305
  const publisher = new PresencePublisher(options);
260
306
  try {
261
307
  rootFor(options.profile, true);
@@ -266,6 +312,50 @@ export class PresencePublisher {
266
312
  return publisher;
267
313
  } catch (error) { publisher.dispose(); throw error; }
268
314
  }
315
+ /** Refresh through the existing publication path, without changing activity generation.
316
+ * The source must be a cheap canonical-name getter; throw when its session is stale. */
317
+ refreshLabel() {
318
+ this.guarded(() => this.publishHeader());
319
+ }
320
+ /** Metadata-only projection; never reads task prompts, results, or threads.
321
+ * Binding to the listener activation prevents reused session IDs from joining. */
322
+ updateDiscovery(peer: { sessionId: string; endpoint: string; createdAt: number }, input: {
323
+ workspace: string; tasks: readonly { id: string; label: string; status: string; cwd: string }[]; scope?: RecordedScope; registered?: readonly string[]; state?: PublishedState;
324
+ }) {
325
+ if (this.disposed) throw new Error("disposed");
326
+ if (sessionHash(peer.sessionId) !== this.target.sessionHash) throw new Error("malformed-discovery");
327
+ // A shortened path could denote a different workspace; do not advertise it.
328
+ const workspace = (path: string) => Array.from(path).length <= 120 && label(path) === path ? path : "";
329
+ const discovery: DiscoveryMetadata = { activation: activationHash(peer), workspace: workspace(input.workspace),
330
+ tasks: input.tasks.slice(0, 8).map(t => ({ id: label(t.id), label: label(t.label), status: t.status, workspace: workspace(t.cwd) })),
331
+ omitted: Math.max(0, input.tasks.length - 8) };
332
+ const state = decodePublishedState(input.state);
333
+ if (state?.sessionId === peer.sessionId) {
334
+ discovery.state = state;
335
+ if (state.aliases) discovery.aliases = structuredClone(state.aliases);
336
+ }
337
+ if (input.scope && validRecordedScope(input.scope)) {
338
+ discovery.scope = structuredClone(input.scope);
339
+ // Leave explicit gaps instead of publishing a sidecar readers cannot fit.
340
+ if (Buffer.byteLength(JSON.stringify(discovery)) > HEADER_LIMIT - 1024) {
341
+ const scope = discovery.scope;
342
+ discovery.scope = { ...scope, tasks: [], registered: [],
343
+ omittedTasks: scope.tasks.length + scope.omittedTasks,
344
+ omittedRegistered: scope.registered.length + scope.omittedRegistered, complete: false };
345
+ }
346
+ }
347
+ if (Buffer.byteLength(JSON.stringify(discovery)) > HEADER_LIMIT - 1024) delete discovery.state;
348
+ if (!validDiscovery(discovery)) throw new Error("malformed-discovery");
349
+ const catalog = projectCatalog(input.tasks, input.registered ?? []);
350
+ if (JSON.stringify(discovery) === JSON.stringify(this.discovery) && JSON.stringify(catalog) === JSON.stringify(this.catalog)) return;
351
+ // Only catalog or routing-activation changes rotate page tokens; private
352
+ // activity generations and unrelated legacy metadata do not invalidate them.
353
+ if (JSON.stringify(catalog) !== JSON.stringify(this.catalog) || discovery.activation !== this.discovery?.activation)
354
+ this.catalogTokens = Array.from({ length: 7 }, () => randomUUID());
355
+ this.catalog = catalog;
356
+ this.discovery = discovery;
357
+ this.writeDiscovery();
358
+ }
269
359
  /** Eagerly projects/serializes each supplied snapshot to detach caller-owned data.
270
360
  * Only disk publication is coalesced; callers should avoid unrelated invalidations. */
271
361
  update(input: readonly ActivityInput[]) {
@@ -285,18 +375,29 @@ export class PresencePublisher {
285
375
  if (this.disposed) return;
286
376
  try { action(); } catch (error) { this.error = reason(error); this.dispose(); }
287
377
  }
288
- private write(kind: "header" | "activity", bytes: string) {
289
- const root = rootFor(this.profile);
290
- const name = filename(this.target, kind);
378
+ private write(kind: "header" | "activity" | "discovery" | "catalog", bytes: string) {
379
+ const root = rootFor(this.profile, kind === "catalog", kind === "catalog");
380
+ const name = kind === "catalog" ? `${this.target.sessionHash}.${this.target.incarnation}.json` : filename(this.target, kind);
291
381
  atomicWrite(root, name, bytes);
292
382
  const { dev, ino } = fs.lstatSync(join(root, name));
293
- this.owned.set(name, { dev, ino });
383
+ this.owned.set(join(root, name), { dev, ino });
384
+ }
385
+ private writeDiscovery() {
386
+ if (!this.discovery) return;
387
+ try {
388
+ if (this.catalog) this.write("catalog", JSON.stringify({ schema: 1, ...this.target,
389
+ generation: this.header.generation, activation: this.discovery.activation, tokens: this.catalogTokens, catalog: this.catalog }));
390
+ } catch { /* Optional catalog failure must not suppress other metadata. */ }
391
+ try {
392
+ this.write("discovery", JSON.stringify({ schema: 1, ...this.target, generation: this.header.generation, metadata: this.discovery }));
393
+ } catch { /* Metadata failure must not withdraw an existing activity peer. */ }
294
394
  }
295
395
  private publishHeader() {
296
- const header = { ...this.header, heartbeat: Date.now() };
396
+ const header = { ...this.header, label: label(this.labelSource?.() ?? this.displayLabel), heartbeat: Date.now() };
297
397
  if (!validHeader(header)) throw new Error("malformed-header");
298
398
  this.write("header", JSON.stringify(header));
299
399
  this.header = header;
400
+ this.writeDiscovery();
300
401
  }
301
402
  private flush() {
302
403
  if (this.pending === this.published) return;
@@ -324,9 +425,9 @@ export class PresencePublisher {
324
425
  clearTimeout(this.timer);
325
426
  clearInterval(this.heartbeat);
326
427
  this.pending = this.published = "";
327
- for (const [name, identity] of this.owned) {
428
+ for (const [path, identity] of this.owned) {
328
429
  try {
329
- const path = join(rootFor(this.profile), name);
430
+ rootFor(this.profile, false, dirname(path) === join(resolve(this.profile), "gentle-agents", "catalog"));
330
431
  const stat = fs.lstatSync(path);
331
432
  regular(stat);
332
433
  if (stat.dev === identity.dev && stat.ino === identity.ino) fs.unlinkSync(path);
@@ -0,0 +1,80 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute } from "node:path";
3
+ import { resolveSessionWorktree, type WorktreeResolver } from "./session-worktree-registry.ts";
4
+
5
+ export interface RepositoryFact {
6
+ root: string | null;
7
+ cloneHash: string | null;
8
+ resolvedAt: number;
9
+ source: "recorded-workspace/git";
10
+ }
11
+ export interface RecordedScope {
12
+ host: RepositoryFact;
13
+ tasks: { id: string; repository: RepositoryFact }[];
14
+ registered: RepositoryFact[];
15
+ omittedTasks: number;
16
+ omittedRegistered: number;
17
+ complete: boolean;
18
+ }
19
+ // Recorded cwd/root is literal, not a user spelling. The shared resolver maps
20
+ // these Unicode separators to ASCII space; reject them before resolving and on
21
+ // output/readback rather than aliasing a distinct directory or truncating a root.
22
+ const safeRoot = (root: string) => isAbsolute(root) && Buffer.byteLength(root) <= 256
23
+ && !/[\p{Cc}\p{Cf}\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(root);
24
+ const object = (v: any) => v && typeof v === "object" && !Array.isArray(v);
25
+ const keys = (v: object, names: string[]) => Object.keys(v).length === names.length && names.every(k => Object.hasOwn(v, k));
26
+ const count = (v: unknown) => Number.isSafeInteger(v) && (v as number) >= 0;
27
+ export function validRecordedScope(value: any): value is RecordedScope {
28
+ const fact = (v: any) => object(v) && keys(v, ["cloneHash", "resolvedAt", "root", "source"])
29
+ && v.source === "recorded-workspace/git" && count(v.resolvedAt)
30
+ && ((v.root === null && v.cloneHash === null) || (typeof v.root === "string" && safeRoot(v.root)
31
+ && typeof v.cloneHash === "string" && /^[a-f0-9]{64}$/.test(v.cloneHash)));
32
+ return object(value) && keys(value, ["complete", "host", "omittedRegistered", "omittedTasks", "registered", "tasks"])
33
+ && fact(value.host) && Array.isArray(value.tasks) && value.tasks.length <= 8
34
+ && value.tasks.every((t: any) => object(t) && keys(t, ["id", "repository"])
35
+ && typeof t.id === "string" && Buffer.byteLength(t.id) <= 512 && fact(t.repository))
36
+ && Array.isArray(value.registered) && value.registered.length <= 8 && value.registered.every(fact)
37
+ && count(value.omittedTasks) && count(value.omittedRegistered)
38
+ && value.complete === (value.omittedTasks === 0 && value.omittedRegistered === 0);
39
+ }
40
+
41
+ /** One bounded derivative snapshot, not a registry or authority. The caller supplies
42
+ * only admitted, unfinished, non-restored owned children and durable registry roots.
43
+ * Membership/cwd changes invalidate even unknown facts; session replacement clears.
44
+ * No resolution runs on a stable heartbeat/token update. */
45
+ export class OrchestratorScopeCache {
46
+ private key = "";
47
+ private snapshot?: RecordedScope;
48
+ private resolver: WorktreeResolver;
49
+ private now: () => number;
50
+ constructor(resolver = resolveSessionWorktree, now = Date.now) {
51
+ this.resolver = resolver;
52
+ this.now = now;
53
+ }
54
+ clear() {
55
+ this.key = "";
56
+ this.snapshot = undefined;
57
+ }
58
+ project(cwd: string, tasks: readonly { id: string; cwd: string }[], roots: readonly string[]): RecordedScope {
59
+ const registered = [...new Set(roots)];
60
+ const key = createHash("sha256").update(JSON.stringify([cwd, tasks.map(t => [t.id, t.cwd]), registered])).digest("hex");
61
+ if (key === this.key && this.snapshot) return this.snapshot;
62
+ const resolvedAt = this.now();
63
+ const facts = new Map<string, RepositoryFact>();
64
+ const fact = (path: string) => {
65
+ if (facts.has(path)) return facts.get(path)!;
66
+ const unknown: RepositoryFact = { root: null, cloneHash: null, resolvedAt, source: "recorded-workspace/git" };
67
+ let identity;
68
+ try { identity = safeRoot(path) ? this.resolver(path, path) : undefined; } catch { /* Unavailable is unknown. */ }
69
+ const result = identity && safeRoot(identity.root) ? { ...unknown, root: identity.root,
70
+ cloneHash: createHash("sha256").update(identity.commonDir).digest("hex") } : unknown;
71
+ facts.set(path, result);
72
+ return result;
73
+ };
74
+ this.snapshot = { host: fact(cwd), tasks: tasks.slice(0, 8).map(t => ({ id: t.id, repository: fact(t.cwd) })),
75
+ registered: registered.slice(0, 8).map(fact), omittedTasks: Math.max(0, tasks.length - 8),
76
+ omittedRegistered: Math.max(0, registered.length - 8), complete: tasks.length <= 8 && registered.length <= 8 };
77
+ this.key = key;
78
+ return this.snapshot;
79
+ }
80
+ }
@@ -0,0 +1,107 @@
1
+ import { isAbsolute } from "node:path";
2
+ import { decodeTaskAliases, normalizeTaskSubject, type TaskAliases } from "./orchestrator-alias.ts";
3
+ import { decodeWork, type PublishedWork } from "./orchestrator-work.ts";
4
+
5
+ export const ORCHESTRATOR_STATE_ENTRY = "gentle-agents.published-state";
6
+ const fields = ["objective", "progress", "decisions", "blockers"] as const;
7
+ export type CuratedState = Partial<Record<typeof fields[number], string>> & { work?: PublishedWork };
8
+ export interface PublishedState {
9
+ schema: 1 | 2; sessionId: string; recordedAt: number; cwd: string | null;
10
+ source: "owner-curated"; ownerReply: false; authority: "none"; state: CuratedState | null;
11
+ aliases?: TaskAliases;
12
+ }
13
+ const object = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
14
+ const safeText = (v: unknown): v is string => typeof v === "string" && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(v);
15
+ const safeCwd = (v: unknown): v is string => safeText(v) && isAbsolute(v) && Buffer.byteLength(v) <= 1024
16
+ && !/[\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(v);
17
+ /** Field whitelist, not secret redaction. Reject rather than truncate meaning. */
18
+ export function decodeCuratedState(value: unknown): CuratedState | null {
19
+ if (value === null) return null;
20
+ if (!object(value) || Object.keys(value).some(k => k !== "work" && !fields.includes(k as typeof fields[number])))
21
+ throw new Error("invalid-published-state");
22
+ const state: CuratedState = {};
23
+ let bytes = 0;
24
+ for (const key of fields) {
25
+ if (!Object.hasOwn(value, key)) continue;
26
+ if (!safeText(value[key])) throw new Error("invalid-published-state");
27
+ state[key] = value[key];
28
+ bytes += Buffer.byteLength(value[key]);
29
+ }
30
+ if (Object.hasOwn(value, "work")) {
31
+ state.work = decodeWork(value.work);
32
+ bytes += Buffer.byteLength(JSON.stringify(state.work));
33
+ }
34
+ if (bytes > 2048) throw new Error("invalid-published-state");
35
+ return state;
36
+ }
37
+ export function decodePublishedState(value: unknown): PublishedState | undefined {
38
+ try {
39
+ const keys = ["schema", "sessionId", "recordedAt", "cwd", "source", "ownerReply", "authority", "state",
40
+ ...(object(value) && Object.hasOwn(value, "aliases") ? ["aliases"] : [])];
41
+ if (!object(value) || Object.keys(value).length !== keys.length || !keys.every(k => Object.hasOwn(value, k))
42
+ || (value.schema !== 1 && value.schema !== 2) || !safeText(value.sessionId) || !value.sessionId || Buffer.byteLength(value.sessionId) > 256
43
+ || !Number.isSafeInteger(value.recordedAt) || (value.recordedAt as number) < 0
44
+ || !(value.cwd === null || safeCwd(value.cwd))
45
+ || value.source !== "owner-curated" || value.ownerReply !== false || value.authority !== "none"
46
+ || Buffer.byteLength(JSON.stringify(value)) > 4096) return undefined;
47
+ const state = decodeCuratedState(value.state);
48
+ const aliases = decodeTaskAliases(value.aliases);
49
+ if ((value.schema === 2) !== !!state?.work || (Object.hasOwn(value, "aliases") && !aliases)) return undefined;
50
+ return { schema: value.schema, sessionId: value.sessionId, recordedAt: value.recordedAt as number,
51
+ cwd: safeText(value.cwd) ? value.cwd : null, source: "owner-curated", ownerReply: false, authority: "none", state,
52
+ ...(aliases ? { aliases } : {}) };
53
+ } catch { return undefined; }
54
+ }
55
+ interface StateManager {
56
+ getSessionId(): string; getCwd(): string;
57
+ getBranch(): readonly { type: string; customType?: string; data?: unknown }[];
58
+ }
59
+ /** One branch-local cache. Never accesses message bodies, summaries, or results. */
60
+ export class OrchestratorStateCache {
61
+ private manager?: StateManager;
62
+ private sessionId?: string;
63
+ private value?: PublishedState;
64
+ private epoch = 0;
65
+ clear() { this.epoch++; this.manager = undefined; this.sessionId = undefined; this.value = undefined; }
66
+ load(manager: StateManager) {
67
+ this.clear();
68
+ this.manager = manager;
69
+ this.sessionId = manager.getSessionId();
70
+ const branch = manager.getBranch();
71
+ for (let i = branch.length - 1; i >= 0; i--) {
72
+ const entry = branch[i];
73
+ if (entry.type !== "custom" || entry.customType !== ORCHESTRATOR_STATE_ENTRY) continue;
74
+ const decoded = decodePublishedState(entry.data);
75
+ // A malformed/foreign latest record suppresses older knowledge.
76
+ this.value = decoded?.sessionId === this.sessionId ? decoded : undefined;
77
+ break;
78
+ }
79
+ }
80
+ get(manager: StateManager): PublishedState | undefined {
81
+ return this.manager === manager && this.sessionId === manager.getSessionId() && this.value
82
+ ? structuredClone(this.value) : undefined;
83
+ }
84
+ publish(manager: StateManager, input: unknown, append: (type: string, data: PublishedState) => void, now = Date.now(), subject?: unknown) {
85
+ const state = input === undefined ? this.value?.state ?? null : decodeCuratedState(input);
86
+ const declared = normalizeTaskSubject(subject);
87
+ if (this.manager !== manager || this.sessionId !== manager.getSessionId()) throw new Error("stale-published-state");
88
+ const epoch = this.epoch;
89
+ const previous = this.value?.aliases;
90
+ // Null/null marks a new publication with no declaration yet; legacy absence
91
+ // cannot establish historical first-task knowledge.
92
+ let aliases = previous;
93
+ if (declared) {
94
+ let initialAlias = previous?.initialAlias ?? null;
95
+ if (!this.value || previous?.currentAlias === null) initialAlias = declared;
96
+ aliases = { initialAlias, currentAlias: declared };
97
+ } else if (!this.value) aliases = { initialAlias: null, currentAlias: null };
98
+ const cwd = input === undefined && this.value ? this.value.cwd : manager.getCwd();
99
+ const record = decodePublishedState({ schema: state?.work ? 2 : 1, sessionId: manager.getSessionId(),
100
+ recordedAt: input === undefined && this.value ? this.value.recordedAt : now, cwd: safeCwd(cwd) ? cwd : null,
101
+ source: "owner-curated", ownerReply: false, authority: "none", state, ...(aliases ? { aliases } : {}) });
102
+ if (!record) throw new Error("invalid-published-state");
103
+ append(ORCHESTRATOR_STATE_ENTRY, structuredClone(record));
104
+ if (this.epoch !== epoch || this.manager !== manager || this.sessionId !== record.sessionId || manager.getSessionId() !== record.sessionId) throw new Error("stale-published-state");
105
+ this.value = record;
106
+ }
107
+ }