@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
@@ -1,8 +1,10 @@
1
1
  import { randomUUID } from 'crypto';
2
2
  import {
3
3
  createHandoffItem,
4
+ listHandoffItems,
4
5
  type HandoffInboxStoreOptions,
5
6
  } from './handoff-inbox-store';
7
+ import { getControllerSession } from './controller-session-store';
6
8
  import {
7
9
  appendVerificationRecord,
8
10
  appendWorkEvidence,
@@ -10,6 +12,7 @@ import {
10
12
  createWorkContract,
11
13
  getWorkContract,
12
14
  listWorkContracts,
15
+ recordWorkScopeEvidence,
13
16
  summarizeWorkContract,
14
17
  transitionWorkContractPhase,
15
18
  updateWorkContract,
@@ -21,6 +24,10 @@ import {
21
24
  getPlanContract,
22
25
  type PlanContractStoreOptions,
23
26
  } from './plan-contract-store';
27
+ import { withPrimaryWorkAdmissionLock } from './semantic-admission';
28
+ import { completeWorkWithReceipt } from '../execution/work-completion-authority';
29
+ import { evaluateReadOnlyReviewSourceIdentity, evaluateWorkCompletionEvidence, evaluateWorkImplementationEvidence, verificationRecordAppliesToCurrentWorkspace } from '../execution/work-evidence-policy';
30
+ import { readRequirement } from '../persistence/requirement-store';
24
31
  import {
25
32
  classifyVerificationOutcome,
26
33
  normalizeCheckIds,
@@ -41,9 +48,11 @@ import type {
41
48
  SuggestedNextAction,
42
49
  VerificationRecord,
43
50
  WorkContract,
51
+ WorkKind,
44
52
  WorkRisk,
45
53
  } from './types';
46
54
  import { selectExecutionMode } from './types';
55
+ import { resolveWorkspaceAdmissionConstraint } from '../routing/workspace-admission';
47
56
 
48
57
  export type GoalWorkloopOperation = 'start' | 'continue' | 'verify' | 'finalize' | 'stop';
49
58
 
@@ -58,8 +67,14 @@ export interface GoalWorkloopContext {
58
67
  principalId?: string;
59
68
  controllerInstanceId?: string;
60
69
  workspaceFingerprint?: string;
70
+ /** Current net repository paths changed from the Work base plus dirty checkout paths. */
71
+ workspaceChangedPaths?: readonly string[];
72
+ /** Durable terminal repository Process ids explicitly bound to this Work + checkout. */
73
+ workBoundProcessEvidenceIds?: readonly string[];
61
74
  now?: () => string;
62
- materializeIsolatedWorkspace?: (input: { workId: string; title: string; baseRef?: string }) => { checkoutId: string; root: string; baseRevision?: string | null; managed: true };
75
+ /** True only while a Gateway caller holds the cross-process primary Work admission lock. */
76
+ semanticAdmissionLocked?: boolean;
77
+ materializeIsolatedWorkspace?: (input: { workId: string; title: string; baseRef?: string; needsDependencies?: boolean }) => { checkoutId: string; root: string; baseRevision?: string | null; managed: true };
63
78
  }
64
79
 
65
80
  export type WorkAdmissionRelation = 'continue' | 'extend' | 'parallel' | 'new_goal';
@@ -68,6 +83,8 @@ export interface GoalWorkloopStartInput {
68
83
  objective: string;
69
84
  acceptanceCriteria?: string[];
70
85
  allowedPaths?: string[];
86
+ /** Non-authoritative first-pass discovery candidates. */
87
+ initialLikelyPaths?: string[];
71
88
  forbiddenPaths?: string[];
72
89
  checks?: string[];
73
90
  constraints?: WorkContract['constraints'];
@@ -83,16 +100,45 @@ export interface GoalWorkloopStartInput {
83
100
  forceMode?: WorkContract['mode'];
84
101
  planId?: string;
85
102
  planStepId?: string;
103
+ /** Explicit technical evidence shape chosen by the semantic Controller. Never inferred from objective/check text. */
104
+ workKind?: Extract<WorkKind, 'repository_change' | 'completed_no_change' | 'read_only_review' | 'investigation' | 'local_effect' | 'remote_effect' | 'reconciliation'>;
105
+ }
106
+
107
+ export interface GoalWorkloopAcceptanceEvidenceBinding {
108
+ criterion: string;
109
+ evidenceIds: string[];
110
+ rationale: string;
86
111
  }
87
112
 
88
113
  export interface GoalWorkloopContinueInput {
89
114
  workId: string;
90
115
  note?: string;
116
+ allowedPaths?: string[];
117
+ forbiddenPaths?: string[];
118
+ checks?: string[];
119
+ additionalLikelyPaths?: string[];
120
+ inspectedPaths?: string[];
121
+ /** Concise semantic findings discovered by a first-class read-only review. */
122
+ reviewFindings?: string[];
123
+ /** Explicit Controller-reviewed bindings from exact declared criteria to durable Work evidence. */
124
+ acceptanceEvidence?: GoalWorkloopAcceptanceEvidenceBinding[];
125
+ /** Explicit Controller decision after deterministic acceptance failure; never inferred from note text. */
126
+ acceptanceFailureDecision?: 'repair' | 'rescope';
91
127
  }
92
128
 
93
129
  export interface GoalWorkloopVerifyInput {
94
130
  workId: string;
95
131
  checkId: string;
132
+ /** Exact Git revision observed by the authoritative Work-bound verification Process. */
133
+ sourceRevision?: string;
134
+ /** Exact dirty-workspace content identity observed by the authoritative check. */
135
+ workspaceFingerprint?: string;
136
+ /** Stable semantic check-input identity used for exact evidence reuse. */
137
+ verificationInputFingerprint?: string;
138
+ /** Bounded command/invocation audit identity; not part of reusable semantic inputs. */
139
+ commandFingerprint?: string;
140
+ /** Persistence-safe Process receipt. Accepted only when its Work/repo/check identity matches this verification. */
141
+ receipt?: VerificationRecord['receipt'];
96
142
  /** When true, simulate infrastructure failure rather than acceptance fail. */
97
143
  infrastructureFailed?: boolean;
98
144
  /** When true and check is valid, record acceptance failure. */
@@ -131,6 +177,7 @@ function handoffIdFor(prefix: string): string {
131
177
  }
132
178
 
133
179
  function workRiskFor(input: GoalWorkloopStartInput): WorkRisk {
180
+ if (input.workKind === 'read_only_review') return 'readonly';
134
181
  const risk = input.modeInput.risk;
135
182
  if (input.modeInput.secretAccess === true || input.modeInput.destructive === true) return 'destructive';
136
183
  if (input.modeInput.remoteWrite === true) return 'high';
@@ -141,13 +188,13 @@ function workRiskFor(input: GoalWorkloopStartInput): WorkRisk {
141
188
  return 'medium';
142
189
  }
143
190
 
144
- function suggestedForWork(work: WorkContract, extras: SuggestedNextAction[] = []): SuggestedNextAction[] {
191
+ function suggestedForWorkIdentity(workId: string, checks: string[], extras: SuggestedNextAction[] = []): SuggestedNextAction[] {
145
192
  const base: SuggestedNextAction[] = [
146
193
  {
147
194
  label: 'Continue workloop',
148
195
  tool: 'rh_work',
149
196
  operation: 'continue',
150
- payload: { work_id: work.workId },
197
+ payload: { work_id: workId },
151
198
  risk: 'readonly',
152
199
  confidence: 'high',
153
200
  },
@@ -155,24 +202,28 @@ function suggestedForWork(work: WorkContract, extras: SuggestedNextAction[] = []
155
202
  label: 'Verify registered checks',
156
203
  tool: 'rh_work',
157
204
  operation: 'verify',
158
- payload: { work_id: work.workId, check_id: work.checks[0] },
205
+ payload: { work_id: workId, check_id: checks[0] },
159
206
  risk: 'workspace_write',
160
- confidence: work.checks[0] ? 'high' : 'low',
207
+ confidence: checks[0] ? 'high' : 'low',
161
208
  },
162
209
  {
163
210
  label: 'Finalize when ready',
164
211
  tool: 'rh_work',
165
212
  operation: 'finalize',
166
- payload: { work_id: work.workId },
213
+ payload: { work_id: workId },
167
214
  risk: 'readonly',
168
215
  confidence: 'medium',
169
216
  },
170
217
  ];
171
218
  return validateSuggestedNextActions([...extras, ...base], {
172
- validCheckIds: work.checks,
219
+ validCheckIds: checks,
173
220
  }).actions;
174
221
  }
175
222
 
223
+ function suggestedForWork(work: WorkContract, extras: SuggestedNextAction[] = []): SuggestedNextAction[] {
224
+ return suggestedForWorkIdentity(work.workId, work.checks, extras);
225
+ }
226
+
176
227
  function initialEvidence(objective: string): EvidenceRef {
177
228
  return {
178
229
  title: 'work contract created',
@@ -181,56 +232,6 @@ function initialEvidence(objective: string): EvidenceRef {
181
232
  };
182
233
  }
183
234
 
184
- type ReconciledVerificationHistory = ReturnType<typeof reconcileVerificationHistory>;
185
-
186
- interface WorkCompletionEvidenceEvaluation {
187
- status: 'complete' | 'failed' | 'incomplete';
188
- history: ReconciledVerificationHistory;
189
- missingChecks: string[];
190
- durableResultEvidence: boolean;
191
- reasons: string[];
192
- }
193
-
194
- function evaluateWorkCompletionEvidence(
195
- work: WorkContract,
196
- history: ReconciledVerificationHistory = reconcileVerificationHistory(
197
- work.checkRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
198
- ),
199
- ): WorkCompletionEvidenceEvaluation {
200
- const missingChecks = work.checks.filter((checkId) => !history.validPasses.includes(checkId));
201
- const durableResultEvidence = work.evidenceRefs.some((evidence) => Boolean(evidence.evidenceId || evidence.artifactId));
202
- const reasons: string[] = [];
203
-
204
- if (history.acceptanceFailures.length > 0) {
205
- reasons.push(`Acceptance checks failed: ${history.acceptanceFailures.join(', ')}.`);
206
- return { status: 'failed', history, missingChecks, durableResultEvidence, reasons };
207
- }
208
- if (history.infrastructureIssues.length > 0) {
209
- reasons.push(`Infrastructure issues remain: ${history.infrastructureIssues.join(', ')}.`);
210
- }
211
- if (history.invalidCheckIds.length > 0) {
212
- reasons.push(`Invalid check ids remain: ${history.invalidCheckIds.join(', ')}.`);
213
- }
214
- if (missingChecks.length > 0) {
215
- reasons.push(`Declared checks are missing valid_pass evidence: ${missingChecks.join(', ')}.`);
216
- }
217
- if (work.checks.length === 0 && !durableResultEvidence) {
218
- reasons.push('No durable result evidence (evidenceId or artifactId) was recorded for this no-check WorkContract.');
219
- }
220
-
221
- const complete = history.infrastructureIssues.length === 0
222
- && history.invalidCheckIds.length === 0
223
- && missingChecks.length === 0
224
- && (work.checks.length > 0 || durableResultEvidence);
225
- return {
226
- status: complete ? 'complete' : 'incomplete',
227
- history,
228
- missingChecks,
229
- durableResultEvidence,
230
- reasons,
231
- };
232
- }
233
-
234
235
  /**
235
236
  * Mode selection for facade routing. Direct control never creates a WorkContract.
236
237
  * Goal workloop creates one. Handoff-only creates a handoff and stops.
@@ -239,19 +240,86 @@ export function routeWorkStart(
239
240
  ctx: GoalWorkloopContext,
240
241
  input: GoalWorkloopStartInput,
241
242
  ): FacadeResult {
243
+ const placementResolution = resolveWorkspaceAdmissionConstraint(input.constraints);
244
+ if (placementResolution.ok === false) {
245
+ return buildFacadeResult({
246
+ status: 'blocked',
247
+ summary: `${placementResolution.code}: ${placementResolution.message}`,
248
+ data: { executionStarted: false, workContractCreated: false, placementConstraintConflict: true },
249
+ rawAvailable: false,
250
+ });
251
+ }
252
+ const placementConstraint = placementResolution.constraint;
253
+ const canonicalConstraints: WorkContract['constraints'] = {
254
+ ...(input.constraints ?? {}),
255
+ workspaceMode: placementConstraint.workspaceMode,
256
+ requireWorktree: placementConstraint.requireWorktree,
257
+ directMainProhibited: placementConstraint.directMainProhibited,
258
+ };
242
259
  const strategyConflictRequiresApproval = input.constraints?.architectureStrategyChange === true
243
260
  || input.constraints?.conflictsWithThinHarnessPolicy === true
244
261
  || input.constraints?.changesDefaultExecutionStrategy === true;
262
+ const relatedLifecycleSource = input.relatedWorkId ? getWorkContract(ctx.workStore, input.relatedWorkId) : undefined;
263
+ const inheritedReadOnlyReview = Boolean(
264
+ input.workKind === undefined
265
+ && input.workRelation === 'new_goal'
266
+ && relatedLifecycleSource?.workKind === 'read_only_review'
267
+ && (relatedLifecycleSource.status === 'cancelled' || relatedLifecycleSource.status === 'completed' || relatedLifecycleSource.status === 'failed'),
268
+ );
269
+ if (
270
+ inheritedReadOnlyReview
271
+ && relatedLifecycleSource?.baseRevision
272
+ && ctx.sourceRevision
273
+ && relatedLifecycleSource.baseRevision !== ctx.sourceRevision
274
+ ) {
275
+ return buildFacadeResult({
276
+ status: 'blocked',
277
+ summary: `READ_ONLY_REVIEW_REPLACEMENT_SOURCE_DRIFT: replacement source ${ctx.sourceRevision} does not match frozen review base ${relatedLifecycleSource.baseRevision}.`,
278
+ data: { executionStarted: false, workContractCreated: false, relatedWorkId: relatedLifecycleSource.workId, sourceDrift: true },
279
+ });
280
+ }
281
+ const requestedWorkKind = input.workKind ?? (inheritedReadOnlyReview ? 'read_only_review' : undefined);
282
+ const typedReadOnlyReviewRequested = requestedWorkKind === undefined
283
+ && (input.modeInput.mutation === false || (input.modeInput.mutation === undefined && input.modeInput.risk === 'readonly'))
284
+ && input.modeInput.requiresInvestigation === true
285
+ && input.modeInput.requiresRecovery === true;
286
+ const effectiveWorkKind = requestedWorkKind ?? (typedReadOnlyReviewRequested ? 'read_only_review' : undefined);
287
+ const readOnlyReviewRequested = effectiveWorkKind === 'read_only_review';
288
+ const readOnlyMutationConflict = readOnlyReviewRequested && (
289
+ input.modeInput.mutation === true
290
+ || (input.modeInput.expectedFiles ?? 0) > 0
291
+ || (input.modeInput.expectedChangedLines ?? 0) > 0
292
+ || input.modeInput.requiresExternalEffect === true
293
+ || input.modeInput.remoteWrite === true
294
+ || input.modeInput.destructive === true
295
+ );
296
+ if (readOnlyMutationConflict) {
297
+ return buildFacadeResult({
298
+ status: 'blocked',
299
+ summary: 'READ_ONLY_REVIEW_MUTATION_CONFLICT: read_only_review cannot request repository mutation, external effects, remote writes, or destructive execution.',
300
+ data: { executionStarted: false, workContractCreated: false, workKind: effectiveWorkKind },
301
+ });
302
+ }
245
303
  const effectiveModeInput: ExecutionModeSelectionInput = {
246
304
  ...input.modeInput,
247
305
  objective: input.objective,
248
306
  knownPaths: input.allowedPaths,
249
- mutation: input.modeInput.mutation ?? input.modeInput.risk !== 'readonly',
307
+ workspacePlacement: placementConstraint.workspaceMode,
308
+ directMainProhibited: placementConstraint.directMainProhibited,
309
+ mutation: readOnlyReviewRequested ? false : input.modeInput.mutation ?? input.modeInput.risk !== 'readonly',
310
+ risk: readOnlyReviewRequested ? 'readonly' : input.modeInput.risk,
250
311
  approvalConfirmed: input.approvalConfirmed === true,
251
312
  requiresUserApproval: input.approvalConfirmed === true
252
313
  ? false
253
314
  : input.modeInput.requiresUserApproval === true || strategyConflictRequiresApproval,
254
315
  };
316
+ if (effectiveModeInput.explicitMode === 'scale' && (!input.planId || !input.planStepId || !ctx.planStore)) {
317
+ return buildFacadeResult({
318
+ status: 'blocked',
319
+ summary: 'SCALE_PLAN_REQUIRED: explicit Scale execution requires a bound approved PlanContract step.',
320
+ data: { executionStarted: false, workContractCreated: false, planRequired: true, explicitMode: 'scale' },
321
+ });
322
+ }
255
323
  const applyForcedMode = (selected: ReturnType<typeof selectExecutionMode>) => input.forceMode
256
324
  ? {
257
325
  ...selected,
@@ -263,7 +331,7 @@ export function routeWorkStart(
263
331
  : selected;
264
332
  const evaluateAccessPolicy = (selected: ReturnType<typeof selectExecutionMode>) => evaluatePolicyGate({
265
333
  capabilityId: selected.mode === 'direct_control' ? 'repository.direct_edit' : selected.mode === 'goal_workloop' ? 'controller.goal_workloop' : 'controller.handoff_inbox',
266
- risk: input.modeInput.risk
334
+ risk: effectiveModeInput.risk
267
335
  ?? (input.modeInput.secretAccess === true ? 'raw_secret_config'
268
336
  : input.modeInput.destructive === true ? 'destructive'
269
337
  : input.modeInput.remoteWrite === true ? 'remote_write'
@@ -283,12 +351,24 @@ export function routeWorkStart(
283
351
  });
284
352
 
285
353
  let selectedMode = selectExecutionMode(effectiveModeInput);
354
+ if (input.forceMode === 'direct_control' && selectedMode.routeDecision.requiresIsolation) {
355
+ return buildFacadeResult({
356
+ status: 'blocked',
357
+ summary: 'WORKSPACE_PLACEMENT_DIRECT_CONTROL_FORBIDDEN: typed placement requires isolation, so force_mode=direct_control cannot override admission policy.',
358
+ data: { executionStarted: false, workContractCreated: false, routeDecision: selectedMode.routeDecision },
359
+ rawAvailable: false,
360
+ });
361
+ }
286
362
  let policy = evaluateAccessPolicy(selectedMode);
287
363
  if (policy.decision === 'allowed' && effectiveModeInput.requiresUserApproval !== true) {
288
364
  selectedMode = selectExecutionMode({ ...effectiveModeInput, approvalConfirmed: true });
289
365
  policy = evaluateAccessPolicy(selectedMode);
290
366
  }
291
367
  let mode = applyForcedMode(selectedMode);
368
+ const routeApprovalRequired = mode.routeDecision.requiresApproval
369
+ && mode.routeDecision.waitForUser
370
+ && mode.routeDecision.approvalState !== 'blocked_by_policy';
371
+ const approvalRequired = policy.decision === 'approval_required' || routeApprovalRequired;
292
372
 
293
373
  // Route Policy is the sole execution-depth authority. Existing Work is
294
374
  // ownership context, not a reason to upgrade an otherwise Direct operation
@@ -375,7 +455,7 @@ export function routeWorkStart(
375
455
  severity: policy.decision === 'denied' ? 'blocked' : 'needs_review',
376
456
  creationReason: !input.modeInput.scopeClear
377
457
  ? 'invalid_objective'
378
- : policy.decision === 'approval_required'
458
+ : approvalRequired
379
459
  ? (input.modeInput.destructive ? 'destructive_action_requires_confirmation' : 'policy_approval_required')
380
460
  : 'missing_authorization',
381
461
  reason: mode.reason,
@@ -388,13 +468,13 @@ export function routeWorkStart(
388
468
  },
389
469
  attemptedActions: ['route_execution_mode'],
390
470
  evidenceRefs: [],
391
- blockingDecision: policy.decision === 'approval_required'
471
+ blockingDecision: approvalRequired
392
472
  ? 'Approve side effects or restate a safer objective.'
393
473
  : 'Clarify objective, scope, and acceptance criteria.',
394
474
  recommendedDecision: 'Provide a clear objective and authorization, or cancel the request.',
395
475
  recommendedPrompt: `Resolve handoff and restate work for repo ${ctx.repoId}.`,
396
476
  recommendedContinuationPrompt: `After approval, start the approved work for ${ctx.repoId}.`,
397
- approvalAction: policy.decision === 'approval_required'
477
+ approvalAction: approvalRequired
398
478
  ? {
399
479
  operation: 'start',
400
480
  label: 'Approve and start work',
@@ -415,6 +495,9 @@ export function routeWorkStart(
415
495
  requiresApproval: input.modeInput.requiresApproval === true || input.modeInput.requiresUserApproval === true,
416
496
  destructive: input.modeInput.destructive === true,
417
497
  accessMode: input.constraints?.accessMode,
498
+ workspaceMode: placementConstraint.workspaceMode,
499
+ requireWorktree: placementConstraint.requireWorktree,
500
+ directMainProhibited: placementConstraint.directMainProhibited,
418
501
  approvalConfirmed: true,
419
502
  forceMode: 'goal_workloop',
420
503
  },
@@ -432,7 +515,7 @@ export function routeWorkStart(
432
515
  });
433
516
 
434
517
  return buildFacadeResult({
435
- status: policy.decision === 'denied' ? 'blocked' : policy.decision === 'approval_required' ? 'approval_required' : 'blocked',
518
+ status: policy.decision === 'denied' ? 'blocked' : approvalRequired ? 'approval_required' : 'blocked',
436
519
  summary: `Handoff-only: no WorkContract created and no execution started. ${mode.reason}`,
437
520
  data: {
438
521
  mode,
@@ -462,7 +545,12 @@ export function routeWorkStart(
462
545
  });
463
546
  }
464
547
 
465
- return startGoalWorkloop(ctx, input, policy, 'goal_workloop', mode.routeDecision);
548
+ return startGoalWorkloop(ctx, {
549
+ ...input,
550
+ constraints: canonicalConstraints,
551
+ modeInput: effectiveModeInput,
552
+ workKind: effectiveWorkKind,
553
+ }, policy, 'goal_workloop', mode.routeDecision);
466
554
  }
467
555
 
468
556
  export function startGoalWorkloop(
@@ -472,9 +560,35 @@ export function startGoalWorkloop(
472
560
  executionMode: 'direct_control' | 'goal_workloop' = 'goal_workloop',
473
561
  routeDecision = selectExecutionMode({ ...input.modeInput, objective: input.objective, knownPaths: input.allowedPaths }).routeDecision,
474
562
  ): FacadeResult {
563
+ const placementResolution = resolveWorkspaceAdmissionConstraint(input.constraints);
564
+ if (placementResolution.ok === false) {
565
+ return buildFacadeResult({
566
+ status: 'blocked',
567
+ summary: `${placementResolution.code}: ${placementResolution.message}`,
568
+ data: { executionStarted: false, workContractCreated: false, placementConstraintConflict: true },
569
+ rawAvailable: false,
570
+ });
571
+ }
572
+ const placementConstraint = placementResolution.constraint;
573
+ const canonicalConstraints: WorkContract['constraints'] = {
574
+ ...(input.constraints ?? {}),
575
+ workspaceMode: placementConstraint.workspaceMode,
576
+ requireWorktree: placementConstraint.requireWorktree,
577
+ directMainProhibited: placementConstraint.directMainProhibited,
578
+ };
579
+ const hasStrongAdmissionBinding = Boolean(input.planId || input.planStepId || input.requirementId || input.relatedWorkId || input.workRelation);
580
+ if (hasStrongAdmissionBinding && !ctx.semanticAdmissionLocked) {
581
+ return withPrimaryWorkAdmissionLock(ctx.workStore, () => startGoalWorkloop(
582
+ { ...ctx, semanticAdmissionLocked: true },
583
+ input,
584
+ policy,
585
+ executionMode,
586
+ routeDecision,
587
+ ));
588
+ }
475
589
  const at = nowIso(ctx);
476
590
  const available = ctx.availableChecks ?? [];
477
- const workspaceMode = input.constraints?.workspaceMode ?? 'auto';
591
+ const workspaceMode = placementConstraint.workspaceMode;
478
592
  const activeWorks = listWorkContracts({ ...ctx.workStore, status: 'active', limit: 100 })
479
593
  .filter((candidate) => (candidate.lifecycleRole ?? 'primary') === 'primary');
480
594
  const workspaceOwner = ctx.checkoutId
@@ -487,6 +601,26 @@ export function startGoalWorkloop(
487
601
  : undefined;
488
602
  const plan = input.planId && ctx.planStore ? getPlanContract(ctx.planStore, input.planId) : undefined;
489
603
  const planStep = plan && input.planStepId ? plan.steps.find((step) => step.id === input.planStepId) : undefined;
604
+ const requestedRequirementId = input.requirementId?.trim() || undefined;
605
+ if (plan && requestedRequirementId && requestedRequirementId !== plan.requirementId) {
606
+ return buildFacadeResult({
607
+ status: 'blocked',
608
+ summary: `PLAN_REQUIREMENT_MISMATCH: ${plan.planId} is bound to ${plan.requirementId ?? 'no Requirement'}, not ${requestedRequirementId}. Use the Plan relationship authority or replan explicitly.`,
609
+ data: { executionStarted: false, workContractCreated: false, planId: plan.planId, planRequirementId: plan.requirementId, requestedRequirementId },
610
+ });
611
+ }
612
+ const effectiveRequirementId = requestedRequirementId || plan?.requirementId;
613
+ if (
614
+ effectiveRequirementId
615
+ && ctx.workStore.controllerHome
616
+ && !readRequirement({ controllerHome: ctx.workStore.controllerHome }, effectiveRequirementId)
617
+ ) {
618
+ return buildFacadeResult({
619
+ status: 'blocked',
620
+ summary: `${plan ? 'PLAN' : 'WORK'}_REQUIREMENT_NOT_FOUND: ${effectiveRequirementId}. Create or reconcile the Requirement authority before admitting Work.`,
621
+ data: { executionStarted: false, workContractCreated: false, requirementId: effectiveRequirementId, planId: plan?.planId },
622
+ });
623
+ }
490
624
  const sameStringSet = (provided: string[] | undefined, frozen: string[]): boolean => {
491
625
  if (provided === undefined) return true;
492
626
  const actual = [...new Set(provided)].sort();
@@ -513,17 +647,23 @@ export function startGoalWorkloop(
513
647
  const effectiveForbiddenPaths = planStep?.forbiddenPaths ?? input.forbiddenPaths ?? [];
514
648
  const effectiveChecks = planStep?.checks ?? input.checks ?? [];
515
649
  const normalized = normalizeCheckIds(effectiveChecks, available);
516
- const planStepWork = planStep?.workId ? activeWorks.find((candidate) => candidate.workId === planStep.workId) : undefined;
517
- const requirementWorks = input.requirementId
518
- ? activeWorks.filter((candidate) => candidate.requirementId === input.requirementId)
650
+ const boundPlanStepWorks = input.planId && input.planStepId
651
+ ? activeWorks.filter((candidate) => candidate.planId === input.planId && candidate.planStepId === input.planStepId)
652
+ : [];
653
+ const explicitPlanStepWork = planStep?.workId ? activeWorks.find((candidate) => candidate.workId === planStep.workId) : undefined;
654
+ const planStepWork = explicitPlanStepWork ?? (boundPlanStepWorks.length === 1 ? boundPlanStepWorks[0] : undefined);
655
+ const requirementWorks = effectiveRequirementId
656
+ ? activeWorks.filter((candidate) => candidate.requirementId === effectiveRequirementId)
519
657
  : [];
520
- const deterministicTarget = explicitRelatedWork ?? planStepWork ?? (requirementWorks.length === 1 ? requirementWorks[0] : undefined);
658
+ const deterministicTarget = input.relatedWorkId
659
+ ? explicitRelatedWork
660
+ : planStepWork ?? (requirementWorks.length === 1 ? requirementWorks[0] : undefined);
521
661
  const requestedRelation = input.workRelation ?? (input.modeInput.requiresParallelism === true ? 'parallel' : undefined);
522
662
  // Only strong semantic bindings participate in ownership resolution. An
523
663
  // unrelated active Work or a checkout writer is a placement fact, not a
524
664
  // semantic candidate for continue/extend/parallel/new_goal.
525
665
  const candidateWorks = [...new Map(
526
- [explicitRelatedWork, planStepWork, ...requirementWorks]
666
+ [explicitRelatedWork, planStepWork, ...boundPlanStepWorks, ...requirementWorks]
527
667
  .filter((candidate): candidate is WorkContract => Boolean(candidate))
528
668
  .map((candidate) => [candidate.workId, candidate]),
529
669
  ).values()].slice(0, 8);
@@ -556,6 +696,27 @@ export function startGoalWorkloop(
556
696
  rawAvailable: false,
557
697
  });
558
698
 
699
+ if (boundPlanStepWorks.length > 1) {
700
+ return resolutionRequired(`PLAN_STEP_MULTIPLE_PRIMARY_WORKS: ${input.planId}/${input.planStepId} has ${boundPlanStepWorks.length} active primary Work records and requires repair before execution.`);
701
+ }
702
+ if (planStep?.workId && !explicitPlanStepWork) {
703
+ return buildFacadeResult({
704
+ status: 'blocked',
705
+ summary: `PLAN_STEP_BOUND_WORK_MISSING: ${input.planId}/${input.planStepId} is bound to ${planStep.workId}, but that Work is not active. Repair the exact binding; do not create a replacement Work.`,
706
+ data: { executionStarted: false, workContractCreated: false, planId: input.planId, planStepId: input.planStepId, boundWorkId: planStep.workId, repairRequired: true },
707
+ suggestedNextActions: [{
708
+ label: 'Diagnose exact Plan step binding',
709
+ tool: 'rh_work',
710
+ operation: 'repair',
711
+ payload: { plan_id: input.planId, plan_step_id: input.planStepId, repair_operation: 'diagnose', dry_run: true },
712
+ risk: 'readonly',
713
+ confidence: 'high',
714
+ reason: 'The Plan step has an exact Work identity but the active Work view cannot resolve it. Diagnose that binding before any replacement Work can be admitted.',
715
+ }],
716
+ rawAvailable: false,
717
+ });
718
+ }
719
+
559
720
  if ((input.requestedBy ?? 'chatgpt') === 'scheduler') {
560
721
  return resolutionRequired(
561
722
  deterministicTarget
@@ -587,7 +748,7 @@ export function startGoalWorkloop(
587
748
  }
588
749
  if (requestedRelation === 'extend' && deterministicTarget.planId) {
589
750
  return resolutionRequired(
590
- `PLAN_EXTENSION_REQUIRES_REPLAN: Work ${deterministicTarget.workId} is governed by Plan ${deterministicTarget.planId}; extend the authoritative Plan/Requirement before continuing rather than silently widening Work scope.`,
751
+ `PLAN_EXTENSION_REQUIRES_REPLAN: Work ${deterministicTarget.workId} is governed by Plan ${deterministicTarget.planId}; record additional discovery in scopeEvidence, but update the authoritative Plan before widening policy or acceptance scope.`,
591
752
  deterministicTarget,
592
753
  );
593
754
  }
@@ -597,6 +758,15 @@ export function startGoalWorkloop(
597
758
  allowedPaths: [...new Set([...deterministicTarget.allowedPaths, ...(input.allowedPaths ?? [])])].slice(0, 50),
598
759
  forbiddenPaths: [...new Set([...deterministicTarget.forbiddenPaths, ...(input.forbiddenPaths ?? [])])].slice(0, 50),
599
760
  checks: [...new Set([...deterministicTarget.checks, ...normalized.validCheckIds])].slice(0, 30),
761
+ scopeEvidence: {
762
+ initialLikelyPaths: [...new Set([
763
+ ...(deterministicTarget.scopeEvidence?.initialLikelyPaths ?? deterministicTarget.allowedPaths),
764
+ ...(input.initialLikelyPaths ?? input.allowedPaths ?? []),
765
+ ])].slice(0, 100),
766
+ inspectedPaths: deterministicTarget.scopeEvidence?.inspectedPaths ?? [],
767
+ actualChangedPaths: deterministicTarget.scopeEvidence?.actualChangedPaths ?? [],
768
+ recordedAt: at,
769
+ },
600
770
  scopeSummary: `Extended serially: ${input.objective}`.slice(0, 500),
601
771
  continuationPrompt: `Continue work ${ctx.repoId}: ${deterministicTarget.objective.slice(0, 160)}. Additional requested scope: ${input.objective.slice(0, 500)}`,
602
772
  })
@@ -624,10 +794,10 @@ export function startGoalWorkloop(
624
794
  }
625
795
 
626
796
  const placementConflict = Boolean(workspaceOwner);
627
- const needsWorktree = executionMode === 'goal_workloop' && (input.constraints?.requireWorktree
628
- ?? (workspaceMode === 'isolated'
629
- || placementConflict
630
- || (workspaceMode === 'auto' && routeDecision.requiresIsolation === true)));
797
+ const needsWorktree = executionMode === 'goal_workloop' && (placementConstraint.requireWorktree
798
+ || placementConflict
799
+ || input.modeInput.requiresParallelism === true
800
+ || routeDecision.requiresIsolation === true);
631
801
  if (!needsWorktree && workspaceOwner) {
632
802
  return buildFacadeResult({
633
803
  status: 'blocked',
@@ -643,74 +813,98 @@ export function startGoalWorkloop(
643
813
  });
644
814
  }
645
815
  const generatedWorkId = workIdFor(input.objective);
646
- let isolatedWorkspace: ReturnType<NonNullable<GoalWorkloopContext['materializeIsolatedWorkspace']>> | undefined;
647
- if (needsWorktree) {
648
- if (!ctx.materializeIsolatedWorkspace) {
649
- return buildFacadeResult({ status: 'blocked', summary: 'ISOLATED_WORKSPACE_MATERIALIZER_REQUIRED: isolated Work cannot enter running state without a concrete managed checkout.', data: { executionStarted: false, workContractCreated: false, worktreeRequired: true } });
650
- }
651
- try {
652
- isolatedWorkspace = ctx.materializeIsolatedWorkspace({ workId: generatedWorkId, title: input.objective, baseRef: ctx.sourceRevision });
653
- if (!isolatedWorkspace.checkoutId || !isolatedWorkspace.root || isolatedWorkspace.checkoutId === ctx.checkoutId) {
654
- throw new Error('ISOLATED_WORKSPACE_NOT_DISTINCT');
655
- }
656
- } catch (error) {
657
- return buildFacadeResult({ status: 'blocked', summary: error instanceof Error ? `ISOLATED_WORKSPACE_MATERIALIZATION_FAILED: ${error.message}` : 'ISOLATED_WORKSPACE_MATERIALIZATION_FAILED', data: { executionStarted: false, workContractCreated: false, worktreeRequired: true } });
658
- }
659
- }
660
816
  if (input.planId || input.planStepId) {
661
- if (!input.planId || !input.planStepId || !ctx.planStore || !ctx.sourceRevision) {
662
- return buildFacadeResult({ status: 'blocked', summary: 'PLAN_CONTEXT_REQUIRED: plan_id, plan_step_id and a current source revision are required.', data: { executionStarted: false } });
817
+ if (!input.planId || !input.planStepId || !ctx.planStore || !ctx.sourceRevision || !plan || !planStep) {
818
+ return buildFacadeResult({ status: 'blocked', summary: 'PLAN_CONTEXT_REQUIRED: plan_id, plan_step_id, an executable Plan step and a current source revision are required.', data: { executionStarted: false } });
663
819
  }
664
- let claimed: PlanContract;
665
- try {
666
- claimed = claimPlanStepForWork(ctx.planStore, {
820
+ if (plan.status !== 'approved' && plan.status !== 'executing') {
821
+ return buildFacadeResult({ status: 'blocked', summary: `PLAN_NOT_EXECUTABLE: ${plan.planId} is ${plan.status}`, data: { executionStarted: false, planId: plan.planId } });
822
+ }
823
+ if (plan.sourceRevision !== ctx.sourceRevision) {
824
+ const invalidated = claimPlanStepForWork(ctx.planStore, {
667
825
  planId: input.planId,
668
826
  stepId: input.planStepId,
669
827
  workId: generatedWorkId,
670
828
  sourceRevision: ctx.sourceRevision,
671
829
  });
672
- } catch (error) {
673
830
  return buildFacadeResult({
674
831
  status: 'blocked',
675
- summary: error instanceof Error ? error.message : 'PLAN_STEP_CLAIM_FAILED',
676
- data: { planId: input.planId, planStepId: input.planStepId, executionStarted: false },
832
+ summary: `PLAN_SOURCE_DRIFT: ${invalidated.planId} was invalidated because its source revision no longer matches. Replan before execution.`,
833
+ data: { planId: invalidated.planId, executionStarted: false, workContractCreated: false, replanRequired: true },
677
834
  });
678
835
  }
679
- if (claimed.status === 'invalidated_by_drift') {
680
- return buildFacadeResult({
681
- status: 'blocked',
682
- summary: `PLAN_SOURCE_DRIFT: ${claimed.planId} was invalidated because its source revision no longer matches. Replan before execution.`,
683
- data: { planId: claimed.planId, executionStarted: false, replanRequired: true },
684
- });
836
+ const unresolved = planStep.dependencies.filter((dependency) => plan.steps.find((candidate) => candidate.id === dependency)?.status !== 'completed');
837
+ if (unresolved.length > 0) {
838
+ return buildFacadeResult({ status: 'blocked', summary: `PLAN_STEP_DEPENDENCIES_PENDING: ${unresolved.join(', ')}`, data: { executionStarted: false, workContractCreated: false, planId: plan.planId, planStepId: planStep.id } });
839
+ }
840
+ if (planStep.status === 'completed') {
841
+ return buildFacadeResult({ status: 'blocked', summary: `PLAN_STEP_ALREADY_COMPLETED: ${planStep.id}`, data: { executionStarted: false, workContractCreated: false, planId: plan.planId, planStepId: planStep.id } });
685
842
  }
686
843
  }
844
+ const initialSuggestedNextActions = suggestedForWorkIdentity(
845
+ generatedWorkId,
846
+ normalized.validCheckIds,
847
+ normalized.suggestedNextActions,
848
+ );
849
+ // allowed_paths and initial_likely_paths are policy/discovery fences, not proof
850
+ // that this Work owns a repository mutation. External-effect Work therefore
851
+ // needs either an explicit repository_change semantic choice or concrete
852
+ // expected source-change evidence before Forge treats it as repository_change.
853
+ const repositoryChangeIntent = input.workKind === 'repository_change'
854
+ || (input.modeInput.expectedFiles ?? 0) > 0
855
+ || (input.modeInput.expectedChangedLines ?? 0) > 0;
856
+ const typedRecoverableReadOnlyReview = input.workKind === undefined
857
+ && input.modeInput.mutation === false
858
+ && input.modeInput.requiresInvestigation === true
859
+ && input.modeInput.requiresRecovery === true;
860
+ const resolvedWorkKind = input.workKind ?? (
861
+ typedRecoverableReadOnlyReview
862
+ ? 'read_only_review'
863
+ : input.modeInput.requiresExternalEffect === true
864
+ && !repositoryChangeIntent
865
+ ? (input.modeInput.remoteWrite === true ? 'remote_effect' : 'local_effect')
866
+ : 'repository_change'
867
+ );
868
+ const remoteDeliveryRequired = resolvedWorkKind === 'repository_change'
869
+ && input.modeInput.requiresExternalEffect === true
870
+ && input.modeInput.remoteWrite === true;
687
871
  const work = createWorkContract(ctx.workStore, {
688
872
  workId: generatedWorkId,
689
873
  repoId: ctx.repoId,
690
- checkoutId: isolatedWorkspace?.checkoutId ?? ctx.checkoutId,
874
+ checkoutId: needsWorktree ? undefined : ctx.checkoutId,
691
875
  principalId: ctx.principalId,
692
876
  controllerInstanceId: ctx.controllerInstanceId,
693
- baseRevision: isolatedWorkspace?.baseRevision ?? ctx.sourceRevision,
694
- workspaceFingerprint: isolatedWorkspace ? undefined : ctx.workspaceFingerprint,
877
+ baseRevision: ctx.sourceRevision,
878
+ workspaceFingerprint: needsWorktree ? undefined : ctx.workspaceFingerprint,
695
879
  routeDecisionFingerprint: routeDecision.inputFingerprint,
696
880
  routeDecision,
697
881
  mode: executionMode,
698
882
  objective: input.objective,
699
883
  acceptanceCriteria: effectiveAcceptanceCriteria,
700
- constraints: input.constraints ?? { requireHandoffOnAmbiguity: true },
884
+ constraints: {
885
+ ...canonicalConstraints,
886
+ ...(remoteDeliveryRequired ? { remoteDeliveryRequired: true } : {}),
887
+ },
701
888
  risk: workRiskFor(input),
702
- workKind: input.modeInput.requiresExternalEffect === true && input.modeInput.remoteWrite === true
703
- ? 'remote_effect'
704
- : undefined,
889
+ // Remote delivery is a risk/effect dimension, not a reason to erase a
890
+ // repository-change Work's semantic identity. Pure external actions with no
891
+ // repository-change signal remain remote_effect and keep plugin receipt semantics.
892
+ workKind: resolvedWorkKind,
705
893
  status: 'running',
706
894
  phase: 'implementation',
707
895
  issueId: input.issueId,
708
896
  taskId: input.taskId,
709
- requirementId: input.requirementId,
897
+ requirementId: effectiveRequirementId,
710
898
  planId: input.planId,
711
899
  planStepId: input.planStepId,
712
900
  planSourceRevision: input.planId ? ctx.sourceRevision : undefined,
713
901
  scopeSummary: input.modeInput.scopeClear ? 'scope declared at start' : 'scope incomplete',
902
+ scopeEvidence: {
903
+ initialLikelyPaths: [...new Set(input.initialLikelyPaths ?? effectiveAllowedPaths)].slice(0, 100),
904
+ inspectedPaths: [],
905
+ actualChangedPaths: [],
906
+ recordedAt: at,
907
+ },
714
908
  allowedPaths: effectiveAllowedPaths,
715
909
  forbiddenPaths: effectiveForbiddenPaths,
716
910
  checks: normalized.validCheckIds,
@@ -727,7 +921,7 @@ export function startGoalWorkloop(
727
921
  ? 'Isolation was explicitly requested or required for parallel execution.'
728
922
  : 'Current workspace is the stability-first default; isolation remains opt-in.',
729
923
  },
730
- worktreeRef: isolatedWorkspace?.root,
924
+ worktreeRef: undefined,
731
925
  evidencePolicy: {
732
926
  defaultDetailLevel: 'summary',
733
927
  allowRawOptIn: true,
@@ -746,21 +940,39 @@ export function startGoalWorkloop(
746
940
  requestedBy: input.requestedBy ?? 'chatgpt',
747
941
  evidenceRefs: [initialEvidence(input.objective)],
748
942
  policyDecisions: policy ? [policy] : [],
749
- suggestedNextActions: [],
943
+ suggestedNextActions: initialSuggestedNextActions,
750
944
  continuationPrompt: `Continue work ${ctx.repoId}: ${input.objective.slice(0, 200)}`,
751
945
  });
752
946
 
753
- const suggested = suggestedForWork(work, normalized.suggestedNextActions);
754
- const updated = updateWorkContract(ctx.workStore, work.workId, {
755
- suggestedNextActions: suggested,
756
- updatedAt: at,
757
- });
947
+ if (input.planId && input.planStepId && ctx.planStore && ctx.sourceRevision) {
948
+ try {
949
+ const claimed = claimPlanStepForWork(ctx.planStore, {
950
+ planId: input.planId,
951
+ stepId: input.planStepId,
952
+ workId: work.workId,
953
+ sourceRevision: ctx.sourceRevision,
954
+ });
955
+ if (claimed.status === 'invalidated_by_drift') {
956
+ return buildFacadeResult({
957
+ status: 'blocked',
958
+ summary: `PLAN_SOURCE_DRIFT: ${claimed.planId} was invalidated because its source revision no longer matches. Repair or replan before execution.`,
959
+ data: { executionStarted: false, workContractCreated: true, work: summarizeWorkContract(work), planId: claimed.planId, canonicalWorkRetained: true, replanRequired: true },
960
+ });
961
+ }
962
+ } catch (error) {
963
+ return buildFacadeResult({
964
+ status: 'blocked',
965
+ summary: error instanceof Error ? error.message : 'PLAN_STEP_CLAIM_FAILED',
966
+ data: { planId: input.planId, planStepId: input.planStepId, executionStarted: false, workContractCreated: true, work: summarizeWorkContract(work), canonicalWorkRetained: true },
967
+ });
968
+ }
969
+ }
758
970
 
759
971
  return buildFacadeResult({
760
972
  status: 'ok',
761
973
  summary: executionMode === 'direct_control'
762
- ? `Direct-control Work lineage started as ${updated.workId}.`
763
- : `Goal workloop started as ${updated.workId}.`,
974
+ ? `Direct-control Work lineage started as ${work.workId}.`
975
+ : `Goal workloop started as ${work.workId}.`,
764
976
  data: {
765
977
  mode: {
766
978
  mode: executionMode,
@@ -774,20 +986,20 @@ export function startGoalWorkloop(
774
986
  routeDecision,
775
987
  },
776
988
  workContractCreated: true,
777
- work: summarizeWorkContract(updated),
778
- worktreeRequired: updated.worktreePolicy.required,
989
+ work: summarizeWorkContract(work),
990
+ worktreeRequired: work.worktreePolicy.required,
779
991
  normalizedChecks: normalized,
780
992
  policy,
781
993
  },
782
- evidenceRefs: updated.evidenceRefs,
994
+ evidenceRefs: work.evidenceRefs,
783
995
  warnings: normalized.warnings,
784
- suggestedNextActions: suggested,
996
+ suggestedNextActions: initialSuggestedNextActions,
785
997
  rawAvailable: false,
786
998
  });
787
999
  }
788
1000
 
789
1001
  export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopContinueInput): FacadeResult {
790
- const work = getWorkContract(ctx.workStore, input.workId);
1002
+ let work = getWorkContract(ctx.workStore, input.workId);
791
1003
  if (!work) {
792
1004
  return buildFacadeResult({
793
1005
  status: 'not_found',
@@ -814,78 +1026,339 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
814
1026
  });
815
1027
  }
816
1028
 
1029
+ if ((input.reviewFindings?.length ?? 0) > 0 && work.workKind !== 'read_only_review') {
1030
+ return buildFacadeResult({
1031
+ status: 'blocked',
1032
+ summary: `READ_ONLY_REVIEW_FINDINGS_KIND_REQUIRED: ${work.workId} is ${work.workKind}, not read_only_review.`,
1033
+ data: { work: summarizeWorkContract(work), reviewFindingsRecorded: false },
1034
+ });
1035
+ }
1036
+ if (work.workKind === 'read_only_review') {
1037
+ const sourceIdentity = evaluateReadOnlyReviewSourceIdentity(work, ctx.sourceRevision, ctx.workspaceChangedPaths);
1038
+ if (sourceIdentity.status !== 'complete') {
1039
+ return buildFacadeResult({
1040
+ status: 'blocked',
1041
+ summary: `READ_ONLY_REVIEW_SOURCE_IDENTITY_REQUIRED: ${sourceIdentity.reasons.join(' ')}`,
1042
+ data: { work: summarizeWorkContract(work), sourceIdentityProven: false },
1043
+ });
1044
+ }
1045
+ }
1046
+
1047
+ const explicitPolicyScope = input.allowedPaths !== undefined
1048
+ || input.forbiddenPaths !== undefined
1049
+ || input.checks !== undefined;
1050
+ if (explicitPolicyScope) {
1051
+ if (work.planId) {
1052
+ return buildFacadeResult({
1053
+ status: 'blocked',
1054
+ summary: `PLAN_EXTENSION_REQUIRES_REPLAN: Work ${work.workId} is governed by Plan ${work.planId}; continue cannot widen frozen policy scope. Replan the authoritative Plan instead.`,
1055
+ data: { work: summarizeWorkContract(work), planId: work.planId, policyScopeUpdated: false },
1056
+ });
1057
+ }
1058
+ const normalizedChecks = normalizeCheckIds(input.checks ?? [], ctx.availableChecks ?? []);
1059
+ if (normalizedChecks.invalidCheckIds.length > 0) {
1060
+ return buildFacadeResult({
1061
+ status: 'blocked',
1062
+ summary: `WORK_CHECKS_INVALID: ${normalizedChecks.invalidCheckIds.join(', ')}. Work scope was not updated.`,
1063
+ data: { work: summarizeWorkContract(work), policyScopeUpdated: false, normalizedChecks },
1064
+ });
1065
+ }
1066
+ work = updateWorkContract(ctx.workStore, work.workId, {
1067
+ allowedPaths: [...new Set([...work.allowedPaths, ...(input.allowedPaths ?? [])])].slice(0, 50),
1068
+ forbiddenPaths: [...new Set([...work.forbiddenPaths, ...(input.forbiddenPaths ?? [])])].slice(0, 50),
1069
+ checks: [...new Set([...work.checks, ...normalizedChecks.validCheckIds])].slice(0, 30),
1070
+ scopeEvidence: {
1071
+ initialLikelyPaths: [...new Set([
1072
+ ...(work.scopeEvidence?.initialLikelyPaths ?? work.allowedPaths),
1073
+ ...(input.allowedPaths ?? []),
1074
+ ])].slice(0, 100),
1075
+ inspectedPaths: work.scopeEvidence?.inspectedPaths ?? [],
1076
+ actualChangedPaths: work.scopeEvidence?.actualChangedPaths ?? [],
1077
+ recordedAt: nowIso(ctx),
1078
+ },
1079
+ scopeSummary: `Continue adopted explicit policy scope for ${work.workId}.`.slice(0, 500),
1080
+ });
1081
+ }
1082
+
1083
+ if ((input.additionalLikelyPaths?.length ?? 0) > 0 || (input.inspectedPaths?.length ?? 0) > 0) {
1084
+ work = recordWorkScopeEvidence(ctx.workStore, work.workId, {
1085
+ initialLikelyPaths: input.additionalLikelyPaths,
1086
+ inspectedPaths: input.inspectedPaths,
1087
+ });
1088
+ }
1089
+ if ((input.acceptanceEvidence?.length ?? 0) > 0) {
1090
+ if (work.workKind !== 'local_effect') {
1091
+ return buildFacadeResult({
1092
+ status: 'blocked',
1093
+ summary: `WORK_SEMANTIC_ACCEPTANCE_LOCAL_EFFECT_REQUIRED: ${work.workId} is ${work.workKind}.`,
1094
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1095
+ });
1096
+ }
1097
+ const declaredCriteria = new Map(work.acceptanceCriteria.map((criterion) => [criterion.trim(), criterion]));
1098
+ const availableEvidenceIds = new Set([
1099
+ ...work.evidenceRefs.flatMap((evidence) => [evidence.evidenceId, evidence.artifactId])
1100
+ .map((value) => value?.trim())
1101
+ .filter((value): value is string => Boolean(value)),
1102
+ ...(ctx.workBoundProcessEvidenceIds ?? []).map((value) => value.trim()).filter(Boolean),
1103
+ ]);
1104
+ const normalizedBindings: Array<{ criterion: string; evidenceIds: string[]; rationale: string }> = [];
1105
+ for (const binding of input.acceptanceEvidence ?? []) {
1106
+ const requestedCriterion = binding.criterion.trim();
1107
+ const criterion = declaredCriteria.get(requestedCriterion);
1108
+ if (!criterion) {
1109
+ return buildFacadeResult({
1110
+ status: 'blocked',
1111
+ summary: `WORK_SEMANTIC_ACCEPTANCE_CRITERION_UNKNOWN: ${requestedCriterion || '<empty>'}.`,
1112
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1113
+ });
1114
+ }
1115
+ const evidenceIds = [...new Set(binding.evidenceIds.map((value) => value.trim()).filter(Boolean))];
1116
+ if (evidenceIds.length === 0) {
1117
+ return buildFacadeResult({
1118
+ status: 'blocked',
1119
+ summary: `WORK_SEMANTIC_ACCEPTANCE_EVIDENCE_REQUIRED: ${criterion}.`,
1120
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1121
+ });
1122
+ }
1123
+ const unknownEvidenceIds = evidenceIds.filter((evidenceId) => !availableEvidenceIds.has(evidenceId));
1124
+ if (unknownEvidenceIds.length > 0) {
1125
+ return buildFacadeResult({
1126
+ status: 'blocked',
1127
+ summary: `WORK_SEMANTIC_ACCEPTANCE_EVIDENCE_UNKNOWN: ${unknownEvidenceIds.join(', ')}.`,
1128
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1129
+ });
1130
+ }
1131
+ const rationale = binding.rationale.trim();
1132
+ if (!rationale) {
1133
+ return buildFacadeResult({
1134
+ status: 'blocked',
1135
+ summary: `WORK_SEMANTIC_ACCEPTANCE_RATIONALE_REQUIRED: ${criterion}.`,
1136
+ data: { work: summarizeWorkContract(work), semanticAcceptanceRecorded: false },
1137
+ });
1138
+ }
1139
+ normalizedBindings.push({ criterion, evidenceIds, rationale: rationale.slice(0, 1_000) });
1140
+ }
1141
+ const recordedAt = nowIso(ctx);
1142
+ const byCriterion = new Map((work.semanticAcceptanceEvidence ?? []).map((review) => [review.criterion, review]));
1143
+ for (const binding of normalizedBindings) {
1144
+ const previous = byCriterion.get(binding.criterion);
1145
+ byCriterion.set(binding.criterion, {
1146
+ criterion: binding.criterion,
1147
+ evidenceIds: [...new Set([...(previous?.evidenceIds ?? []), ...binding.evidenceIds])].slice(0, 50),
1148
+ rationale: binding.rationale,
1149
+ recordedAt,
1150
+ });
1151
+ }
1152
+ work = updateWorkContract(ctx.workStore, work.workId, {
1153
+ semanticAcceptanceEvidence: [...byCriterion.values()].slice(0, 20),
1154
+ });
1155
+ work = appendWorkEvidence(ctx.workStore, work.workId, {
1156
+ title: 'Controller semantic acceptance review',
1157
+ summary: `Controller reviewed ${normalizedBindings.length} exact acceptance criterion binding(s) against durable Work evidence.`,
1158
+ detailLevel: 'summary',
1159
+ });
1160
+ }
1161
+
1162
+ if (work.workKind === 'read_only_review' && ((input.inspectedPaths?.length ?? 0) > 0 || input.reviewFindings !== undefined)) {
1163
+ const findings = [...new Set([
1164
+ ...(work.readOnlyReviewEvidence?.findings ?? []),
1165
+ ...(input.reviewFindings ?? []).map((finding) => finding.trim()).filter(Boolean),
1166
+ ])].slice(0, 50);
1167
+ const inspectedPaths = [...new Set(work.scopeEvidence?.inspectedPaths ?? [])].slice(0, 200);
1168
+ work = updateWorkContract(ctx.workStore, work.workId, {
1169
+ readOnlyReviewEvidence: {
1170
+ sourceRevision: ctx.sourceRevision!,
1171
+ ...(ctx.workspaceFingerprint ? { workspaceFingerprint: ctx.workspaceFingerprint } : {}),
1172
+ inspectedPaths,
1173
+ findings,
1174
+ recordedAt: nowIso(ctx),
1175
+ },
1176
+ });
1177
+ for (const finding of input.reviewFindings ?? []) {
1178
+ const summary = finding.trim();
1179
+ if (!summary) continue;
1180
+ work = appendWorkEvidence(ctx.workStore, work.workId, {
1181
+ title: 'read-only review finding',
1182
+ summary: summary.slice(0, 1_500),
1183
+ detailLevel: 'summary',
1184
+ });
1185
+ }
1186
+ }
1187
+
1188
+ const currentCheckRefs = work.checkRefs.filter((record) =>
1189
+ verificationRecordAppliesToCurrentWorkspace(record, ctx.sourceRevision, ctx.workspaceFingerprint));
817
1190
  const history = reconcileVerificationHistory(
818
- work.checkRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
1191
+ currentCheckRefs.map((record) => ({ checkId: record.checkId, outcome: record.outcome, recordedAt: record.recordedAt })),
819
1192
  );
820
1193
 
821
- // Ambiguous: acceptance failure present → handoff for ChatGPT review rather than pretend progress.
822
- if (history.acceptanceFailures.length > 0 && work.recoveryPolicy.handoffOnAmbiguity) {
823
- const continuation = buildWorkContinuationSnapshot(work);
824
- const handoff = createHandoffItem(ctx.handoffStore, {
825
- id: handoffIdFor('continue'),
826
- repoId: ctx.repoId,
827
- workId: work.workId,
828
- title: 'Acceptance failure needs review',
829
- severity: 'needs_review',
830
- creationReason: 'ambiguous_outcome',
831
- reason: `Acceptance checks failed: ${history.acceptanceFailures.join(', ')}`,
832
- summary: 'Continue paused; ChatGPT must decide repair vs re-scope.',
833
- currentState: {
834
- repoId: ctx.repoId,
835
- workId: work.workId,
836
- mode: work.mode,
837
- statusSummary: 'waiting_for_review after acceptance failure',
838
- checks: history.acceptanceFailures.map((checkId) => ({ checkId, ok: false, outcome: 'valid_fail' as const })),
839
- workSemantics: continuation.semantics,
840
- reconciliationRequired: continuation.reconciliationRequired,
841
- nextSafeAction: continuation.nextSafeAction,
842
- },
843
- attemptedActions: ['continue'],
844
- evidenceRefs: work.evidenceRefs.slice(0, 5),
845
- blockingDecision: 'Decide whether to repair code, adjust acceptance criteria, or stop.',
846
- recommendedDecision: 'Inspect evidence and either repair or stop the workloop.',
847
- recommendedPrompt: work.continuationPrompt ?? `Continue from work ${work.workId}.`,
848
- recommendedContinuationPrompt: continuation.continuationPrompt,
849
- suggestedNextActions: [
850
- {
851
- label: 'Read work context',
852
- tool: 'rh_context',
853
- operation: 'get',
854
- payload: { work_id: work.workId },
855
- risk: 'readonly',
856
- },
857
- ],
1194
+ const explicitAcceptanceRepair = history.acceptanceFailures.length > 0
1195
+ && (input.acceptanceFailureDecision === 'repair' || input.acceptanceFailureDecision === 'rescope');
1196
+ if (explicitAcceptanceRepair) {
1197
+ const decision = input.acceptanceFailureDecision!;
1198
+ work = appendWorkEvidence(ctx.workStore, work.workId, {
1199
+ title: 'Controller acceptance-failure continuation decision',
1200
+ summary: `Controller explicitly chose bounded ${decision} after deterministic acceptance failure: ${history.acceptanceFailures.join(', ')}.`,
1201
+ detailLevel: 'summary',
858
1202
  });
859
1203
  transitionWorkContractPhase(ctx.workStore, work.workId, {
860
- status: 'ready',
861
- phase: 'verification',
862
- state: 'blocked',
863
- summary: `Acceptance failure requires review through handoff ${handoff.id}.`,
1204
+ status: 'running',
1205
+ phase: 'implementation',
1206
+ state: 'active',
1207
+ summary: `Controller chose bounded ${decision} after acceptance failure; implementation may resume before re-verification.`,
864
1208
  evidenceRefs: work.evidenceRefs,
865
1209
  });
866
- const updated = appendWorkHandoffRef(ctx.workStore, work.workId, handoff.id);
867
-
1210
+ work = getWorkContract(ctx.workStore, work.workId) ?? work;
868
1211
  return buildFacadeResult({
869
- status: 'blocked',
870
- summary: `Continue paused for ChatGPT review; handoff ${handoff.id} created. No background execution pretended.`,
1212
+ status: 'ok',
1213
+ summary: `Continue accepted explicit bounded ${decision} decision after acceptance failure; implementation may resume.`,
871
1214
  data: {
872
- work: summarizeWorkContract(updated),
873
- handoffId: handoff.id,
1215
+ work: summarizeWorkContract(work),
874
1216
  acceptanceFailures: history.acceptanceFailures,
875
1217
  infrastructureIssues: history.infrastructureIssues,
876
1218
  backgroundCompleted: false,
1219
+ nextStep: work.workKind === 'read_only_review' ? 'review' : 'execute',
1220
+ acceptanceFailureDecision: decision,
877
1221
  },
878
1222
  evidenceRefs: work.evidenceRefs.slice(0, 5),
879
- suggestedNextActions: [
880
- {
1223
+ suggestedNextActions: suggestedForWork(work),
1224
+ });
1225
+ }
1226
+
1227
+ // Ambiguous acceptance failure without an explicit bounded Controller decision →
1228
+ // ask once per distinct failure evidence. Repeated continue calls must not
1229
+ // manufacture an unbounded handoff loop. A resolved handoff remains the decision
1230
+ // authority until newer valid-fail evidence; an unresolved matching handoff is reused.
1231
+ if (history.acceptanceFailures.length > 0 && !explicitAcceptanceRepair && work.recoveryPolicy.handoffOnAmbiguity) {
1232
+ const failureReason = `Acceptance checks failed: ${history.acceptanceFailures.join(', ')}`;
1233
+ const latestFailureAt = work.checkRefs
1234
+ .filter((record) => record.outcome === 'valid_fail' && history.acceptanceFailures.includes(record.checkId))
1235
+ .map((record) => record.recordedAt)
1236
+ .sort((left, right) => right.localeCompare(left))[0];
1237
+ const matchingHandoffs = listHandoffItems({ ...ctx.handoffStore, status: 'all', limit: 100 })
1238
+ .filter((item) => (
1239
+ item.workId === input.workId
1240
+ && item.creationReason === 'ambiguous_outcome'
1241
+ && item.title === 'Acceptance failure needs review'
1242
+ && item.reason === failureReason
1243
+ && (!latestFailureAt || item.createdAt >= latestFailureAt)
1244
+ ));
1245
+ const activeHandoff = matchingHandoffs.find((item) => item.status === 'pending' || item.status === 'acknowledged');
1246
+ if (activeHandoff) {
1247
+ return buildFacadeResult({
1248
+ status: 'blocked',
1249
+ summary: `Continue remains paused for ChatGPT review through existing handoff ${activeHandoff.id}.`,
1250
+ data: {
1251
+ work: summarizeWorkContract(work),
1252
+ handoffId: activeHandoff.id,
1253
+ acceptanceFailures: history.acceptanceFailures,
1254
+ infrastructureIssues: history.infrastructureIssues,
1255
+ backgroundCompleted: false,
1256
+ },
1257
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1258
+ suggestedNextActions: [{
881
1259
  label: 'Get handoff',
882
1260
  tool: 'rh_inbox',
883
1261
  operation: 'get',
884
- payload: { handoff_id: handoff.id },
1262
+ payload: { handoff_id: activeHandoff.id },
885
1263
  risk: 'readonly',
886
1264
  confidence: 'high',
1265
+ }],
1266
+ });
1267
+ }
1268
+
1269
+ const resolvedHandoff = matchingHandoffs.find((item) => item.status === 'resolved');
1270
+ if (!resolvedHandoff) {
1271
+ const continuation = buildWorkContinuationSnapshot(work);
1272
+ const handoff = createHandoffItem(ctx.handoffStore, {
1273
+ id: handoffIdFor('continue'),
1274
+ repoId: ctx.repoId,
1275
+ workId: work.workId,
1276
+ title: 'Acceptance failure needs review',
1277
+ severity: 'needs_review',
1278
+ creationReason: 'ambiguous_outcome',
1279
+ reason: failureReason,
1280
+ summary: 'Continue paused; ChatGPT must decide repair vs re-scope.',
1281
+ currentState: {
1282
+ repoId: ctx.repoId,
1283
+ workId: work.workId,
1284
+ mode: work.mode,
1285
+ statusSummary: 'waiting_for_review after acceptance failure',
1286
+ checks: history.acceptanceFailures.map((checkId) => ({ checkId, ok: false, outcome: 'valid_fail' as const })),
1287
+ workSemantics: continuation.semantics,
1288
+ reconciliationRequired: continuation.reconciliationRequired,
1289
+ nextSafeAction: continuation.nextSafeAction,
887
1290
  },
888
- ],
1291
+ attemptedActions: ['continue'],
1292
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1293
+ blockingDecision: 'Decide whether to repair code, adjust acceptance criteria, or stop.',
1294
+ recommendedDecision: 'Inspect evidence and either repair or stop the workloop.',
1295
+ recommendedPrompt: work.continuationPrompt ?? `Continue from work ${work.workId}.`,
1296
+ recommendedContinuationPrompt: continuation.continuationPrompt,
1297
+ suggestedNextActions: [
1298
+ {
1299
+ label: 'Read work context',
1300
+ tool: 'rh_context',
1301
+ operation: 'get',
1302
+ payload: { work_id: work.workId },
1303
+ risk: 'readonly',
1304
+ },
1305
+ ],
1306
+ });
1307
+ transitionWorkContractPhase(ctx.workStore, work.workId, {
1308
+ status: 'ready',
1309
+ phase: 'verification',
1310
+ state: 'blocked',
1311
+ summary: `Acceptance failure requires review through handoff ${handoff.id}.`,
1312
+ evidenceRefs: work.evidenceRefs,
1313
+ });
1314
+ const updated = appendWorkHandoffRef(ctx.workStore, work.workId, handoff.id);
1315
+
1316
+ return buildFacadeResult({
1317
+ status: 'blocked',
1318
+ summary: `Continue paused for ChatGPT review; handoff ${handoff.id} created. No background execution pretended.`,
1319
+ data: {
1320
+ work: summarizeWorkContract(updated),
1321
+ handoffId: handoff.id,
1322
+ acceptanceFailures: history.acceptanceFailures,
1323
+ infrastructureIssues: history.infrastructureIssues,
1324
+ backgroundCompleted: false,
1325
+ },
1326
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1327
+ suggestedNextActions: [
1328
+ {
1329
+ label: 'Get handoff',
1330
+ tool: 'rh_inbox',
1331
+ operation: 'get',
1332
+ payload: { handoff_id: handoff.id },
1333
+ risk: 'readonly',
1334
+ confidence: 'high',
1335
+ },
1336
+ ],
1337
+ });
1338
+ }
1339
+
1340
+ transitionWorkContractPhase(ctx.workStore, work.workId, {
1341
+ status: 'running',
1342
+ phase: 'verification',
1343
+ state: 'active',
1344
+ summary: `Resolved acceptance-failure handoff ${resolvedHandoff.id} authorizes bounded continuation to re-verification.`,
1345
+ evidenceRefs: work.evidenceRefs,
1346
+ });
1347
+ work = getWorkContract(ctx.workStore, work.workId) ?? work;
1348
+ return buildFacadeResult({
1349
+ status: 'ok',
1350
+ summary: `Continue accepted resolved acceptance-failure handoff ${resolvedHandoff.id}; re-verification may resume.`,
1351
+ data: {
1352
+ work: summarizeWorkContract(work),
1353
+ handoffId: resolvedHandoff.id,
1354
+ acceptanceFailures: history.acceptanceFailures,
1355
+ infrastructureIssues: history.infrastructureIssues,
1356
+ backgroundCompleted: false,
1357
+ nextStep: 'verify',
1358
+ remainingChecks: history.acceptanceFailures,
1359
+ },
1360
+ evidenceRefs: work.evidenceRefs.slice(0, 5),
1361
+ suggestedNextActions: suggestedForWork(work),
889
1362
  });
890
1363
  }
891
1364
 
@@ -927,6 +1400,47 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
927
1400
  });
928
1401
  }
929
1402
 
1403
+ const implementationEvidence = evaluateWorkImplementationEvidence(work, ctx.workspaceChangedPaths);
1404
+ if (implementationEvidence.status !== 'complete') {
1405
+ const suggested = validateSuggestedNextActions([
1406
+ {
1407
+ label: 'Implement the repository change before verification',
1408
+ tool: 'rh_context',
1409
+ operation: 'get',
1410
+ payload: { work_id: work.workId },
1411
+ risk: 'readonly',
1412
+ confidence: 'high',
1413
+ reason: 'Verification proves behavior only after the repository-change Work has current source changes.',
1414
+ },
1415
+ ]).actions;
1416
+ transitionWorkContractPhase(ctx.workStore, work.workId, {
1417
+ status: 'running',
1418
+ phase: 'implementation',
1419
+ state: 'active',
1420
+ summary: implementationEvidence.reasons.join(' '),
1421
+ evidenceRefs: work.evidenceRefs,
1422
+ });
1423
+ const updated = updateWorkContract(ctx.workStore, work.workId, { suggestedNextActions: suggested });
1424
+ return buildFacadeResult({
1425
+ status: 'blocked',
1426
+ summary: `Continue requires implementation before verification. ${implementationEvidence.reasons.join(' ')}`,
1427
+ data: {
1428
+ work: summarizeWorkContract(updated),
1429
+ backgroundCompleted: false,
1430
+ nextStep: 'execute',
1431
+ implementationEvidencePresent: false,
1432
+ workspaceChangedPaths: implementationEvidence.changedPaths,
1433
+ },
1434
+ suggestedNextActions: suggested,
1435
+ });
1436
+ }
1437
+
1438
+ if (implementationEvidence.changedPaths.length > 0) {
1439
+ recordWorkScopeEvidence(ctx.workStore, work.workId, {
1440
+ actualChangedPaths: implementationEvidence.changedPaths,
1441
+ });
1442
+ }
1443
+
930
1444
  if (work.checks.length > 0 && history.validPasses.length < work.checks.length) {
931
1445
  const remaining = work.checks.filter((checkId) => !history.validPasses.includes(checkId));
932
1446
  const suggested = validateSuggestedNextActions(
@@ -966,7 +1480,13 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
966
1480
  });
967
1481
  }
968
1482
 
969
- const completionEvidence = evaluateWorkCompletionEvidence(work, history);
1483
+ const completionEvidence = evaluateWorkCompletionEvidence(
1484
+ work,
1485
+ ctx.sourceRevision,
1486
+ ctx.workspaceFingerprint,
1487
+ ctx.workBoundProcessEvidenceIds,
1488
+ ctx.workspaceChangedPaths,
1489
+ );
970
1490
  if (completionEvidence.status !== 'complete') {
971
1491
  const suggested = validateSuggestedNextActions([
972
1492
  {
@@ -981,9 +1501,11 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
981
1501
  ]).actions;
982
1502
  transitionWorkContractPhase(ctx.workStore, work.workId, {
983
1503
  status: 'running',
984
- phase: 'implementation',
1504
+ phase: work.workKind === 'read_only_review' ? 'verification' : 'implementation',
985
1505
  state: 'active',
986
- summary: `Meaningful implementation evidence is still missing: ${completionEvidence.reasons.join(' ')}`,
1506
+ summary: work.workKind === 'read_only_review'
1507
+ ? `Read-only review completion evidence is incomplete: ${completionEvidence.reasons.join(' ')}`
1508
+ : `Meaningful implementation evidence is still missing: ${completionEvidence.reasons.join(' ')}`,
987
1509
  evidenceRefs: work.evidenceRefs,
988
1510
  });
989
1511
  const updated = updateWorkContract(ctx.workStore, work.workId, {
@@ -996,7 +1518,7 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
996
1518
  work: summarizeWorkContract(updated),
997
1519
  backgroundCompleted: false,
998
1520
  nextStep: 'execute',
999
- executionEvidencePresent: false,
1521
+ executionEvidencePresent: Boolean(ctx.workBoundProcessEvidenceIds?.length),
1000
1522
  missingChecks: completionEvidence.missingChecks,
1001
1523
  durableResultEvidence: completionEvidence.durableResultEvidence,
1002
1524
  ignoredWeakReferences: {
@@ -1022,7 +1544,9 @@ export function continueGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1022
1544
  status: 'running',
1023
1545
  phase: 'delivery',
1024
1546
  state: 'active',
1025
- summary: 'Implementation and verification evidence are sufficient; exact delivery and cleanup receipt is next.',
1547
+ summary: work.workKind === 'read_only_review'
1548
+ ? 'Read-only review evidence is source-bound and clean; semantic no-change finalization is next.'
1549
+ : 'Implementation and verification evidence are sufficient; exact delivery and cleanup receipt is next.',
1026
1550
  evidenceRefs: work.evidenceRefs,
1027
1551
  });
1028
1552
  const updated = updateWorkContract(ctx.workStore, work.workId, {
@@ -1058,16 +1582,60 @@ export function verifyGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloop
1058
1582
  checkFailed: input.checkFailed,
1059
1583
  skipped: input.skipped,
1060
1584
  });
1585
+ if (work.status === 'completed' || work.status === 'cancelled' || work.status === 'failed') {
1586
+ const resolvedCheckId = classified.normalizedCheckId ?? classified.checkId;
1587
+ const existing = [...work.checkRefs].reverse().find((record) => record.checkId === resolvedCheckId);
1588
+ const completed = work.status === 'completed';
1589
+ return buildFacadeResult({
1590
+ status: completed ? 'ok' : 'blocked',
1591
+ summary: `WorkContract ${work.workId} is terminal (${work.status}); verification was not re-executed.`,
1592
+ data: {
1593
+ work: summarizeWorkContract(work),
1594
+ verification: {
1595
+ checkId: resolvedCheckId,
1596
+ ...(existing ? { outcome: existing.outcome } : {}),
1597
+ terminal: true,
1598
+ idempotent: true,
1599
+ reexecuted: false,
1600
+ isAcceptanceFailure: existing?.outcome === 'valid_fail',
1601
+ isInfrastructureIssue: existing?.outcome === 'invalid_check_id' || existing?.outcome === 'infrastructure_failure',
1602
+ doesNotRequestTaskChanges: existing?.outcome !== 'valid_fail',
1603
+ },
1604
+ backgroundCompleted: false,
1605
+ },
1606
+ evidenceRefs: existing?.evidenceRef ? [existing.evidenceRef] : [],
1607
+ warnings: classified.warnings,
1608
+ suggestedNextActions: [{ label: 'Inspect work via context', tool: 'rh_context', operation: 'get', payload: { work_id: work.workId }, risk: 'readonly', confidence: 'high' }],
1609
+ });
1610
+ }
1061
1611
 
1062
1612
  const at = nowIso(ctx);
1613
+ const resolvedCheckId = classified.normalizedCheckId ?? classified.checkId;
1614
+ const receipt = input.receipt
1615
+ && input.receipt.repoId === work.repoId
1616
+ && input.receipt.workId === work.workId
1617
+ && input.receipt.checkId === resolvedCheckId
1618
+ ? input.receipt
1619
+ : undefined;
1620
+ const sourceRevision = receipt && input.sourceRevision?.trim() ? input.sourceRevision.trim() : undefined;
1621
+ const verificationSummary = receipt
1622
+ ? `${classified.summary} Durable Process receipt ${receipt.receiptId}.`
1623
+ : classified.summary;
1063
1624
  const record: VerificationRecord = {
1064
- checkId: classified.normalizedCheckId ?? classified.checkId,
1625
+ checkId: resolvedCheckId,
1065
1626
  outcome: classified.outcome,
1066
- summary: classified.summary,
1627
+ summary: verificationSummary,
1067
1628
  recordedAt: at,
1629
+ sourceRevision,
1630
+ workspaceFingerprint: receipt && sourceRevision ? input.workspaceFingerprint : undefined,
1631
+ verificationInputFingerprint: receipt && sourceRevision ? input.verificationInputFingerprint : undefined,
1632
+ commandFingerprint: receipt && sourceRevision ? input.commandFingerprint : undefined,
1633
+ startedAt: receipt?.startedAt,
1634
+ completedAt: receipt?.finishedAt,
1635
+ receipt,
1068
1636
  evidenceRef: {
1069
1637
  title: `verification:${classified.outcome}`,
1070
- summary: classified.summary,
1638
+ summary: verificationSummary,
1071
1639
  detailLevel: 'summary',
1072
1640
  },
1073
1641
  };
@@ -1097,12 +1665,16 @@ export function verifyGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloop
1097
1665
  ? 'failed'
1098
1666
  : 'ok';
1099
1667
 
1668
+ const completionEvidence = classified.outcome === 'valid_pass'
1669
+ ? evaluateWorkCompletionEvidence(updated, sourceRevision, input.workspaceFingerprint)
1670
+ : undefined;
1671
+ const validPassReadyToFinalize = completionEvidence?.status === 'complete';
1100
1672
  const suggested = validateSuggestedNextActions(
1101
1673
  classified.outcome === 'valid_pass'
1102
1674
  ? [{
1103
- label: 'Continue workloop',
1675
+ label: validPassReadyToFinalize ? 'Finalize work' : 'Continue workloop',
1104
1676
  tool: 'rh_work',
1105
- operation: 'continue',
1677
+ operation: validPassReadyToFinalize ? 'finalize' : 'continue',
1106
1678
  payload: { work_id: work.workId },
1107
1679
  risk: 'readonly',
1108
1680
  confidence: 'high',
@@ -1141,6 +1713,9 @@ export function verifyGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloop
1141
1713
  doesNotRequestTaskChanges: !classified.isAcceptanceFailure,
1142
1714
  },
1143
1715
  backgroundCompleted: false,
1716
+ ...(classified.outcome === 'valid_pass'
1717
+ ? { nextStep: validPassReadyToFinalize ? 'finalize' : 'continue' }
1718
+ : {}),
1144
1719
  },
1145
1720
  warnings: classified.warnings,
1146
1721
  evidenceRefs: record.evidenceRef ? [record.evidenceRef] : [],
@@ -1189,7 +1764,13 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1189
1764
  });
1190
1765
  }
1191
1766
 
1192
- const completionEvidence = evaluateWorkCompletionEvidence(work);
1767
+ const completionEvidence = evaluateWorkCompletionEvidence(
1768
+ work,
1769
+ ctx.sourceRevision,
1770
+ ctx.workspaceFingerprint,
1771
+ ctx.workBoundProcessEvidenceIds,
1772
+ ctx.workspaceChangedPaths,
1773
+ );
1193
1774
  const history = completionEvidence.history;
1194
1775
 
1195
1776
  if (input.forceFailed || completionEvidence.status === 'failed') {
@@ -1264,6 +1845,91 @@ export function finalizeGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorklo
1264
1845
  });
1265
1846
  }
1266
1847
 
1848
+ // Read-only review completion is semantic no-change authority, not Git delivery.
1849
+ // The exact source/workspace fence is re-evaluated above at finalize time; only
1850
+ // a clean review with persisted inspected paths and zero findings can close.
1851
+ if (work.workKind === 'read_only_review' && !work.completionReceipt) {
1852
+ const reviewEvidence = work.readOnlyReviewEvidence!;
1853
+ const recordedAt = nowIso(ctx);
1854
+ const completed = completeWorkWithReceipt(
1855
+ ctx.workStore,
1856
+ work.workId,
1857
+ {
1858
+ schemaVersion: 1,
1859
+ receiptId: `ROR-WORK-${randomUUID()}`,
1860
+ source: 'read_only_review',
1861
+ workId: work.workId,
1862
+ baseRevision: work.baseRevision!,
1863
+ sourceRevision: ctx.sourceRevision!,
1864
+ ...(ctx.workspaceFingerprint ? { workspaceFingerprint: ctx.workspaceFingerprint } : {}),
1865
+ workspaceChangedPaths: [],
1866
+ inspectedPaths: reviewEvidence.inspectedPaths,
1867
+ findingCount: 0,
1868
+ recordedAt,
1869
+ },
1870
+ 'completed_no_change',
1871
+ 'read_only_review',
1872
+ );
1873
+ if (completed.planId && completed.planStepId && ctx.planStore) {
1874
+ completePlanStepForWork(ctx.planStore, { planId: completed.planId, stepId: completed.planStepId, work: completed });
1875
+ }
1876
+ return buildFacadeResult({
1877
+ status: 'ok',
1878
+ summary: `Finalize result: clean read-only review completed with no source change for ${work.workId}.`,
1879
+ data: {
1880
+ work: summarizeWorkContract(completed),
1881
+ finalStatus: 'completed',
1882
+ completionOutcome: 'completed_no_change',
1883
+ completionReceipt: completed.completionReceipt,
1884
+ inspectedPathCount: reviewEvidence.inspectedPaths.length,
1885
+ hiddenFailure: false,
1886
+ },
1887
+ evidenceRefs: completed.evidenceRefs.slice(0, 5),
1888
+ suggestedNextActions: [{ label: 'Read controller status', tool: 'rh_status', operation: 'get', risk: 'readonly' }],
1889
+ });
1890
+ }
1891
+
1892
+ // For a controller-local effect Work, the semantic Controller's explicit
1893
+ // finalize call is the terminalization decision, but it is admissible only
1894
+ // after durable result evidence exists. Record one canonical Work receipt here
1895
+ // instead of routing an effect-only Work through Git delivery. Remote effects
1896
+ // remain bound to their durable plugin action receipt in the plugin layer.
1897
+ if (work.workKind === 'local_effect' && !work.completionReceipt && completionEvidence.durableResultEvidence) {
1898
+ const recordedAt = nowIso(ctx);
1899
+ const completed = completeWorkWithReceipt(
1900
+ ctx.workStore,
1901
+ work.workId,
1902
+ {
1903
+ schemaVersion: 1,
1904
+ receiptId: `LFX-WORK-${randomUUID()}`,
1905
+ source: 'local_effect',
1906
+ workId: work.workId,
1907
+ operation: 'controller_work/local_effect',
1908
+ target: { kind: 'controller_local', id: work.workId },
1909
+ changed: true,
1910
+ recordedAt,
1911
+ },
1912
+ 'completed_local',
1913
+ 'local_effect',
1914
+ );
1915
+ if (completed.planId && completed.planStepId && ctx.planStore) {
1916
+ completePlanStepForWork(ctx.planStore, { planId: completed.planId, stepId: completed.planStepId, work: completed });
1917
+ }
1918
+ return buildFacadeResult({
1919
+ status: 'ok',
1920
+ summary: `Finalize result: succeeded for ${work.workId}.`,
1921
+ data: {
1922
+ work: summarizeWorkContract(completed),
1923
+ finalStatus: 'completed',
1924
+ completionReceipt: completed.completionReceipt,
1925
+ validPasses: history.validPasses,
1926
+ hiddenFailure: false,
1927
+ },
1928
+ evidenceRefs: completed.evidenceRefs.slice(0, 5),
1929
+ suggestedNextActions: [{ label: 'Read controller status', tool: 'rh_status', operation: 'get', risk: 'readonly' }],
1930
+ });
1931
+ }
1932
+
1267
1933
  // Checks and result evidence are necessary but not sufficient. Delivery and
1268
1934
  // cleanup must produce the exact Work-owned receipt before Work can become
1269
1935
  // terminal; a Run exit or a missing receipt is never completion authority.
@@ -1318,6 +1984,38 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
1318
1984
  });
1319
1985
  }
1320
1986
 
1987
+ const owner = ctx.workStore.controllerHome
1988
+ ? getControllerSession({ controllerHome: ctx.workStore.controllerHome, repoId: ctx.repoId }, work.workId)
1989
+ : undefined;
1990
+ if (owner) {
1991
+ const ownerPrincipal = owner.principalId?.trim() || owner.controllerId;
1992
+ const ownerInstanceId = owner.controllerInstanceId?.trim();
1993
+ const callerPrincipal = ctx.principalId?.trim();
1994
+ const callerInstanceId = ctx.controllerInstanceId?.trim();
1995
+ const sameAuthority = Boolean(
1996
+ callerPrincipal
1997
+ && ownerPrincipal === callerPrincipal
1998
+ && (!ownerInstanceId || ownerInstanceId === callerInstanceId),
1999
+ );
2000
+ if (!sameAuthority) {
2001
+ return buildFacadeResult({
2002
+ status: 'blocked',
2003
+ summary: `WORK_TERMINALIZATION_ACTIVE_CONTROLLER_FENCE: ${work.workId} has a newer active Controller claim generation ${owner.claimGeneration ?? 1}.`,
2004
+ data: {
2005
+ work: summarizeWorkContract(work),
2006
+ activeController: {
2007
+ controllerId: owner.controllerId,
2008
+ principalId: ownerPrincipal,
2009
+ controllerInstanceId: owner.controllerInstanceId,
2010
+ sessionId: owner.sessionId,
2011
+ claimGeneration: owner.claimGeneration ?? 1,
2012
+ },
2013
+ finalStatus: work.status,
2014
+ },
2015
+ });
2016
+ }
2017
+ }
2018
+
1321
2019
  const destructiveCleanup = input.authorizeDestructiveCleanup === true;
1322
2020
  transitionWorkContractPhase(ctx.workStore, work.workId, {
1323
2021
  status: 'cancelled',
@@ -1334,9 +2032,17 @@ export function stopGoalWorkloop(ctx: GoalWorkloopContext, input: GoalWorkloopSt
1334
2032
  // cleanup handler verifies ownership, cleanliness and successful removal.
1335
2033
  worktreeRef: work.worktreeRef,
1336
2034
  });
1337
- const plan = updated.planId && updated.planStepId && ctx.planStore
1338
- ? completePlanStepForWork(ctx.planStore, { planId: updated.planId, stepId: updated.planStepId, work: updated })
2035
+ const planId = updated.planId;
2036
+ const planStepId = updated.planStepId;
2037
+ let plan = planId && ctx.planStore
2038
+ ? getPlanContract(ctx.planStore, planId)
1339
2039
  : undefined;
2040
+ if (plan && planId && planStepId && ctx.planStore) {
2041
+ const step = plan.steps.find((entry) => entry.id === planStepId);
2042
+ if (step?.workId === updated.workId) {
2043
+ plan = completePlanStepForWork(ctx.planStore, { planId, stepId: planStepId, work: updated });
2044
+ }
2045
+ }
1340
2046
 
1341
2047
  return buildFacadeResult({
1342
2048
  status: 'ok',
@@ -1373,6 +2079,7 @@ export function runGoalWorkloop(
1373
2079
  objective: String(args.objective ?? ''),
1374
2080
  acceptanceCriteria: Array.isArray(args.acceptance_criteria) ? args.acceptance_criteria.map(String) : undefined,
1375
2081
  allowedPaths: Array.isArray(args.allowed_paths) ? args.allowed_paths.map(String) : undefined,
2082
+ initialLikelyPaths: Array.isArray(args.initial_likely_paths) ? args.initial_likely_paths.map(String) : undefined,
1376
2083
  forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
1377
2084
  checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
1378
2085
  modeInput: {
@@ -1383,6 +2090,7 @@ export function runGoalWorkloop(
1383
2090
  requiresInvestigation: args.requires_investigation === true,
1384
2091
  requiresLongRunningChecks: args.requires_long_running_checks === true,
1385
2092
  requiresParallelism: args.requires_parallelism === true,
2093
+ explicitMode: args.mode === 'scale' ? 'scale' : undefined,
1386
2094
  needsDependencies: args.needs_dependencies === true,
1387
2095
  requiresRecovery: args.requires_recovery === true,
1388
2096
  requiresWorker: args.requires_worker === true,
@@ -1402,13 +2110,43 @@ export function runGoalWorkloop(
1402
2110
  forceMode: args.force_mode === 'direct_control' || args.force_mode === 'goal_workloop' || args.force_mode === 'handoff_only'
1403
2111
  ? args.force_mode
1404
2112
  : undefined,
2113
+ relatedWorkId: typeof args.related_work_id === 'string' ? args.related_work_id : undefined,
2114
+ workRelation: args.work_relation === 'continue' || args.work_relation === 'extend' || args.work_relation === 'parallel' || args.work_relation === 'new_goal'
2115
+ ? args.work_relation
2116
+ : undefined,
2117
+ requirementId: typeof args.requirement_id === 'string' ? args.requirement_id : undefined,
1405
2118
  planId: typeof args.plan_id === 'string' ? args.plan_id : undefined,
1406
2119
  planStepId: typeof args.plan_step_id === 'string' ? args.plan_step_id : undefined,
2120
+ workKind: args.work_kind === 'repository_change'
2121
+ || args.work_kind === 'completed_no_change'
2122
+ || args.work_kind === 'read_only_review'
2123
+ || args.work_kind === 'investigation'
2124
+ || args.work_kind === 'local_effect'
2125
+ || args.work_kind === 'remote_effect'
2126
+ || args.work_kind === 'reconciliation'
2127
+ ? args.work_kind
2128
+ : undefined,
1407
2129
  });
1408
2130
  case 'continue':
1409
2131
  return continueGoalWorkloop(ctx, {
1410
2132
  workId: String(args.work_id ?? ''),
1411
2133
  note: typeof args.note === 'string' ? args.note : undefined,
2134
+ allowedPaths: Array.isArray(args.allowed_paths) ? args.allowed_paths.map(String) : undefined,
2135
+ forbiddenPaths: Array.isArray(args.forbidden_paths) ? args.forbidden_paths.map(String) : undefined,
2136
+ checks: Array.isArray(args.check_ids) ? args.check_ids.map(String) : undefined,
2137
+ additionalLikelyPaths: Array.isArray(args.additional_likely_paths) ? args.additional_likely_paths.map(String) : undefined,
2138
+ inspectedPaths: Array.isArray(args.inspected_paths) ? args.inspected_paths.map(String) : undefined,
2139
+ reviewFindings: Array.isArray(args.review_findings) ? args.review_findings.map(String) : undefined,
2140
+ acceptanceEvidence: Array.isArray(args.acceptance_evidence)
2141
+ ? args.acceptance_evidence.filter((value): value is Record<string, unknown> => Boolean(value) && typeof value === 'object').map((value) => ({
2142
+ criterion: typeof value.criterion === 'string' ? value.criterion : '',
2143
+ evidenceIds: Array.isArray(value.evidence_ids) ? value.evidence_ids.map(String) : [],
2144
+ rationale: typeof value.rationale === 'string' ? value.rationale : '',
2145
+ }))
2146
+ : undefined,
2147
+ acceptanceFailureDecision: args.acceptance_failure_decision === 'repair' || args.acceptance_failure_decision === 'rescope'
2148
+ ? args.acceptance_failure_decision
2149
+ : undefined,
1412
2150
  });
1413
2151
  case 'verify':
1414
2152
  return verifyGoalWorkloop(ctx, {