@moretea-labs/forge 1.6.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (396) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -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/03-orchestration.partial.md +1 -1
  17. package/assets/plugin-registry.v1.json +1 -1
  18. package/assets/reference-configs/document-generation.md +1 -1
  19. package/assets/reference-configs/harness-overview.md +1 -1
  20. package/assets/reference-configs/sprint-contracts.md +3 -3
  21. package/assets/skill-version.json +2 -2
  22. package/assets/templates/contract.template.md +2 -7
  23. package/assets/templates/helpers/capture-plan.sh +2 -2
  24. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  25. package/assets/templates/helpers/contract-worktree.sh +1 -21
  26. package/assets/templates/helpers/ensure-task-workflow.sh +11 -36
  27. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  28. package/assets/templates/helpers/new-plan.sh +1 -1
  29. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  30. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  31. package/assets/templates/helpers/verify-contract.sh +1 -111
  32. package/assets/templates/helpers/verify-sprint.sh +3 -180
  33. package/assets/templates/plan.template.md +1 -1
  34. package/assets/templates/review.template.md +9 -34
  35. package/assets/workflow-contract.v1.json +10 -19
  36. package/docs/README.md +10 -10
  37. package/docs/architecture/CURRENT.md +170 -0
  38. package/docs/architecture/EVOLUTION.md +42 -0
  39. package/docs/architecture/current/README.md +4 -156
  40. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  41. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  42. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  43. package/docs/architecture/history/README.md +7 -0
  44. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  45. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  46. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  47. package/docs/architecture/history.md +3 -34
  48. package/docs/architecture/index.md +15 -98
  49. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  50. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  51. package/docs/architecture/versions/1.6.md +38 -0
  52. package/docs/architecture/versions/1.7.md +41 -0
  53. package/docs/forge-chatgpt-browser-engine.md +2 -2
  54. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  55. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  56. package/docs/forge-github-issue-launcher.md +1 -1
  57. package/docs/forge-runtime-self-healing-loop.md +1 -1
  58. package/docs/operations/controller-browser-plugin.md +26 -6
  59. package/docs/operations/controller-performance-and-502.md +124 -0
  60. package/docs/operations/controller-reliability-runbook.md +1 -1
  61. package/docs/operations/features.md +2 -2
  62. package/docs/operations/github-repository.md +1 -1
  63. package/docs/operations/homebrew.md +2 -2
  64. package/docs/operations/mcp-tool-exposure.md +5 -3
  65. package/docs/wiki/Architecture.md +6 -6
  66. package/docs/wiki/Implementation.md +2 -2
  67. package/docs/wiki/Integrations.md +2 -2
  68. package/docs/wiki/Work-Lifecycle.md +1 -1
  69. package/package.json +29 -10
  70. package/scripts/activate-source-baseline.ts +17 -13
  71. package/scripts/benchmark-background-check-overlap.ts +228 -0
  72. package/scripts/benchmark-mcp-host.sh +117 -0
  73. package/scripts/benchmark-mcp-transport.ts +170 -0
  74. package/scripts/benchmark-quality-harness.ts +146 -0
  75. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  76. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  77. package/scripts/benchmark-typescript-navigation.ts +186 -0
  78. package/scripts/benchmark-work-efficiency.ts +334 -0
  79. package/scripts/capture-plan.sh +3 -3
  80. package/scripts/check-agent-tooling.sh +8 -35
  81. package/scripts/check-architecture-sync.sh +17 -86
  82. package/scripts/check-mcp-compatibility.ts +14 -0
  83. package/scripts/check-runtime-architecture.mjs +107 -64
  84. package/scripts/check-stable-baseline.ts +92 -0
  85. package/scripts/check-task-workflow.sh +4 -4
  86. package/scripts/contract-worktree.sh +1 -21
  87. package/scripts/ensure-task-workflow.sh +22 -55
  88. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  89. package/scripts/install-standalone-recovery.ts +24 -0
  90. package/scripts/lib/project-init-lib.sh +15 -44
  91. package/scripts/migrate-project-template.sh +23 -1179
  92. package/scripts/new-plan.sh +1 -1
  93. package/scripts/plan-to-todo.sh +11 -36
  94. package/scripts/run-governed-gate.ts +2 -1
  95. package/scripts/ship-worktrees.sh +2 -29
  96. package/scripts/smoke-ios-physical-device.ts +4 -2
  97. package/scripts/smoke-runtime-recovery.ts +1 -35
  98. package/scripts/smoke-schedule-engine.ts +1 -20
  99. package/scripts/verify-contract.sh +1 -111
  100. package/scripts/verify-forge-runtime.sh +0 -1
  101. package/scripts/verify-sprint.sh +3 -180
  102. package/scripts/workflow-contract.ts +1 -5
  103. package/src/cli/commands/init-hook.ts +3 -3
  104. package/src/cli/commands/mcp.ts +9 -5
  105. package/src/cli/commands/plugin.ts +1 -1
  106. package/src/cli/commands/recovery.ts +25 -3
  107. package/src/cli/commands/runtime.ts +55 -3
  108. package/src/cli/commands/security.ts +12 -1
  109. package/src/cli/commands/setup-profile.ts +31 -21
  110. package/src/cli/controller/check-runner.ts +106 -3
  111. package/src/cli/controller/composite-result.ts +1 -1
  112. package/src/cli/controller/context/focus.ts +51 -0
  113. package/src/cli/controller/context/instruction-context.ts +114 -0
  114. package/src/cli/controller/context/known-paths.ts +76 -0
  115. package/src/cli/controller/context/query-planning.ts +110 -0
  116. package/src/cli/controller/context/rule-resolution.ts +532 -0
  117. package/src/cli/controller/context/source-materializer.ts +278 -0
  118. package/src/cli/controller/context/structural.ts +16 -0
  119. package/src/cli/controller/context/types.ts +286 -0
  120. package/src/cli/controller/context-pack.ts +785 -469
  121. package/src/cli/controller/launch-agents.ts +77 -2
  122. package/src/cli/controller/operational-plan.ts +2 -2
  123. package/src/cli/controller/work-mode.ts +6 -4
  124. package/src/cli/editing/edit-session.ts +60 -7
  125. package/src/cli/github/github.ts +133 -0
  126. package/src/cli/installer/managed-entries.ts +15 -1
  127. package/src/cli/local-bridge/console-automations.ts +54 -78
  128. package/src/cli/local-bridge/console-view-models.ts +1 -35
  129. package/src/cli/local-bridge/facade-api.ts +0 -217
  130. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  131. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +23 -13
  132. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  133. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +6 -4
  134. package/src/cli/local-bridge/frontend/src/lib/api.ts +1 -6
  135. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  136. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +2 -2
  137. package/src/cli/local-bridge/frontend/src/styles.css +1 -1
  138. package/src/cli/local-bridge/frontend/src/types.ts +5 -8
  139. package/src/cli/local-bridge/job-store.ts +1 -490
  140. package/src/cli/local-bridge/server.ts +0 -513
  141. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  142. package/src/cli/local-bridge/ui-dist/app.js +3703 -3757
  143. package/src/cli/mcp/auth.ts +5 -5
  144. package/src/cli/mcp/instructions.ts +3 -2
  145. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  146. package/src/cli/mcp/multi-repository.ts +16 -5
  147. package/src/cli/mcp/oauth.ts +175 -26
  148. package/src/cli/mcp/repository-tools.ts +247 -107
  149. package/src/cli/mcp/server.ts +537 -111
  150. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  151. package/src/cli/mcp/toolset-selection.ts +1 -1
  152. package/src/cli/mcp/toolset.ts +42 -5
  153. package/src/cli/mcp/transports/http.ts +101 -14
  154. package/src/cli/mcp/transports/session-registry.ts +43 -5
  155. package/src/cli/mcp/types.ts +3 -3
  156. package/src/cli/repositories/command-classifier.ts +101 -19
  157. package/src/cli/repositories/command-executor.ts +55 -521
  158. package/src/cli/repositories/command-process.ts +108 -0
  159. package/src/cli/repositories/command-scope.ts +99 -41
  160. package/src/cli/repositories/controller-home.ts +7 -8
  161. package/src/cli/repositories/git-command-executor.ts +2 -0
  162. package/src/cli/repositories/locks.ts +53 -16
  163. package/src/cli/repositories/repository-snapshot.ts +121 -0
  164. package/src/cli/repositories/selected-path-actions.ts +17 -9
  165. package/src/cli/repositories/structured-git.ts +61 -3
  166. package/src/cli/repository/inspector.ts +553 -63
  167. package/src/cli/repository/session-cache.ts +58 -17
  168. package/src/cli/v81-entry.ts +1 -4
  169. package/src/effects/fs-transaction.ts +85 -17
  170. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  171. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  172. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  173. package/src/runtime/context/lsp-client.ts +283 -0
  174. package/src/runtime/context/semantic-navigation.ts +275 -0
  175. package/src/runtime/context/swift-navigation.ts +565 -0
  176. package/src/runtime/context/typescript-navigation.ts +226 -0
  177. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  178. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +47 -8
  179. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  180. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  181. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  182. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  183. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  184. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  185. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  186. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +143 -8
  187. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +22 -14
  188. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +85 -8
  189. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  190. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  191. package/src/runtime/control-plane/facade/controller-session-store.ts +132 -1
  192. package/src/runtime/control-plane/facade/goal-workloop-access.ts +2 -0
  193. package/src/runtime/control-plane/facade/goal-workloop.ts +686 -129
  194. package/src/runtime/control-plane/facade/index.ts +1 -0
  195. package/src/runtime/control-plane/facade/plan-contract-store.ts +185 -20
  196. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  197. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  198. package/src/runtime/control-plane/facade/types.ts +22 -5
  199. package/src/runtime/control-plane/facade/work-contract-store.ts +175 -73
  200. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  201. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  202. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  203. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  204. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  205. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  206. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  207. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  208. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  209. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  210. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  211. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  212. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  213. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  214. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  215. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  216. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  217. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  218. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  219. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +355 -116
  220. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  221. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  222. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  223. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  224. package/src/runtime/control-plane/release-retention.ts +62 -2
  225. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  226. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  227. package/src/runtime/diagnostics/mcp-timing.ts +13 -0
  228. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  229. package/src/runtime/diagnostics/performance.ts +24 -1
  230. package/src/runtime/evidence/event-ledger.ts +1 -1
  231. package/src/runtime/execution/jobs/store.ts +58 -4
  232. package/src/runtime/execution/jobs/types.ts +1 -1
  233. package/src/runtime/execution/managed-workspace.ts +203 -1
  234. package/src/runtime/execution/process-runtime/check-facade.ts +51 -4
  235. package/src/runtime/execution/process-runtime/check-result.ts +53 -0
  236. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  237. package/src/runtime/execution/process-runtime/command-facade.ts +345 -40
  238. package/src/runtime/execution/process-runtime/gc.ts +38 -6
  239. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  240. package/src/runtime/execution/process-runtime/persisted-check.ts +47 -4
  241. package/src/runtime/execution/process-runtime/resource-claims.ts +10 -6
  242. package/src/runtime/execution/process-runtime/runtime.ts +43 -3
  243. package/src/runtime/execution/process-runtime/store.ts +1 -0
  244. package/src/runtime/execution/process-runtime/types.ts +2 -0
  245. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  246. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  247. package/src/runtime/execution/workers/executor.ts +0 -48
  248. package/src/runtime/gateway/mcp/execution-tools.ts +1192 -114
  249. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  250. package/src/runtime/gateway/mcp/router.ts +184 -82
  251. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  252. package/src/runtime/gateway/mcp/runtime-tools.ts +2224 -1589
  253. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  254. package/src/runtime/plugins/browser-adapter.ts +2901 -241
  255. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  256. package/src/runtime/plugins/browser-handoff.ts +92 -2
  257. package/src/runtime/plugins/browser-macos-bridge.ts +582 -49
  258. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  259. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  260. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  261. package/src/runtime/plugins/browser-runtime.ts +183 -0
  262. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  263. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  264. package/src/runtime/plugins/desktop-operator-registration.ts +8 -2
  265. package/src/runtime/plugins/external-adapter.ts +324 -3
  266. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  267. package/src/runtime/plugins/first-party-registry.ts +2 -0
  268. package/src/runtime/plugins/github-adapter.ts +125 -0
  269. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  270. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  271. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  272. package/src/runtime/plugins/ios-adapter.ts +106 -16
  273. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  274. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  275. package/src/runtime/plugins/lightweight-action.ts +153 -0
  276. package/src/runtime/plugins/local-system-adapter.ts +123 -6
  277. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  278. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  279. package/src/runtime/plugins/plugin-management-adapter.ts +49 -8
  280. package/src/runtime/plugins/resend-adapter.ts +1 -1
  281. package/src/runtime/plugins/store.ts +269 -29
  282. package/src/runtime/plugins/types.ts +35 -2
  283. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  284. package/src/runtime/projections/git-status-sampler.ts +16 -16
  285. package/src/runtime/projections/materialized-view.ts +28 -10
  286. package/src/runtime/recovery/maintenance-executor.ts +393 -28
  287. package/src/runtime/root/mcp-transport.ts +180 -10
  288. package/src/runtime/root/package-connector-service.ts +186 -8
  289. package/src/runtime/root/package-runtime-release.ts +143 -17
  290. package/src/runtime/root/package-runtime-service.ts +430 -22
  291. package/src/runtime/root/release-manifest.ts +7 -2
  292. package/src/runtime/root/release-materialize.ts +19 -0
  293. package/src/runtime/root/release-store.ts +48 -11
  294. package/src/runtime/root/runtime.ts +23 -5
  295. package/src/runtime/root/service.ts +100 -29
  296. package/src/runtime/root/types.ts +2 -0
  297. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  298. package/src/runtime/shared/trusted-executable.ts +62 -0
  299. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  300. package/src/runtime/standalone-recovery/core.ts +700 -92
  301. package/src/runtime/standalone-recovery/entry.ts +99 -23
  302. package/src/runtime/standalone-recovery/installer.ts +19 -3
  303. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  304. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  305. package/src/runtime/workflow/schedules/browser-probe.ts +81 -3
  306. package/src/runtime/workflow/schedules/engine.ts +287 -151
  307. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  308. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  309. package/src/runtime/workflow/schedules/store.ts +131 -10
  310. package/src/runtime/workflow/schedules/types.ts +2 -4
  311. package/src/runtime/workflow/schedules/work-continuation.ts +26 -13
  312. package/src/testing/test-governance.ts +8 -40
  313. package/assets/reference-configs/heartbeat-triage.md +0 -57
  314. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  315. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  316. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  317. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  318. package/docs/architecture/current/architecture-invariants.md +0 -331
  319. package/docs/architecture/current/automation-and-schedule-engine.md +0 -459
  320. package/docs/architecture/current/codegraph-context-provider.md +0 -238
  321. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  322. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  323. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -373
  324. package/docs/architecture/current/entity-model.md +0 -612
  325. package/docs/architecture/current/external-plugin-governance.md +0 -396
  326. package/docs/architecture/current/failure-recovery.md +0 -447
  327. package/docs/architecture/current/governance.md +0 -213
  328. package/docs/architecture/current/human-interaction-plane.md +0 -87
  329. package/docs/architecture/current/implementation-status.md +0 -128
  330. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  331. package/docs/architecture/current/legacy-governance.md +0 -58
  332. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  333. package/docs/architecture/current/migration-roadmap.md +0 -105
  334. package/docs/architecture/current/multi-repository-execution.md +0 -371
  335. package/docs/architecture/current/operations-runbook.md +0 -45
  336. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  337. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  338. package/docs/architecture/current/repository-access-modes.md +0 -102
  339. package/docs/architecture/current/route-policy.md +0 -15
  340. package/docs/architecture/current/route-session-concurrency.md +0 -85
  341. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  342. package/docs/architecture/current/runtime-directory-map.md +0 -49
  343. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  344. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  345. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  346. package/docs/architecture/current/system-overview.md +0 -169
  347. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  348. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  349. package/docs/architecture/current/thin-harness-v1.md +0 -321
  350. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  351. package/docs/forge-autonomous-goal-loop.md +0 -9
  352. package/docs/forge-provider-configuration.md +0 -7
  353. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -93
  354. package/docs/operations/gmail-assistant-routines.md +0 -186
  355. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  356. package/docs/operations/provider-configuration.md +0 -31
  357. package/scripts/harness-trace-grade.sh +0 -150
  358. package/scripts/heartbeat-triage.sh +0 -361
  359. package/scripts/maintenance-triage.sh +0 -167
  360. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  361. package/src/cli/repositories/goal-registry.ts +0 -239
  362. package/src/runtime/assistant/action-proposals.ts +0 -279
  363. package/src/runtime/assistant/intent.ts +0 -395
  364. package/src/runtime/assistant/model-provider.ts +0 -460
  365. package/src/runtime/assistant/openapi.ts +0 -223
  366. package/src/runtime/assistant/policy.ts +0 -89
  367. package/src/runtime/assistant/readiness.ts +0 -258
  368. package/src/runtime/assistant/routine-runtime.ts +0 -477
  369. package/src/runtime/assistant/schedule-binding.ts +0 -171
  370. package/src/runtime/assistant/standing-grants.ts +0 -375
  371. package/src/runtime/assistant/store.ts +0 -243
  372. package/src/runtime/assistant/types.ts +0 -135
  373. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  374. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  375. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  376. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  377. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  378. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  379. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  380. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  381. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  382. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  383. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  384. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  385. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  386. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  387. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  388. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  389. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  390. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  391. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  392. package/src/runtime/workflow/findings/store.ts +0 -204
  393. package/src/runtime/workflow/findings/types.ts +0 -58
  394. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  395. package/src/runtime/workflow/portfolio/store.ts +0 -185
  396. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -0,0 +1,158 @@
1
+ import { execFile } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { resolveTrustedExecutable } from '../../shared/trusted-executable';
4
+
5
+ const execFileAsync = promisify(execFile);
6
+
7
+ export interface GithubIssueWatchIssue {
8
+ number: number;
9
+ title: string;
10
+ state: 'open' | 'closed';
11
+ updatedAt: string;
12
+ url: string;
13
+ }
14
+
15
+ export interface GithubIssueWatchState {
16
+ schemaVersion: 1;
17
+ observedAt: string;
18
+ signatures: Record<string, string>;
19
+ }
20
+
21
+ export interface GithubIssueWatchObservation {
22
+ status: 'baseline' | 'unchanged' | 'changed';
23
+ shouldWake: boolean;
24
+ changedOpenIssues: GithubIssueWatchIssue[];
25
+ issues: GithubIssueWatchIssue[];
26
+ nextState: GithubIssueWatchState;
27
+ }
28
+
29
+ function stringValue(value: unknown): string | undefined {
30
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
31
+ }
32
+
33
+ function boundedNumber(value: unknown, fallback: number, min: number, max: number): number {
34
+ return typeof value === 'number' && Number.isFinite(value)
35
+ ? Math.max(min, Math.min(Math.trunc(value), max))
36
+ : fallback;
37
+ }
38
+
39
+ function issueSignature(issue: GithubIssueWatchIssue): string {
40
+ return `${issue.state}:${issue.updatedAt}`;
41
+ }
42
+
43
+ function normalizeIssue(value: unknown): GithubIssueWatchIssue | undefined {
44
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
45
+ const record = value as Record<string, unknown>;
46
+ const number = typeof record.number === 'number' && Number.isFinite(record.number) ? Math.trunc(record.number) : undefined;
47
+ const title = stringValue(record.title);
48
+ const state = record.state === 'open' || record.state === 'closed' ? record.state : undefined;
49
+ const updatedAt = stringValue(record.updatedAt);
50
+ const url = stringValue(record.url);
51
+ if (!number || !title || !state || !updatedAt || !url) return undefined;
52
+ return { number, title, state, updatedAt, url };
53
+ }
54
+
55
+ export function normalizeGithubIssueWatchState(value: unknown): GithubIssueWatchState | undefined {
56
+ if (!value || typeof value !== 'object' || Array.isArray(value)) return undefined;
57
+ const record = value as Record<string, unknown>;
58
+ const signaturesValue = record.signatures;
59
+ if (!signaturesValue || typeof signaturesValue !== 'object' || Array.isArray(signaturesValue)) return undefined;
60
+ const signatures = Object.fromEntries(Object.entries(signaturesValue as Record<string, unknown>)
61
+ .filter((entry): entry is [string, string] => typeof entry[1] === 'string'));
62
+ return {
63
+ schemaVersion: 1,
64
+ observedAt: stringValue(record.observedAt) ?? new Date(0).toISOString(),
65
+ signatures,
66
+ };
67
+ }
68
+
69
+ export function classifyGithubIssueWatchObservation(
70
+ issues: GithubIssueWatchIssue[],
71
+ previousState: GithubIssueWatchState | undefined,
72
+ options: { since?: string; wakeOnFirstObservation?: boolean; observedAt?: string } = {},
73
+ ): GithubIssueWatchObservation {
74
+ const observedAt = options.observedAt ?? new Date().toISOString();
75
+ const signatures = Object.fromEntries(issues.map((issue) => [String(issue.number), issueSignature(issue)]));
76
+ const firstObservation = !previousState;
77
+ const sinceMs = options.since ? Date.parse(options.since) : Number.NaN;
78
+ const changedOpenIssues = issues.filter((issue) => {
79
+ if (issue.state !== 'open') return false;
80
+ if (!firstObservation) return previousState.signatures[String(issue.number)] !== issueSignature(issue);
81
+ if (options.wakeOnFirstObservation === true) return true;
82
+ return Number.isFinite(sinceMs) && Date.parse(issue.updatedAt) > sinceMs;
83
+ });
84
+ return {
85
+ status: firstObservation ? 'baseline' : changedOpenIssues.length > 0 ? 'changed' : 'unchanged',
86
+ shouldWake: changedOpenIssues.length > 0,
87
+ changedOpenIssues,
88
+ issues,
89
+ nextState: { schemaVersion: 1, observedAt, signatures },
90
+ };
91
+ }
92
+
93
+ const DEFAULT_GH_EXECUTABLE_CANDIDATES = [
94
+ '/opt/homebrew/bin/gh',
95
+ '/usr/local/bin/gh',
96
+ '/usr/bin/gh',
97
+ ] as const;
98
+
99
+ export function resolveGithubIssueWatchExecutable(
100
+ args: Record<string, unknown>,
101
+ env: NodeJS.ProcessEnv = process.env,
102
+ preferredPaths: readonly string[] = DEFAULT_GH_EXECUTABLE_CANDIDATES,
103
+ ): string {
104
+ const configured = stringValue(args.gh_executable) ?? stringValue(env.FORGE_GH_EXECUTABLE);
105
+ const resolved = resolveTrustedExecutable({
106
+ name: 'gh',
107
+ configured,
108
+ preferredPaths,
109
+ env,
110
+ });
111
+ if (resolved.configuredInvalid) {
112
+ throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_INVALID: gh_executable/FORGE_GH_EXECUTABLE must be an absolute executable path.');
113
+ }
114
+ if (!resolved.executable) {
115
+ throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_NOT_FOUND: GitHub CLI was not found in trusted Homebrew/system locations or PATH.');
116
+ }
117
+ return resolved.executable;
118
+ }
119
+
120
+ export async function executeScheduledGithubIssueWatch(input: {
121
+ repoRoot: string;
122
+ args: Record<string, unknown>;
123
+ observedAt?: string;
124
+ }): Promise<GithubIssueWatchObservation> {
125
+ const repository = stringValue(input.args.github_repository);
126
+ if (!repository || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository)) {
127
+ throw new Error('SCHEDULE_GITHUB_ISSUE_WATCH_REPOSITORY_REQUIRED');
128
+ }
129
+ const limit = boundedNumber(input.args.issue_limit, 50, 10, 100);
130
+ const timeoutMs = boundedNumber(input.args.timeout_ms, 15_000, 1_000, 60_000);
131
+ try {
132
+ const ghExecutable = resolveGithubIssueWatchExecutable(input.args);
133
+ const result = await execFileAsync(ghExecutable, [
134
+ 'api',
135
+ '-H', 'Accept: application/vnd.github+json',
136
+ `repos/${repository}/issues?state=all&sort=updated&direction=desc&per_page=${limit}`,
137
+ '--jq', 'map(select(.pull_request|not)) | map({number,title,state,updatedAt:.updated_at,url:.html_url})',
138
+ ], {
139
+ cwd: input.repoRoot,
140
+ encoding: 'utf8',
141
+ timeout: timeoutMs,
142
+ maxBuffer: 2 * 1024 * 1024,
143
+ });
144
+ const parsed = JSON.parse(String(result.stdout)) as unknown;
145
+ if (!Array.isArray(parsed)) throw new Error('GitHub issue response was not an array.');
146
+ const issues = parsed.map(normalizeIssue).filter((issue): issue is GithubIssueWatchIssue => Boolean(issue));
147
+ const previousState = normalizeGithubIssueWatchState(input.args.issue_watch_state);
148
+ return classifyGithubIssueWatchObservation(issues, previousState, {
149
+ since: stringValue(input.args.issue_watch_since),
150
+ wakeOnFirstObservation: input.args.wake_on_first_observation === true,
151
+ observedAt: input.observedAt,
152
+ });
153
+ } catch (error) {
154
+ const message = error instanceof Error ? error.message : String(error);
155
+ if (message.startsWith('SCHEDULE_GITHUB_ISSUE_WATCH_GH_EXECUTABLE_')) throw error;
156
+ throw new Error(`SCHEDULE_GITHUB_ISSUE_WATCH_FAILED: ${message}`);
157
+ }
158
+ }
@@ -11,14 +11,21 @@ import {
11
11
 
12
12
  const TERMINAL_OCCURRENCE_STATUSES = new Set<ScheduleOccurrence['status']>(['succeeded', 'failed', 'shadowed', 'skipped']);
13
13
 
14
- function computeScheduleFailureState(schedule: ReturnType<typeof getSchedule>, nextFailures: number): Pick<ReturnType<typeof getSchedule>, 'consecutiveFailures' | 'nextEligibleAt' | 'enabled' | 'pausedReason'> {
15
- const shouldPause = nextFailures >= schedule.policy.maxFailures;
14
+ function computeScheduleBackoff(schedule: ReturnType<typeof getSchedule>, nextFailures: number): string {
16
15
  const backoffBase = Math.max(1, schedule.policy.backoffBaseMinutes ?? schedule.policy.cooldownMinutes ?? 1);
17
16
  const backoffMax = Math.max(backoffBase, schedule.policy.backoffMaxMinutes ?? 24 * 60);
18
- const backoffMinutes = Math.min(backoffMax, backoffBase * (2 ** Math.max(0, nextFailures - 1)));
17
+ // Persistent readiness failures may span hours or days. Cap the exponent so
18
+ // the calculation stays bounded even while the observable failure count grows.
19
+ const exponent = Math.min(16, Math.max(0, nextFailures - 1));
20
+ const backoffMinutes = Math.min(backoffMax, backoffBase * (2 ** exponent));
21
+ return new Date(Date.now() + backoffMinutes * 60_000).toISOString();
22
+ }
23
+
24
+ function computeScheduleFailureState(schedule: ReturnType<typeof getSchedule>, nextFailures: number): Pick<ReturnType<typeof getSchedule>, 'consecutiveFailures' | 'nextEligibleAt' | 'enabled' | 'pausedReason'> {
25
+ const shouldPause = nextFailures >= schedule.policy.maxFailures;
19
26
  return {
20
27
  consecutiveFailures: nextFailures,
21
- nextEligibleAt: new Date(Date.now() + backoffMinutes * 60_000).toISOString(),
28
+ nextEligibleAt: computeScheduleBackoff(schedule, nextFailures),
22
29
  enabled: shouldPause ? false : schedule.enabled,
23
30
  pausedReason: shouldPause ? 'Maximum consecutive failures reached.' : undefined,
24
31
  };
@@ -68,6 +75,43 @@ export function applyScheduleFailure(
68
75
  return { schedule: nextSchedule, occurrence: nextOccurrence };
69
76
  }
70
77
 
78
+ export function applyScheduleRetryableFailure(
79
+ controllerHome: string,
80
+ scheduleId: string,
81
+ repoId: string,
82
+ occurrenceId: string,
83
+ options: {
84
+ outcome: 'failed' | 'skipped';
85
+ decision: ScheduleDecisionType;
86
+ reason: string;
87
+ countFailure?: boolean;
88
+ },
89
+ ): { schedule: ReturnType<typeof getSchedule>; occurrence?: ScheduleOccurrence } {
90
+ const schedule = getSchedule(controllerHome, repoId, scheduleId);
91
+ const occurrence = getOccurrence(controllerHome, repoId, occurrenceId);
92
+ let nextOccurrence = occurrence;
93
+ if (occurrence && !TERMINAL_OCCURRENCE_STATUSES.has(occurrence.status)) {
94
+ nextOccurrence = saveOccurrence(controllerHome, {
95
+ ...occurrence,
96
+ status: options.outcome,
97
+ decision: options.decision,
98
+ reason: options.reason,
99
+ });
100
+ }
101
+ const countFailure = options.countFailure !== false;
102
+ const nextFailures = countFailure ? schedule.consecutiveFailures + 1 : schedule.consecutiveFailures;
103
+ const nextSchedule = saveSchedule(controllerHome, {
104
+ ...schedule,
105
+ consecutiveFailures: nextFailures,
106
+ nextEligibleAt: computeScheduleBackoff(schedule, Math.max(1, nextFailures)),
107
+ // Retryable readiness/semantic-wait failures never turn a durable schedule
108
+ // into a manual-resume task. Respect a concurrent explicit pause, however.
109
+ enabled: schedule.enabled,
110
+ pausedReason: schedule.enabled ? undefined : schedule.pausedReason,
111
+ });
112
+ return { schedule: nextSchedule, occurrence: nextOccurrence };
113
+ }
114
+
71
115
  export function markScheduledExecutionRunning(controllerHome: string, job: ExecutionJob): void {
72
116
  if (job.type !== 'scheduled-occurrence') return;
73
117
  const occurrenceId = typeof job.payload.occurrenceId === 'string' ? job.payload.occurrenceId : undefined;
@@ -1,9 +1,14 @@
1
1
  import { createHash, randomUUID } from 'crypto';
2
- import { existsSync, readdirSync } from 'fs';
2
+ import { existsSync, readdirSync, rmSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
5
5
  import { withControllerLock } from '../../../cli/repositories/locks';
6
- import { createHandoffItem, getHandoffItem } from '../../control-plane/facade/handoff-inbox-store';
6
+ import {
7
+ createHandoffItem,
8
+ getHandoffItem,
9
+ listHandoffItems,
10
+ resolveHandoffItem,
11
+ } from '../../control-plane/facade/handoff-inbox-store';
7
12
  import { readJsonFile, writeJsonAtomic } from '../../shared/json-files';
8
13
  import { appendRuntimeEvent } from '../../evidence/event-ledger';
9
14
  import type { RepositorySchedule, ScheduleDecision, ScheduleOccurrence } from './types';
@@ -159,17 +164,120 @@ export function listSchedules(controllerHome: string, repoId: string): Repositor
159
164
  } catch { return []; }
160
165
  }
161
166
 
167
+ export function deleteSchedule(controllerHome: string, repoId: string, scheduleId: string): RepositorySchedule {
168
+ return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `schedule-${scheduleId}` }, `delete-schedule:${scheduleId}`, () => {
169
+ const current = getSchedule(controllerHome, repoId, scheduleId);
170
+ if (current.enabled) throw new Error(`SCHEDULE_DELETE_REQUIRES_PAUSED: ${scheduleId}`);
171
+ if (listActiveOccurrences(controllerHome, repoId, scheduleId).length > 0) {
172
+ throw new Error(`SCHEDULE_DELETE_ACTIVE_OCCURRENCE: ${scheduleId}`);
173
+ }
174
+ rmSync(schedulePath(controllerHome, repoId, scheduleId), { force: true });
175
+ const requestRecordPath = requestPath(controllerHome, repoId, current.requestId);
176
+ if (existsSync(requestRecordPath)) {
177
+ const record = readJsonFile<ScheduleRequestRecord>(requestRecordPath);
178
+ if (record.scheduleId === scheduleId) rmSync(requestRecordPath, { force: true });
179
+ }
180
+ appendRuntimeEvent(controllerHome, {
181
+ repoId,
182
+ entityType: 'schedule',
183
+ entityId: scheduleId,
184
+ eventType: 'schedule_deleted',
185
+ requestId: current.requestId,
186
+ revision: current.revision + 1,
187
+ data: { name: current.name },
188
+ });
189
+ return current;
190
+ }, 10_000);
191
+ }
192
+
193
+ function scheduleFailureClass(reason: string): string {
194
+ const trimmed = reason.trim();
195
+ const explicitCode = trimmed.match(/^([A-Z][A-Z0-9_]{2,})(?::|\b)/)?.[1];
196
+ if (explicitCode) return explicitCode;
197
+ const prefix = (trimmed.split(':', 1)[0] ?? trimmed)
198
+ .replace(/\s+/g, ' ')
199
+ .trim()
200
+ .toLowerCase()
201
+ .slice(0, 120);
202
+ return prefix || 'unknown_failure';
203
+ }
204
+
205
+ function activeScheduleFailureHandoff(
206
+ controllerHome: string,
207
+ repoId: string,
208
+ scheduleId: string,
209
+ creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
210
+ reason: string,
211
+ ) {
212
+ const failureClass = scheduleFailureClass(reason);
213
+ return listHandoffItems({ controllerHome, repoId, status: 'active', limit: 100 }).find((item) => (
214
+ item.currentState?.taskId === scheduleId
215
+ && item.creationReason === creationReason
216
+ && scheduleFailureClass(item.reason) === failureClass
217
+ ));
218
+ }
219
+
220
+ function stableScheduleFailureHandoffId(
221
+ controllerHome: string,
222
+ repoId: string,
223
+ scheduleId: string,
224
+ creationReason: ScheduleOccurrenceHandoffInput['creationReason'],
225
+ reason: string,
226
+ occurrenceId: string,
227
+ ): string {
228
+ const digest = createHash('sha256')
229
+ .update(`${scheduleId}:${creationReason}:${scheduleFailureClass(reason)}`)
230
+ .digest('hex')
231
+ .slice(0, 20);
232
+ const baseId = `schedule-failure-${digest}`;
233
+ if (!getHandoffItem({ controllerHome, repoId }, baseId)) return baseId;
234
+ // A previous failure streak may already have resolved the stable item. Handoff
235
+ // records are immutable apart from lifecycle status, so use one new epoch id;
236
+ // subsequent failures in this streak will discover and reuse the active item.
237
+ const epoch = createHash('sha256').update(occurrenceId).digest('hex').slice(0, 8);
238
+ return `${baseId}-${epoch}`;
239
+ }
240
+
241
+ function resolveRecoveredScheduleFailureHandoffs(controllerHome: string, occurrence: ScheduleOccurrence): void {
242
+ try {
243
+ const active = listHandoffItems({ controllerHome, repoId: occurrence.repoId, status: 'active', limit: 100 })
244
+ .filter((item) => (
245
+ item.currentState?.taskId === occurrence.scheduleId
246
+ && item.creationReason === 'repeated_infrastructure_failure'
247
+ ));
248
+ for (const item of active) {
249
+ try {
250
+ resolveHandoffItem(
251
+ { controllerHome, repoId: occurrence.repoId },
252
+ item.id,
253
+ {
254
+ decision: `Schedule ${occurrence.scheduleId} recovered on occurrence ${occurrence.occurrenceId}.`,
255
+ resolver: 'schedule-runtime',
256
+ },
257
+ );
258
+ } catch {
259
+ // The succeeded occurrence remains authoritative. Inbox cleanup is
260
+ // best-effort and must never turn schedule recovery into a failure.
261
+ }
262
+ }
263
+ } catch {
264
+ // Preserve successful Schedule settlement even if Inbox storage is unavailable.
265
+ }
266
+ }
267
+
162
268
  export function saveOccurrence(controllerHome: string, occurrence: ScheduleOccurrence): ScheduleOccurrence {
163
- return withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
269
+ const next = withControllerLock(controllerHome, { scope: 'task', repoId: occurrence.repoId, taskId: `schedule-${occurrence.scheduleId}` }, `save-occurrence:${occurrence.occurrenceId}`, () => {
164
270
  const existingPath = occurrencePath(controllerHome, occurrence.repoId, occurrence.occurrenceId);
165
271
  const previous = existsSync(existingPath) ? readJsonFile<ScheduleOccurrence>(existingPath) : undefined;
166
- const next = { ...occurrence, revision: (previous?.revision ?? occurrence.revision ?? 0) + 1, updatedAt: new Date().toISOString() };
167
- writeJsonAtomic(existingPath, next);
168
- upsertOccurrenceIndexUnlocked(controllerHome, next);
169
- const schedule = getSchedule(controllerHome, next.repoId, next.scheduleId);
170
- appendRuntimeEvent(controllerHome, { repoId: next.repoId, entityType: 'occurrence', entityId: next.occurrenceId, eventType: `occurrence_${next.status}`, requestId: `${schedule.requestId}:${next.windowKey}`, revision: next.revision, correlationId: next.scheduleId, data: { decision: next.decision, jobId: next.jobId, reason: next.reason } });
171
- return next;
272
+ const saved = { ...occurrence, revision: (previous?.revision ?? occurrence.revision ?? 0) + 1, updatedAt: new Date().toISOString() };
273
+ writeJsonAtomic(existingPath, saved);
274
+ upsertOccurrenceIndexUnlocked(controllerHome, saved);
275
+ const schedule = getSchedule(controllerHome, saved.repoId, saved.scheduleId);
276
+ appendRuntimeEvent(controllerHome, { repoId: saved.repoId, entityType: 'occurrence', entityId: saved.occurrenceId, eventType: `occurrence_${saved.status}`, requestId: `${schedule.requestId}:${saved.windowKey}`, revision: saved.revision, correlationId: saved.scheduleId, data: { decision: saved.decision, jobId: saved.jobId, reason: saved.reason } });
277
+ return saved;
172
278
  }, 10_000);
279
+ if (next.status === 'succeeded') resolveRecoveredScheduleFailureHandoffs(controllerHome, next);
280
+ return next;
173
281
  }
174
282
 
175
283
  export function getOccurrence(controllerHome: string, repoId: string, occurrenceId: string): ScheduleOccurrence | undefined {
@@ -184,7 +292,20 @@ export function recordScheduleOccurrenceHandoff(
184
292
  input: ScheduleOccurrenceHandoffInput,
185
293
  ): ScheduleOccurrence {
186
294
  if (occurrence.handoffId) return occurrence;
187
- const handoffId = `schedule-${occurrence.occurrenceId}`;
295
+ const reusable = input.creationReason === 'repeated_infrastructure_failure'
296
+ ? activeScheduleFailureHandoff(controllerHome, occurrence.repoId, schedule.scheduleId, input.creationReason, input.reason)
297
+ : undefined;
298
+ if (reusable) return saveOccurrence(controllerHome, { ...occurrence, handoffId: reusable.id });
299
+ const handoffId = input.creationReason === 'repeated_infrastructure_failure'
300
+ ? stableScheduleFailureHandoffId(
301
+ controllerHome,
302
+ occurrence.repoId,
303
+ schedule.scheduleId,
304
+ input.creationReason,
305
+ input.reason,
306
+ occurrence.occurrenceId,
307
+ )
308
+ : `schedule-${occurrence.occurrenceId}`;
188
309
  const evidenceRefs: NonNullable<ScheduleOccurrenceHandoffInput['evidenceRefs']> = [
189
310
  { title: `Occurrence ${occurrence.occurrenceId}`, summary: occurrence.reason ?? input.reason, detailLevel: 'summary' as const },
190
311
  ];
@@ -10,10 +10,8 @@ export type ScheduleTriggerType =
10
10
  | 'manual';
11
11
 
12
12
  export interface ScheduleCondition {
13
- kind: 'repository_clean' | 'job_succeeded' | 'job_terminal' | 'candidate_observation_threshold';
13
+ kind: 'repository_clean' | 'job_succeeded' | 'job_terminal';
14
14
  jobId?: string;
15
- semanticKey?: string;
16
- observationThreshold?: number;
17
15
  }
18
16
 
19
17
  export interface ScheduleTrigger {
@@ -73,7 +71,7 @@ export interface RepositorySchedule {
73
71
  consecutiveNoops?: number;
74
72
  nextEligibleAt?: string;
75
73
  pausedReason?: string;
76
- /** Last deterministic external observation for browser_watch/browser_keepalive. */
74
+ /** Last deterministic external observation for browser or issue watchers. */
77
75
  lastObservationAt?: string;
78
76
  lastObservationFingerprint?: string;
79
77
  lastObservationChangedAt?: string;
@@ -22,7 +22,7 @@ export type ContinuationControllerType = 'chatgpt' | 'codex' | 'claude' | 'grok'
22
22
  export type WorkScheduleMode = 'continuation' | 'browser_watch' | 'browser_keepalive';
23
23
 
24
24
  export interface WorkContinuationScheduleInput {
25
- workId: string;
25
+ workId?: string;
26
26
  scheduleMode?: WorkScheduleMode;
27
27
  controllerType?: ContinuationControllerType;
28
28
  executable?: string;
@@ -106,8 +106,9 @@ function normalizedTrigger(input: WorkContinuationScheduleInput): ScheduleTrigge
106
106
  }
107
107
 
108
108
  function wakeArguments(input: WorkContinuationScheduleInput, controllerType: ContinuationControllerType): Record<string, unknown> {
109
+ const workId = input.workId?.trim();
109
110
  return {
110
- work_id: input.workId.trim(),
111
+ ...(workId ? { work_id: workId } : {}),
111
112
  controller_type: controllerType,
112
113
  ...(input.executable?.trim() ? { executable: input.executable.trim() } : {}),
113
114
  ...(input.launchArgs ? { launch_args: input.launchArgs.map(String) } : {}),
@@ -146,10 +147,13 @@ export function createWorkContinuationSchedule(
146
147
  controllerHome: string,
147
148
  repoId: string,
148
149
  input: WorkContinuationScheduleInput,
149
- ): { schedule: RepositorySchedule; work: WorkContract } {
150
- const work = activeWork(controllerHome, repoId, input.workId);
150
+ ): { schedule: RepositorySchedule; work?: WorkContract } {
151
151
  const controllerType = input.controllerType ?? 'chatgpt';
152
152
  const scheduleMode = input.scheduleMode ?? 'continuation';
153
+ const requestedWorkId = input.workId?.trim();
154
+ if (!requestedWorkId && scheduleMode !== 'browser_keepalive') throw new Error('WORK_ID_REQUIRED');
155
+ if (!requestedWorkId && scheduleMode === 'browser_keepalive' && controllerType !== 'chatgpt') throw new Error('STANDALONE_BROWSER_KEEPALIVE_CHATGPT_REQUIRED');
156
+ const work = requestedWorkId ? activeWork(controllerHome, repoId, requestedWorkId) : undefined;
153
157
  const trigger = normalizedTrigger(input);
154
158
  const operation = scheduleMode === 'continuation' ? 'external_controller_wake' : 'browser_probe';
155
159
  const actionArguments = scheduleMode === 'continuation'
@@ -157,9 +161,10 @@ export function createWorkContinuationSchedule(
157
161
  : probeArguments(input, controllerType, scheduleMode === 'browser_keepalive');
158
162
  assertAutomatedOperationAllowed(operation, actionArguments);
159
163
  const name = input.scheduleName?.trim()
160
- || (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work.workId}`
161
- : scheduleMode === 'browser_keepalive' ? `Keep browser session alive for Work ${work.workId}`
162
- : `Continue Work ${work.workId}`);
164
+ || (scheduleMode === 'browser_watch' ? `Watch external state for Work ${work!.workId}`
165
+ : scheduleMode === 'browser_keepalive'
166
+ ? (work ? `Keep browser session alive for Work ${work.workId}` : 'Keep browser session alive')
167
+ : `Continue Work ${work!.workId}`);
163
168
  const policy = {
164
169
  maxActiveOccurrences: 1,
165
170
  maxFailures: input.maxFailures !== undefined ? Math.max(1, Math.trunc(input.maxFailures)) : 3,
@@ -169,7 +174,7 @@ export function createWorkContinuationSchedule(
169
174
  backoffBaseMinutes: input.backoffBaseMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffBaseMinutes)) : 5,
170
175
  backoffMaxMinutes: input.backoffMaxMinutes !== undefined ? Math.max(1, Math.trunc(input.backoffMaxMinutes)) : 24 * 60,
171
176
  };
172
- const stopConditions = input.stopConditions ?? ['work_terminal', 'human_review_required', 'external_blocker'];
177
+ const stopConditions = input.stopConditions ?? (work ? ['work_terminal', 'human_review_required', 'external_blocker'] : []);
173
178
 
174
179
  // A durable Work has one authoritative continuation lane. Changing cadence,
175
180
  // prompt, controller, or policy updates that lane instead of minting another
@@ -178,7 +183,7 @@ export function createWorkContinuationSchedule(
178
183
  if (scheduleMode === 'continuation') {
179
184
  const existing = listSchedules(controllerHome, repoId)
180
185
  .filter((candidate) => candidate.action.operation === 'external_controller_wake')
181
- .filter((candidate) => candidate.action.arguments?.work_id === work.workId);
186
+ .filter((candidate) => candidate.action.arguments?.work_id === work!.workId);
182
187
  const authoritative = [...existing].reverse().find((candidate) => candidate.enabled) ?? existing.at(-1);
183
188
  if (authoritative) {
184
189
  const schedule = saveSchedule(controllerHome, {
@@ -196,7 +201,7 @@ export function createWorkContinuationSchedule(
196
201
  saveSchedule(controllerHome, {
197
202
  ...duplicate,
198
203
  enabled: false,
199
- pausedReason: `Superseded by authoritative continuation ${schedule.scheduleId} for Work ${work.workId}.`,
204
+ pausedReason: `Superseded by authoritative continuation ${schedule.scheduleId} for Work ${work!.workId}.`,
200
205
  });
201
206
  }
202
207
  return { schedule, work };
@@ -205,7 +210,7 @@ export function createWorkContinuationSchedule(
205
210
 
206
211
  const semantic = JSON.stringify({
207
212
  repoId,
208
- workId: work.workId,
213
+ workId: work?.workId,
209
214
  controllerType,
210
215
  scheduleMode,
211
216
  trigger,
@@ -215,7 +220,9 @@ export function createWorkContinuationSchedule(
215
220
  stopConditions,
216
221
  });
217
222
  const requestId = input.requestId?.trim()
218
- || `work-continuation:${repoId}:${work.workId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`;
223
+ || (work
224
+ ? `work-continuation:${repoId}:${work.workId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`
225
+ : `browser-keepalive:${repoId}:${createHash('sha256').update(semantic).digest('hex').slice(0, 16)}`);
219
226
  const schedule = createSchedule(controllerHome, {
220
227
  requestId,
221
228
  repoId,
@@ -277,7 +284,13 @@ export function resumeWorkContinuationSchedule(controllerHome: string, repoId: s
277
284
  const schedule = continuationSchedule(getSchedule(controllerHome, repoId, scheduleId));
278
285
  const workId = typeof schedule.action.arguments?.work_id === 'string' ? schedule.action.arguments.work_id : '';
279
286
  activeWork(controllerHome, repoId, workId);
280
- return saveSchedule(controllerHome, { ...schedule, enabled: true, pausedReason: undefined });
287
+ return saveSchedule(controllerHome, {
288
+ ...schedule,
289
+ enabled: true,
290
+ pausedReason: undefined,
291
+ consecutiveFailures: 0,
292
+ nextEligibleAt: undefined,
293
+ });
281
294
  }
282
295
 
283
296
  export async function triggerWorkContinuationSchedule(
@@ -26,30 +26,24 @@ export type TestModule = (typeof TEST_MODULES)[number];
26
26
  export type TestResource = (typeof TEST_RESOURCES)[number];
27
27
  export type TestGate = 'affected' | 'core' | 'integration' | 'infrastructure' | 'fault' | 'full';
28
28
 
29
+ /**
30
+ * Acceptance evidence is the default. Tests must protect externally observable
31
+ * contracts, migrations/rollback, failure injection, security boundaries, or
32
+ * a recorded production incident. Do not add tests for private helpers,
33
+ * implementation payloads, copy, enum order, or mock-only lifecycles.
34
+ */
35
+ export const TEST_GOVERNANCE_POLICY = 'acceptance-first-no-internal-unit-tests' as const;
36
+
29
37
  export interface TestManifestEntry {
30
38
  module: TestModule;
31
39
  resource: TestResource;
32
40
  smoke?: boolean;
33
41
  }
34
42
 
35
- export interface TestBudgetPolicy {
36
- maxTestFiles: number;
37
- maxTestLines: number;
38
- maxPureFiles: number;
39
- maxTempIsolatedFiles: number;
40
- maxProcessTreeFiles: number;
41
- maxGitWorktreeFiles: number;
42
- maxFixedPortFiles: number;
43
- maxRuntimeSingletonFiles: number;
44
- maxControllerHomeIsolatedFiles: number;
45
- maxDestructiveFiles: number;
46
- }
47
-
48
43
  export interface TestManifest {
49
44
  version: 1;
50
45
  modules: TestModule[];
51
46
  resources: TestResource[];
52
- policy: TestBudgetPolicy;
53
47
  pathModuleRules: Array<{ prefix: string; modules: TestModule[] }>;
54
48
  tests: Record<string, TestManifestEntry>;
55
49
  }
@@ -232,32 +226,6 @@ export function validateTestManifest(repoRoot: string, manifest = loadTestManife
232
226
  if (!TEST_RESOURCES.includes(entry.resource)) errors.push(`${file}: invalid resource ${String(entry.resource)}`);
233
227
  }
234
228
  if (!Object.values(manifest.tests).some((entry) => entry.smoke)) errors.push('manifest has no core smoke tests');
235
- const resourceCounts = Object.values(manifest.tests).reduce<Record<string, number>>((counts, entry) => {
236
- counts[entry.resource] = (counts[entry.resource] ?? 0) + 1;
237
- return counts;
238
- }, {});
239
- const testLines = declaredFiles.reduce((total, file) => {
240
- try {
241
- return total + readFileSync(resolve(repoRoot, file), 'utf8').split('\n').length;
242
- } catch (_error) {
243
- return total;
244
- }
245
- }, 0);
246
- const budgetChecks: Array<[string, number, number | undefined]> = [
247
- ['maxTestFiles', declaredFiles.length, manifest.policy?.maxTestFiles],
248
- ['maxTestLines', testLines, manifest.policy?.maxTestLines],
249
- ['maxPureFiles', resourceCounts.pure ?? 0, manifest.policy?.maxPureFiles],
250
- ['maxTempIsolatedFiles', resourceCounts['temp-isolated'] ?? 0, manifest.policy?.maxTempIsolatedFiles],
251
- ['maxProcessTreeFiles', resourceCounts['process-tree'] ?? 0, manifest.policy?.maxProcessTreeFiles],
252
- ['maxGitWorktreeFiles', resourceCounts['git-worktree'] ?? 0, manifest.policy?.maxGitWorktreeFiles],
253
- ['maxFixedPortFiles', resourceCounts['fixed-port'] ?? 0, manifest.policy?.maxFixedPortFiles],
254
- ['maxRuntimeSingletonFiles', resourceCounts['runtime-singleton'] ?? 0, manifest.policy?.maxRuntimeSingletonFiles],
255
- ['maxControllerHomeIsolatedFiles', resourceCounts['controller-home-isolated'] ?? 0, manifest.policy?.maxControllerHomeIsolatedFiles],
256
- ['maxDestructiveFiles', resourceCounts.destructive ?? 0, manifest.policy?.maxDestructiveFiles],
257
- ];
258
- for (const [name, actual, limit] of budgetChecks) {
259
- if (typeof limit !== 'number' || actual > limit) errors.push(`test budget ${name} exceeded: ${actual} > ${String(limit)}`);
260
- }
261
229
  return errors;
262
230
  }
263
231
 
@@ -1,57 +0,0 @@
1
- # Heartbeat Triage
2
-
3
- Heartbeat triage is the repo-local scheduled discovery loop. It records findings
4
- for a human to review; it does not approve plans, execute fixes, spawn agents,
5
- open PRs, or install a persistent scheduler.
6
-
7
- ## Command
8
-
9
- Run from the repository root:
10
-
11
- ```bash
12
- bash .ai/harness/scripts/heartbeat-triage.sh run --source scheduled
13
- ```
14
-
15
- The command appends a run section to `.ai/harness/triage/inbox.md` and writes a
16
- JSON snapshot under `.ai/harness/runs/`. Each run records:
17
-
18
- - `workflow-check`: `bash .ai/harness/scripts/check-task-workflow.sh --strict`
19
- - `sprint-next`: `bash .ai/harness/scripts/sprint-backlog.sh next` when an active sprint
20
- marker exists, with a read-only sprint-file fallback when it does not
21
- - `drift-requests`: pending files under `docs/architecture/requests/`
22
-
23
- ## cron
24
-
25
- Example cron entry for a local checkout:
26
-
27
- ```cron
28
- 17 8 * * * cd /path/to/repo && bash .ai/harness/scripts/heartbeat-triage.sh run --source scheduled >/tmp/forge-heartbeat.log 2>&1
29
- ```
30
-
31
- ## loop
32
-
33
- Example long-running loop for a supervised terminal, launch wrapper, or local
34
- process manager:
35
-
36
- ```bash
37
- while true; do
38
- cd /path/to/repo && bash .ai/harness/scripts/heartbeat-triage.sh run --source scheduled
39
- sleep 86400
40
- done
41
- ```
42
-
43
- ## Adoption Review
44
-
45
- Every run includes an "Adoption review due" date fourteen days out. The review
46
- question is narrow: did the inbox produce any human-accepted triage item? If no
47
- item is accepted during the review window, remove the scheduler and keep the
48
- runner as an on-demand diagnostic command.
49
-
50
- ## Guardrails
51
-
52
- - Keep scheduler installation manual and host-local.
53
- - Treat workflow failures and drift requests as inbox entries, not process
54
- crashes.
55
- - Keep `.ai/harness/triage/inbox.md` as runtime state, not a source-of-truth
56
- plan or task ledger.
57
- - Keep execution authority in the normal plan -> contract -> verify flow.