@pellux/goodvibes-tui 1.27.0 → 2.0.1

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 (404) hide show
  1. package/CHANGELOG.md +125 -2
  2. package/README.md +7 -9
  3. package/bin/goodvibes +2 -2
  4. package/bin/launcher-support.js +8 -4
  5. package/docs/README.md +11 -2
  6. package/docs/foundation-artifacts/operator-contract.json +6403 -2347
  7. package/package.json +14 -25
  8. package/scripts/postinstall.js +35 -35
  9. package/src/audio/spoken-turn-model-routing.ts +1 -1
  10. package/src/cli/bundle-command.ts +5 -5
  11. package/src/cli/completions/generate.ts +1 -1
  12. package/src/cli/doctor.ts +3 -3
  13. package/src/cli/entrypoint.ts +12 -7
  14. package/src/cli/help.ts +4 -5
  15. package/src/cli/hooks-command.ts +1 -1
  16. package/src/cli/index.ts +1 -4
  17. package/src/cli/launch-auto-update.ts +59 -7
  18. package/src/cli/management-commands.ts +6 -6
  19. package/src/cli/management-utils.ts +5 -5
  20. package/src/cli/management.ts +7 -7
  21. package/src/cli/network-posture.ts +1 -1
  22. package/src/cli/package-verification.ts +7 -6
  23. package/src/cli/plugin-bundles-command.ts +1 -1
  24. package/src/cli/plugin-command.ts +2 -2
  25. package/src/cli/service-command.ts +2 -2
  26. package/src/cli/service-posture.ts +4 -4
  27. package/src/cli/status.ts +9 -8
  28. package/src/cli/surface-command.ts +4 -4
  29. package/src/cli/tui-startup.ts +12 -3
  30. package/src/cli-flags.ts +3 -3
  31. package/src/config/goodvibes-home-audit.ts +0 -2
  32. package/src/config/secret-config.ts +37 -5
  33. package/src/config/tui-extension-settings.ts +1 -1
  34. package/src/core/approval-alert.ts +2 -2
  35. package/src/core/budget-breach-notifier.ts +3 -3
  36. package/src/core/conversation-compaction-render.ts +1 -1
  37. package/src/core/conversation-line-cache.ts +1 -1
  38. package/src/core/conversation-render-context.ts +1 -1
  39. package/src/core/conversation-rendering.ts +2 -2
  40. package/src/core/conversation.ts +10 -10
  41. package/src/core/long-task-notifier.ts +2 -2
  42. package/src/core/memory-diagnostics-gateway.ts +1 -1
  43. package/src/core/scriptable-statusline.ts +1 -1
  44. package/src/core/session-auto-titler.ts +1 -1
  45. package/src/core/session-resume-core.ts +2 -2
  46. package/src/core/stream-event-wiring.ts +24 -44
  47. package/src/core/terminal-notifier.ts +3 -3
  48. package/src/core/turn-cancellation.ts +1 -1
  49. package/src/core/turn-event-wiring.ts +5 -5
  50. package/src/input/command-registry.ts +6 -6
  51. package/src/input/commands/channel-pairing.ts +1 -1
  52. package/src/input/commands/cluster-runtime.ts +1 -1
  53. package/src/input/commands/codebase-runtime.ts +1 -1
  54. package/src/input/commands/devices-runtime.ts +1 -1
  55. package/src/input/commands/hosted-runtime.ts +330 -0
  56. package/src/input/commands/local-setup-review.ts +1 -1
  57. package/src/input/commands/local-setup-transfer.ts +2 -2
  58. package/src/input/commands/local-setup.ts +3 -3
  59. package/src/input/commands/managed-runtime.ts +2 -2
  60. package/src/input/commands/operator-rpc.ts +24 -110
  61. package/src/input/commands/payment-card-intake.ts +1 -1
  62. package/src/input/commands/permissions-provenance.ts +2 -2
  63. package/src/input/commands/plugin-runtime.ts +1 -1
  64. package/src/input/commands/provider.ts +1 -1
  65. package/src/input/commands/relay-runtime.ts +18 -9
  66. package/src/input/commands/rewind-runtime.ts +1 -1
  67. package/src/input/commands/runtime-services.ts +1 -1
  68. package/src/input/commands/services-runtime.ts +1 -1
  69. package/src/input/commands/session-workflow.ts +1 -1
  70. package/src/input/commands/settings-sync-runtime.ts +2 -2
  71. package/src/input/commands/share-runtime.ts +1 -1
  72. package/src/input/commands/tasks-runtime.ts +145 -55
  73. package/src/input/commands/update-runtime.ts +31 -28
  74. package/src/input/commands/work-plan-runtime.ts +2 -2
  75. package/src/input/commands/workstream-attempts.ts +1 -1
  76. package/src/input/commands/workstream-runtime.ts +2 -2
  77. package/src/input/commands.ts +2 -0
  78. package/src/input/feature-unit-layout.ts +1 -1
  79. package/src/input/feed-context-factory.ts +3 -3
  80. package/src/input/handler-content-actions.ts +2 -2
  81. package/src/input/handler-feed-routes.ts +1 -1
  82. package/src/input/handler-feed.ts +3 -3
  83. package/src/input/handler-modal-token-routes.ts +1 -1
  84. package/src/input/handler-onboarding-daemon-adopt.ts +3 -2
  85. package/src/input/handler-onboarding.ts +1 -1
  86. package/src/input/handler-types.ts +2 -2
  87. package/src/input/handler-ui-state.ts +1 -1
  88. package/src/input/handler.ts +4 -4
  89. package/src/input/input-history.ts +1 -1
  90. package/src/input/onboarding/onboarding-feature-units.ts +2 -2
  91. package/src/input/onboarding/onboarding-runtime-status.ts +15 -1
  92. package/src/input/onboarding/onboarding-wizard-apply.ts +17 -8
  93. package/src/input/onboarding/onboarding-wizard-cloudflare-step.ts +5 -3
  94. package/src/input/onboarding/onboarding-wizard-external-surfaces.ts +1 -1
  95. package/src/input/onboarding/onboarding-wizard-types.ts +1 -1
  96. package/src/input/payments-config.ts +6 -6
  97. package/src/input/search.ts +1 -1
  98. package/src/input/session-picker-modal.ts +46 -0
  99. package/src/input/settings-modal-activation.ts +1 -1
  100. package/src/input/settings-modal-behavior.ts +4 -4
  101. package/src/input/settings-modal-commit.ts +11 -0
  102. package/src/input/settings-modal-data.ts +13 -80
  103. package/src/input/settings-modal-mutations.ts +33 -0
  104. package/src/input/settings-modal-secrets.ts +34 -0
  105. package/src/input/settings-modal-subscriptions.ts +1 -1
  106. package/src/input/settings-modal-types.ts +15 -1
  107. package/src/input/settings-modal.ts +28 -3
  108. package/src/input/worktree-setup-config.ts +1 -1
  109. package/src/main.ts +11 -9
  110. package/src/panels/agent-inspector-shared.ts +2 -2
  111. package/src/panels/base-panel.ts +2 -2
  112. package/src/panels/builtin/operations.ts +6 -8
  113. package/src/panels/builtin/session.ts +17 -0
  114. package/src/panels/builtin/shared.ts +5 -5
  115. package/src/panels/builtin-modals.ts +3 -3
  116. package/src/panels/confirm-state.ts +1 -1
  117. package/src/panels/cost-tracker-panel.ts +4 -4
  118. package/src/panels/diff-panel.ts +2 -2
  119. package/src/panels/diff-review-panel.ts +2 -2
  120. package/src/panels/expandable-list-panel.ts +2 -2
  121. package/src/panels/fleet-acts.ts +1 -1
  122. package/src/panels/fleet-observed-render.ts +1 -1
  123. package/src/panels/fleet-panel-format.ts +1 -1
  124. package/src/panels/fleet-panel.ts +1 -1
  125. package/src/panels/fleet-spawn.ts +1 -1
  126. package/src/panels/fleet-steer.ts +1 -1
  127. package/src/panels/fleet-transcript.ts +1 -1
  128. package/src/panels/git-panel.ts +1 -1
  129. package/src/panels/hosted-session-feed.ts +352 -0
  130. package/src/panels/hosted-session-panel.ts +221 -0
  131. package/src/panels/local-auth-panel.ts +3 -3
  132. package/src/panels/modals/devices-modal.ts +1 -1
  133. package/src/panels/modals/local-auth-modal.ts +1 -1
  134. package/src/panels/modals/pairing-modal.ts +1 -1
  135. package/src/panels/modals/services-modal.ts +1 -1
  136. package/src/panels/modals/settings-sync-modal.ts +1 -1
  137. package/src/panels/modals/subscription-modal.ts +1 -1
  138. package/src/panels/modals/work-plan-modal.ts +1 -1
  139. package/src/panels/notifications-panel.ts +1 -1
  140. package/src/panels/panel-confirm-overlay.ts +1 -1
  141. package/src/panels/plugins-panel.ts +2 -2
  142. package/src/panels/polish-core.ts +2 -2
  143. package/src/panels/polish-tables.ts +1 -1
  144. package/src/panels/polish.ts +2 -2
  145. package/src/panels/scrollable-list-panel.ts +3 -3
  146. package/src/panels/skills-panel.ts +3 -3
  147. package/src/panels/token-budget-panel.ts +5 -5
  148. package/src/panels/types.ts +1 -2
  149. package/src/permissions/prompt.ts +1 -1
  150. package/src/renderer/autocomplete-overlay.ts +1 -1
  151. package/src/renderer/block-actions-overlay.ts +1 -1
  152. package/src/renderer/bookmark-modal.ts +1 -1
  153. package/src/renderer/buffer.ts +1 -1
  154. package/src/renderer/code-block.ts +1 -1
  155. package/src/renderer/composer-fragments.ts +1 -1
  156. package/src/renderer/compositor.ts +2 -2
  157. package/src/renderer/config-modal.ts +1 -1
  158. package/src/renderer/context-inspector.ts +1 -1
  159. package/src/renderer/conversation-layout.ts +2 -2
  160. package/src/renderer/conversation-overlays.ts +1 -1
  161. package/src/renderer/conversation-surface.ts +2 -2
  162. package/src/renderer/diff-view.ts +1 -1
  163. package/src/renderer/diff.ts +1 -1
  164. package/src/renderer/file-picker-overlay.ts +1 -1
  165. package/src/renderer/fleet-tab-strip.ts +1 -1
  166. package/src/renderer/fullscreen-primitives.ts +2 -2
  167. package/src/renderer/fullscreen-workspace.ts +1 -1
  168. package/src/renderer/header-line.ts +1 -1
  169. package/src/renderer/help-overlay.ts +1 -1
  170. package/src/renderer/history-search-overlay.ts +2 -2
  171. package/src/renderer/layout.ts +5 -8
  172. package/src/renderer/markdown-inline.ts +1 -1
  173. package/src/renderer/markdown-table.ts +1 -1
  174. package/src/renderer/markdown.ts +1 -1
  175. package/src/renderer/mcp-workspace.ts +1 -1
  176. package/src/renderer/modal-factory.ts +2 -2
  177. package/src/renderer/model-workspace.ts +2 -2
  178. package/src/renderer/onboarding/onboarding-wizard.ts +1 -1
  179. package/src/renderer/overlay-box.ts +1 -1
  180. package/src/renderer/panel-composite.ts +1 -1
  181. package/src/renderer/panel-workspace-bar.ts +1 -1
  182. package/src/renderer/process-indicator.ts +1 -1
  183. package/src/renderer/profile-picker-modal.ts +1 -1
  184. package/src/renderer/search-overlay.ts +2 -2
  185. package/src/renderer/selection-modal-overlay.ts +1 -1
  186. package/src/renderer/session-picker-modal.ts +124 -3
  187. package/src/renderer/settings-modal-helpers.ts +2 -0
  188. package/src/renderer/settings-modal.ts +2 -2
  189. package/src/renderer/shell-surface.ts +2 -2
  190. package/src/renderer/system-message.ts +1 -1
  191. package/src/renderer/tab-strip.ts +1 -1
  192. package/src/renderer/theme-mode-config.ts +1 -1
  193. package/src/renderer/theme.ts +11 -12
  194. package/src/renderer/thinking.ts +1 -1
  195. package/src/renderer/tool-call.ts +2 -2
  196. package/src/renderer/tool-result-expanded-lines.ts +1 -1
  197. package/src/renderer/turn-injection.ts +7 -8
  198. package/src/renderer/ui-factory.ts +3 -3
  199. package/src/runtime/bootstrap-command-context.ts +4 -4
  200. package/src/runtime/bootstrap-command-parts.ts +5 -5
  201. package/src/runtime/bootstrap-core.ts +30 -30
  202. package/src/runtime/bootstrap-shell.ts +4 -4
  203. package/src/runtime/bootstrap.ts +128 -157
  204. package/src/runtime/client/approval-updates.ts +82 -0
  205. package/src/runtime/client/build-floors.ts +167 -0
  206. package/src/runtime/client/daemon-attach-handshake.ts +115 -0
  207. package/src/runtime/client/fleet-union.ts +147 -0
  208. package/src/runtime/client/host-service-status.ts +71 -0
  209. package/src/runtime/client/hosted-exit.ts +86 -0
  210. package/src/runtime/client/hosted-roster.ts +110 -0
  211. package/src/runtime/client/hosted-session-stream.ts +178 -0
  212. package/src/runtime/client/hosted-sessions.ts +187 -0
  213. package/src/runtime/client/operator-endpoint.ts +213 -0
  214. package/src/runtime/client/spine-adoption.ts +99 -0
  215. package/src/runtime/cloudflare-control-plane.ts +1 -1
  216. package/src/runtime/context-accounting-source.ts +1 -1
  217. package/src/runtime/context.ts +2 -2
  218. package/src/runtime/conversation-rewind-port.ts +42 -14
  219. package/src/runtime/daemon-attach-notices.ts +55 -19
  220. package/src/runtime/diagnostics/panels/ops.ts +1 -1
  221. package/src/runtime/disposal-wiring.ts +4 -24
  222. package/src/runtime/index.ts +119 -105
  223. package/src/runtime/install-self-check-startup.ts +2 -2
  224. package/src/runtime/interaction-seams.ts +1 -1
  225. package/src/runtime/knowledge-services.ts +8 -2
  226. package/src/runtime/legacy-daemon-migration.ts +15 -3
  227. package/src/runtime/legacy-daemon-reconcile.ts +3 -2
  228. package/src/runtime/memory-spine-transport.ts +37 -267
  229. package/src/runtime/notification-dispatch.ts +2 -2
  230. package/src/runtime/onboarding/apply.ts +2 -2
  231. package/src/runtime/onboarding/derivation.ts +2 -2
  232. package/src/runtime/onboarding/markers.ts +2 -2
  233. package/src/runtime/onboarding/progress.ts +2 -2
  234. package/src/runtime/onboarding/snapshot.ts +1 -1
  235. package/src/runtime/onboarding/state.ts +2 -2
  236. package/src/runtime/onboarding/types.ts +2 -2
  237. package/src/runtime/orchestrator-core-services.ts +1 -1
  238. package/src/runtime/path-shadow-startup.ts +28 -221
  239. package/src/runtime/power-keepawake-remote.ts +3 -2
  240. package/src/runtime/process-lifecycle.ts +2 -2
  241. package/src/runtime/provider-fallback.ts +10 -66
  242. package/src/runtime/recovery-autosave.ts +1 -1
  243. package/src/runtime/recovery-offer-wiring.ts +39 -23
  244. package/src/runtime/recovery-prompt.ts +31 -17
  245. package/src/runtime/relay-reachability-bridge.ts +53 -42
  246. package/src/runtime/release-artifacts.ts +2 -1
  247. package/src/runtime/runtime-services-types.ts +82 -35
  248. package/src/runtime/services.ts +305 -300
  249. package/src/runtime/session-resume-liveness-confirm.ts +1 -1
  250. package/src/runtime/ui-services.ts +7 -6
  251. package/src/runtime/update-check.ts +23 -43
  252. package/src/runtime/workspace-checkpointing.ts +1 -1
  253. package/src/runtime/wrfc-persistence.ts +2 -2
  254. package/src/shell/ui-openers.ts +30 -7
  255. package/src/utils/clipboard.ts +1 -1
  256. package/src/verification/verification-ledger.ts +10 -8
  257. package/src/version.ts +1 -1
  258. package/bin/goodvibes-daemon +0 -90
  259. package/src/cli/config-overrides.ts +0 -267
  260. package/src/cli/endpoints.ts +0 -109
  261. package/src/cli/parser.ts +0 -395
  262. package/src/cli/redaction.ts +0 -170
  263. package/src/cli/types.ts +0 -84
  264. package/src/cluster/commands.ts +0 -382
  265. package/src/cluster/remote-daemon-target.ts +0 -225
  266. package/src/cluster/render.ts +0 -268
  267. package/src/config/atomic-write.ts +0 -70
  268. package/src/config/credential-availability.ts +0 -122
  269. package/src/config/goodvibes-home.ts +0 -149
  270. package/src/config/index.ts +0 -88
  271. package/src/config/provider-model.ts +0 -23
  272. package/src/config/read-versioned.ts +0 -228
  273. package/src/core/alert-gating.ts +0 -67
  274. package/src/core/consolidation-receipt.ts +0 -41
  275. package/src/core/focus-tracker.ts +0 -41
  276. package/src/core/history.ts +0 -45
  277. package/src/core/memory-status.ts +0 -118
  278. package/src/core/orchestrator.ts +0 -7
  279. package/src/core/pairing-devices.ts +0 -49
  280. package/src/core/pairing-handoff.ts +0 -101
  281. package/src/core/pairing-offers.ts +0 -83
  282. package/src/core/pairing-origin.ts +0 -78
  283. package/src/core/rewind-turn-anchors.ts +0 -311
  284. package/src/core/session-recovery.ts +0 -210
  285. package/src/core/stable-host.ts +0 -171
  286. package/src/core/transcript-journal.ts +0 -503
  287. package/src/daemon/cli.ts +0 -641
  288. package/src/daemon/handlers/context.ts +0 -29
  289. package/src/daemon/handlers/contracts.ts +0 -77
  290. package/src/daemon/handlers/credentials.ts +0 -139
  291. package/src/daemon/handlers/drafts/draft-store.ts +0 -427
  292. package/src/daemon/handlers/drafts/index.ts +0 -17
  293. package/src/daemon/handlers/drafts/register.ts +0 -331
  294. package/src/daemon/handlers/errors.ts +0 -18
  295. package/src/daemon/handlers/inbox/cursor-store.ts +0 -457
  296. package/src/daemon/handlers/inbox/index.ts +0 -284
  297. package/src/daemon/handlers/inbox/mapping.ts +0 -192
  298. package/src/daemon/handlers/inbox/poller.ts +0 -212
  299. package/src/daemon/handlers/inbox/provider-adapter.ts +0 -156
  300. package/src/daemon/handlers/inbox/providers/discord.ts +0 -252
  301. package/src/daemon/handlers/inbox/providers/email.ts +0 -152
  302. package/src/daemon/handlers/inbox/providers/imap-client.ts +0 -300
  303. package/src/daemon/handlers/inbox/providers/route-util.ts +0 -24
  304. package/src/daemon/handlers/inbox/providers/slack.ts +0 -263
  305. package/src/daemon/handlers/index.ts +0 -117
  306. package/src/daemon/handlers/register.ts +0 -168
  307. package/src/daemon/handlers/remote/backends/cloud-terminal.ts +0 -142
  308. package/src/daemon/handlers/remote/backends/docker.ts +0 -79
  309. package/src/daemon/handlers/remote/backends/index.ts +0 -40
  310. package/src/daemon/handlers/remote/backends/local-process.ts +0 -113
  311. package/src/daemon/handlers/remote/backends/process-runner.ts +0 -127
  312. package/src/daemon/handlers/remote/backends/ssh.ts +0 -125
  313. package/src/daemon/handlers/remote/backends/types.ts +0 -97
  314. package/src/daemon/handlers/remote/dispatcher.ts +0 -181
  315. package/src/daemon/handlers/remote/index.ts +0 -119
  316. package/src/daemon/handlers/remote/peer-registry.ts +0 -357
  317. package/src/daemon/handlers/remote/service.ts +0 -191
  318. package/src/daemon/handlers/routing/inbox-bridge.ts +0 -71
  319. package/src/daemon/handlers/routing/index.ts +0 -261
  320. package/src/daemon/handlers/routing/route-store.ts +0 -319
  321. package/src/daemon/handlers/routing/routing-resolver.ts +0 -75
  322. package/src/daemon/handlers/sqlite-store.ts +0 -278
  323. package/src/daemon/handlers/triage/index.ts +0 -57
  324. package/src/daemon/handlers/triage/integration.ts +0 -213
  325. package/src/daemon/handlers/triage/pipeline.ts +0 -274
  326. package/src/daemon/handlers/triage/scorer.ts +0 -287
  327. package/src/daemon/handlers/triage/tagger/discord.ts +0 -187
  328. package/src/daemon/handlers/triage/tagger/imap.ts +0 -384
  329. package/src/daemon/handlers/triage/tagger/index.ts +0 -184
  330. package/src/daemon/handlers/triage/tagger/shared.ts +0 -70
  331. package/src/daemon/handlers/triage/tagger/slack.ts +0 -69
  332. package/src/daemon/handlers/triage/types.ts +0 -50
  333. package/src/daemon/lifecycle.ts +0 -41
  334. package/src/daemon/provision-wake-model.ts +0 -81
  335. package/src/daemon/safe-serve.ts +0 -61
  336. package/src/daemon/send/channels.ts +0 -200
  337. package/src/daemon/send/command.ts +0 -332
  338. package/src/daemon/send/composition.ts +0 -99
  339. package/src/daemon/send/failure-text.ts +0 -93
  340. package/src/daemon/send/inert-text.ts +0 -225
  341. package/src/daemon/send/stdin.ts +0 -24
  342. package/src/daemon/service-commands.ts +0 -329
  343. package/src/export/cost-utils.ts +0 -319
  344. package/src/input/selection.ts +0 -135
  345. package/src/plugins/loader.ts +0 -15
  346. package/src/renderer/bottom-bar.ts +0 -58
  347. package/src/renderer/conversation-tree.ts +0 -228
  348. package/src/runtime/agent-graph-composition.ts +0 -78
  349. package/src/runtime/channel-composition.ts +0 -46
  350. package/src/runtime/cluster-composition.ts +0 -124
  351. package/src/runtime/cluster-group-composition.ts +0 -284
  352. package/src/runtime/code-index-services.ts +0 -167
  353. package/src/runtime/daemon-handler-composition.ts +0 -76
  354. package/src/runtime/device-posture-composition.ts +0 -110
  355. package/src/runtime/durability-housekeeping.ts +0 -183
  356. package/src/runtime/durability-services.ts +0 -155
  357. package/src/runtime/embedded-service-factories.ts +0 -58
  358. package/src/runtime/feature-settings.ts +0 -154
  359. package/src/runtime/fleet-needs-input-push.ts +0 -70
  360. package/src/runtime/fleet-services.ts +0 -41
  361. package/src/runtime/idle-power-services.ts +0 -85
  362. package/src/runtime/mail-composition.ts +0 -65
  363. package/src/runtime/memory-fold.ts +0 -55
  364. package/src/runtime/operator-token-cleanup.ts +0 -109
  365. package/src/runtime/path-shadow.ts +0 -418
  366. package/src/runtime/perf/panel-health-monitor.ts +0 -18
  367. package/src/runtime/provider-optimizer-wiring.ts +0 -55
  368. package/src/runtime/reachability-notice.ts +0 -100
  369. package/src/runtime/remote-execution-composition.ts +0 -62
  370. package/src/runtime/session-liveness-marker.ts +0 -257
  371. package/src/runtime/session-pointer-surface.ts +0 -22
  372. package/src/runtime/session-storage-services.ts +0 -41
  373. package/src/runtime/surface-feature-flags.ts +0 -96
  374. package/src/runtime/terminal-output-guard.ts +0 -235
  375. package/src/runtime/trust/workspace-registration.ts +0 -192
  376. package/src/runtime/trust/workspace-trust.ts +0 -229
  377. package/src/runtime/ui/index.ts +0 -39
  378. package/src/runtime/ui/model-picker/data-provider.ts +0 -182
  379. package/src/runtime/ui/model-picker/health-enrichment.ts +0 -231
  380. package/src/runtime/ui/model-picker/index.ts +0 -59
  381. package/src/runtime/ui/model-picker/types.ts +0 -153
  382. package/src/runtime/ui/provider-health/data-provider.ts +0 -244
  383. package/src/runtime/ui/provider-health/fallback-visualizer.ts +0 -71
  384. package/src/runtime/ui/provider-health/index.ts +0 -46
  385. package/src/runtime/ui/provider-health/types.ts +0 -146
  386. package/src/runtime/ui-events.ts +0 -1
  387. package/src/runtime/ui-read-model-helpers.ts +0 -1
  388. package/src/runtime/ui-read-models-observability-maintenance.ts +0 -1
  389. package/src/runtime/ui-read-models-observability-options.ts +0 -1
  390. package/src/runtime/ui-read-models-observability-remote.ts +0 -1
  391. package/src/runtime/ui-read-models-observability-security.ts +0 -1
  392. package/src/runtime/ui-read-models-observability-system.ts +0 -1
  393. package/src/runtime/ui-read-models-observability.ts +0 -1
  394. package/src/runtime/ui-service-queries.ts +0 -1
  395. package/src/runtime/voice-setup-services.ts +0 -98
  396. package/src/runtime/workstream-attempts-validation.ts +0 -77
  397. package/src/runtime/workstream-draft-edits.ts +0 -95
  398. package/src/runtime/workstream-draft-store.ts +0 -274
  399. package/src/runtime/workstream-draft-types.ts +0 -42
  400. package/src/runtime/workstream-services.ts +0 -373
  401. package/src/tools/index.ts +0 -1
  402. package/src/types/grid.ts +0 -48
  403. package/src/types/sql-js.d.ts +0 -15
  404. package/src/work-plans/work-plan-store.ts +0 -702
@@ -1,702 +0,0 @@
1
- // ---------------------------------------------------------------------------
2
- // work-plan-store.ts — per-project work plan persisted at
3
- // <home>/.goodvibes/tui/work-plans/<projectId>.json
4
- //
5
- // Recovery discipline (applies to every read):
6
- //
7
- // * CONTENT, NOT EXISTENCE, decides whether the file is usable. A plan file
8
- // can exist and still be unusable — zero-byte, truncated mid-write, or
9
- // otherwise torn by a crash. Every read parses defensively and validates the
10
- // parsed SHAPE; an unusable file degrades to an empty plan instead of
11
- // throwing out of the store's public methods (which used to brick every
12
- // /work-plan operation until the user deleted the file by hand).
13
- // * AN UNREADABLE FILE IS PRESERVED, NOT OVERWRITTEN. It is renamed aside to
14
- // `<plan>.json.corrupt-<ms>-<rand>` so a user can still recover their list.
15
- // Those quarantine copies are themselves bounded (age TTL + count cap) so
16
- // the recovery path cannot become its own leak.
17
- // * THE STORE IS BOUNDED. Terminal (done/cancelled) items age out and are
18
- // capped by count. Items the user still has open — pending, in_progress,
19
- // blocked, failed — are NEVER reclaimed by either bound: garbage collection
20
- // must not delete live work.
21
- // * NOTHING IS DELETED SILENTLY. Whatever a sweep reclaimed is recorded on the
22
- // plan as `housekeeping` and rendered by `toMarkdown()` (which /work-plan
23
- // show prints), so a reclaim is distinguishable from data loss.
24
- //
25
- // Sweeps are idempotent and safe to run from more than one process: removal is
26
- // computed from the file's own contents, writes go through atomicWriteFileSync
27
- // (rename(2)), and unlink races are treated as already-done.
28
- // ---------------------------------------------------------------------------
29
-
30
- import { createHash, randomUUID } from 'node:crypto';
31
- import { readdirSync, readFileSync, renameSync, unlinkSync } from 'node:fs';
32
- import { atomicWriteFileSync } from '@/config/atomic-write.ts';
33
- import { basename, dirname, join } from 'node:path';
34
-
35
- /**
36
- * Age TTL for TERMINAL (done/cancelled) items. Once a completed or cancelled
37
- * item has been untouched this long it is reclaimed by the next read. Chosen to
38
- * comfortably outlive "I finished that last month and want to see it" while
39
- * still bounding a plan that is never manually cleared.
40
- */
41
- export const WORK_PLAN_TERMINAL_ITEM_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 days
42
-
43
- /**
44
- * Count cap for TERMINAL (done/cancelled) items: the most recently completed
45
- * this many are kept, older ones are reclaimed oldest-first. Live items are
46
- * neither counted against this cap nor removed by it.
47
- */
48
- export const WORK_PLAN_TERMINAL_ITEM_CAP = 200;
49
-
50
- /** Age TTL for quarantined (unreadable) plan files — the recovery copy's own bound. */
51
- export const WORK_PLAN_QUARANTINE_TTL_MS = 14 * 24 * 60 * 60 * 1000; // 14 days
52
-
53
- /** Count cap for quarantined plan files kept alongside one plan; oldest go first. */
54
- export const WORK_PLAN_QUARANTINE_CAP = 5;
55
-
56
- /**
57
- * Minimum gap between quarantine-directory scans for one store instance. The
58
- * item bounds are evaluated on every read (they are a pure pass over in-memory
59
- * items); the directory scan is throttled so a redrawing modal cannot turn it
60
- * into a per-frame readdir. This is what makes the sweep periodic rather than
61
- * startup-only: a long-lived session keeps sweeping on this cadence.
62
- */
63
- export const WORK_PLAN_QUARANTINE_SWEEP_INTERVAL_MS = 5 * 60 * 1000; // 5 minutes
64
-
65
- /** Filename marker for a preserved copy of a plan file that could not be parsed. */
66
- const QUARANTINE_SUFFIX = '.corrupt-';
67
-
68
- export const WORK_PLAN_STATUSES = [
69
- 'pending',
70
- 'in_progress',
71
- 'blocked',
72
- 'done',
73
- 'failed',
74
- 'cancelled',
75
- ] as const;
76
-
77
- export type WorkPlanItemStatus = typeof WORK_PLAN_STATUSES[number];
78
-
79
- export interface WorkPlanLinkTargets {
80
- readonly agentId?: string;
81
- readonly wrfcId?: string;
82
- readonly taskId?: string;
83
- readonly sessionId?: string;
84
- }
85
-
86
- export interface WorkPlanItem {
87
- readonly id: string;
88
- readonly title: string;
89
- readonly status: WorkPlanItemStatus;
90
- readonly owner?: string;
91
- readonly source?: string;
92
- readonly notes?: string;
93
- readonly linked?: WorkPlanLinkTargets;
94
- readonly createdAt: number;
95
- readonly updatedAt: number;
96
- readonly completedAt?: number;
97
- }
98
-
99
- /**
100
- * What the last recovery/bounding sweep reclaimed. Persisted with the plan and
101
- * rendered by `toMarkdown()` so a removal is always disclosed — counts and
102
- * paths only, never item text.
103
- */
104
- export interface WorkPlanHousekeeping {
105
- /** Unix ms of the sweep that produced these counts. */
106
- readonly at: number;
107
- /** Terminal items reclaimed by the age TTL. */
108
- readonly expiredItems: number;
109
- /** Terminal items reclaimed by the count cap. */
110
- readonly cappedItems: number;
111
- /** True when the plan file on disk could not be parsed and the plan was reset. */
112
- readonly resetFromUnreadableFile?: boolean;
113
- /** Where the unreadable file was preserved, when there was content worth keeping. */
114
- readonly quarantinePath?: string;
115
- /** Quarantined files removed by their own TTL / count cap. */
116
- readonly quarantinesRemoved?: number;
117
- }
118
-
119
- export interface WorkPlan {
120
- readonly id: string;
121
- readonly projectId: string;
122
- readonly projectRoot: string;
123
- readonly title: string;
124
- readonly items: readonly WorkPlanItem[];
125
- readonly activeItemId?: string;
126
- readonly source?: string;
127
- readonly createdAt: number;
128
- readonly updatedAt: number;
129
- /** Result of the most recent sweep that actually reclaimed something. */
130
- readonly housekeeping?: WorkPlanHousekeeping;
131
- }
132
-
133
- export interface WorkPlanStoreOptions {
134
- readonly homeDirectory: string;
135
- readonly projectId: string;
136
- readonly projectRoot: string;
137
- }
138
-
139
- export interface AddWorkPlanItemOptions {
140
- readonly status?: WorkPlanItemStatus;
141
- readonly owner?: string;
142
- readonly source?: string;
143
- readonly notes?: string;
144
- readonly linked?: WorkPlanLinkTargets;
145
- }
146
-
147
- export interface UpdateWorkPlanItemPatch {
148
- readonly title?: string;
149
- readonly status?: WorkPlanItemStatus;
150
- readonly owner?: string | null;
151
- readonly source?: string | null;
152
- readonly notes?: string | null;
153
- readonly linked?: WorkPlanLinkTargets | null;
154
- }
155
-
156
- function nowMs(): number {
157
- return Date.now();
158
- }
159
-
160
- function isObject(value: unknown): value is Record<string, unknown> {
161
- return typeof value === 'object' && value !== null && !Array.isArray(value);
162
- }
163
-
164
- function readString(value: unknown): string | undefined {
165
- return typeof value === 'string' && value.trim().length > 0 ? value : undefined;
166
- }
167
-
168
- function isWorkPlanStatus(value: unknown): value is WorkPlanItemStatus {
169
- return typeof value === 'string' && WORK_PLAN_STATUSES.includes(value as WorkPlanItemStatus);
170
- }
171
-
172
- function safeFileId(projectId: string, projectRoot: string): string {
173
- const normalized = projectId.trim() || 'project';
174
- const safe = normalized.replace(/[^A-Za-z0-9_.-]+/g, '-').replace(/^-+|-+$/g, '');
175
- if (safe.length > 0 && safe.length <= 96) return safe;
176
- const hash = createHash('sha256').update(`${projectId}\0${projectRoot}`).digest('hex').slice(0, 16);
177
- return `${safe.slice(0, 80) || 'project'}-${hash}`;
178
- }
179
-
180
- function createPlanId(projectId: string, projectRoot: string): string {
181
- const hash = createHash('sha256').update(`${projectId}\0${projectRoot}`).digest('hex').slice(0, 12);
182
- return `wp-${hash}`;
183
- }
184
-
185
- function createItemId(): string {
186
- return `wpi-${randomUUID().slice(0, 8)}`;
187
- }
188
-
189
- function normalizeLinked(value: unknown): WorkPlanLinkTargets | undefined {
190
- if (!isObject(value)) return undefined;
191
- const agentId = readString(value.agentId);
192
- const wrfcId = readString(value.wrfcId);
193
- const taskId = readString(value.taskId);
194
- const sessionId = readString(value.sessionId);
195
- const linked: WorkPlanLinkTargets = {
196
- ...(agentId ? { agentId } : {}),
197
- ...(wrfcId ? { wrfcId } : {}),
198
- ...(taskId ? { taskId } : {}),
199
- ...(sessionId ? { sessionId } : {}),
200
- };
201
- return Object.keys(linked).length > 0 ? linked : undefined;
202
- }
203
-
204
- function normalizeItem(value: unknown, fallbackCreatedAt: number): WorkPlanItem | null {
205
- if (!isObject(value)) return null;
206
- const title = readString(value.title);
207
- if (!title) return null;
208
- const status = isWorkPlanStatus(value.status) ? value.status : 'pending';
209
- const createdAt = typeof value.createdAt === 'number' ? value.createdAt : fallbackCreatedAt;
210
- const updatedAt = typeof value.updatedAt === 'number' ? value.updatedAt : createdAt;
211
- const completedAt = typeof value.completedAt === 'number' ? value.completedAt : undefined;
212
- const owner = readString(value.owner);
213
- const source = readString(value.source);
214
- const notes = readString(value.notes);
215
- const linked = normalizeLinked(value.linked);
216
- return {
217
- id: readString(value.id) ?? createItemId(),
218
- title,
219
- status,
220
- ...(owner ? { owner } : {}),
221
- ...(source ? { source } : {}),
222
- ...(notes ? { notes } : {}),
223
- ...(linked ? { linked } : {}),
224
- createdAt,
225
- updatedAt,
226
- ...(completedAt !== undefined ? { completedAt } : {}),
227
- };
228
- }
229
-
230
- function formatStatus(status: WorkPlanItemStatus): string {
231
- return status.replace(/_/g, ' ');
232
- }
233
-
234
- /**
235
- * Terminal = the user is done with it. Only these two statuses are eligible for
236
- * the age TTL and the count cap. `blocked` and `failed` still need attention and
237
- * `pending`/`in_progress` are live, so none of them are ever auto-reclaimed.
238
- */
239
- function isTerminalStatus(status: WorkPlanItemStatus): boolean {
240
- return status === 'done' || status === 'cancelled';
241
- }
242
-
243
- /** The timestamp a terminal item is aged from. */
244
- function terminalAgeStamp(item: WorkPlanItem): number {
245
- return item.completedAt ?? item.updatedAt ?? item.createdAt;
246
- }
247
-
248
- function normalizeHousekeeping(value: unknown): WorkPlanHousekeeping | undefined {
249
- if (!isObject(value)) return undefined;
250
- if (typeof value.at !== 'number') return undefined;
251
- const expiredItems = typeof value.expiredItems === 'number' ? value.expiredItems : 0;
252
- const cappedItems = typeof value.cappedItems === 'number' ? value.cappedItems : 0;
253
- const quarantinePath = readString(value.quarantinePath);
254
- const quarantinesRemoved = typeof value.quarantinesRemoved === 'number' ? value.quarantinesRemoved : undefined;
255
- return {
256
- at: value.at,
257
- expiredItems,
258
- cappedItems,
259
- ...(value.resetFromUnreadableFile === true ? { resetFromUnreadableFile: true } : {}),
260
- ...(quarantinePath ? { quarantinePath } : {}),
261
- ...(quarantinesRemoved !== undefined ? { quarantinesRemoved } : {}),
262
- };
263
- }
264
-
265
- /**
266
- * Parse a plan file's CONTENT. Returns null for anything that is not a JSON
267
- * object — empty/whitespace-only (a zero-byte file left by a crash), truncated
268
- * JSON, or a valid JSON scalar/array. Never throws.
269
- */
270
- function parsePlanDocument(raw: string): Record<string, unknown> | null {
271
- if (raw.trim().length === 0) return null;
272
- let parsed: unknown;
273
- try {
274
- parsed = JSON.parse(raw) as unknown;
275
- } catch {
276
- return null;
277
- }
278
- return isObject(parsed) ? parsed : null;
279
- }
280
-
281
- /** One-line, content-free disclosure of what the last sweep reclaimed. */
282
- function formatHousekeeping(record: WorkPlanHousekeeping): string {
283
- const parts: string[] = [];
284
- if (record.resetFromUnreadableFile) {
285
- parts.push(record.quarantinePath
286
- ? `the previous plan file could not be read and was preserved at ${record.quarantinePath}`
287
- : 'the previous plan file was empty or unreadable and was reset');
288
- }
289
- if (record.expiredItems > 0) parts.push(`${record.expiredItems} completed item(s) aged out`);
290
- if (record.cappedItems > 0) parts.push(`${record.cappedItems} completed item(s) over the retention cap removed`);
291
- if (record.quarantinesRemoved && record.quarantinesRemoved > 0) {
292
- parts.push(`${record.quarantinesRemoved} old quarantined plan file(s) deleted`);
293
- }
294
- if (parts.length === 0) parts.push('nothing reclaimed');
295
- return `Housekeeping (${new Date(record.at).toISOString()}): ${parts.join('; ')}.`;
296
- }
297
-
298
- export function nextWorkPlanStatus(status: WorkPlanItemStatus): WorkPlanItemStatus {
299
- switch (status) {
300
- case 'pending':
301
- return 'in_progress';
302
- case 'in_progress':
303
- return 'done';
304
- case 'done':
305
- return 'pending';
306
- case 'blocked':
307
- case 'failed':
308
- case 'cancelled':
309
- return 'pending';
310
- }
311
- }
312
-
313
- export class WorkPlanStore {
314
- readonly filePath: string;
315
-
316
- /**
317
- * Unix ms of this instance's last quarantine-directory scan. Zero means the
318
- * first read of the session sweeps — that read IS the recovery point.
319
- */
320
- private lastQuarantineSweepAt = 0;
321
-
322
- constructor(private readonly options: WorkPlanStoreOptions) {
323
- const fileName = `${safeFileId(options.projectId, options.projectRoot)}.json`;
324
- this.filePath = join(options.homeDirectory, '.goodvibes', 'tui', 'work-plans', fileName);
325
- }
326
-
327
- getActivePlan(): WorkPlan {
328
- return this.readPlan();
329
- }
330
-
331
- listItems(): readonly WorkPlanItem[] {
332
- return this.getActivePlan().items;
333
- }
334
-
335
- addItem(title: string, options: AddWorkPlanItemOptions = {}): WorkPlanItem {
336
- const normalizedTitle = title.trim();
337
- if (!normalizedTitle) throw new Error('Work plan item title is required.');
338
- const plan = this.readPlan();
339
- const time = nowMs();
340
- const item: WorkPlanItem = {
341
- id: createItemId(),
342
- title: normalizedTitle,
343
- status: options.status ?? 'pending',
344
- ...(options.owner ? { owner: options.owner } : {}),
345
- ...(options.source ? { source: options.source } : {}),
346
- ...(options.notes ? { notes: options.notes } : {}),
347
- ...(options.linked ? { linked: options.linked } : {}),
348
- createdAt: time,
349
- updatedAt: time,
350
- ...(options.status === 'done' ? { completedAt: time } : {}),
351
- };
352
- this.writePlan({
353
- ...plan,
354
- items: [...plan.items, item],
355
- activeItemId: item.id,
356
- updatedAt: time,
357
- });
358
- return item;
359
- }
360
-
361
- updateItem(idOrPrefix: string, patch: UpdateWorkPlanItemPatch): WorkPlanItem {
362
- const plan = this.readPlan();
363
- const item = this.resolveItem(plan, idOrPrefix);
364
- const time = nowMs();
365
- const nextStatus = patch.status ?? item.status;
366
- const next: WorkPlanItem = this.pruneItem({
367
- ...item,
368
- ...(patch.title !== undefined ? { title: patch.title.trim() } : {}),
369
- status: nextStatus,
370
- ...(patch.owner !== undefined ? { owner: patch.owner || undefined } : {}),
371
- ...(patch.source !== undefined ? { source: patch.source || undefined } : {}),
372
- ...(patch.notes !== undefined ? { notes: patch.notes || undefined } : {}),
373
- ...(patch.linked !== undefined ? { linked: patch.linked || undefined } : {}),
374
- updatedAt: time,
375
- ...(nextStatus === 'done' ? { completedAt: item.completedAt ?? time } : { completedAt: undefined }),
376
- });
377
- if (!next.title) throw new Error('Work plan item title is required.');
378
- this.writePlan({
379
- ...plan,
380
- items: plan.items.map((candidate) => candidate.id === item.id ? next : candidate),
381
- activeItemId: next.id,
382
- updatedAt: time,
383
- });
384
- return next;
385
- }
386
-
387
- setItemStatus(idOrPrefix: string, status: WorkPlanItemStatus): WorkPlanItem {
388
- return this.updateItem(idOrPrefix, { status });
389
- }
390
-
391
- cycleItemStatus(idOrPrefix: string): WorkPlanItem {
392
- const item = this.resolveItem(this.readPlan(), idOrPrefix);
393
- return this.setItemStatus(item.id, nextWorkPlanStatus(item.status));
394
- }
395
-
396
- removeItem(idOrPrefix: string): WorkPlanItem {
397
- const plan = this.readPlan();
398
- const item = this.resolveItem(plan, idOrPrefix);
399
- const time = nowMs();
400
- const remaining = plan.items.filter((candidate) => candidate.id !== item.id);
401
- this.writePlan({
402
- ...plan,
403
- items: remaining,
404
- activeItemId: remaining[0]?.id,
405
- updatedAt: time,
406
- });
407
- return item;
408
- }
409
-
410
- clearCompleted(): number {
411
- const plan = this.readPlan();
412
- const remaining = plan.items.filter((item) => item.status !== 'done' && item.status !== 'cancelled');
413
- const removed = plan.items.length - remaining.length;
414
- if (removed === 0) return 0;
415
- this.writePlan({
416
- ...plan,
417
- items: remaining,
418
- activeItemId: remaining[0]?.id,
419
- updatedAt: nowMs(),
420
- });
421
- return removed;
422
- }
423
-
424
- /**
425
- * Writes the current plan's `toMarkdown()` output to a sibling `.md` file
426
- * next to the JSON store file, so the checklist can be opened outside the
427
- * TUI. Returns the written path alongside the markdown that was written.
428
- */
429
- exportMarkdown(): { readonly path: string; readonly markdown: string } {
430
- const plan = this.readPlan();
431
- const markdown = this.toMarkdown(plan);
432
- const path = this.filePath.replace(/\.json$/, '.md');
433
- atomicWriteFileSync(path, `${markdown}\n`, { mkdirp: true });
434
- return { path, markdown };
435
- }
436
-
437
- toMarkdown(plan: WorkPlan = this.readPlan()): string {
438
- const lines = [
439
- `# ${plan.title}`,
440
- '',
441
- `Project: ${plan.projectRoot}`,
442
- `Project ID: ${plan.projectId}`,
443
- `Updated: ${new Date(plan.updatedAt).toISOString()}`,
444
- ];
445
- // Disclosure: whatever the last sweep reclaimed is stated here, so a
446
- // removal can never be mistaken for silent data loss.
447
- if (plan.housekeeping) lines.push(formatHousekeeping(plan.housekeeping));
448
- lines.push('');
449
- if (plan.items.length === 0) {
450
- lines.push('No work plan items recorded.');
451
- return lines.join('\n');
452
- }
453
- for (const item of plan.items) {
454
- const marker = item.status === 'done' ? 'x' : ' ';
455
- const suffix = item.status === 'pending' ? '' : ` (${formatStatus(item.status)})`;
456
- lines.push(`- [${marker}] ${item.title}${suffix}`);
457
- if (item.owner) lines.push(` - Owner: ${item.owner}`);
458
- if (item.source) lines.push(` - Source: ${item.source}`);
459
- if (item.notes) lines.push(` - Notes: ${item.notes}`);
460
- }
461
- return lines.join('\n');
462
- }
463
-
464
- /**
465
- * The plan as callers see it: loaded from disk (content-validated), then put
466
- * through the recovery/bounding sweep. When the sweep reclaimed anything the
467
- * bounded plan — carrying its housekeeping disclosure — is written back
468
- * immediately, so the disclosure survives even if the caller only reads.
469
- */
470
- private readPlan(): WorkPlan {
471
- const loaded = this.loadFromDisk();
472
- const swept = this.sweep(loaded.plan, loaded.recovery);
473
- if (swept.changed) this.writePlan(swept.plan);
474
- return swept.plan;
475
- }
476
-
477
- /**
478
- * Read and validate the plan file's CONTENT. A missing file is the ordinary
479
- * empty case. A file that exists but does not parse into a plan object is
480
- * moved aside (never overwritten in place) and reported as a recovery.
481
- */
482
- private loadFromDisk(): { plan: WorkPlan; recovery: WorkPlanHousekeeping | null } {
483
- let raw: string;
484
- try {
485
- raw = readFileSync(this.filePath, 'utf8');
486
- } catch {
487
- // No file yet (the common first-run case), or it is unreadable right now.
488
- // Nothing existed to preserve, so there is nothing to disclose.
489
- return { plan: this.createEmptyPlan(), recovery: null };
490
- }
491
- const parsed = parsePlanDocument(raw);
492
- if (parsed === null) {
493
- // The file EXISTED but its contents are not a usable plan: zero-byte,
494
- // truncated mid-write, or otherwise torn. Existence is not validity.
495
- const quarantinePath = raw.trim().length > 0 ? this.quarantineUnreadableFile() : undefined;
496
- return {
497
- plan: this.createEmptyPlan(),
498
- recovery: {
499
- at: nowMs(),
500
- expiredItems: 0,
501
- cappedItems: 0,
502
- resetFromUnreadableFile: true,
503
- ...(quarantinePath ? { quarantinePath } : {}),
504
- },
505
- };
506
- }
507
- const time = nowMs();
508
- const createdAt = typeof parsed.createdAt === 'number' ? parsed.createdAt : time;
509
- const updatedAt = typeof parsed.updatedAt === 'number' ? parsed.updatedAt : createdAt;
510
- const items = Array.isArray(parsed.items)
511
- ? parsed.items.map((item) => normalizeItem(item, createdAt)).filter((item): item is WorkPlanItem => item !== null)
512
- : [];
513
- const activeItemId = readString(parsed.activeItemId);
514
- const source = readString(parsed.source);
515
- const housekeeping = normalizeHousekeeping(parsed.housekeeping);
516
- return {
517
- plan: {
518
- id: readString(parsed.id) ?? createPlanId(this.options.projectId, this.options.projectRoot),
519
- projectId: readString(parsed.projectId) ?? this.options.projectId,
520
- projectRoot: readString(parsed.projectRoot) ?? this.options.projectRoot,
521
- title: readString(parsed.title) ?? 'Work Plan',
522
- items,
523
- ...(activeItemId && items.some((item) => item.id === activeItemId) ? { activeItemId } : {}),
524
- ...(source ? { source } : {}),
525
- createdAt,
526
- updatedAt,
527
- ...(housekeeping ? { housekeeping } : {}),
528
- },
529
- recovery: null,
530
- };
531
- }
532
-
533
- /**
534
- * Apply both bounds (age TTL, then count cap) over TERMINAL items only, plus
535
- * the throttled quarantine-file sweep. Pure with respect to disk except for
536
- * quarantine deletions; the caller persists the result when `changed`.
537
- *
538
- * Idempotent: re-running it on the returned plan reclaims nothing further,
539
- * which is what makes it safe to run concurrently from several processes.
540
- */
541
- private sweep(
542
- plan: WorkPlan,
543
- recovery: WorkPlanHousekeeping | null,
544
- ): { plan: WorkPlan; changed: boolean } {
545
- const now = nowMs();
546
- const expired = new Set<WorkPlanItem>();
547
- for (const item of plan.items) {
548
- if (!isTerminalStatus(item.status)) continue;
549
- if (now - terminalAgeStamp(item) > WORK_PLAN_TERMINAL_ITEM_TTL_MS) expired.add(item);
550
- }
551
- const survivors = plan.items.filter((item) => !expired.has(item));
552
- // Count cap applies to terminal survivors only — newest completions kept.
553
- const terminalSurvivors = survivors
554
- .filter((item) => isTerminalStatus(item.status))
555
- .sort((a, b) => terminalAgeStamp(b) - terminalAgeStamp(a));
556
- const capped = new Set<WorkPlanItem>(terminalSurvivors.slice(WORK_PLAN_TERMINAL_ITEM_CAP));
557
- const kept = survivors.filter((item) => !capped.has(item));
558
- const quarantinesRemoved = this.sweepQuarantines(now);
559
-
560
- const changed = expired.size > 0 || capped.size > 0 || recovery !== null || quarantinesRemoved > 0;
561
- if (!changed) return { plan, changed: false };
562
-
563
- const housekeeping: WorkPlanHousekeeping = {
564
- at: now,
565
- expiredItems: expired.size,
566
- cappedItems: capped.size,
567
- ...(recovery?.resetFromUnreadableFile ? { resetFromUnreadableFile: true } : {}),
568
- ...(recovery?.quarantinePath ? { quarantinePath: recovery.quarantinePath } : {}),
569
- ...(quarantinesRemoved > 0 ? { quarantinesRemoved } : {}),
570
- };
571
- const activeItemId = plan.activeItemId && kept.some((item) => item.id === plan.activeItemId)
572
- ? plan.activeItemId
573
- : undefined;
574
- return {
575
- plan: {
576
- id: plan.id,
577
- projectId: plan.projectId,
578
- projectRoot: plan.projectRoot,
579
- title: plan.title,
580
- items: kept,
581
- ...(activeItemId ? { activeItemId } : {}),
582
- ...(plan.source ? { source: plan.source } : {}),
583
- createdAt: plan.createdAt,
584
- // `updatedAt` tracks the user's edits; a garbage-collection pass is not
585
- // one, so it is deliberately left alone. housekeeping.at dates the sweep.
586
- updatedAt: plan.updatedAt,
587
- housekeeping,
588
- },
589
- changed: true,
590
- };
591
- }
592
-
593
- /**
594
- * Move an unparseable plan file aside so the user can still recover it.
595
- * Returns the quarantine path, or undefined if the rename did not happen
596
- * (already moved by a concurrent process, or the directory is not writable).
597
- */
598
- private quarantineUnreadableFile(): string | undefined {
599
- const target = `${this.filePath}${QUARANTINE_SUFFIX}${nowMs()}-${randomUUID().slice(0, 8)}`;
600
- try {
601
- renameSync(this.filePath, target);
602
- return target;
603
- } catch {
604
- // Gone already (another process quarantined it first) or unwritable —
605
- // either way the plan still degrades to empty rather than throwing.
606
- return undefined;
607
- }
608
- }
609
-
610
- /**
611
- * Bound the quarantine copies: delete any past WORK_PLAN_QUARANTINE_TTL_MS
612
- * and any beyond WORK_PLAN_QUARANTINE_CAP (oldest first). Throttled to
613
- * WORK_PLAN_QUARANTINE_SWEEP_INTERVAL_MS per store instance so a long-lived
614
- * session keeps sweeping without scanning the directory on every read.
615
- */
616
- private sweepQuarantines(now: number): number {
617
- if (now - this.lastQuarantineSweepAt < WORK_PLAN_QUARANTINE_SWEEP_INTERVAL_MS) return 0;
618
- this.lastQuarantineSweepAt = now;
619
- const directory = dirname(this.filePath);
620
- const prefix = `${basename(this.filePath)}${QUARANTINE_SUFFIX}`;
621
- let entries: string[];
622
- try {
623
- entries = readdirSync(directory);
624
- } catch {
625
- return 0;
626
- }
627
- const candidates = entries
628
- .filter((entry) => entry.startsWith(prefix))
629
- .map((entry) => {
630
- const stamp = Number.parseInt(entry.slice(prefix.length).split('-')[0] ?? '', 10);
631
- return { entry, at: Number.isFinite(stamp) ? stamp : 0 };
632
- })
633
- .sort((a, b) => b.at - a.at); // newest first
634
- let removed = 0;
635
- for (let index = 0; index < candidates.length; index += 1) {
636
- const candidate = candidates[index]!;
637
- const tooOld = now - candidate.at > WORK_PLAN_QUARANTINE_TTL_MS;
638
- const overCap = index >= WORK_PLAN_QUARANTINE_CAP;
639
- if (!tooOld && !overCap) continue;
640
- try {
641
- unlinkSync(join(directory, candidate.entry));
642
- removed += 1;
643
- } catch {
644
- // Already removed by a concurrent sweep — idempotent, not an error, and
645
- // not counted so the disclosed number stays truthful.
646
- }
647
- }
648
- return removed;
649
- }
650
-
651
- private createEmptyPlan(): WorkPlan {
652
- const time = nowMs();
653
- return {
654
- id: createPlanId(this.options.projectId, this.options.projectRoot),
655
- projectId: this.options.projectId,
656
- projectRoot: this.options.projectRoot,
657
- title: 'Work Plan',
658
- items: [],
659
- source: 'tui',
660
- createdAt: time,
661
- updatedAt: time,
662
- };
663
- }
664
-
665
- private writePlan(plan: WorkPlan): void {
666
- atomicWriteFileSync(this.filePath, `${JSON.stringify(plan, null, 2)}\n`, { mkdirp: true });
667
- }
668
-
669
- private resolveItem(plan: WorkPlan, idOrPrefix: string): WorkPlanItem {
670
- const needle = idOrPrefix.trim();
671
- if (!needle) throw new Error('Work plan item id is required.');
672
- const exact = plan.items.find((item) => item.id === needle);
673
- if (exact) return exact;
674
- const matches = plan.items.filter((item) => item.id.startsWith(needle));
675
- if (matches.length === 1) return matches[0]!;
676
- if (matches.length > 1) {
677
- throw new Error(`Work plan item id "${needle}" is ambiguous: ${matches.map((item) => item.id).join(', ')}`);
678
- }
679
- throw new Error(`Work plan item not found: ${needle}`);
680
- }
681
-
682
- private pruneItem(item: WorkPlanItem & {
683
- owner?: string | undefined;
684
- source?: string | undefined;
685
- notes?: string | undefined;
686
- linked?: WorkPlanLinkTargets | undefined;
687
- completedAt?: number | undefined;
688
- }): WorkPlanItem {
689
- return {
690
- id: item.id,
691
- title: item.title,
692
- status: item.status,
693
- ...(item.owner ? { owner: item.owner } : {}),
694
- ...(item.source ? { source: item.source } : {}),
695
- ...(item.notes ? { notes: item.notes } : {}),
696
- ...(item.linked ? { linked: item.linked } : {}),
697
- createdAt: item.createdAt,
698
- updatedAt: item.updatedAt,
699
- ...(item.completedAt !== undefined ? { completedAt: item.completedAt } : {}),
700
- };
701
- }
702
- }