@moretea-labs/forge 1.6.0 → 1.7.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 (469) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +39 -0
  3. package/CLAUDE.md +4 -0
  4. package/README.md +2 -2
  5. package/README.zh-CN.md +1 -1
  6. package/SKILL.md +2 -2
  7. package/THIRD_PARTY_NOTICES.md +1 -1
  8. package/agents/openai.yaml +1 -1
  9. package/assets/hooks/hook-input.sh +11 -8
  10. package/assets/hooks/lib/session-state.sh +10 -7
  11. package/assets/hooks/lib/workflow-state.sh +13 -177
  12. package/assets/hooks/post-tool-observer.sh +8 -2
  13. package/assets/hooks/prompt-guard.sh +5 -81
  14. package/assets/hooks/session-start-context.sh +56 -1
  15. package/assets/partials/05-workflow.partial.md +1 -1
  16. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  17. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  18. package/assets/plugin-registry.v1.json +3 -3
  19. package/assets/reference-configs/document-generation.md +1 -1
  20. package/assets/reference-configs/harness-overview.md +1 -1
  21. package/assets/reference-configs/sprint-contracts.md +3 -3
  22. package/assets/skill-version.json +2 -2
  23. package/assets/templates/contract.template.md +2 -7
  24. package/assets/templates/helpers/capture-plan.sh +2 -2
  25. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  26. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  27. package/assets/templates/helpers/contract-worktree.sh +1 -21
  28. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  29. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  32. package/assets/templates/helpers/new-plan.sh +1 -1
  33. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  34. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/helpers/workflow-contract.ts +5 -1
  38. package/assets/templates/plan.template.md +1 -1
  39. package/assets/templates/review.template.md +9 -34
  40. package/assets/workflow-contract.v1.json +10 -19
  41. package/docs/README.md +10 -10
  42. package/docs/architecture/CURRENT.md +178 -0
  43. package/docs/architecture/EVOLUTION.md +42 -0
  44. package/docs/architecture/current/README.md +4 -156
  45. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  46. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  47. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  48. package/docs/architecture/history/README.md +7 -0
  49. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  50. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  51. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  52. package/docs/architecture/history.md +3 -34
  53. package/docs/architecture/index.md +15 -98
  54. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  55. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  56. package/docs/architecture/versions/1.6.md +38 -0
  57. package/docs/architecture/versions/1.7.md +41 -0
  58. package/docs/forge-chatgpt-browser-engine.md +2 -2
  59. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  60. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  61. package/docs/forge-github-issue-launcher.md +1 -1
  62. package/docs/forge-plugin-management.md +1 -1
  63. package/docs/forge-runtime-self-healing-loop.md +1 -1
  64. package/docs/operations/controller-browser-plugin.md +26 -6
  65. package/docs/operations/controller-performance-and-502.md +143 -0
  66. package/docs/operations/controller-reliability-runbook.md +1 -1
  67. package/docs/operations/features.md +2 -2
  68. package/docs/operations/github-repository.md +1 -1
  69. package/docs/operations/homebrew.md +2 -2
  70. package/docs/operations/mcp-tool-exposure.md +5 -3
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Integrations.md +2 -2
  74. package/docs/wiki/Work-Lifecycle.md +1 -1
  75. package/package.json +33 -11
  76. package/scripts/activate-source-baseline.ts +17 -13
  77. package/scripts/architecture-event.ts +2 -820
  78. package/scripts/architecture-queue.sh +2 -645
  79. package/scripts/archive-architecture-request.sh +2 -242
  80. package/scripts/archive-workflow.sh +1 -291
  81. package/scripts/benchmark-background-check-overlap.ts +228 -0
  82. package/scripts/benchmark-mcp-host.sh +117 -0
  83. package/scripts/benchmark-mcp-transport.ts +170 -0
  84. package/scripts/benchmark-quality-harness.ts +146 -0
  85. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  86. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  87. package/scripts/benchmark-typescript-navigation.ts +186 -0
  88. package/scripts/benchmark-work-efficiency.ts +334 -0
  89. package/scripts/capability-config.ts +2 -392
  90. package/scripts/capability-resolver.ts +2 -503
  91. package/scripts/capture-plan.sh +1 -332
  92. package/scripts/check-agent-tooling.sh +2 -1558
  93. package/scripts/check-architecture-sync.sh +2 -371
  94. package/scripts/check-brain-manifest.sh +2 -253
  95. package/scripts/check-context-files.sh +2 -47
  96. package/scripts/check-deploy-sql-order.sh +2 -115
  97. package/scripts/check-mcp-compatibility.ts +14 -0
  98. package/scripts/check-runtime-architecture.mjs +349 -79
  99. package/scripts/check-skill-version.ts +3 -191
  100. package/scripts/check-source-duplication.mjs +61 -0
  101. package/scripts/check-stable-baseline.ts +92 -0
  102. package/scripts/check-task-sync.sh +2 -63
  103. package/scripts/check-task-workflow.sh +2 -1000
  104. package/scripts/codex-handoff-resume.sh +2 -267
  105. package/scripts/context-contract-sync.sh +2 -514
  106. package/scripts/contract-run.ts +2 -463
  107. package/scripts/contract-worktree.sh +1 -722
  108. package/scripts/ensure-task-workflow.sh +1 -1225
  109. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  110. package/scripts/inspect-project-state.ts +4 -279
  111. package/scripts/install-standalone-recovery.ts +24 -0
  112. package/scripts/lib/project-init-lib.sh +22 -46
  113. package/scripts/lib/run-forge-helper.sh +19 -0
  114. package/scripts/lib/run-forge-helper.ts +26 -0
  115. package/scripts/migrate-project-template.sh +1 -1189
  116. package/scripts/migrate-workflow-docs.ts +4 -412
  117. package/scripts/new-plan.sh +1 -180
  118. package/scripts/new-spec.sh +1 -47
  119. package/scripts/new-sprint.sh +1 -46
  120. package/scripts/plan-to-todo.sh +1 -796
  121. package/scripts/prepare-codex-handoff.sh +2 -201
  122. package/scripts/prepare-handoff.sh +1 -78
  123. package/scripts/refresh-current-status.sh +2 -467
  124. package/scripts/run-governed-gate.ts +5 -1
  125. package/scripts/select-agent-context-blocks.sh +2 -64
  126. package/scripts/ship-worktrees.sh +1 -558
  127. package/scripts/smoke-ios-physical-device.ts +4 -2
  128. package/scripts/smoke-runtime-recovery.ts +1 -35
  129. package/scripts/smoke-schedule-engine.ts +1 -20
  130. package/scripts/sprint-backlog.sh +1 -834
  131. package/scripts/stage-runtime-release.ts +2 -1
  132. package/scripts/summarize-failures.sh +2 -123
  133. package/scripts/switch-plan.sh +1 -150
  134. package/scripts/sync-brain-docs.sh +2 -379
  135. package/scripts/sync-generated-authority.mjs +175 -0
  136. package/scripts/verify-contract.sh +2 -703
  137. package/scripts/verify-forge-runtime.sh +0 -1
  138. package/scripts/verify-sprint.sh +1 -677
  139. package/scripts/workflow-contract.ts +4 -205
  140. package/scripts/workstream-sync.sh +2 -261
  141. package/src/cli/agent-jobs/executor-health.ts +1 -1
  142. package/src/cli/commands/init-hook.ts +3 -3
  143. package/src/cli/commands/mcp.ts +9 -5
  144. package/src/cli/commands/plugin.ts +12 -3
  145. package/src/cli/commands/recovery.ts +25 -3
  146. package/src/cli/commands/runtime.ts +55 -3
  147. package/src/cli/commands/security.ts +12 -1
  148. package/src/cli/commands/setup-profile.ts +31 -21
  149. package/src/cli/controller/check-runner.ts +113 -5
  150. package/src/cli/controller/composite-result.ts +1 -1
  151. package/src/cli/controller/context/focus.ts +51 -0
  152. package/src/cli/controller/context/instruction-context.ts +114 -0
  153. package/src/cli/controller/context/known-paths.ts +76 -0
  154. package/src/cli/controller/context/query-planning.ts +110 -0
  155. package/src/cli/controller/context/rule-resolution.ts +532 -0
  156. package/src/cli/controller/context/source-materializer.ts +278 -0
  157. package/src/cli/controller/context/structural.ts +16 -0
  158. package/src/cli/controller/context/types.ts +286 -0
  159. package/src/cli/controller/context-pack.ts +828 -487
  160. package/src/cli/controller/launch-agents.ts +77 -2
  161. package/src/cli/controller/operational-plan.ts +2 -2
  162. package/src/cli/controller/work-mode.ts +6 -4
  163. package/src/cli/editing/edit-session.ts +60 -7
  164. package/src/cli/github/contracts.ts +16 -0
  165. package/src/cli/github/github.ts +135 -16
  166. package/src/cli/github/session.ts +2 -1
  167. package/src/cli/installer/managed-entries.ts +15 -1
  168. package/src/cli/local-bridge/console-automations.ts +54 -78
  169. package/src/cli/local-bridge/console-view-models.ts +1 -35
  170. package/src/cli/local-bridge/facade-api.ts +37 -264
  171. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  172. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  173. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  174. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  175. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  176. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  177. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  178. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  179. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  180. package/src/cli/local-bridge/job-store.ts +1 -490
  181. package/src/cli/local-bridge/server.ts +2 -515
  182. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  183. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  184. package/src/cli/mcp/auth.ts +5 -5
  185. package/src/cli/mcp/instructions.ts +3 -2
  186. package/src/cli/mcp/legacy-tool-service.ts +10 -222
  187. package/src/cli/mcp/multi-repository.ts +16 -5
  188. package/src/cli/mcp/oauth.ts +175 -26
  189. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  190. package/src/cli/mcp/repository-tools.ts +282 -146
  191. package/src/cli/mcp/server.ts +586 -116
  192. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  193. package/src/cli/mcp/tool-contract.ts +14 -0
  194. package/src/cli/mcp/toolset-selection.ts +1 -1
  195. package/src/cli/mcp/toolset.ts +44 -7
  196. package/src/cli/mcp/transports/http.ts +124 -14
  197. package/src/cli/mcp/transports/session-registry.ts +43 -5
  198. package/src/cli/mcp/types.ts +3 -3
  199. package/src/cli/repositories/command-classifier.ts +103 -21
  200. package/src/cli/repositories/command-executor.ts +78 -526
  201. package/src/cli/repositories/command-process.ts +108 -0
  202. package/src/cli/repositories/command-scope.ts +99 -41
  203. package/src/cli/repositories/controller-home.ts +7 -8
  204. package/src/cli/repositories/git-command-executor.ts +2 -0
  205. package/src/cli/repositories/locks.ts +53 -16
  206. package/src/cli/repositories/repository-snapshot.ts +124 -0
  207. package/src/cli/repositories/selected-path-actions.ts +17 -9
  208. package/src/cli/repositories/structured-git.ts +118 -6
  209. package/src/cli/repository/inspector.ts +563 -65
  210. package/src/cli/repository/session-cache.ts +58 -17
  211. package/src/cli/v81-entry.ts +1 -4
  212. package/src/effects/fs-transaction.ts +85 -17
  213. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  214. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  215. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  216. package/src/runtime/context/lsp-client.ts +283 -0
  217. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  218. package/src/runtime/context/semantic-navigation.ts +223 -0
  219. package/src/runtime/context/swift-navigation.ts +565 -0
  220. package/src/runtime/context/typescript-navigation.ts +226 -0
  221. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  222. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +48 -16
  223. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  224. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  225. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  226. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  227. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  228. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  229. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  230. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  231. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  232. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  233. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  234. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  235. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  236. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  237. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  238. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  239. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  240. package/src/runtime/control-plane/execution/work-task-receipt.ts +22 -18
  241. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +156 -11
  242. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  243. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  244. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  245. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +90 -9
  246. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  247. package/src/runtime/control-plane/facade/controller-round-relay.ts +1118 -0
  248. package/src/runtime/control-plane/facade/controller-session-store.ts +302 -12
  249. package/src/runtime/control-plane/facade/goal-workloop-access.ts +17 -1
  250. package/src/runtime/control-plane/facade/goal-workloop.ts +928 -190
  251. package/src/runtime/control-plane/facade/index.ts +3 -0
  252. package/src/runtime/control-plane/facade/plan-contract-store.ts +275 -42
  253. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  254. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  255. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  256. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  257. package/src/runtime/control-plane/facade/types.ts +90 -10
  258. package/src/runtime/control-plane/facade/work-contract-store.ts +157 -261
  259. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  260. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  261. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  262. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  263. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  264. package/src/runtime/control-plane/global-scheduler/maintenance.ts +152 -0
  265. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  266. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  267. package/src/runtime/control-plane/global-scheduler/scheduler.ts +278 -543
  268. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  269. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  270. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  271. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  272. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  273. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  274. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  275. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  276. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  277. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  278. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +63 -0
  279. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +365 -117
  280. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +89 -0
  281. package/src/runtime/control-plane/launcher/thin-launcher.ts +244 -11
  282. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  283. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  284. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  285. package/src/runtime/control-plane/release-retention.ts +62 -2
  286. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  287. package/src/runtime/control-plane/routing/route-policy.ts +24 -21
  288. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  289. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  290. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  291. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  292. package/src/runtime/diagnostics/performance.ts +24 -1
  293. package/src/runtime/evidence/event-ledger.ts +1 -1
  294. package/src/runtime/evidence/process-check-execution.ts +18 -0
  295. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  296. package/src/runtime/execution/jobs/store.ts +58 -4
  297. package/src/runtime/execution/jobs/types.ts +1 -1
  298. package/src/runtime/execution/managed-workspace.ts +203 -1
  299. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  300. package/src/runtime/execution/process-runtime/check-result.ts +88 -0
  301. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  302. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  303. package/src/runtime/execution/process-runtime/command-facade.ts +367 -40
  304. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  305. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1201 -0
  306. package/src/runtime/execution/process-runtime/persisted-check.ts +51 -5
  307. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  308. package/src/runtime/execution/process-runtime/runtime.ts +53 -3
  309. package/src/runtime/execution/process-runtime/store.ts +1 -0
  310. package/src/runtime/execution/process-runtime/types.ts +4 -12
  311. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  312. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  313. package/src/runtime/execution/work-activity.ts +21 -0
  314. package/src/runtime/execution/workers/executor.ts +18 -61
  315. package/src/runtime/gateway/mcp/execution-tools.ts +35 -1852
  316. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  317. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  318. package/src/runtime/gateway/mcp/router.ts +186 -84
  319. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +565 -0
  320. package/src/runtime/gateway/mcp/runtime-tools.ts +2378 -1947
  321. package/src/runtime/plugins/app-store-connect-adapter.ts +216 -47
  322. package/src/runtime/plugins/browser-adapter.ts +2662 -829
  323. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  324. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  325. package/src/runtime/plugins/browser-handoff.ts +110 -5
  326. package/src/runtime/plugins/browser-macos-bridge.ts +701 -63
  327. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  328. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  329. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  330. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  331. package/src/runtime/plugins/browser-runtime-contract.ts +154 -0
  332. package/src/runtime/plugins/browser-runtime.ts +193 -0
  333. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  334. package/src/runtime/plugins/browser-session-store.ts +121 -0
  335. package/src/runtime/plugins/browser-session-types.ts +98 -0
  336. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  337. package/src/runtime/plugins/desktop-operator-registration.ts +10 -4
  338. package/src/runtime/plugins/external-adapter.ts +358 -3
  339. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  340. package/src/runtime/plugins/first-party-registry.ts +2 -0
  341. package/src/runtime/plugins/github-adapter.ts +125 -0
  342. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  343. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  344. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  345. package/src/runtime/plugins/ios-adapter.ts +106 -16
  346. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  347. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  348. package/src/runtime/plugins/lightweight-action.ts +153 -0
  349. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  350. package/src/runtime/plugins/macos-capability-broker.ts +47 -9
  351. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  352. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  353. package/src/runtime/plugins/resend-adapter.ts +1 -1
  354. package/src/runtime/plugins/store.ts +330 -33
  355. package/src/runtime/plugins/types.ts +38 -3
  356. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  357. package/src/runtime/projections/git-status-sampler.ts +16 -16
  358. package/src/runtime/projections/materialized-view.ts +28 -10
  359. package/src/runtime/recovery/maintenance-executor.ts +426 -25
  360. package/src/runtime/root/mcp-transport.ts +180 -10
  361. package/src/runtime/root/package-connector-service.ts +230 -10
  362. package/src/runtime/root/package-runtime-release.ts +143 -17
  363. package/src/runtime/root/package-runtime-service.ts +461 -22
  364. package/src/runtime/root/release-manifest.ts +144 -18
  365. package/src/runtime/root/release-materialize.ts +125 -32
  366. package/src/runtime/root/release-store.ts +48 -11
  367. package/src/runtime/root/runtime.ts +23 -5
  368. package/src/runtime/root/service.ts +100 -29
  369. package/src/runtime/root/types.ts +12 -0
  370. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  371. package/src/runtime/shared/trusted-executable.ts +62 -0
  372. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  373. package/src/runtime/standalone-recovery/core.ts +1079 -240
  374. package/src/runtime/standalone-recovery/entry.ts +100 -24
  375. package/src/runtime/standalone-recovery/installer.ts +20 -4
  376. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  377. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  378. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  379. package/src/runtime/workflow/schedules/engine.ts +364 -202
  380. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  381. package/src/runtime/workflow/schedules/settlement.ts +64 -17
  382. package/src/runtime/workflow/schedules/store.ts +209 -19
  383. package/src/runtime/workflow/schedules/types.ts +2 -4
  384. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  385. package/src/testing/test-governance.ts +8 -40
  386. package/assets/reference-configs/heartbeat-triage.md +0 -57
  387. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  388. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  389. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  390. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  391. package/docs/architecture/current/architecture-invariants.md +0 -331
  392. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  393. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  394. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  395. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  396. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  397. package/docs/architecture/current/entity-model.md +0 -612
  398. package/docs/architecture/current/external-plugin-governance.md +0 -396
  399. package/docs/architecture/current/failure-recovery.md +0 -447
  400. package/docs/architecture/current/governance.md +0 -213
  401. package/docs/architecture/current/human-interaction-plane.md +0 -87
  402. package/docs/architecture/current/implementation-status.md +0 -128
  403. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  404. package/docs/architecture/current/legacy-governance.md +0 -58
  405. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  406. package/docs/architecture/current/migration-roadmap.md +0 -105
  407. package/docs/architecture/current/multi-repository-execution.md +0 -371
  408. package/docs/architecture/current/operations-runbook.md +0 -45
  409. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  410. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  411. package/docs/architecture/current/repository-access-modes.md +0 -102
  412. package/docs/architecture/current/route-policy.md +0 -15
  413. package/docs/architecture/current/route-session-concurrency.md +0 -85
  414. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  415. package/docs/architecture/current/runtime-directory-map.md +0 -49
  416. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  417. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  418. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  419. package/docs/architecture/current/system-overview.md +0 -169
  420. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  421. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  422. package/docs/architecture/current/thin-harness-v1.md +0 -321
  423. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  424. package/docs/forge-autonomous-goal-loop.md +0 -9
  425. package/docs/forge-provider-configuration.md +0 -7
  426. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  427. package/docs/operations/gmail-assistant-routines.md +0 -186
  428. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  429. package/docs/operations/provider-configuration.md +0 -31
  430. package/scripts/harness-trace-grade.sh +0 -150
  431. package/scripts/heartbeat-triage.sh +0 -361
  432. package/scripts/maintenance-triage.sh +0 -167
  433. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  434. package/src/cli/repositories/goal-registry.ts +0 -239
  435. package/src/runtime/assistant/action-proposals.ts +0 -279
  436. package/src/runtime/assistant/intent.ts +0 -395
  437. package/src/runtime/assistant/model-provider.ts +0 -460
  438. package/src/runtime/assistant/openapi.ts +0 -223
  439. package/src/runtime/assistant/policy.ts +0 -89
  440. package/src/runtime/assistant/readiness.ts +0 -258
  441. package/src/runtime/assistant/routine-runtime.ts +0 -477
  442. package/src/runtime/assistant/schedule-binding.ts +0 -171
  443. package/src/runtime/assistant/standing-grants.ts +0 -375
  444. package/src/runtime/assistant/store.ts +0 -243
  445. package/src/runtime/assistant/types.ts +0 -135
  446. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  447. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  448. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  449. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  450. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  451. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  452. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  453. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  454. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  455. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  456. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  457. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  458. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  459. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  460. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  461. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  462. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  463. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  464. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  465. package/src/runtime/workflow/findings/store.ts +0 -204
  466. package/src/runtime/workflow/findings/types.ts +0 -58
  467. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  468. package/src/runtime/workflow/portfolio/store.ts +0 -185
  469. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -0,0 +1,193 @@
1
+ import { AssistantPluginError } from './errors';
2
+ import type { AssistantPluginActionExecutionInput } from './types';
3
+ import {
4
+ validateBrowserTransaction,
5
+ type BrowserActionReplaySafety,
6
+ type BrowserForegroundRequirement,
7
+ type BrowserProviderCapability,
8
+ type BrowserTargetIdentity,
9
+ type BrowserTransaction,
10
+ } from './browser-runtime-contract';
11
+ import {
12
+ BrowserProviderRegistry,
13
+ BrowserProviderUnavailableBeforeActionError,
14
+ type BrowserRuntimeProvider,
15
+ } from './browser-provider-registry';
16
+
17
+ export const ALL_BROWSER_PROVIDER_CAPABILITIES = [
18
+ 'dom.read',
19
+ 'dom.interact',
20
+ 'browser.internal_resources',
21
+ 'browser.screenshot',
22
+ 'browser.trusted_input',
23
+ 'browser.foreground',
24
+ 'browser.transaction',
25
+ 'browser.persistent_handle',
26
+ ] as const satisfies readonly BrowserProviderCapability[];
27
+
28
+ const READ_ACTIONS = new Set([
29
+ 'list_sessions', 'reconcile_sessions', 'get_handoff_status',
30
+ 'get_text', 'get_html', 'query_selector', 'query_all', 'get_attribute', 'list_frames', 'verify_state',
31
+ 'extract_links', 'extract_tables', 'extract_forms', 'snapshot_interactive', 'get_console_errors', 'get_failed_requests',
32
+ 'wait_for_load_state', 'wait_for_selector', 'await_file_transfer',
33
+ ]);
34
+ const SCREENSHOT_ACTIONS = new Set(['screenshot']);
35
+ const TRUSTED_INPUT_ACTIONS = new Set(['trusted_input', 'activate_page']);
36
+ const DOM_INTERACTION_ACTIONS = new Set([
37
+ 'create_session', 'open_page', 'navigate', 'reload', 'go_back',
38
+ 'click', 'click_text', 'double_click', 'hover', 'focus', 'type', 'fill', 'select_option', 'check', 'uncheck',
39
+ 'press', 'keyboard_shortcut', 'dispatch_event', 'attach_local_file', 'close_page',
40
+ ]);
41
+ const IDEMPOTENT_ACTIONS = new Set(['configure', 'close_session', 'clear_session', 'request_human_handoff', 'resolve_handoff']);
42
+
43
+ interface BrowserRuntimeActionPolicy {
44
+ requiredCapabilities: readonly BrowserProviderCapability[];
45
+ foreground: BrowserForegroundRequirement;
46
+ replaySafety: BrowserActionReplaySafety;
47
+ }
48
+
49
+ const registries = new Map<string, BrowserProviderRegistry<AssistantPluginActionExecutionInput, Record<string, unknown>>>();
50
+
51
+ function registryFor(runtimeKey: string): BrowserProviderRegistry<AssistantPluginActionExecutionInput, Record<string, unknown>> {
52
+ let registry = registries.get(runtimeKey);
53
+ if (!registry) {
54
+ registry = new BrowserProviderRegistry();
55
+ registries.set(runtimeKey, registry);
56
+ }
57
+ return registry;
58
+ }
59
+
60
+ export function invalidateBrowserRuntime(runtimeKey: string, reason: string): void {
61
+ registries.get(runtimeKey)?.invalidateAll(reason);
62
+ }
63
+
64
+ export function browserRuntimeActionPolicy(actionId: string): BrowserRuntimeActionPolicy {
65
+ if (SCREENSHOT_ACTIONS.has(actionId)) {
66
+ return { requiredCapabilities: ['browser.screenshot', 'browser.transaction'], foreground: 'none', replaySafety: 'read_only' };
67
+ }
68
+ if (TRUSTED_INPUT_ACTIONS.has(actionId)) {
69
+ return {
70
+ requiredCapabilities: ['browser.trusted_input', 'browser.foreground', 'browser.transaction'],
71
+ foreground: 'explicit_required',
72
+ replaySafety: 'non_idempotent',
73
+ };
74
+ }
75
+ if (READ_ACTIONS.has(actionId)) {
76
+ return { requiredCapabilities: ['dom.read', 'browser.transaction'], foreground: 'none', replaySafety: 'read_only' };
77
+ }
78
+ if (DOM_INTERACTION_ACTIONS.has(actionId)) {
79
+ return { requiredCapabilities: ['dom.interact', 'browser.transaction'], foreground: 'none', replaySafety: 'non_idempotent' };
80
+ }
81
+ return {
82
+ requiredCapabilities: ['browser.transaction'],
83
+ foreground: 'none',
84
+ replaySafety: IDEMPOTENT_ACTIONS.has(actionId) ? 'idempotent' : 'non_idempotent',
85
+ };
86
+ }
87
+
88
+
89
+ /**
90
+ * Post-dispatch replay is a semantic BrowserRuntime decision. Only read-only
91
+ * actions fenced to an already-existing resource may be replayed after a
92
+ * transient provider failure; mutations retain unknown-outcome semantics.
93
+ */
94
+ export function browserActionCanReplayAfterDispatch(actionId: string, requireExistingResource = true): boolean {
95
+ return requireExistingResource && browserRuntimeActionPolicy(actionId).replaySafety === 'read_only';
96
+ }
97
+
98
+ function stringArgument(args: Record<string, unknown>, key: string): string | undefined {
99
+ const value = args[key];
100
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
101
+ }
102
+
103
+ function stableTargetFor(input: AssistantPluginActionExecutionInput, providerId: string): BrowserTargetIdentity {
104
+ const sessionId = stringArgument(input.args, 'session_id');
105
+ const nativeProduct = stringArgument(input.args, 'native_browser_product');
106
+ const nativeWindowId = stringArgument(input.args, 'native_window_id');
107
+ const nativeTabId = stringArgument(input.args, 'native_tab_id');
108
+ if (nativeWindowId && nativeTabId) {
109
+ return {
110
+ providerId,
111
+ resourceKind: 'tab',
112
+ resourceId: `window:${nativeWindowId}/tab:${nativeTabId}`,
113
+ ...(nativeProduct ? { browserProduct: nativeProduct } : {}),
114
+ ownership: 'user_owned',
115
+ };
116
+ }
117
+ if (sessionId) {
118
+ return {
119
+ providerId,
120
+ resourceKind: 'page',
121
+ resourceId: `session:${sessionId}`,
122
+ ownership: 'provider_owned',
123
+ };
124
+ }
125
+ return {
126
+ providerId,
127
+ resourceKind: 'page',
128
+ resourceId: `request:${input.requestId}:${input.actionId}`,
129
+ ownership: 'plugin_owned',
130
+ };
131
+ }
132
+
133
+ function transactionFor(
134
+ input: AssistantPluginActionExecutionInput,
135
+ providerId: string,
136
+ policy: BrowserRuntimeActionPolicy,
137
+ ): BrowserTransaction {
138
+ return {
139
+ transactionId: input.requestId,
140
+ target: stableTargetFor(input, providerId),
141
+ requiredCapabilities: policy.requiredCapabilities,
142
+ foreground: policy.foreground,
143
+ replaySafety: policy.replaySafety,
144
+ action: {
145
+ kind: policy.replaySafety === 'read_only' ? 'read' : policy.foreground === 'explicit_required' ? 'trusted_input' : 'dom',
146
+ operation: input.actionId,
147
+ arguments: input.args,
148
+ },
149
+ ...(policy.replaySafety === 'read_only'
150
+ ? {}
151
+ : { postconditions: [{ kind: 'provider_assertion', assertion: 'semantic_action_result', expected: true }] as const }),
152
+ timeoutMs: typeof input.args.timeout_ms === 'number' && Number.isFinite(input.args.timeout_ms)
153
+ ? Math.max(1, Math.trunc(input.args.timeout_ms))
154
+ : 60_000,
155
+ };
156
+ }
157
+
158
+ export async function executeBrowserRuntimeAction(options: {
159
+ runtimeKey: string;
160
+ input: AssistantPluginActionExecutionInput;
161
+ providers: readonly BrowserRuntimeProvider<AssistantPluginActionExecutionInput, Record<string, unknown>>[];
162
+ }): Promise<Record<string, unknown>> {
163
+ const registry = registryFor(options.runtimeKey);
164
+ for (const provider of options.providers) registry.register(provider);
165
+
166
+ const policy = browserRuntimeActionPolicy(options.input.actionId);
167
+ const excluded = new Set<string>();
168
+ while (true) {
169
+ const selection = await registry.select({
170
+ input: options.input,
171
+ requiredCapabilities: policy.requiredCapabilities,
172
+ foreground: policy.foreground,
173
+ excludedProviderIds: excluded,
174
+ });
175
+ const transaction = transactionFor(options.input, selection.provider.providerId, policy);
176
+ const validationErrors = validateBrowserTransaction(transaction);
177
+ if (validationErrors.length > 0) {
178
+ throw new AssistantPluginError('PLUGIN_BROWSER_TRANSACTION_INVALID', validationErrors[0]!, {
179
+ retryable: false,
180
+ details: { transactionId: transaction.transactionId, errors: validationErrors },
181
+ });
182
+ }
183
+
184
+ try {
185
+ return await selection.provider.execute(options.input);
186
+ } catch (error) {
187
+ if (!(error instanceof BrowserProviderUnavailableBeforeActionError)) throw error;
188
+ excluded.add(selection.provider.providerId);
189
+ // This is provider reselection, not action replay: the typed error proves the rejected provider
190
+ // never attempted the action. Unknown outcomes from non-idempotent actions always propagate.
191
+ }
192
+ }
193
+ }
@@ -0,0 +1,332 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { createHash } from 'crypto';
3
+ import { readdirSync, readFileSync } from 'fs';
4
+ import { join, resolve } from 'path';
5
+ import {
6
+ listControlPlaneRecords,
7
+ readControlPlaneRecord,
8
+ readControlPlaneRecordWithinTransaction,
9
+ withControlPlaneTransaction,
10
+ writeControlPlaneRecordWithinTransaction,
11
+ } from '../control-plane/persistence/sqlite-store';
12
+
13
+ const SESSION_NAMESPACE = 'browser_session';
14
+ const SESSION_SCOPE = 'controller';
15
+ const IMPORT_NAMESPACE = 'browser_session_legacy_import';
16
+ const MAX_AUTHORITY_RECORDS = 5_000;
17
+ export const DEFAULT_BROWSER_SESSION_LIST_LIMIT = 50;
18
+ export const MAX_BROWSER_SESSION_LIST_LIMIT = 200;
19
+
20
+ export interface BrowserSessionAuthorityContext {
21
+ controllerHome: string;
22
+ repoId: string;
23
+ }
24
+
25
+ export interface BrowserSessionAuthoritySession {
26
+ schemaVersion: 1;
27
+ sessionId: string;
28
+ url: string;
29
+ title?: string;
30
+ createdAt: string;
31
+ updatedAt: string;
32
+ browser?: {
33
+ provider?: string;
34
+ browserProduct?: string;
35
+ tab?: {
36
+ windowId?: string;
37
+ tabId?: string;
38
+ ownership?: string;
39
+ };
40
+ };
41
+ }
42
+
43
+ interface BrowserSessionAuthorityEntry<T extends BrowserSessionAuthoritySession = BrowserSessionAuthoritySession> {
44
+ schemaVersion: 1;
45
+ status: 'active' | 'tombstoned';
46
+ session: T;
47
+ aliases: string[];
48
+ repositoryIds: string[];
49
+ nativeIdentity?: string;
50
+ tombstonedAt?: string;
51
+ importedFromLegacy?: boolean;
52
+ }
53
+
54
+ interface LegacyImportMarker {
55
+ schemaVersion: 1;
56
+ repoId: string;
57
+ repoRootFingerprint: string;
58
+ importedAt: string;
59
+ importedRecordCount: number;
60
+ }
61
+
62
+ export interface BrowserSessionAuthorityPage<T extends BrowserSessionAuthoritySession = BrowserSessionAuthoritySession> {
63
+ sessions: T[];
64
+ limit: number;
65
+ totalCount: number;
66
+ nextCursor?: string;
67
+ }
68
+
69
+ const authorityContextStorage = new AsyncLocalStorage<BrowserSessionAuthorityContext>();
70
+
71
+ export function withBrowserSessionAuthorityContext<T>(
72
+ context: BrowserSessionAuthorityContext,
73
+ operation: () => T,
74
+ ): T {
75
+ return authorityContextStorage.run(context, operation);
76
+ }
77
+
78
+ export function currentBrowserSessionAuthorityContext(): BrowserSessionAuthorityContext | undefined {
79
+ return authorityContextStorage.getStore();
80
+ }
81
+
82
+ function digest(value: string): string {
83
+ return createHash('sha256').update(value).digest('hex');
84
+ }
85
+
86
+ function nativeIdentity(session: BrowserSessionAuthoritySession): string | undefined {
87
+ const browser = session.browser;
88
+ const tab = browser?.tab;
89
+ if (browser?.provider !== 'macos-apple-events' || !browser.browserProduct || !tab?.windowId || !tab.tabId) return undefined;
90
+ return `${browser.provider}:${browser.browserProduct}:${tab.windowId}:${tab.tabId}`;
91
+ }
92
+
93
+ function recordKey(repoId: string, session: BrowserSessionAuthoritySession): string {
94
+ const native = nativeIdentity(session);
95
+ return native ? `native-${digest(native).slice(0, 40)}` : `session-${digest(`${repoId}:${session.sessionId}`).slice(0, 40)}`;
96
+ }
97
+
98
+ function importMarkerKey(repoRoot: string): string {
99
+ return digest(resolve(repoRoot)).slice(0, 40);
100
+ }
101
+
102
+ function unique(values: string[]): string[] {
103
+ return [...new Set(values.filter(Boolean))].sort();
104
+ }
105
+
106
+ function parseLegacySession(path: string): BrowserSessionAuthoritySession {
107
+ let parsed: unknown;
108
+ try {
109
+ parsed = JSON.parse(readFileSync(path, 'utf8'));
110
+ } catch (error) {
111
+ throw new Error(`PLUGIN_BROWSER_SESSION_STATE_CORRUPT: ${path}: ${error instanceof Error ? error.message : String(error)}`);
112
+ }
113
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
114
+ throw new Error(`PLUGIN_BROWSER_SESSION_STATE_CORRUPT: ${path}: expected object`);
115
+ }
116
+ const session = parsed as Partial<BrowserSessionAuthoritySession>;
117
+ if (session.schemaVersion !== 1 || typeof session.sessionId !== 'string' || typeof session.url !== 'string'
118
+ || typeof session.createdAt !== 'string' || typeof session.updatedAt !== 'string') {
119
+ throw new Error(`PLUGIN_BROWSER_SESSION_STATE_CORRUPT: ${path}: required session fields are missing`);
120
+ }
121
+ return session as BrowserSessionAuthoritySession;
122
+ }
123
+
124
+ function legacySessions(repoRoot: string): BrowserSessionAuthoritySession[] {
125
+ const root = join(repoRoot, '.forge', 'browser', 'sessions');
126
+ let names: string[];
127
+ try {
128
+ names = readdirSync(root);
129
+ } catch (error) {
130
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
131
+ throw error;
132
+ }
133
+ return names.filter((name) => name.endsWith('.json')).sort().map((name) => parseLegacySession(join(root, name)));
134
+ }
135
+
136
+ function mergeEntry<T extends BrowserSessionAuthoritySession>(
137
+ existing: BrowserSessionAuthorityEntry<T> | undefined,
138
+ repoId: string,
139
+ session: T,
140
+ options: { importedFromLegacy?: boolean } = {},
141
+ ): BrowserSessionAuthorityEntry<T> {
142
+ const canonicalSessionId = existing?.session.sessionId ?? session.sessionId;
143
+ const canonicalCreatedAt = existing?.session.createdAt ?? session.createdAt;
144
+ const normalizedSession = {
145
+ ...session,
146
+ sessionId: canonicalSessionId,
147
+ createdAt: canonicalCreatedAt,
148
+ } as T;
149
+ return {
150
+ schemaVersion: 1,
151
+ status: 'active',
152
+ session: normalizedSession,
153
+ aliases: unique([...(existing?.aliases ?? []), existing?.session.sessionId ?? '', session.sessionId, canonicalSessionId]),
154
+ repositoryIds: unique([...(existing?.repositoryIds ?? []), repoId]),
155
+ nativeIdentity: nativeIdentity(session) ?? existing?.nativeIdentity,
156
+ importedFromLegacy: options.importedFromLegacy === true || existing?.importedFromLegacy === true,
157
+ };
158
+ }
159
+
160
+ export function ensureLegacyBrowserSessionsImported(controllerHome: string, repoId: string, repoRoot: string): number {
161
+ const markerKey = importMarkerKey(repoRoot);
162
+ if (readControlPlaneRecord<LegacyImportMarker>(controllerHome, IMPORT_NAMESPACE, repoId, markerKey)) return 0;
163
+ const sessions = legacySessions(repoRoot);
164
+ return withControlPlaneTransaction(controllerHome, (database) => {
165
+ const marker = readControlPlaneRecordWithinTransaction<LegacyImportMarker>(database, IMPORT_NAMESPACE, repoId, markerKey);
166
+ if (marker) return 0;
167
+ let imported = 0;
168
+ for (const session of sessions) {
169
+ const key = recordKey(repoId, session);
170
+ const current = readControlPlaneRecordWithinTransaction<BrowserSessionAuthorityEntry>(database, SESSION_NAMESPACE, SESSION_SCOPE, key);
171
+ const next = mergeEntry(current?.value, repoId, session, { importedFromLegacy: true });
172
+ writeControlPlaneRecordWithinTransaction(database, {
173
+ namespace: SESSION_NAMESPACE,
174
+ scope: SESSION_SCOPE,
175
+ key,
176
+ schemaVersion: 1,
177
+ value: next,
178
+ expectedRevision: current?.revision ?? null,
179
+ action: current ? 'legacy_merge' : 'legacy_import',
180
+ });
181
+ imported += 1;
182
+ }
183
+ writeControlPlaneRecordWithinTransaction(database, {
184
+ namespace: IMPORT_NAMESPACE,
185
+ scope: repoId,
186
+ key: markerKey,
187
+ schemaVersion: 1,
188
+ value: {
189
+ schemaVersion: 1,
190
+ repoId,
191
+ repoRootFingerprint: markerKey,
192
+ importedAt: new Date().toISOString(),
193
+ importedRecordCount: imported,
194
+ } satisfies LegacyImportMarker,
195
+ expectedRevision: null,
196
+ action: 'legacy_import_complete',
197
+ });
198
+ return imported;
199
+ });
200
+ }
201
+
202
+ function authorityEntries(controllerHome: string): BrowserSessionAuthorityEntry[] {
203
+ return listControlPlaneRecords<BrowserSessionAuthorityEntry>(controllerHome, {
204
+ namespace: SESSION_NAMESPACE,
205
+ scope: SESSION_SCOPE,
206
+ limit: MAX_AUTHORITY_RECORDS,
207
+ }).map((record) => record.value);
208
+ }
209
+
210
+ function visibleToRepository(entry: BrowserSessionAuthorityEntry, repoId: string): boolean {
211
+ return Boolean(entry.nativeIdentity) || entry.repositoryIds.includes(repoId);
212
+ }
213
+
214
+ function activeEntries(controllerHome: string, repoId: string, repoRoot: string): BrowserSessionAuthorityEntry[] {
215
+ ensureLegacyBrowserSessionsImported(controllerHome, repoId, repoRoot);
216
+ return authorityEntries(controllerHome)
217
+ .filter((entry) => entry.status === 'active' && visibleToRepository(entry, repoId))
218
+ .sort((left, right) => {
219
+ const updated = right.session.updatedAt.localeCompare(left.session.updatedAt);
220
+ return updated !== 0 ? updated : left.session.sessionId.localeCompare(right.session.sessionId);
221
+ });
222
+ }
223
+
224
+ export function saveBrowserSession<T extends BrowserSessionAuthoritySession>(
225
+ controllerHome: string,
226
+ repoId: string,
227
+ repoRoot: string,
228
+ session: T,
229
+ ): T {
230
+ ensureLegacyBrowserSessionsImported(controllerHome, repoId, repoRoot);
231
+ const key = recordKey(repoId, session);
232
+ return withControlPlaneTransaction(controllerHome, (database) => {
233
+ const current = readControlPlaneRecordWithinTransaction<BrowserSessionAuthorityEntry<T>>(database, SESSION_NAMESPACE, SESSION_SCOPE, key);
234
+ const next = mergeEntry(current?.value, repoId, session);
235
+ const written = writeControlPlaneRecordWithinTransaction(database, {
236
+ namespace: SESSION_NAMESPACE,
237
+ scope: SESSION_SCOPE,
238
+ key,
239
+ schemaVersion: 1,
240
+ value: next,
241
+ expectedRevision: current?.revision ?? null,
242
+ action: current ? 'save' : 'create',
243
+ });
244
+ return written.value.session;
245
+ });
246
+ }
247
+
248
+ export function findBrowserSession<T extends BrowserSessionAuthoritySession>(
249
+ controllerHome: string,
250
+ repoId: string,
251
+ repoRoot: string,
252
+ sessionId: string,
253
+ ): T | undefined {
254
+ return activeEntries(controllerHome, repoId, repoRoot)
255
+ .find((entry) => entry.session.sessionId === sessionId || entry.aliases.includes(sessionId))?.session as T | undefined;
256
+ }
257
+
258
+ export function listBrowserSessions<T extends BrowserSessionAuthoritySession>(
259
+ controllerHome: string,
260
+ repoId: string,
261
+ repoRoot: string,
262
+ options: { limit?: number; cursor?: string } = {},
263
+ ): BrowserSessionAuthorityPage<T> {
264
+ const entries = activeEntries(controllerHome, repoId, repoRoot);
265
+ const limit = Math.max(1, Math.min(Math.trunc(options.limit ?? DEFAULT_BROWSER_SESSION_LIST_LIMIT), MAX_BROWSER_SESSION_LIST_LIMIT));
266
+ let offset = 0;
267
+ if (options.cursor) {
268
+ try {
269
+ const parsed = JSON.parse(Buffer.from(options.cursor, 'base64url').toString('utf8')) as { offset?: unknown };
270
+ if (!Number.isInteger(parsed.offset) || Number(parsed.offset) < 0) throw new Error('invalid offset');
271
+ offset = Number(parsed.offset);
272
+ } catch {
273
+ throw new Error('PLUGIN_BROWSER_SESSION_CURSOR_INVALID: cursor is invalid or expired');
274
+ }
275
+ }
276
+ const page = entries.slice(offset, offset + limit);
277
+ const nextOffset = offset + page.length;
278
+ const nextCursor = nextOffset < entries.length
279
+ ? Buffer.from(JSON.stringify({ offset: nextOffset }), 'utf8').toString('base64url')
280
+ : undefined;
281
+ return {
282
+ sessions: page.map((entry) => entry.session as T),
283
+ limit,
284
+ totalCount: entries.length,
285
+ nextCursor,
286
+ };
287
+ }
288
+
289
+ export function listAllBrowserSessionsForRepository<T extends BrowserSessionAuthoritySession>(
290
+ controllerHome: string,
291
+ repoId: string,
292
+ repoRoot: string,
293
+ ): T[] {
294
+ return activeEntries(controllerHome, repoId, repoRoot).map((entry) => entry.session as T);
295
+ }
296
+
297
+ export function tombstoneBrowserSession(
298
+ controllerHome: string,
299
+ repoId: string,
300
+ repoRoot: string,
301
+ sessionId: string,
302
+ ): boolean {
303
+ ensureLegacyBrowserSessionsImported(controllerHome, repoId, repoRoot);
304
+ const record = listControlPlaneRecords<BrowserSessionAuthorityEntry>(controllerHome, {
305
+ namespace: SESSION_NAMESPACE,
306
+ scope: SESSION_SCOPE,
307
+ limit: MAX_AUTHORITY_RECORDS,
308
+ }).find((candidate) => {
309
+ const entry = candidate.value;
310
+ return entry.status === 'active' && visibleToRepository(entry, repoId)
311
+ && (entry.session.sessionId === sessionId || entry.aliases.includes(sessionId));
312
+ });
313
+ if (!record) return false;
314
+ return withControlPlaneTransaction(controllerHome, (database) => {
315
+ const current = readControlPlaneRecordWithinTransaction<BrowserSessionAuthorityEntry>(database, SESSION_NAMESPACE, SESSION_SCOPE, record.key);
316
+ if (!current || current.value.status !== 'active') return false;
317
+ writeControlPlaneRecordWithinTransaction(database, {
318
+ namespace: SESSION_NAMESPACE,
319
+ scope: SESSION_SCOPE,
320
+ key: record.key,
321
+ schemaVersion: 1,
322
+ value: {
323
+ ...current.value,
324
+ status: 'tombstoned',
325
+ tombstonedAt: new Date().toISOString(),
326
+ },
327
+ expectedRevision: current.revision,
328
+ action: 'tombstone',
329
+ });
330
+ return true;
331
+ });
332
+ }
@@ -0,0 +1,121 @@
1
+ import { readdirSync, readFileSync, rmSync } from 'fs';
2
+ import { basename, join } from 'path';
3
+ import { writeJsonAtomic } from '../shared/json-files';
4
+ import { AssistantPluginError } from './errors';
5
+ import {
6
+ currentBrowserSessionAuthorityContext,
7
+ findBrowserSession as findBrowserSessionAuthority,
8
+ listAllBrowserSessionsForRepository,
9
+ saveBrowserSession as saveBrowserSessionAuthority,
10
+ tombstoneBrowserSession,
11
+ } from './browser-session-authority';
12
+
13
+ const BROWSER_STATE_ROOT = '.forge/browser';
14
+
15
+ import type { BrowserSessionState } from './browser-session-types';
16
+
17
+ /**
18
+ * Repository-local browser paths remain compatibility storage only. During
19
+ * Runtime/plugin execution, BrowserSessionAuthority (SQLite) is the sole writer;
20
+ * the JSON files are read/written only when no Controller authority context is
21
+ * present (for legacy migration/tests/standalone helpers).
22
+ */
23
+ export function browserStateDir(
24
+ repoRoot: string,
25
+ name: 'sessions' | 'screenshots' | 'profiles' | 'downloads' | 'diagnostics',
26
+ ): string {
27
+ return join(repoRoot, BROWSER_STATE_ROOT, name);
28
+ }
29
+
30
+ function sessionPath(repoRoot: string, sessionId: string): string {
31
+ return join(browserStateDir(repoRoot, 'sessions'), `${sessionId.replace(/[^a-zA-Z0-9_-]/g, '_')}.json`);
32
+ }
33
+
34
+ function readLegacyBrowserSessionJson(path: string): BrowserSessionState | undefined {
35
+ let raw: string;
36
+ try {
37
+ raw = readFileSync(path, 'utf-8');
38
+ } catch (error) {
39
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return undefined;
40
+ throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session metadata could not be read.', {
41
+ retryable: true,
42
+ details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
43
+ });
44
+ }
45
+ try {
46
+ return JSON.parse(raw) as BrowserSessionState;
47
+ } catch (error) {
48
+ throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_CORRUPT', 'Saved browser session metadata is malformed; refusing to treat corrupt state as a missing session.', {
49
+ retryable: false,
50
+ details: { fileName: basename(path), cause: error instanceof Error ? error.message : String(error) },
51
+ });
52
+ }
53
+ }
54
+
55
+ export function saveBrowserSession(repoRoot: string, session: BrowserSessionState): BrowserSessionState {
56
+ const authority = currentBrowserSessionAuthorityContext();
57
+ if (!authority) {
58
+ writeJsonAtomic(sessionPath(repoRoot, session.sessionId), session);
59
+ return session;
60
+ }
61
+ const saved = saveBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, session);
62
+ if (saved.sessionId === session.sessionId || !saved.browser?.sessionResume) return saved;
63
+ return {
64
+ ...saved,
65
+ browser: {
66
+ ...saved.browser,
67
+ sessionResume: { ...saved.browser.sessionResume, sessionId: saved.sessionId },
68
+ },
69
+ };
70
+ }
71
+
72
+ export function findBrowserSession(repoRoot: string, sessionId?: string): BrowserSessionState | undefined {
73
+ if (!sessionId) return undefined;
74
+ const authority = currentBrowserSessionAuthorityContext();
75
+ return authority
76
+ ? findBrowserSessionAuthority<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot, sessionId)
77
+ : readLegacyBrowserSessionJson(sessionPath(repoRoot, sessionId));
78
+ }
79
+
80
+ export function listSavedBrowserSessions(repoRoot: string): BrowserSessionState[] {
81
+ const authority = currentBrowserSessionAuthorityContext();
82
+ if (authority) {
83
+ return listAllBrowserSessionsForRepository<BrowserSessionState>(authority.controllerHome, authority.repoId, repoRoot);
84
+ }
85
+ const root = browserStateDir(repoRoot, 'sessions');
86
+ let names: string[];
87
+ try {
88
+ names = readdirSync(root);
89
+ } catch (error) {
90
+ if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') return [];
91
+ throw new AssistantPluginError('PLUGIN_BROWSER_SESSION_STATE_READ_FAILED', 'Saved browser session directory could not be read.', {
92
+ retryable: true,
93
+ details: { cause: error instanceof Error ? error.message : String(error) },
94
+ });
95
+ }
96
+ return names
97
+ .filter((name) => name.endsWith('.json'))
98
+ .map((name) => readLegacyBrowserSessionJson(join(root, name)))
99
+ .filter((session): session is BrowserSessionState => Boolean(session));
100
+ }
101
+
102
+ export function removeBrowserSession(repoRoot: string, sessionId: string): void {
103
+ const authority = currentBrowserSessionAuthorityContext();
104
+ if (authority) {
105
+ tombstoneBrowserSession(authority.controllerHome, authority.repoId, repoRoot, sessionId);
106
+ return;
107
+ }
108
+ rmSync(sessionPath(repoRoot, sessionId), { force: true });
109
+ }
110
+
111
+ export function loadBrowserSession(repoRoot: string, sessionId?: string): BrowserSessionState | undefined {
112
+ return sessionId ? findBrowserSession(repoRoot, sessionId) : undefined;
113
+ }
114
+
115
+ export function requireBrowserSession(repoRoot: string, sessionId: string): BrowserSessionState {
116
+ const session = findBrowserSession(repoRoot, sessionId);
117
+ if (!session) {
118
+ throw new AssistantPluginError('PLUGIN_SESSION_NOT_FOUND', `Browser session not found: ${sessionId}`, { retryable: false });
119
+ }
120
+ return session;
121
+ }