gentle-pi 3.7.0 → 4.1.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 (590) hide show
  1. package/README.md +155 -10
  2. package/assets/agents/gentle-ai-explore.md +13 -6
  3. package/assets/agents/gentle-ai-verify.md +27 -6
  4. package/assets/agents/gentle-ai-worker.md +13 -15
  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 +75 -169
  9. package/assets/orchestrator-memory.md +12 -26
  10. package/assets/orchestrator-prompts.md +48 -0
  11. package/assets/orchestrator-skills.md +1 -1
  12. package/assets/orchestrator-tracking.md +53 -0
  13. package/assets/orchestrator-verification.md +28 -0
  14. package/assets/orchestrator-writer.md +38 -0
  15. package/assets/orchestrator.md +30 -38
  16. package/assets/sounds/LICENSE.md +52 -0
  17. package/assets/sounds/attention.wav +0 -0
  18. package/assets/sounds/error.wav +0 -0
  19. package/assets/sounds/success.wav +0 -0
  20. package/assets/support/strict-tdd-verify.md +4 -266
  21. package/assets/support/strict-tdd.md +8 -360
  22. package/bin/engram-guard +9 -0
  23. package/bin/gentle-shell-install.mjs +302 -0
  24. package/bin/gentle-shell.mjs +956 -50
  25. package/contracts/review-provider-contract-mirror/provider-contract.lock.json +7 -7
  26. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/manifest.json +2 -2
  27. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/orchestration/pi.md +16 -4
  28. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-capabilities.baseline.json +2 -2
  29. package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/generated/provider-roles.baseline.json +1 -1
  30. package/docs/bundled-install.md +397 -0
  31. package/docs/delegated-verification.md +27 -4
  32. package/docs/gentle-agents-activity.md +537 -0
  33. package/docs/gentle-shell-uninstall.md +136 -0
  34. package/docs/gentle-shell.md +165 -51
  35. package/docs/install-wizard.md +2040 -0
  36. package/docs/native-authority-architecture.md +3 -3
  37. package/docs/permission-modes.md +82 -0
  38. package/docs/prompt-history.md +280 -0
  39. package/docs/readme-reference.md +364 -231
  40. package/docs/render-cache-performance.md +28 -0
  41. package/docs/sound-notifications-proposal.md +191 -0
  42. package/docs/sound-notifications.md +114 -0
  43. package/docs/telemetry.md +1 -1
  44. package/docs/yolo-mode.md +86 -0
  45. package/extensions/child-context.ts +35 -0
  46. package/extensions/child-safety.ts +25 -0
  47. package/extensions/codegraph-tools.ts +18 -5
  48. package/extensions/gentle-agents.ts +1093 -326
  49. package/extensions/gentle-ai.ts +1636 -890
  50. package/extensions/gentle-herdr-activity.ts +81 -0
  51. package/extensions/gentle-notifications.ts +104 -0
  52. package/extensions/gentle-shell.ts +1619 -136
  53. package/extensions/gentle-stats.ts +101 -0
  54. package/extensions/gentle-todo.ts +126 -20
  55. package/extensions/history/atomic-write.ts +38 -0
  56. package/extensions/history/hide-prompts.ts +183 -0
  57. package/extensions/history/index.ts +1424 -0
  58. package/extensions/history/load-shared-history.ts +39 -0
  59. package/extensions/history/selector-helpers.ts +538 -0
  60. package/extensions/history/session-scan.ts +233 -0
  61. package/extensions/history/store.ts +1119 -0
  62. package/extensions/nan-provider.ts +6 -0
  63. package/extensions/pi-pretty.ts +39 -3
  64. package/extensions/quiet-tools.ts +254 -92
  65. package/extensions/resume-hint.ts +60 -0
  66. package/extensions/skill-registry.ts +191 -58
  67. package/extensions/startup-banner.ts +71 -33
  68. package/lib/agent-assets.ts +604 -0
  69. package/lib/agent-home.ts +10 -1
  70. package/lib/agent-model-resolution.ts +80 -0
  71. package/lib/agent-profile-pin.ts +12 -0
  72. package/lib/agent-profiles.ts +13 -3
  73. package/lib/agents-completion-delivery.ts +11 -3
  74. package/lib/agents-message-delivery.ts +181 -0
  75. package/lib/agents-messaging.ts +1 -1
  76. package/lib/agents-protocol.ts +99 -0
  77. package/lib/agents-runner.ts +180 -107
  78. package/lib/agents-view.ts +26 -3
  79. package/lib/agents-widget.ts +16 -9
  80. package/lib/append-system-prompt.ts +21 -0
  81. package/lib/auto-permission-classifier.ts +280 -0
  82. package/lib/auto-permission-runtime.ts +268 -0
  83. package/lib/background-jobs-tools.ts +202 -0
  84. package/lib/background-jobs.ts +253 -0
  85. package/lib/background-monitor.ts +154 -0
  86. package/lib/bounded-writer-admission.ts +220 -0
  87. package/lib/builtin-codemode-optout.ts +166 -0
  88. package/lib/card-content-policy.ts +85 -0
  89. package/lib/card-style-policy.ts +60 -0
  90. package/lib/child-context-files.ts +166 -0
  91. package/lib/child-package-injection.ts +62 -0
  92. package/lib/codemode-renderer.ts +195 -0
  93. package/lib/command-palette-catalog.ts +5 -10
  94. package/lib/command-palette.ts +25 -14
  95. package/lib/context-threshold.ts +26 -0
  96. package/lib/destructive-command-guard.ts +144 -0
  97. package/lib/gentle-ai-binary.ts +109 -4
  98. package/lib/gentle-ai-elapsed-store.ts +87 -0
  99. package/lib/gentle-ai-renderer.ts +196 -39
  100. package/lib/gentle-shell-bundled.ts +517 -0
  101. package/lib/gentle-shell-launcher.ts +89 -27
  102. package/lib/gentle-shell-resume-hint.ts +176 -0
  103. package/lib/gentle-shell-startup-flow.ts +54 -0
  104. package/lib/gentle-shell-startup-update.ts +92 -0
  105. package/lib/gentle-shell-uninstall.ts +840 -0
  106. package/lib/gentle-shell-update-panel.ts +231 -0
  107. package/lib/herdr-activity.ts +183 -0
  108. package/lib/history-capture-policy.ts +95 -0
  109. package/lib/inprocess-reviewer.ts +3 -0
  110. package/lib/jobs-sidebar-state.ts +20 -0
  111. package/lib/jobs-view.ts +161 -0
  112. package/lib/model-price-ratio.ts +83 -0
  113. package/lib/model-routing-authority.ts +5 -1
  114. package/lib/nan-provider.ts +249 -0
  115. package/lib/native-review-cli.ts +242 -180
  116. package/lib/notification-audio-native.ts +162 -0
  117. package/lib/notification-audio-windows.ts +399 -0
  118. package/lib/notification-audio.ts +332 -0
  119. package/lib/notification-customize.ts +241 -0
  120. package/lib/notification-events.ts +113 -0
  121. package/lib/notification-policy.ts +148 -0
  122. package/lib/notification-pulse-client.ts +367 -0
  123. package/lib/notification-pulse-protocol.ts +305 -0
  124. package/lib/notification-pulse-stream.ts +213 -0
  125. package/lib/notification-pulse-worker.ts +66 -0
  126. package/lib/notification-scheduler.ts +173 -0
  127. package/lib/notification-service.ts +202 -0
  128. package/lib/notification-ui.ts +87 -0
  129. package/lib/odd-phase-inference.ts +231 -0
  130. package/lib/odd-phase.ts +141 -0
  131. package/lib/orchestrator-alias.ts +20 -0
  132. package/lib/orchestrator-catalog.ts +79 -0
  133. package/lib/orchestrator-consultation.ts +61 -0
  134. package/lib/orchestrator-discovery.ts +49 -0
  135. package/lib/orchestrator-helper-consent.ts +122 -0
  136. package/lib/orchestrator-helper.ts +167 -0
  137. package/lib/orchestrator-message-card.ts +105 -0
  138. package/lib/orchestrator-presence.ts +115 -14
  139. package/lib/orchestrator-scope.ts +80 -0
  140. package/lib/orchestrator-state.ts +107 -0
  141. package/lib/orchestrator-tool-card.ts +280 -0
  142. package/lib/orchestrator-work-search.ts +218 -0
  143. package/lib/orchestrator-work.ts +79 -0
  144. package/lib/overlay-repaint.ts +26 -0
  145. package/lib/permission-mode-policy.ts +59 -0
  146. package/lib/pi-tui-keys.ts +53 -0
  147. package/lib/review-candidate-view-owner.ts +73 -21
  148. package/lib/review-candidate-view.ts +115 -27
  149. package/lib/review-compact-contract.ts +2 -2
  150. package/lib/review-host-relay.ts +7 -2
  151. package/lib/review-integration-v2.ts +100 -22
  152. package/lib/review-publication-gate.ts +1 -1
  153. package/lib/review-reminder-receipt.ts +48 -8
  154. package/lib/review-risk-assessment.ts +227 -56
  155. package/lib/review-sidebar-state.ts +224 -0
  156. package/lib/review-transaction.ts +72 -3
  157. package/lib/runtime-metrics-children.ts +1 -1
  158. package/lib/runtime-metrics-native.ts +3 -2
  159. package/lib/selection-engine.ts +527 -0
  160. package/lib/session-change-capture.ts +5 -4
  161. package/lib/session-changes.ts +27 -22
  162. package/lib/session-messaging-grants.ts +135 -0
  163. package/lib/session-profile-binding.ts +83 -0
  164. package/lib/session-profile-freeze.ts +266 -0
  165. package/lib/session-worktree-registry.ts +87 -9
  166. package/lib/shell-bar.ts +215 -30
  167. package/lib/shell-card.ts +287 -18
  168. package/lib/shell-changes-view.ts +42 -14
  169. package/lib/shell-changes.ts +86 -71
  170. package/lib/shell-prompt.ts +102 -6
  171. package/lib/shell-sidebar-layout.ts +83 -15
  172. package/lib/shell-sidebar.ts +15 -1
  173. package/lib/shell-todo.ts +84 -31
  174. package/lib/shell-usage-view.ts +9 -4
  175. package/lib/shell-usage.ts +70 -13
  176. package/lib/stats-collector.ts +381 -0
  177. package/lib/stats-view.ts +431 -0
  178. package/lib/status-timing-diagnostics.ts +201 -0
  179. package/lib/telemetry-trigger.ts +3 -1
  180. package/lib/theme-customization.ts +52 -0
  181. package/lib/vim-editor-adapter.ts +383 -0
  182. package/lib/vim-normal-engine.ts +154 -0
  183. package/lib/vim-operator-engine.ts +416 -0
  184. package/lib/vim-policy.ts +49 -0
  185. package/lib/vim-visual-engine.ts +107 -0
  186. package/lib/visual-customization-policy.ts +119 -0
  187. package/lib/visual-customize-view.ts +481 -0
  188. package/lib/visual-profiles.ts +228 -0
  189. package/lib/writer-surfaces.ts +162 -0
  190. package/lib/yolo-session-policy.ts +317 -0
  191. package/package.json +20 -8
  192. package/runtime/child-package-injection.mjs +63 -0
  193. package/runtime/gentle-ai-binary.mjs +109 -4
  194. package/runtime/gentle-shell-bundled.mjs +518 -0
  195. package/runtime/gentle-shell-launcher.mjs +87 -25
  196. package/runtime/gentle-shell-resume-hint.mjs +177 -0
  197. package/runtime/gentle-shell-startup-flow.mjs +55 -0
  198. package/runtime/gentle-shell-startup-update.mjs +93 -0
  199. package/runtime/gentle-shell-uninstall.mjs +841 -0
  200. package/runtime/gentle-shell-update-panel.mjs +232 -0
  201. package/runtime/native-review-cli.mjs +239 -177
  202. package/runtime/review-integration-v2.mjs +100 -22
  203. package/runtime/review-risk-assessment.mjs +225 -54
  204. package/runtime/status-timing-diagnostics.mjs +202 -0
  205. package/runtime/telemetry-trigger.mjs +3 -1
  206. package/scripts/bootstrap.cmd +255 -0
  207. package/scripts/bootstrap.sh +168 -0
  208. package/scripts/build-distribution.mjs +157 -0
  209. package/scripts/build-installer-bundles.mjs +136 -0
  210. package/scripts/build-runtime-modules.mjs +8 -0
  211. package/scripts/bundled-install.mjs +1308 -0
  212. package/scripts/bundled-wizard.mjs +399 -0
  213. package/scripts/check-types.mjs +4 -6
  214. package/scripts/gentle-ai-installer.mjs +83 -35
  215. package/scripts/install-gentle-ai.mjs +48 -1
  216. package/scripts/install-tui-mode-setting.mjs +10 -1
  217. package/scripts/install-wizard-preview.mjs +154 -0
  218. package/scripts/installer-downloads.mjs +648 -0
  219. package/scripts/installer-preflight.mjs +387 -0
  220. package/scripts/installer-probes.mjs +564 -0
  221. package/scripts/installer-runner.mjs +1102 -0
  222. package/scripts/installer-server.mjs +881 -0
  223. package/scripts/installer-windows-artifacts.json +15 -0
  224. package/scripts/installer-windows.mjs +752 -0
  225. package/scripts/main-channel.mjs +452 -0
  226. package/scripts/mirror-odd-routing.mjs +2 -2
  227. package/scripts/npm-pack-result.mjs +31 -0
  228. package/scripts/render-cache-performance-worker.mjs +205 -0
  229. package/scripts/render-cache-performance.mjs +115 -0
  230. package/scripts/run-test-suite.mjs +90 -0
  231. package/scripts/test-installer-acceptance.sh +161 -0
  232. package/scripts/test-packed-runner.mjs +54 -19
  233. package/scripts/types-baseline.json +2 -92
  234. package/scripts/verify-package-files.mjs +91 -38
  235. package/skills/branch-pr/SKILL.md +24 -52
  236. package/skills/chained-pr/SKILL.md +31 -15
  237. package/skills/chained-pr/references/chaining-details.md +31 -20
  238. package/skills/gentle-ai/SKILL.md +16 -21
  239. package/skills/issue-creation/SKILL.md +8 -2
  240. package/skills/issue-creation/references/delegated-workflow-actions.md +19 -0
  241. package/skills/work-unit-commits/SKILL.md +4 -3
  242. package/tests/agent-home.test.ts +56 -3
  243. package/tests/agent-model-resolution.test.ts +113 -0
  244. package/tests/agent-profiles.test.ts +30 -0
  245. package/tests/agent-risk-escalation.test.ts +123 -0
  246. package/tests/agents-completion-delivery.test.ts +17 -0
  247. package/tests/agents-fake-child.ts +2 -2
  248. package/tests/agents-message-delivery.test.ts +106 -0
  249. package/tests/agents-protocol.test.ts +97 -0
  250. package/tests/agents-queries.test.ts +9 -9
  251. package/tests/agents-runner-process.test.ts +31 -0
  252. package/tests/agents-runner.test.ts +467 -93
  253. package/tests/agents-view-thread-identity.test.ts +169 -0
  254. package/tests/agents-view.test.ts +8 -2
  255. package/tests/agents-widget.test.ts +154 -15
  256. package/tests/append-system-prompt-route.test.ts +160 -0
  257. package/tests/append-system-prompt.test.ts +46 -0
  258. package/tests/artifact-language.test.ts +19 -213
  259. package/tests/ask-user-question.test.ts +44 -1
  260. package/tests/asset-installation-runtime.test.ts +5 -16
  261. package/tests/auto-mode-invariants.test.ts +258 -0
  262. package/tests/auto-mode-sdk.test.ts +57 -0
  263. package/tests/auto-mode.test.ts +49 -0
  264. package/tests/auto-permission-classifier.test.ts +283 -0
  265. package/tests/auto-permission-runtime.test.ts +196 -0
  266. package/tests/autonomous-guard.test.ts +69 -1
  267. package/tests/background-cache-warming.test.ts +2 -1
  268. package/tests/background-jobs.test.ts +227 -0
  269. package/tests/background-monitor.test.ts +195 -0
  270. package/tests/background-subagents-default-mode.test.ts +28 -5
  271. package/tests/bounded-stop-contract.test.ts +85 -0
  272. package/tests/bounded-writer-admission.test.ts +146 -0
  273. package/tests/branch-pr-skill.test.ts +43 -0
  274. package/tests/bridge-wake-lifecycle.test.ts +129 -0
  275. package/tests/bridge-wake-rendering.test.ts +56 -0
  276. package/tests/build-distribution.test.ts +269 -0
  277. package/tests/builtin-codemode-optout.test.ts +242 -0
  278. package/tests/bundled-install.test.ts +1319 -0
  279. package/tests/bundled-wizard.test.ts +505 -0
  280. package/tests/card-content-policy.test.ts +68 -0
  281. package/tests/card-style-policy.test.ts +87 -0
  282. package/tests/chained-pr-skill.test.ts +124 -0
  283. package/tests/child-codegraph-bootstrap.test.ts +74 -0
  284. package/tests/child-context-files.test.ts +255 -0
  285. package/tests/child-package-entrypoints.test.ts +65 -0
  286. package/tests/child-package-injection.test.ts +74 -0
  287. package/tests/child-safety.test.ts +94 -0
  288. package/tests/child-web-package.test.ts +224 -0
  289. package/tests/codegraph-tools.test.ts +35 -0
  290. package/tests/codemode-rendering.test.ts +596 -0
  291. package/tests/command-palette.test.ts +43 -7
  292. package/tests/context-threshold.test.ts +31 -0
  293. package/tests/delegate-for-reason-contract.test.ts +327 -0
  294. package/tests/delegated-key-learnings-contract.test.ts +3 -78
  295. package/tests/destructive-command-guard.test.ts +84 -0
  296. package/tests/devbinary/native-review-parity.devtest.ts +171 -3
  297. package/tests/devbinary/non-git-subagent-bootstrap.devtest.ts +193 -0
  298. package/tests/devbinary/pi-host-relay.devtest.ts +173 -3
  299. package/tests/engram-safety.test.ts +288 -0
  300. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +7 -0
  301. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +3 -0
  302. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T08-00-00-000Z_ddd.jsonl +2 -0
  303. package/tests/fixtures/stats/sessions/--work-alpha--/2026-09-30T09-00-00-000Z_eee.jsonl +3 -0
  304. package/tests/fixtures/stats/sessions/--work-alpha--/run-1/session.jsonl +2 -0
  305. package/tests/fixtures/stats/sessions/--work-beta--/2026-09-01T12-00-00-000Z_ccc.jsonl +2 -0
  306. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-28T10-00-00-000Z_aaa.jsonl +2 -0
  307. package/tests/fixtures/stats/user-pi/sessions/--work-alpha--/2026-09-29T23-00-00-000Z_bbb.jsonl +4 -0
  308. package/tests/fixtures/stats/user-pi/sessions/--work-gamma--/2026-09-20T09-00-00-000Z_fff.jsonl +3 -0
  309. package/tests/generic-agent-tools.test.ts +69 -0
  310. package/tests/gentle-agents.test.ts +2804 -346
  311. package/tests/gentle-ai-binary-isolation.test.ts +135 -0
  312. package/tests/gentle-ai-binary.test.ts +21 -4
  313. package/tests/gentle-ai-child-guards.test.ts +202 -0
  314. package/tests/gentle-ai-dev-binary-surfacing.test.ts +125 -15
  315. package/tests/gentle-ai-elapsed-store.test.ts +68 -0
  316. package/tests/gentle-ai-installer.test.ts +566 -62
  317. package/tests/gentle-ai-prompt-model-routing.test.ts +123 -0
  318. package/tests/gentle-ai-renderer.test.ts +493 -14
  319. package/tests/gentle-ai.test.ts +1427 -236
  320. package/tests/gentle-card-text.ts +2 -1
  321. package/tests/gentle-herdr-activity.test.ts +100 -0
  322. package/tests/gentle-notifications.test.ts +427 -0
  323. package/tests/gentle-shell-bin.test.ts +896 -115
  324. package/tests/gentle-shell-bundled.test.ts +1002 -0
  325. package/tests/gentle-shell-launcher.test.ts +333 -65
  326. package/tests/gentle-shell-resume-hint.test.ts +270 -0
  327. package/tests/gentle-shell-startup-flow.test.ts +75 -0
  328. package/tests/gentle-shell-startup-update.test.ts +85 -0
  329. package/tests/gentle-shell-uninstall.test.ts +978 -0
  330. package/tests/gentle-shell-update-panel.test.ts +158 -0
  331. package/tests/gentle-shell.test.ts +4216 -119
  332. package/tests/gentle-stats.test.ts +152 -0
  333. package/tests/gentle-theme.test.ts +4 -1
  334. package/tests/gentle-todo.test.ts +333 -10
  335. package/tests/herdr-activity.test.ts +243 -0
  336. package/tests/history-agent-home.test.ts +81 -0
  337. package/tests/history-atomic-write.test.ts +57 -0
  338. package/tests/history-capture-policy.test.ts +102 -0
  339. package/tests/history-child-guard.test.ts +70 -0
  340. package/tests/history-command-registration.test.ts +164 -0
  341. package/tests/history-dedupe-entries.test.ts +123 -0
  342. package/tests/history-delete-backfill.test.ts +190 -0
  343. package/tests/history-delete-confirm.test.ts +460 -0
  344. package/tests/history-dispatch.test.ts +180 -0
  345. package/tests/history-drain-hidden.test.ts +110 -0
  346. package/tests/history-drain-order.test.ts +98 -0
  347. package/tests/history-expanded-globals.test.ts +62 -0
  348. package/tests/history-gc.test.ts +832 -0
  349. package/tests/history-header-layout.test.ts +265 -0
  350. package/tests/history-hide-prompts.test.ts +275 -0
  351. package/tests/history-lazy-windowing.test.ts +508 -0
  352. package/tests/history-legacy-migrate-v2.test.ts +297 -0
  353. package/tests/history-load-shared-history.test.ts +53 -0
  354. package/tests/history-max-results-cap.test.ts +76 -0
  355. package/tests/history-multi-reader.test.ts +203 -0
  356. package/tests/history-off-path.test.ts +170 -0
  357. package/tests/history-openflow-integration.test.ts +173 -0
  358. package/tests/history-overlay-margin.test.ts +326 -0
  359. package/tests/history-preview-layout.test.ts +93 -0
  360. package/tests/history-registry.test.ts +143 -0
  361. package/tests/history-scope-delete.test.ts +411 -0
  362. package/tests/history-search-caret-keys.test.ts +142 -0
  363. package/tests/history-seed-bootstrap.test.ts +170 -0
  364. package/tests/history-seed-regen.test.ts +129 -0
  365. package/tests/history-selector-windowing.test.ts +94 -0
  366. package/tests/history-session-scan-directory.test.ts +87 -0
  367. package/tests/history-session-scan-extract.test.ts +583 -0
  368. package/tests/history-session-writer.test.ts +351 -0
  369. package/tests/history-store-paths.test.ts +79 -0
  370. package/tests/history-tombstone-exact.test.ts +139 -0
  371. package/tests/history-wheel-mouse.test.ts +242 -0
  372. package/tests/inprocess-reviewer.test.ts +49 -19
  373. package/tests/install-gentle-ai-failure.test.ts +69 -0
  374. package/tests/install-tui-mode-setting.test.ts +15 -0
  375. package/tests/install-wizard.test.ts +1223 -0
  376. package/tests/installer-bundles.test.ts +122 -0
  377. package/tests/installer-posix-bootstrap.test.ts +1201 -0
  378. package/tests/installer-preflight.test.ts +750 -0
  379. package/tests/installer-probes.test.ts +1106 -0
  380. package/tests/installer-runner.test.ts +2158 -0
  381. package/tests/installer-server.test.ts +1567 -0
  382. package/tests/installer-windows-bootstrap.test.ts +2410 -0
  383. package/tests/issue-creation-skill.test.ts +61 -0
  384. package/tests/jobs-view.test.ts +160 -0
  385. package/tests/main-channel-upgrade.test.ts +389 -0
  386. package/tests/main-channel.test.ts +344 -0
  387. package/tests/maintainer/provider-relay.maintest.ts +13 -12
  388. package/tests/model-price-ratio.test.ts +116 -0
  389. package/tests/model-profiles-preservation.test.ts +153 -0
  390. package/tests/model-routing-authority.test.ts +16 -0
  391. package/tests/model-routing-package.test.ts +10 -0
  392. package/tests/nan-provider-runtime.test.ts +156 -0
  393. package/tests/nan-provider.test.ts +481 -0
  394. package/tests/native-binary-gate.test.ts +2 -0
  395. package/tests/native-review-capability-contract.test.ts +13 -1
  396. package/tests/native-review-cli.test.ts +239 -118
  397. package/tests/native-review-consent.test.ts +2 -2
  398. package/tests/native-review-parity-runtime.test.ts +101 -4
  399. package/tests/native-review-parity.test.ts +12 -48
  400. package/tests/native-risk-evidence-parity.test.ts +21 -0
  401. package/tests/no-self-pause-contract.test.ts +87 -0
  402. package/tests/notification-audio-native.test.ts +281 -0
  403. package/tests/notification-audio-windows.test.ts +581 -0
  404. package/tests/notification-audio.test.ts +548 -0
  405. package/tests/notification-customize.test.ts +655 -0
  406. package/tests/notification-events.test.ts +115 -0
  407. package/tests/notification-policy.test.ts +201 -0
  408. package/tests/notification-pulse-client.test.ts +356 -0
  409. package/tests/notification-pulse-protocol.test.ts +213 -0
  410. package/tests/notification-pulse-stream.test.ts +320 -0
  411. package/tests/notification-scheduler.test.ts +258 -0
  412. package/tests/notification-ui.test.ts +157 -0
  413. package/tests/npm-pack-result.test.ts +41 -0
  414. package/tests/odd-integration.test.ts +76 -0
  415. package/tests/odd-phase-inference.test.ts +213 -0
  416. package/tests/odd-phase-loader.test.ts +253 -0
  417. package/tests/odd-phase.test.ts +307 -0
  418. package/tests/odd-routing-canonical-ratchet.test.ts +60 -14
  419. package/tests/odd-routing-contract.test.ts +158 -50
  420. package/tests/orchestrator-budget.test.ts +43 -45
  421. package/tests/orchestrator-catalog.test.ts +146 -0
  422. package/tests/orchestrator-consultation-sdk.test.ts +614 -0
  423. package/tests/orchestrator-consultation.test.ts +111 -0
  424. package/tests/orchestrator-discovery.test.ts +241 -0
  425. package/tests/orchestrator-helper-consent.test.ts +171 -0
  426. package/tests/orchestrator-helper.test.ts +297 -0
  427. package/tests/orchestrator-message-card.test.ts +190 -0
  428. package/tests/orchestrator-presence.test.ts +23 -0
  429. package/tests/orchestrator-rdd-ownership.test.ts +8 -7
  430. package/tests/orchestrator-scope.test.ts +107 -0
  431. package/tests/orchestrator-state.test.ts +114 -0
  432. package/tests/orchestrator-status-publication-contract.test.ts +34 -0
  433. package/tests/orchestrator-tool-card.test.ts +398 -0
  434. package/tests/orchestrator-work-search.test.ts +318 -0
  435. package/tests/orchestrator-work.test.ts +147 -0
  436. package/tests/overlay-repaint.test.ts +74 -0
  437. package/tests/package-manifest.test.ts +344 -123
  438. package/tests/packed-runner-owned-path.test.ts +80 -0
  439. package/tests/parallel-reason-contract.test.ts +21 -0
  440. package/tests/parallel-writer-admission.test.ts +210 -0
  441. package/tests/permission-mode-policy.test.ts +194 -0
  442. package/tests/persona-single-channel.test.ts +10 -18
  443. package/tests/pi-pretty.test.ts +56 -0
  444. package/tests/provider-defect-handoff.test.ts +7 -14
  445. package/tests/quiet-bash-runtime.test.ts +76 -0
  446. package/tests/quiet-tool-rendering.test.ts +569 -187
  447. package/tests/rdd-aware-verification-contract.test.ts +103 -40
  448. package/tests/rdd-status-line.test.ts +9 -4
  449. package/tests/render-cache-performance.test.ts +48 -0
  450. package/tests/resume-hint-extension.test.ts +122 -0
  451. package/tests/review-agent-end-preflight.test.ts +237 -21
  452. package/tests/review-candidate-owner-retry.test.ts +22 -1
  453. package/tests/review-candidate-view.test.ts +399 -12
  454. package/tests/review-contract-prompt.test.ts +138 -44
  455. package/tests/review-controller-lock-status.test.ts +0 -1
  456. package/tests/review-controller-native-recovery.test.ts +327 -17
  457. package/tests/review-controller-native-routing.test.ts +866 -6
  458. package/tests/review-controller-workspace-root.test.ts +206 -6
  459. package/tests/review-controller.test.ts +2 -2
  460. package/tests/review-correction-lifecycle.test.ts +2 -1
  461. package/tests/review-gate.test.ts +7 -4
  462. package/tests/review-graph-schema.test.ts +2 -2
  463. package/tests/review-host-relay-routing.test.ts +342 -4
  464. package/tests/review-host-relay.test.ts +49 -2
  465. package/tests/review-integration-v2-forward.test.ts +221 -1
  466. package/tests/review-json-arguments.test.ts +167 -0
  467. package/tests/review-last-event-closure.test.ts +157 -8
  468. package/tests/review-ledger-contract.test.ts +14 -37
  469. package/tests/review-policy-judgment-day.test.ts +1 -1
  470. package/tests/review-policy-ordinary.test.ts +2 -2
  471. package/tests/review-relay-transport-agent.test.ts +66 -1
  472. package/tests/review-reminder-receipt.test.ts +47 -1
  473. package/tests/review-risk-assessment.test.ts +594 -26
  474. package/tests/review-session-standing-permission-controller.test.ts +16 -17
  475. package/tests/review-session-standing-permission-ipc.test.ts +6 -5
  476. package/tests/review-sidebar-state.test.ts +402 -0
  477. package/tests/review-test-fixtures.ts +7 -1
  478. package/tests/review-transaction.test.ts +18 -1
  479. package/tests/run-test-suite.test.ts +202 -0
  480. package/tests/runtime-harness.mjs +186 -800
  481. package/tests/runtime-metrics-children.test.ts +16 -23
  482. package/tests/runtime-metrics-extension.test.ts +1 -1
  483. package/tests/runtime-metrics-native.test.ts +31 -1
  484. package/tests/selection-engine.test.ts +451 -0
  485. package/tests/session-change-capture.test.ts +10 -10
  486. package/tests/session-changes-shell.test.ts +242 -48
  487. package/tests/session-changes.test.ts +17 -24
  488. package/tests/session-messaging-grants.test.ts +255 -0
  489. package/tests/session-profile-binding.test.ts +163 -0
  490. package/tests/session-profile-freeze.test.ts +381 -0
  491. package/tests/session-subject-routing-contract.test.ts +34 -0
  492. package/tests/session-worktree-registry.test.ts +134 -2
  493. package/tests/shell-bar.test.ts +422 -1
  494. package/tests/shell-card.test.ts +353 -1
  495. package/tests/shell-changes-view.test.ts +135 -32
  496. package/tests/shell-changes.test.ts +61 -62
  497. package/tests/shell-prompt.test.ts +94 -2
  498. package/tests/shell-render-cache-runtime.test.ts +69 -0
  499. package/tests/shell-render-cache.test.ts +129 -0
  500. package/tests/shell-sidebar-layout.test.ts +351 -22
  501. package/tests/shell-sidebar-scroll-benchmark.test.ts +255 -0
  502. package/tests/shell-todo.test.ts +207 -3
  503. package/tests/shell-usage-view.test.ts +28 -1
  504. package/tests/shell-usage.test.ts +108 -0
  505. package/tests/short-check-routing-contract.test.ts +49 -0
  506. package/tests/skill-registry-watcher.test.ts +119 -0
  507. package/tests/skill-registry.test.ts +357 -2
  508. package/tests/startup-banner.test.ts +246 -4
  509. package/tests/startup-home-repo.test.ts +170 -0
  510. package/tests/stats-collector.test.ts +195 -0
  511. package/tests/stats-view.test.ts +202 -0
  512. package/tests/status-timing-diagnostics.test.ts +283 -0
  513. package/tests/support/orchestrator-modules.ts +19 -0
  514. package/tests/task-size-routing-contract.test.ts +241 -0
  515. package/tests/telemetry-trigger.test.ts +107 -20
  516. package/tests/test-strength-contract.test.ts +124 -0
  517. package/tests/theme-customization.test.ts +72 -0
  518. package/tests/verify-agent-contract.test.ts +90 -0
  519. package/tests/verify-first-launch-contract.test.ts +139 -0
  520. package/tests/verify-package-files.test.ts +56 -0
  521. package/tests/vim-editor-adapter-host-resolution.test.ts +37 -0
  522. package/tests/vim-editor-adapter.test.ts +804 -0
  523. package/tests/vim-normal-engine.test.ts +101 -0
  524. package/tests/vim-operator-engine.test.ts +215 -0
  525. package/tests/vim-policy.test.ts +19 -0
  526. package/tests/vim-visual-engine.test.ts +52 -0
  527. package/tests/visual-customization-policy.test.ts +139 -0
  528. package/tests/visual-customize-view.test.ts +696 -0
  529. package/tests/visual-profiles.test.ts +87 -0
  530. package/tests/writer-edit-surface-scope.test.ts +59 -2
  531. package/tests/writer-surfaces.test.ts +199 -0
  532. package/tests/yolo-customize.test.ts +271 -0
  533. package/tests/yolo-mode-runtime.test.ts +188 -0
  534. package/tests/yolo-mode.test.ts +289 -0
  535. package/tests/yolo-session-policy.test.ts +59 -0
  536. package/themes/Gentle.json +2 -1
  537. package/themes/Gentleman-Cute.json +2 -1
  538. package/themes/Gentleman-Sexy.json +2 -1
  539. package/assets/agents/sdd-apply.md +0 -159
  540. package/assets/agents/sdd-archive.md +0 -228
  541. package/assets/agents/sdd-design.md +0 -49
  542. package/assets/agents/sdd-explore.md +0 -48
  543. package/assets/agents/sdd-init.md +0 -56
  544. package/assets/agents/sdd-onboard.md +0 -52
  545. package/assets/agents/sdd-proposal.md +0 -64
  546. package/assets/agents/sdd-remediate.md +0 -37
  547. package/assets/agents/sdd-research.md +0 -49
  548. package/assets/agents/sdd-spec.md +0 -192
  549. package/assets/agents/sdd-status.md +0 -54
  550. package/assets/agents/sdd-tasks.md +0 -108
  551. package/assets/agents/sdd-verify.md +0 -124
  552. package/assets/chains/sdd-full.chain.md +0 -83
  553. package/assets/chains/sdd-plan.chain.md +0 -56
  554. package/assets/chains/sdd-verify.chain.md +0 -43
  555. package/assets/sdd-orchestrator-workflow.md +0 -319
  556. package/assets/support/sdd-status-contract.md +0 -77
  557. package/docs/assets/diagrams/sdd-cycle.svg +0 -14
  558. package/extensions/sdd-init.ts +0 -816
  559. package/lib/openspec-deltas.ts +0 -156
  560. package/lib/sdd-preflight.ts +0 -1066
  561. package/lib/sdd-research-capabilities.ts +0 -94
  562. package/lib/sdd-status.ts +0 -26
  563. package/tests/fixtures/legacy/sdd-research-v2.5.0.md +0 -54
  564. package/tests/fixtures/native-review-cli/v2.1.3/bind-sdd.json +0 -25
  565. package/tests/fixtures/v0.10.7/assets/agents/sdd-apply.md +0 -132
  566. package/tests/openspec-deltas.test.ts +0 -209
  567. package/tests/sdd-agent-tools.test.ts +0 -156
  568. package/tests/sdd-archive-replay.test.ts +0 -82
  569. package/tests/sdd-classical-continuation.test.ts +0 -74
  570. package/tests/sdd-execution-routing-contract.test.ts +0 -44
  571. package/tests/sdd-managed-runtime-settlement.test.ts +0 -155
  572. package/tests/sdd-native-managed-uptake.test.ts +0 -243
  573. package/tests/sdd-no-attempts-contract.test.ts +0 -15
  574. package/tests/sdd-odd-integration.test.ts +0 -33
  575. package/tests/sdd-optional-research.test.ts +0 -124
  576. package/tests/sdd-planning-routing-contract.test.ts +0 -45
  577. package/tests/sdd-preflight-rpc-input.test.ts +0 -125
  578. package/tests/sdd-preflight.test.ts +0 -541
  579. package/tests/sdd-research-capabilities.test.ts +0 -114
  580. package/tests/sdd-research-live.test.ts +0 -241
  581. package/tests/sdd-selection-transport.test.ts +0 -653
  582. package/tests/sdd-status.test.ts +0 -9
  583. package/tests/sdd-task-truth.test.ts +0 -43
  584. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/README.md +0 -0
  585. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/lens.schema.json +0 -0
  586. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/refuter.schema.json +0 -0
  587. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/schemas/targeted-validator.schema.json +0 -0
  588. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/lens.json +0 -0
  589. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/refuter.json +0 -0
  590. /package/contracts/review-provider-contract-mirror/{v1.2.0 → v1.3.0}/bundle/vectors/targeted-validator.json +0 -0
@@ -0,0 +1,231 @@
1
+ // Deterministic ODD phase inference from observed tool activity. The Gentle
2
+ // Shell working label used to depend only on the model calling
3
+ // gentle_odd_phase, which models routinely skip; the tools the primary
4
+ // session actually runs are a reliable, observable signal instead.
5
+ //
6
+ // The mapping is deliberately conservative: an unknown tool, unknown subagent
7
+ // role, or an ambiguous shell command returns undefined so the caller leaves
8
+ // the current label unchanged. This module is pure (no Pi or registry
9
+ // imports); precedence against explicit reports lives in OddPhaseRegistry.
10
+
11
+ import type { OddPhase } from "./odd-phase.ts";
12
+
13
+ const TOOL_PHASES: Readonly<Record<string, OddPhase>> = {
14
+ read: "exploring",
15
+ grep: "exploring",
16
+ find: "exploring",
17
+ ls: "exploring",
18
+ codegraph: "exploring",
19
+ ask_user_choice: "deciding",
20
+ ask_user_question: "deciding",
21
+ todo: "planning",
22
+ gentle_review: "checking",
23
+ gentle_review_scope: "checking",
24
+ gentle_review_capture: "checking",
25
+ gentle_review_capture_group: "checking",
26
+ };
27
+
28
+ const DELEGATED_PHASES: Readonly<Record<string, OddPhase>> = {
29
+ "gentle-ai-worker": "implementing",
30
+ "gentle-ai-verify": "checking",
31
+ "gentle-ai-explore": "exploring",
32
+ };
33
+
34
+ const WRITE_TOOLS = new Set(["edit", "write"]);
35
+ const SHELL_TOOLS = new Set(["bash", "powershell"]);
36
+
37
+ // Test, typecheck, lint, and build runners. Matched per shell segment.
38
+ const CHECKING_COMMANDS: readonly RegExp[] = [
39
+ /^(pnpm|npm|yarn|bun)\s+(run\s+)?(test|typecheck|lint|build|check)\b/,
40
+ /^node\b.*\s--test\b/,
41
+ /^node\s+\S*(test|check|lint|typecheck)[\w-]*\.m?[jt]s\b/,
42
+ /^((npx|pnpx|bunx)\s+|(pnpm|yarn)\s+exec\s+)?(vitest|jest|mocha|tsc|eslint|pytest)\b/,
43
+ /^python3?\s+-m\s+pytest\b/,
44
+ /^go\s+(test|build|vet)\b/,
45
+ /^cargo\s+(test|build|check|clippy)\b/,
46
+ /^make\b/,
47
+ // Waiting on or reading CI results verifies work; it is not exploration.
48
+ /^gh\s+pr\s+checks\b/,
49
+ /^gh\s+run\s+(view|watch|list)\b/,
50
+ ];
51
+
52
+ // Git listing flags that may take one non-flag argument (`--contains <rev>`)
53
+ // without turning `git branch`/`git tag` into a create.
54
+ const GIT_LIST_FLAG = String.raw`(\s+(-a|-r|-l|-v|-vv|-n\d*|--all|--remotes|--list|--show-current|--contains|--merged|--no-merged|--points-at|--sort=\S+)(\s+[^-\s]\S*)?)*$`;
55
+
56
+ // Read-only inspection. Matched per shell segment.
57
+ const EXPLORING_COMMANDS: readonly RegExp[] = [
58
+ /^git\s+(status|log|diff|show|blame|rev-parse|rev-list|merge-base|ls-files|ls-remote|ls-tree|cat-file|describe|shortlog|reflog|grep|check-ignore|for-each-ref|name-rev|count-objects)\b/,
59
+ new RegExp(String.raw`^git\s+(branch|tag)${GIT_LIST_FLAG}`),
60
+ /^git\s+remote(\s+(-v|--verbose|show|get-url)\b.*)?$/,
61
+ /^git\s+(worktree|stash)\s+list\b/,
62
+ /^git\s+config\s+(--get|--get-all|--get-regexp|-l|--list)\b/,
63
+ /^gh\s+(pr|issue|repo|release)\s+(view|list|diff|status)\b/,
64
+ /^(ls|cat|head|tail|grep|egrep|rg|wc|pwd|tree|stat|file|which|type|readlink|realpath|dirname|basename|cut|uniq|tr|nl|column|strings|diff|cmp|od|xxd|hexdump|shasum|sha256sum|md5|md5sum|du|df|date|uname|whoami|ps|pgrep|lsof|jq|awk)\b/,
65
+ /^command\s+-v\b/,
66
+ /^(\[\[?|test)\s/,
67
+ /^sort\b(?!.*\s(-o|--output)\b)/,
68
+ /^sed\b(?!.*\s(-[a-zA-Z]*i|--in-place)\b)/,
69
+ /^find\b(?!.*\s-(delete|exec|execdir|ok|okdir|fprint\w*)\b)/,
70
+ ];
71
+
72
+ // Segments that neither inspect nor change anything worth labeling: shell
73
+ // control words, no-op builtins, and bare variable assignments (their
74
+ // command substitutions are classified as commands of their own).
75
+ const NEUTRAL_COMMANDS: readonly RegExp[] = [
76
+ /^(cd|pushd|popd|echo|printf|sleep|true|false|export|set)\b/,
77
+ /^(done|fi|esac|\}|\))$/,
78
+ /^for\s+\w+(\s+in\b.*)?$/,
79
+ // Each assignment must end at whitespace or the end: an optional separator
80
+ // would let `a=a=a=…` split anywhere and backtrack exponentially.
81
+ /^([A-Za-z_][A-Za-z0-9_]*=("[^"]*"|'[^']*'|[^\s"'])*(\s+|$))+$/,
82
+ ];
83
+
84
+ // Shell keywords that prefix the command they introduce (`do grep x`).
85
+ const LEADING_KEYWORDS = /^(do|then|else|elif|if|while|until|time|!|\{|\()\s+/;
86
+
87
+ /**
88
+ * Infers the ODD phase implied by a tool call, or undefined when the call
89
+ * carries no reliable phase signal and the label must stay as it is.
90
+ */
91
+ export function inferOddPhase(toolName: string, args: unknown): OddPhase | undefined {
92
+ const name = normalizeToolName(toolName);
93
+ if (name === "subagent_run") {
94
+ const agent = stringArg(args, "agent");
95
+ return agent && Object.hasOwn(DELEGATED_PHASES, agent) ? DELEGATED_PHASES[agent] : undefined;
96
+ }
97
+ if (WRITE_TOOLS.has(name)) return isOddTaskPath(stringArg(args, "path") ?? stringArg(args, "file_path")) ? "planning" : "implementing";
98
+ if (SHELL_TOOLS.has(name)) return inferShellPhase(stringArg(args, "command"));
99
+ return Object.hasOwn(TOOL_PHASES, name) ? TOOL_PHASES[name] : undefined;
100
+ }
101
+
102
+ // Some runtimes expose tools through an MCP proxy as `mcp__<server>__<tool>`.
103
+ function normalizeToolName(toolName: string): string {
104
+ return toolName.replace(/^mcp__.+?__/, "");
105
+ }
106
+
107
+ function stringArg(args: unknown, key: string): string | undefined {
108
+ if (typeof args !== "object" || args === null) return undefined;
109
+ const value = (args as Record<string, unknown>)[key];
110
+ return typeof value === "string" ? value : undefined;
111
+ }
112
+
113
+ function isOddTaskPath(path: string | undefined): boolean {
114
+ return path !== undefined && /(^|\/)odd\/tasks\//.test(path.replaceAll("\\", "/"));
115
+ }
116
+
117
+ /**
118
+ * A command checks when any segment runs a checker; it explores only when
119
+ * every segment is read-only inspection (or neutral like `cd`). Anything
120
+ * else — a mutation, an install, an unknown program — is ambiguous.
121
+ */
122
+ function inferShellPhase(command: string | undefined): OddPhase | undefined {
123
+ if (command === undefined) return undefined;
124
+ const segments = splitShellCommands(command)
125
+ .map(stripSegment)
126
+ .filter((segment) => segment.length > 0 && !isNeutral(segment));
127
+ if (segments.length === 0) return undefined;
128
+ if (segments.some((segment) => matchesAny(CHECKING_COMMANDS, segment))) return "checking";
129
+ return segments.every(isReadOnlyInspection) ? "exploring" : undefined;
130
+ }
131
+
132
+ /**
133
+ * Splits a shell command into simple commands. Top-level `&&`, `||`, `;`,
134
+ * `|`, `&`, and newlines separate them; quoted text never does; and every
135
+ * `$(...)` substitution contributes its own commands, leaving a `$_`
136
+ * placeholder behind. Not a full shell parser: whatever it cannot follow
137
+ * lands in a segment that matches no pattern, so the label stays unchanged.
138
+ */
139
+ function splitShellCommands(command: string): string[] {
140
+ const commands: string[] = [];
141
+ let current = "";
142
+ let quote: "'" | '"' | undefined;
143
+ for (let i = 0; i < command.length; i++) {
144
+ const char = command[i];
145
+ if (quote === "'") {
146
+ current += char;
147
+ if (char === "'") quote = undefined;
148
+ } else if (char === "\\") {
149
+ current += char + (command[i + 1] ?? "");
150
+ i++;
151
+ } else if (char === "$" && command[i + 1] === "(") {
152
+ const end = findSubstitutionEnd(command, i + 2);
153
+ if (end < 0) return [...commands, current + command.slice(i)];
154
+ commands.push(...splitShellCommands(command.slice(i + 2, end)));
155
+ current += "$_";
156
+ i = end;
157
+ } else if (quote === '"') {
158
+ current += char;
159
+ if (char === '"') quote = undefined;
160
+ } else if (char === "'" || char === '"') {
161
+ quote = char;
162
+ current += char;
163
+ } else if (isSeparator(command, i)) {
164
+ commands.push(current);
165
+ current = "";
166
+ if ((char === "|" || char === "&") && command[i + 1] === char) i++;
167
+ } else {
168
+ current += char;
169
+ }
170
+ }
171
+ commands.push(current);
172
+ return commands;
173
+ }
174
+
175
+ // `&` inside a redirection (`2>&1`, `&>/dev/null`) is not a separator.
176
+ function isSeparator(command: string, i: number): boolean {
177
+ const char = command[i];
178
+ if (char === "&") return command[i - 1] !== ">" && command[i + 1] !== ">";
179
+ return char === ";" || char === "|" || char === "\n";
180
+ }
181
+
182
+ // Index of the `)` closing a `$(` whose body starts at `start`, or -1.
183
+ function findSubstitutionEnd(command: string, start: number): number {
184
+ let depth = 1;
185
+ let quote: "'" | '"' | undefined;
186
+ for (let i = start; i < command.length; i++) {
187
+ const char = command[i];
188
+ if (quote) {
189
+ if (char === quote) quote = undefined;
190
+ else if (char === "\\" && quote === '"') i++;
191
+ } else if (char === "\\") i++;
192
+ else if (char === "'" || char === '"') quote = char;
193
+ else if (char === "(") depth++;
194
+ else if (char === ")" && --depth === 0) return i;
195
+ }
196
+ return -1;
197
+ }
198
+
199
+ // Drops harmless redirections (`2>&1`, `2>/dev/null`), leading shell
200
+ // keywords (`do grep x`), environment assignments (`CI=1 pnpm test`), git
201
+ // global options (`git -C dir status`), and whitespace. A bare assignment
202
+ // is left intact for NEUTRAL_COMMANDS.
203
+ function stripSegment(segment: string): string {
204
+ let stripped = segment.replace(HARMLESS_REDIRECTS, "").trim();
205
+ while (LEADING_KEYWORDS.test(stripped)) stripped = stripped.replace(LEADING_KEYWORDS, "");
206
+ return stripped
207
+ .replace(/^([A-Za-z_][A-Za-z0-9_]*=("[^"]*"|'[^']*'|[^\s"'])*\s+)+(?=\S)/, "")
208
+ .replace(/^git\s+((-C|-c)\s+\S+\s+|--no-pager\s+)+/, "git ");
209
+ }
210
+
211
+ // A segment that writes a file (`echo x > out`) is never a no-op.
212
+ function isNeutral(segment: string): boolean {
213
+ return !writesFile(segment) && matchesAny(NEUTRAL_COMMANDS, segment);
214
+ }
215
+
216
+ function isReadOnlyInspection(segment: string): boolean {
217
+ return !writesFile(segment) && matchesAny(EXPLORING_COMMANDS, segment);
218
+ }
219
+
220
+ // Stream merges and discarded output; neither writes a file.
221
+ const HARMLESS_REDIRECTS = /\s*(\d?>&\d|&?\d?>\s*\/dev\/null)/g;
222
+
223
+ // Output redirection to a file is a write. Deliberately naive: a `>` inside
224
+ // quoted text also counts, which only ever leaves the label unchanged.
225
+ function writesFile(segment: string): boolean {
226
+ return segment.replace(HARMLESS_REDIRECTS, "").includes(">");
227
+ }
228
+
229
+ function matchesAny(patterns: readonly RegExp[], segment: string): boolean {
230
+ return patterns.some((pattern) => pattern.test(segment));
231
+ }
@@ -0,0 +1,141 @@
1
+ // Bounded ODD phase signal for the Gentle prompt's working label. There is
2
+ // no Pi runtime event for ODD phases, so the label comes from two sources:
3
+ // phases inferred deterministically from the primary session's tool activity
4
+ // (lib/odd-phase-inference.ts, wired in extensions/gentle-shell.ts) and
5
+ // explicit reports by the orchestrator through gentle_odd_phase, which
6
+ // refine it with phases tools cannot show (see
7
+ // assets/orchestrator-tracking.md). Never inferred from assistant prose;
8
+ // the prompt falls back to the generic "working…" label when nothing applies.
9
+
10
+ // Covers the ODD protocol steps in AGENTS.md (1 authorize .. 7 close); not a
11
+ // strict one-to-one mapping. "researching"/"deciding" both cover step 3
12
+ // (resolve uncertainty), and "deciding" also covers step 4's classify
13
+ // decision (no dedicated label: classifying is typically an instantaneous
14
+ // internal decision, not standalone visible work). "checking"/"closing"
15
+ // both cover step 7 (close).
16
+ export const ODD_PHASES = [
17
+ "authorizing",
18
+ "exploring",
19
+ "researching",
20
+ "deciding",
21
+ "planning",
22
+ "implementing",
23
+ "checking",
24
+ "closing",
25
+ ] as const;
26
+
27
+ export type OddPhase = (typeof ODD_PHASES)[number];
28
+
29
+ export function isOddPhase(value: unknown): value is OddPhase {
30
+ return typeof value === "string" && (ODD_PHASES as readonly string[]).includes(value);
31
+ }
32
+
33
+ export function oddPhaseLabel(phase: OddPhase): string {
34
+ return `${phase}…`;
35
+ }
36
+
37
+ /** Who set a session's phase: the orchestrator through gentle_odd_phase, or tool-activity inference. */
38
+ export type OddPhaseSource = "explicit" | "inferred";
39
+
40
+ interface ReportedOddPhase {
41
+ phase: OddPhase;
42
+ source: OddPhaseSource;
43
+ }
44
+
45
+ /**
46
+ * Session-scoped, best-effort ODD phase signal. Keyed by Pi session id so a
47
+ * background/child agent — which runs as its own OS process with its own
48
+ * module state (see lib/agents-runner.ts) — can never see or override the
49
+ * primary session's reported phase. The owning extension clears a session's
50
+ * entry at turn and session boundaries (agent_start, agent_settled,
51
+ * session_shutdown) so a stale phase from a finished turn never leaks into
52
+ * the next one.
53
+ */
54
+ export class OddPhaseRegistry {
55
+ private readonly phases = new Map<string, ReportedOddPhase>();
56
+ // The WORKING-state pulse loop does not run at all under the "potato"
57
+ // animation policy (see GentlePromptEditor.startPulse), and workingLabel
58
+ // is otherwise only read on the next incidental render. Pi's own docs
59
+ // (docs/tui.md) instruct components to call requestRender() themselves
60
+ // after a state change; no host re-render is implicitly guaranteed. The
61
+ // owning prompt registers its own redraw callback here so a phase change
62
+ // is visible immediately regardless of animation policy.
63
+ private readonly renderRequests = new Map<string, () => void>();
64
+
65
+ /**
66
+ * Sets the current ODD phase for a session. Only accepts an
67
+ * already-validated OddPhase; callers must validate untrusted input
68
+ * with isOddPhase() (or use clear() for the explicit "clear" token)
69
+ * before calling this. Returns undefined, without mutating any state or
70
+ * requesting a redraw, when there is no session id to scope the report
71
+ * to. The source defaults to "explicit"; tool-activity inference goes
72
+ * through infer() so its precedence rule applies.
73
+ */
74
+ report(sessionId: string | undefined, phase: OddPhase, source: OddPhaseSource = "explicit"): OddPhase | undefined {
75
+ if (!sessionId) return undefined;
76
+ this.phases.set(sessionId, { phase, source });
77
+ this.renderRequests.get(sessionId)?.();
78
+ return phase;
79
+ }
80
+
81
+ /**
82
+ * Applies a phase inferred from tool activity and returns the session's
83
+ * resulting phase. An explicit report is not overridden by an inferred
84
+ * "exploring" (read-only tools are routine while researching or
85
+ * deciding). Incidental reads and planning bookkeeping also cannot
86
+ * displace active implementation/checking. A known delegated exploration
87
+ * is a deliberate transition, not an incidental read; later edits and
88
+ * checks can still move between active work phases. Inferring the phase
89
+ * already shown changes nothing,
90
+ * keeps an explicit source explicit, and never requests a redraw, so
91
+ * repeated tool calls do not repaint the prompt.
92
+ */
93
+ infer(sessionId: string | undefined, phase: OddPhase, cause: "tool" | "delegation" = "tool"): OddPhase | undefined {
94
+ if (!sessionId) return undefined;
95
+ const current = this.phases.get(sessionId);
96
+ if (current?.phase === phase) return phase;
97
+ if (phase === "exploring" && cause !== "delegation" &&
98
+ (current?.source === "explicit" || current?.phase === "implementing" || current?.phase === "checking")) return current?.phase;
99
+ if (phase === "planning" && (current?.phase === "implementing" || current?.phase === "checking")) return current.phase;
100
+ return this.report(sessionId, phase, "inferred");
101
+ }
102
+
103
+ /**
104
+ * Clears a session's reported phase (turn/session boundary, or the
105
+ * explicit "clear" token). An invalid/unrecognized phase report is
106
+ * never a reason to clear: only this method resets the session, so a
107
+ * malformed report leaves the previously reported phase in place.
108
+ * Clearing also drops the report's source.
109
+ */
110
+ clear(sessionId: string | undefined): void {
111
+ if (!sessionId) return;
112
+ if (this.phases.delete(sessionId)) this.renderRequests.get(sessionId)?.();
113
+ }
114
+
115
+ get(sessionId: string | undefined): OddPhase | undefined {
116
+ return sessionId ? this.phases.get(sessionId)?.phase : undefined;
117
+ }
118
+
119
+ /** Working label for the reported phase, or undefined to fall back to the generic "working…" label. */
120
+ label(sessionId: string | undefined): string | undefined {
121
+ const phase = this.get(sessionId);
122
+ return phase ? oddPhaseLabel(phase) : undefined;
123
+ }
124
+
125
+ /** Registers (replacing any previous registration) the callback used to request an immediate redraw when this session's phase changes. */
126
+ setRenderRequest(sessionId: string | undefined, requestRender: () => void): void {
127
+ if (sessionId) this.renderRequests.set(sessionId, requestRender);
128
+ }
129
+
130
+ clearRenderRequest(sessionId: string | undefined): void {
131
+ if (sessionId) this.renderRequests.delete(sessionId);
132
+ }
133
+ }
134
+
135
+ // Pi loads extensions with separate jiti moduleCache:false loaders, so a
136
+ // module-local singleton is duplicated. The global symbol bridges only those
137
+ // loaders inside this process; subagents run in separate OS processes. Keep
138
+ // the session key inside the registry and clear it at turn/session boundaries.
139
+ const ODD_PHASE_REGISTRY = Symbol.for("gentle-pi.odd-phase-registry");
140
+ const processState = globalThis as typeof globalThis & { [ODD_PHASE_REGISTRY]?: OddPhaseRegistry };
141
+ export const oddPhaseRegistry = processState[ODD_PHASE_REGISTRY] ??= new OddPhaseRegistry();
@@ -0,0 +1,20 @@
1
+ import { stripVTControlCharacters } from "node:util";
2
+
3
+ export interface TaskAliases { initialAlias: string | null; currentAlias: string | null }
4
+ /** Explicit public topic only; never infer from names or private conversation. */
5
+ export function normalizeTaskSubject(value: unknown): string | undefined {
6
+ if (value === undefined) return undefined;
7
+ if (typeof value !== "string" || /\p{Cs}/u.test(value)) throw new Error("invalid-task-subject");
8
+ const clean = stripVTControlCharacters(value).replace(/[\p{Cc}\p{Cf}]/gu, " ").replace(/\s+/g, " ").trim();
9
+ return Array.from(clean).slice(0, 120).join("").trimEnd() || undefined;
10
+ }
11
+ export function decodeTaskAliases(value: unknown): TaskAliases | undefined {
12
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
13
+ const input = value as Record<string, unknown>;
14
+ const valid = (v: unknown): v is string => typeof v === "string" && !!v && !/[\p{Cc}\p{Cf}\p{Cs}]/u.test(v)
15
+ && Array.from(v).length <= 120 && normalizeTaskSubject(v) === v;
16
+ if (Object.keys(input).length !== 2 || !Object.hasOwn(input, "initialAlias") || !Object.hasOwn(input, "currentAlias")
17
+ || !(input.initialAlias === null || valid(input.initialAlias))
18
+ || !(valid(input.currentAlias) || (input.currentAlias === null && input.initialAlias === null))) return undefined;
19
+ return { initialAlias: input.initialAlias as string | null, currentAlias: input.currentAlias as string | null };
20
+ }
@@ -0,0 +1,79 @@
1
+ import { createHash } from "node:crypto";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { boundedRead, rootFor, sanitizeDisplayLabel, type Header } from "./orchestrator-presence.ts";
4
+
5
+ // Private bounded derivative, never an identity/authority registry. Each request
6
+ // reads at most one 64 KiB snapshot and returns eight entries of each category.
7
+ export const CATALOG_BYTES = 64 * 1024;
8
+ export const CATALOG_ENTRIES = 64;
9
+ export const CATALOG_PAGES = 8;
10
+ type Task = { id: string; label: string; status: string; cwd: string | null };
11
+ type Catalog = { tasks: Task[]; registered: (string | null)[]; omittedTasks: number; omittedRegistered: number };
12
+ export type CatalogPage = Catalog & { cursor?: string };
13
+ const hash = (bytes: string | Buffer) => createHash("sha256").update(bytes).digest("hex");
14
+ const safePath = (path: string) => isAbsolute(path) && Buffer.byteLength(path) <= 256
15
+ && !/[\p{Cc}\p{Cf}\u00a0\u2000-\u200a\u202f\u205f\u3000]/u.test(path);
16
+ const pathFact = (path: string) => safePath(path) ? path : null;
17
+ const exact = (value: any, keys: string[]) => value && typeof value === "object" && !Array.isArray(value)
18
+ && Object.keys(value).length === keys.length && keys.every(k => Object.hasOwn(value, k));
19
+ const count = (n: unknown) => Number.isSafeInteger(n) && (n as number) >= 0;
20
+ const text = (s: unknown) => typeof s === "string" && s === sanitizeDisplayLabel(s);
21
+ const path = (s: unknown) => s === null || typeof s === "string" && safePath(s);
22
+ function valid(c: any): c is Catalog {
23
+ return exact(c, ["tasks", "registered", "omittedTasks", "omittedRegistered"])
24
+ && count(c.omittedTasks) && count(c.omittedRegistered)
25
+ && Array.isArray(c.tasks) && c.tasks.length <= CATALOG_ENTRIES
26
+ && c.tasks.every((t: any) => exact(t, ["id", "label", "status", "cwd"])
27
+ && text(t.id) && text(t.label) && ["running", "queued", "waiting"].includes(t.status) && path(t.cwd))
28
+ && Array.isArray(c.registered) && c.registered.length <= CATALOG_ENTRIES && c.registered.every(path);
29
+ }
30
+ /** Explicit whitelist; eagerly detached, no task spreads or thread access. */
31
+ export function projectCatalog(tasks: readonly { id: string; label: string; status: string; cwd: string }[], roots: readonly string[]): Catalog {
32
+ const c: Catalog = { tasks: [], registered: [], omittedTasks: tasks.length, omittedRegistered: roots.length };
33
+ // Reserve envelope space; byte overflow is an exact omission, not truncation.
34
+ for (let i = 0; i < Math.min(CATALOG_ENTRIES, Math.max(tasks.length, roots.length)); i++) {
35
+ if (i < tasks.length) {
36
+ const t = tasks[i];
37
+ c.tasks.push({ id: sanitizeDisplayLabel(t.id), label: sanitizeDisplayLabel(t.label), status: t.status, cwd: pathFact(t.cwd) });
38
+ if (Buffer.byteLength(JSON.stringify(c)) > CATALOG_BYTES - 1024) c.tasks.pop();
39
+ else c.omittedTasks--;
40
+ }
41
+ if (i < roots.length) {
42
+ c.registered.push(pathFact(roots[i]));
43
+ if (Buffer.byteLength(JSON.stringify(c)) > CATALOG_BYTES - 1024) c.registered.pop();
44
+ else c.omittedRegistered--;
45
+ }
46
+ }
47
+ if (!valid(c)) throw new Error("malformed-catalog");
48
+ return c;
49
+ }
50
+ /** Cursor binds only public catalog fields and producer activation, not private
51
+ * activity churn. Current envelope/header generation must still agree on every
52
+ * read; no cached pages, paths or authority are carried by the cursor. */
53
+ export function readCatalog(profile: string, h: Header, activation: string, cursor?: string): { page?: CatalogPage; unavailable?: string } {
54
+ try {
55
+ const bytes = boundedRead(join(rootFor(profile, false, true), `${h.sessionHash}.${h.incarnation}.json`), CATALOG_BYTES);
56
+ const value = JSON.parse(bytes.toString("utf8"));
57
+ if (!exact(value, ["schema", "sessionHash", "incarnation", "generation", "activation", "tokens", "catalog"])
58
+ || value.schema !== 1 || value.sessionHash !== h.sessionHash || value.incarnation !== h.incarnation
59
+ || value.generation !== h.generation || value.activation !== activation || !valid(value.catalog)
60
+ || !Array.isArray(value.tokens) || value.tokens.length !== 7 || !value.tokens.every((s: unknown) => typeof s === "string" && /^[a-f0-9-]{36}$/.test(s))) throw new Error("unavailable");
61
+ const c: Catalog = value.catalog;
62
+ const publicBytes = JSON.stringify({ tasks: c.tasks.map(t => ({ id: t.id, label: t.label, status: t.status, cwd: t.cwd })),
63
+ registered: c.registered, omittedTasks: c.omittedTasks, omittedRegistered: c.omittedRegistered });
64
+ const binding = [h.sessionHash, h.incarnation, activation, hash(publicBytes)];
65
+ let offset = 0;
66
+ const token = (n: number) => Buffer.from(JSON.stringify([...binding, n, value.tokens[n / 8 - 1]])).toString("base64url");
67
+ if (cursor !== undefined) {
68
+ if (typeof cursor !== "string" || cursor.length > 1024) throw new Error("invalid-cursor");
69
+ const decoded = JSON.parse(Buffer.from(cursor, "base64url").toString("utf8"));
70
+ offset = decoded?.[4];
71
+ if (!count(offset) || offset < 8 || offset >= CATALOG_ENTRIES || offset % 8 || token(offset) !== cursor) throw new Error("invalid-cursor");
72
+ }
73
+ if (cursor && offset >= Math.max(c.tasks.length, c.registered.length)) throw new Error("invalid-cursor");
74
+ const end = offset + 8;
75
+ return { page: { tasks: c.tasks.slice(offset, end), registered: c.registered.slice(offset, end),
76
+ omittedTasks: c.omittedTasks, omittedRegistered: c.omittedRegistered,
77
+ ...(end < Math.max(c.tasks.length, c.registered.length) ? { cursor: token(end) } : {}) } };
78
+ } catch { return { unavailable: "catalog-unknown-or-invalid-cursor" }; }
79
+ }
@@ -0,0 +1,61 @@
1
+ import { createHash } from "node:crypto";
2
+ import type { PresenceRecord } from "./agents-session-transport.ts";
3
+ import { discoverOrchestrators } from "./orchestrator-discovery.ts";
4
+
5
+ export const CONSULTATION_BYTES = 16 * 1024;
6
+ export interface MetadataSelection { recipientSessionId: string; cursor?: string }
7
+ type Candidate = ReturnType<typeof discoverOrchestrators>[number];
8
+ export interface MetadataReceipt {
9
+ schema: "gentle-agents.consultation/v1";
10
+ kind: "metadata"; status: "available" | "unavailable";
11
+ source: "published_snapshot"; ownerReply: false; authority: "none";
12
+ targetSessionId: string; observedAt: number;
13
+ freshness: "unknown" | "recent" | "stale";
14
+ presenceObservedAt?: number;
15
+ digest?: string;
16
+ snapshot?: { label: string | null; aliases?: Candidate["aliases"]; workspace: string | null; tasks: Candidate["tasks"];
17
+ omittedTasks: number; scope: Candidate["scope"] | null; catalog: Candidate["catalog"] | null;
18
+ state: Candidate["state"] | null };
19
+ unknowns: string[]; omissions: string[];
20
+ }
21
+ function freeze<T>(value: T): T {
22
+ if (value && typeof value === "object") {
23
+ for (const child of Object.values(value)) freeze(child);
24
+ Object.freeze(value);
25
+ }
26
+ return value;
27
+ }
28
+ export function unavailableMetadata(targetSessionId: string, reason: string, now = Date.now(), freshness: MetadataReceipt["freshness"] = "unknown"): MetadataReceipt {
29
+ return freeze({ schema: "gentle-agents.consultation/v1", kind: "metadata", status: "unavailable",
30
+ source: "published_snapshot", ownerReply: false, authority: "none", targetSessionId, observedAt: now,
31
+ freshness, unknowns: [reason], omissions: ["private-context", "owner-decision", "reachability", "exclusive-writer-ownership"] });
32
+ }
33
+ /** Reads the existing bounded, selected-activation projection, never activity.
34
+ * Identity excludes private activity generation/digests and observation clocks.
35
+ * Existing readers validate exact field whitelists before this detached capture. */
36
+ export function consultPublishedMetadata(profile: string, peers: readonly PresenceRecord[], selection: MetadataSelection, now = Date.now()): MetadataReceipt {
37
+ const peer = discoverOrchestrators(profile, peers, now, { recipientSessionId: selection.recipientSessionId,
38
+ cursor: selection.cursor, consultation: true })[0];
39
+ if (!peer?.publication || peer.freshness !== "recent") return unavailableMetadata(selection.recipientSessionId, "publication-unknown-or-stale", now, peer?.freshness);
40
+ if (selection.cursor !== undefined && !peer.catalog) return unavailableMetadata(selection.recipientSessionId, "catalog-unknown-or-invalid-cursor", now);
41
+ const snapshot = {
42
+ label: peer.label || null, ...(peer.aliases ? { aliases: peer.aliases } : {}), workspace: peer.workspace || null,
43
+ tasks: peer.tasks?.map(t => ({ id: t.id, label: t.label, status: t.status, workspace: t.workspace })) ?? [],
44
+ omittedTasks: peer.omitted ?? 0, scope: peer.scope ?? null, catalog: peer.catalog ?? null, state: peer.state ?? null,
45
+ };
46
+ const unknowns = ["reachability", "exclusive-writer-ownership", "owner-decision"];
47
+ if (!snapshot.workspace) unknowns.push("workspace");
48
+ if (!snapshot.scope) unknowns.push("repository-scope");
49
+ if (!snapshot.catalog) unknowns.push("catalog");
50
+ if (!snapshot.state) unknowns.push("curated-state");
51
+ const digest = createHash("sha256").update(JSON.stringify([selection.recipientSessionId,
52
+ peer.publication.incarnation, peer.publication.activation, snapshot])).digest("hex");
53
+ const receipt: MetadataReceipt = { schema: "gentle-agents.consultation/v1", kind: "metadata", status: "available",
54
+ source: "published_snapshot", ownerReply: false, authority: "none", targetSessionId: selection.recipientSessionId,
55
+ observedAt: now, freshness: peer.freshness, presenceObservedAt: peer.publication.heartbeat, digest, snapshot,
56
+ unknowns, omissions: ["private-context", "git-facts-beyond-published-prefix"] };
57
+ // JSON capture detaches every nested list/record and guarantees plain JSON.
58
+ const bytes = JSON.stringify(receipt);
59
+ if (Buffer.byteLength(bytes) > CONSULTATION_BYTES) return unavailableMetadata(selection.recipientSessionId, "snapshot-too-large", now);
60
+ return freeze(JSON.parse(bytes) as MetadataReceipt);
61
+ }
@@ -0,0 +1,49 @@
1
+ import { readCatalog, type CatalogPage } from "./orchestrator-catalog.ts";
2
+ import type { PublishedWork } from "./orchestrator-work.ts";
3
+ import type { PresenceRecord } from "./agents-session-transport.ts";
4
+ import { activationHash, listPresence, readDiscovery, sessionHash, type DiscoveryMetadata } from "./orchestrator-presence.ts";
5
+
6
+ export interface OrchestratorCandidate {
7
+ sessionId: string;
8
+ reachability: "unknown";
9
+ freshness: "unknown" | "recent" | "stale";
10
+ label?: string;
11
+ aliases?: DiscoveryMetadata["aliases"];
12
+ workspace?: string;
13
+ tasks?: DiscoveryMetadata["tasks"];
14
+ omitted?: number;
15
+ scope?: DiscoveryMetadata["scope"];
16
+ state?: DiscoveryMetadata["state"];
17
+ workRecord?: { work: PublishedWork | null; recordedAt: number };
18
+ catalog?: CatalogPage;
19
+ catalogUnavailable?: string;
20
+ /** Internal binding for targeted consultation; never expose transport records. */
21
+ publication?: { incarnation: string; activation: string; heartbeat: number };
22
+ }
23
+
24
+ /** One bounded metadata page, no thread reads or transport probes. The registry
25
+ * selects a canonical routing activation; metadata must bind to that exact one.
26
+ * Incomplete scans and duplicate presence headers fail closed. */
27
+ export function discoverOrchestrators(profile: string, peers: readonly PresenceRecord[], now = Date.now(), selection?: { recipientSessionId?: string; cursor?: string; consultation?: boolean; includeWork?: boolean }): OrchestratorCandidate[] {
28
+ const page = listPresence(profile, now);
29
+ const ids = [...new Set(peers.map(peer => peer.sessionId))];
30
+ return ids.filter(id => selection?.recipientSessionId === undefined || selection.recipientSessionId === id).map(sessionId => {
31
+ const unknown: OrchestratorCandidate = { sessionId, reachability: "unknown", freshness: "unknown" };
32
+ const activations = peers.filter(peer => peer.sessionId === sessionId);
33
+ if (page.unavailable || page.overflow || page.rejected || activations.length !== 1) return unknown;
34
+ const matches = page.entries.filter(h => h.sessionHash === sessionHash(sessionId));
35
+ // Even a stale duplicate could represent another process with the same ID.
36
+ if (matches.length !== 1) return unknown;
37
+ const header = matches[0];
38
+ const metadata = readDiscovery(profile, header);
39
+ if (metadata?.activation !== activationHash(activations[0])) return unknown;
40
+ if (!header.recent) return { ...unknown, freshness: "stale" };
41
+ const catalog = readCatalog(profile, header, metadata.activation, selection?.recipientSessionId === sessionId ? selection.cursor : undefined);
42
+ return { ...unknown, ...(catalog.page ? { catalog: catalog.page } : { catalogUnavailable: catalog.unavailable }), freshness: "recent", label: header.label, workspace: metadata.workspace,
43
+ tasks: metadata.tasks, omitted: metadata.omitted, ...(metadata.aliases ? { aliases: metadata.aliases } : {}), ...(metadata.scope ? { scope: metadata.scope } : {}),
44
+ ...(selection?.consultation ? { publication: { incarnation: header.incarnation, activation: metadata.activation, heartbeat: header.heartbeat } } : {}),
45
+ ...(selection?.includeWork && metadata.state?.sessionId === sessionId
46
+ ? { workRecord: { work: metadata.state.state?.work ?? null, recordedAt: metadata.state.recordedAt } } : {}),
47
+ ...(!selection?.includeWork && selection?.recipientSessionId === sessionId && metadata.state?.sessionId === sessionId ? { state: metadata.state } : {}) };
48
+ });
49
+ }