@moretea-labs/forge 1.5.1 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (434) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +46 -0
  3. package/CLAUDE.md +2 -0
  4. package/DESIGN.md +7 -2
  5. package/README.md +4 -4
  6. package/README.zh-CN.md +2 -2
  7. package/SKILL.md +2 -2
  8. package/THIRD_PARTY_NOTICES.md +1 -1
  9. package/agents/openai.yaml +1 -1
  10. package/assets/hooks/hook-input.sh +11 -8
  11. package/assets/hooks/lib/session-state.sh +10 -7
  12. package/assets/hooks/lib/workflow-state.sh +13 -177
  13. package/assets/hooks/post-tool-observer.sh +8 -2
  14. package/assets/hooks/pre-edit-guard.sh +8 -39
  15. package/assets/hooks/prompt-guard.sh +7 -83
  16. package/assets/hooks/session-start-context.sh +56 -1
  17. package/assets/partials/05-workflow.partial.md +1 -1
  18. package/assets/partials-agents/03-orchestration.partial.md +1 -1
  19. package/assets/plugin-registry.v1.json +54 -3
  20. package/assets/reference-configs/document-generation.md +1 -1
  21. package/assets/reference-configs/harness-overview.md +1 -1
  22. package/assets/reference-configs/hook-operations.md +1 -1
  23. package/assets/reference-configs/sprint-contracts.md +3 -3
  24. package/assets/skill-version.json +2 -2
  25. package/assets/templates/contract.template.md +2 -7
  26. package/assets/templates/helpers/capture-plan.sh +2 -2
  27. package/assets/templates/helpers/check-task-workflow.sh +4 -4
  28. package/assets/templates/helpers/contract-worktree.sh +1 -21
  29. package/assets/templates/helpers/ensure-task-workflow.sh +13 -38
  30. package/assets/templates/helpers/migrate-project-template.sh +22 -45
  31. package/assets/templates/helpers/new-plan.sh +1 -1
  32. package/assets/templates/helpers/plan-to-todo.sh +12 -37
  33. package/assets/templates/helpers/ship-worktrees.sh +2 -29
  34. package/assets/templates/helpers/sprint-backlog.sh +6 -6
  35. package/assets/templates/helpers/verify-contract.sh +1 -111
  36. package/assets/templates/helpers/verify-sprint.sh +3 -180
  37. package/assets/templates/plan.template.md +1 -1
  38. package/assets/templates/review.template.md +9 -34
  39. package/assets/workflow-contract.v1.json +10 -19
  40. package/docs/README.md +10 -10
  41. package/docs/architecture/CURRENT.md +170 -0
  42. package/docs/architecture/EVOLUTION.md +42 -0
  43. package/docs/architecture/current/README.md +4 -156
  44. package/docs/architecture/decisions/20260803-release-identity-binding-and-exit-policy.md +2 -2
  45. package/docs/architecture/decisions/20260823-immediate-controller-relay.md +53 -0
  46. package/docs/architecture/decisions/20260826-browser-runtime-v2.md +73 -0
  47. package/docs/architecture/history/README.md +7 -0
  48. package/docs/architecture/{chatgpt-handoff-facade.md → history/chatgpt-handoff-facade.md} +4 -0
  49. package/docs/architecture/{global-hook-runtime.md → history/global-hook-runtime.md} +7 -3
  50. package/docs/architecture/{ios-semantic-automation-provider-v2.md → history/ios-semantic-automation-provider-v2.md} +6 -2
  51. package/docs/architecture/history.md +3 -34
  52. package/docs/architecture/index.md +15 -98
  53. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +1 -1
  54. package/docs/architecture/reviews/2026-08-22-semantic-harness-convergence-acceptance.md +39 -0
  55. package/docs/architecture/versions/1.6.md +38 -0
  56. package/docs/architecture/versions/1.7.md +41 -0
  57. package/docs/forge-chatgpt-browser-engine.md +2 -2
  58. package/docs/forge-chatgpt-mcp-setup.md +4 -28
  59. package/docs/forge-chatgpt-supervised-automation.md +3 -3
  60. package/docs/forge-github-issue-launcher.md +1 -1
  61. package/docs/forge-runtime-self-healing-loop.md +1 -1
  62. package/docs/operations/controller-browser-plugin.md +38 -22
  63. package/docs/operations/controller-performance-and-502.md +124 -0
  64. package/docs/operations/controller-reliability-runbook.md +1 -1
  65. package/docs/operations/features.md +3 -3
  66. package/docs/operations/github-repository.md +1 -1
  67. package/docs/operations/homebrew.md +2 -2
  68. package/docs/operations/mcp-tool-exposure.md +5 -3
  69. package/docs/tutorials/01-install-and-start.md +1 -1
  70. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  71. package/docs/wiki/Architecture.md +6 -6
  72. package/docs/wiki/Implementation.md +2 -2
  73. package/docs/wiki/Installation.md +1 -1
  74. package/docs/wiki/Integrations.md +2 -2
  75. package/docs/wiki/Quick-Start.md +1 -1
  76. package/docs/wiki/Work-Lifecycle.md +1 -1
  77. package/package.json +29 -10
  78. package/scripts/activate-source-baseline.ts +17 -13
  79. package/scripts/benchmark-background-check-overlap.ts +228 -0
  80. package/scripts/benchmark-mcp-host.sh +117 -0
  81. package/scripts/benchmark-mcp-transport.ts +170 -0
  82. package/scripts/benchmark-quality-harness.ts +146 -0
  83. package/scripts/benchmark-route-session-concurrency.ts +207 -6
  84. package/scripts/benchmark-thin-harness-mcp-e2e.ts +7 -1
  85. package/scripts/benchmark-typescript-navigation.ts +186 -0
  86. package/scripts/benchmark-work-efficiency.ts +334 -0
  87. package/scripts/capture-plan.sh +3 -3
  88. package/scripts/check-agent-tooling.sh +8 -35
  89. package/scripts/check-architecture-sync.sh +17 -86
  90. package/scripts/check-mcp-compatibility.ts +14 -0
  91. package/scripts/check-public-docs.sh +14 -1
  92. package/scripts/check-runtime-architecture.mjs +107 -64
  93. package/scripts/check-stable-baseline.ts +92 -0
  94. package/scripts/check-task-workflow.sh +4 -4
  95. package/scripts/contract-worktree.sh +1 -21
  96. package/scripts/ensure-task-workflow.sh +22 -55
  97. package/scripts/host-temporary-cloud-mcp-e2e.sh +265 -0
  98. package/scripts/install-standalone-recovery.ts +24 -0
  99. package/scripts/lib/project-init-lib.sh +15 -44
  100. package/scripts/migrate-chatgpt-automation-schedules.ts +64 -0
  101. package/scripts/migrate-project-template.sh +23 -1179
  102. package/scripts/new-plan.sh +1 -1
  103. package/scripts/plan-to-todo.sh +11 -36
  104. package/scripts/run-governed-gate.ts +2 -1
  105. package/scripts/ship-worktrees.sh +2 -29
  106. package/scripts/smoke-ios-physical-device.ts +4 -2
  107. package/scripts/smoke-runtime-recovery.ts +1 -35
  108. package/scripts/smoke-schedule-engine.ts +1 -20
  109. package/scripts/sprint-backlog.sh +1 -1
  110. package/scripts/verify-contract.sh +1 -111
  111. package/scripts/verify-forge-runtime.sh +0 -1
  112. package/scripts/verify-sprint.sh +3 -180
  113. package/scripts/workflow-contract.ts +1 -5
  114. package/src/cli/commands/chatgpt.ts +31 -2
  115. package/src/cli/commands/init-hook.ts +3 -3
  116. package/src/cli/commands/mcp.ts +9 -5
  117. package/src/cli/commands/plugin.ts +37 -10
  118. package/src/cli/commands/recovery.ts +25 -3
  119. package/src/cli/commands/runtime.ts +55 -3
  120. package/src/cli/commands/security.ts +12 -1
  121. package/src/cli/commands/setup-profile.ts +31 -21
  122. package/src/cli/controller/check-runner.ts +106 -3
  123. package/src/cli/controller/composite-result.ts +1 -1
  124. package/src/cli/controller/context/focus.ts +51 -0
  125. package/src/cli/controller/context/instruction-context.ts +114 -0
  126. package/src/cli/controller/context/known-paths.ts +76 -0
  127. package/src/cli/controller/context/query-planning.ts +110 -0
  128. package/src/cli/controller/context/rule-resolution.ts +532 -0
  129. package/src/cli/controller/context/source-materializer.ts +278 -0
  130. package/src/cli/controller/context/structural.ts +16 -0
  131. package/src/cli/controller/context/types.ts +286 -0
  132. package/src/cli/controller/context-pack.ts +898 -430
  133. package/src/cli/controller/launch-agents.ts +77 -2
  134. package/src/cli/controller/operational-plan.ts +2 -2
  135. package/src/cli/controller/work-mode.ts +6 -4
  136. package/src/cli/editing/edit-session.ts +60 -7
  137. package/src/cli/github/github.ts +133 -0
  138. package/src/cli/hook/prompt-guard-decision.ts +17 -18
  139. package/src/cli/hook/prompt-intents.ts +1 -1
  140. package/src/cli/installer/managed-entries.ts +15 -1
  141. package/src/cli/local-bridge/console-automations.ts +225 -67
  142. package/src/cli/local-bridge/console-view-models.ts +3 -35
  143. package/src/cli/local-bridge/facade-api.ts +2 -217
  144. package/src/cli/local-bridge/frontend/src/App.tsx +3 -3
  145. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +153 -3
  146. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +37 -4
  147. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +183 -3
  148. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +1 -1
  149. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +4 -3
  150. package/src/cli/local-bridge/frontend/src/lib/api.ts +18 -11
  151. package/src/cli/local-bridge/frontend/src/lib/routes.ts +1 -1
  152. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +1 -1
  153. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +4 -3
  154. package/src/cli/local-bridge/frontend/src/styles.css +27 -11
  155. package/src/cli/local-bridge/frontend/src/types.ts +8 -7
  156. package/src/cli/local-bridge/job-store.ts +1 -490
  157. package/src/cli/local-bridge/server.ts +29 -532
  158. package/src/cli/local-bridge/ui-dist/app.css +1 -1
  159. package/src/cli/local-bridge/ui-dist/app.js +5101 -4967
  160. package/src/cli/mcp/auth.ts +5 -5
  161. package/src/cli/mcp/instructions.ts +3 -2
  162. package/src/cli/mcp/legacy-tool-service.ts +0 -189
  163. package/src/cli/mcp/multi-repository.ts +16 -5
  164. package/src/cli/mcp/oauth.ts +175 -26
  165. package/src/cli/mcp/paths.ts +8 -2
  166. package/src/cli/mcp/repository-tools.ts +356 -101
  167. package/src/cli/mcp/server.ts +625 -54
  168. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  169. package/src/cli/mcp/toolset-selection.ts +1 -1
  170. package/src/cli/mcp/toolset.ts +42 -5
  171. package/src/cli/mcp/transports/http.ts +125 -15
  172. package/src/cli/mcp/transports/session-registry.ts +67 -5
  173. package/src/cli/mcp/types.ts +3 -3
  174. package/src/cli/repositories/command-classifier.ts +209 -14
  175. package/src/cli/repositories/command-executor.ts +55 -521
  176. package/src/cli/repositories/command-process.ts +108 -0
  177. package/src/cli/repositories/command-scope.ts +99 -41
  178. package/src/cli/repositories/controller-home.ts +7 -8
  179. package/src/cli/repositories/git-command-executor.ts +2 -0
  180. package/src/cli/repositories/locks.ts +53 -16
  181. package/src/cli/repositories/repository-snapshot.ts +121 -0
  182. package/src/cli/repositories/safe-patch.ts +6 -4
  183. package/src/cli/repositories/selected-path-actions.ts +17 -9
  184. package/src/cli/repositories/structured-git.ts +110 -10
  185. package/src/cli/repository/inspector.ts +565 -62
  186. package/src/cli/repository/session-cache.ts +58 -17
  187. package/src/cli/v81-entry.ts +1 -4
  188. package/src/effects/fs-transaction.ts +85 -17
  189. package/src/runtime/context/codegraph-read-provider.ts +233 -2
  190. package/src/runtime/context/codegraph-sidecar.cjs +74 -40
  191. package/src/runtime/context/generic-lsp-provider.ts +355 -0
  192. package/src/runtime/context/lsp-client.ts +283 -0
  193. package/src/runtime/context/semantic-navigation.ts +275 -0
  194. package/src/runtime/context/swift-navigation.ts +565 -0
  195. package/src/runtime/context/typescript-navigation.ts +226 -0
  196. package/src/runtime/control-plane/dispatch-priority.ts +28 -0
  197. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +196 -2
  198. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +86 -24
  199. package/src/runtime/control-plane/execution/execution-identity.ts +65 -14
  200. package/src/runtime/control-plane/execution/session-store.ts +18 -0
  201. package/src/runtime/control-plane/execution/validation.ts +15 -2
  202. package/src/runtime/control-plane/execution/verification-evidence.ts +58 -3
  203. package/src/runtime/control-plane/execution/work-handle-store.ts +27 -2
  204. package/src/runtime/control-plane/execution/work-lifecycle-audit.ts +258 -0
  205. package/src/runtime/control-plane/execution/work-remote-delivery.ts +215 -0
  206. package/src/runtime/control-plane/execution/work-task-receipt.ts +54 -11
  207. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +175 -8
  208. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +28 -21
  209. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +87 -9
  210. package/src/runtime/control-plane/facade/capability-registry.ts +129 -25
  211. package/src/runtime/control-plane/facade/controller-round-relay.ts +991 -0
  212. package/src/runtime/control-plane/facade/controller-session-store.ts +144 -14
  213. package/src/runtime/control-plane/facade/goal-workloop-access.ts +9 -2
  214. package/src/runtime/control-plane/facade/goal-workloop.ts +885 -105
  215. package/src/runtime/control-plane/facade/index.ts +1 -0
  216. package/src/runtime/control-plane/facade/plan-contract-store.ts +211 -26
  217. package/src/runtime/control-plane/facade/requirement-board.ts +24 -8
  218. package/src/runtime/control-plane/facade/semantic-admission.ts +205 -0
  219. package/src/runtime/control-plane/facade/suggested-actions.ts +5 -1
  220. package/src/runtime/control-plane/facade/types.ts +55 -11
  221. package/src/runtime/control-plane/facade/work-contract-store.ts +204 -73
  222. package/src/runtime/control-plane/generated-cache-retention.ts +271 -0
  223. package/src/runtime/control-plane/global-scheduler/config.ts +44 -0
  224. package/src/runtime/control-plane/global-scheduler/dispatch-capacity.ts +80 -0
  225. package/src/runtime/control-plane/global-scheduler/durable-admission.ts +46 -0
  226. package/src/runtime/control-plane/global-scheduler/maintenance.ts +145 -0
  227. package/src/runtime/control-plane/global-scheduler/projection-refresh.ts +84 -0
  228. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +3 -3
  229. package/src/runtime/control-plane/global-scheduler/scheduler.ts +273 -542
  230. package/src/runtime/control-plane/global-scheduler/source-scan.ts +59 -0
  231. package/src/runtime/control-plane/global-scheduler/state.ts +88 -0
  232. package/src/runtime/control-plane/global-scheduler/worker-attachment.ts +57 -0
  233. package/src/runtime/control-plane/global-scheduler/worker-exit-decision.ts +38 -0
  234. package/src/runtime/control-plane/global-scheduler/worker-exit-reconciler.ts +175 -0
  235. package/src/runtime/control-plane/global-scheduler/worker-launch.ts +111 -0
  236. package/src/runtime/control-plane/global-scheduler/worker-lifecycle-store.ts +64 -0
  237. package/src/runtime/control-plane/global-scheduler/worker-lifecycle.ts +150 -0
  238. package/src/runtime/control-plane/global-scheduler/worker-process.ts +119 -0
  239. package/src/runtime/control-plane/global-scheduler/worker-stderr.ts +60 -0
  240. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +50 -0
  241. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +642 -51
  242. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +40 -0
  243. package/src/runtime/control-plane/launcher/thin-launcher.ts +194 -10
  244. package/src/runtime/control-plane/persistence/controller-home-migration.ts +487 -0
  245. package/src/runtime/control-plane/persistence/requirement-store.ts +15 -11
  246. package/src/runtime/control-plane/persistence/sqlite-store.ts +33 -2
  247. package/src/runtime/control-plane/release-retention.ts +62 -2
  248. package/src/runtime/control-plane/routing/route-policy.ts +7 -16
  249. package/src/runtime/control-plane/runtime-status-client.ts +0 -2
  250. package/src/runtime/diagnostics/mcp-timing.ts +19 -0
  251. package/src/runtime/diagnostics/mcp-transport-benchmark.ts +129 -0
  252. package/src/runtime/diagnostics/performance.ts +24 -1
  253. package/src/runtime/evidence/event-ledger.ts +1 -1
  254. package/src/runtime/execution/jobs/store.ts +58 -4
  255. package/src/runtime/execution/jobs/types.ts +1 -1
  256. package/src/runtime/execution/managed-workspace.ts +203 -1
  257. package/src/runtime/execution/process-runtime/check-facade.ts +52 -14
  258. package/src/runtime/execution/process-runtime/check-result.ts +63 -2
  259. package/src/runtime/execution/process-runtime/check-scheduling.ts +114 -0
  260. package/src/runtime/execution/process-runtime/command-facade.ts +346 -40
  261. package/src/runtime/execution/process-runtime/gc.ts +98 -12
  262. package/src/runtime/execution/process-runtime/lightweight-managed.ts +1184 -0
  263. package/src/runtime/execution/process-runtime/persisted-check.ts +57 -6
  264. package/src/runtime/execution/process-runtime/resource-claims.ts +97 -8
  265. package/src/runtime/execution/process-runtime/runtime.ts +59 -4
  266. package/src/runtime/execution/process-runtime/store.ts +7 -0
  267. package/src/runtime/execution/process-runtime/types.ts +2 -0
  268. package/src/runtime/execution/thin-harness/async-process.ts +12 -2
  269. package/src/runtime/execution/thin-harness/search-worker.ts +54 -5
  270. package/src/runtime/execution/workers/executor.ts +0 -48
  271. package/src/runtime/gateway/mcp/execution-tools.ts +1219 -126
  272. package/src/runtime/gateway/mcp/process-tools.ts +83 -37
  273. package/src/runtime/gateway/mcp/router.ts +185 -83
  274. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +546 -0
  275. package/src/runtime/gateway/mcp/runtime-tools.ts +2555 -1616
  276. package/src/runtime/model-clients/deepseek-adapter.ts +0 -32
  277. package/src/runtime/plugins/app-store-connect-adapter.ts +123 -19
  278. package/src/runtime/plugins/browser-adapter.ts +2925 -318
  279. package/src/runtime/plugins/browser-automation-service.ts +10 -0
  280. package/src/runtime/plugins/browser-handoff-host.ts +6 -31
  281. package/src/runtime/plugins/browser-handoff.ts +92 -5
  282. package/src/runtime/plugins/browser-macos-bridge.ts +586 -45
  283. package/src/runtime/plugins/browser-node-bridge.ts +6 -34
  284. package/src/runtime/plugins/browser-provider-registry.ts +109 -0
  285. package/src/runtime/plugins/browser-runtime-contract.ts +152 -0
  286. package/src/runtime/plugins/browser-runtime.ts +183 -0
  287. package/src/runtime/plugins/browser-session-authority.ts +332 -0
  288. package/src/runtime/plugins/capability-authorization-grants.ts +354 -0
  289. package/src/runtime/plugins/desktop-operator-registration.ts +15 -12
  290. package/src/runtime/plugins/external-adapter.ts +324 -3
  291. package/src/runtime/plugins/external-registration.ts +84 -5
  292. package/src/runtime/plugins/external-unix-socket.ts +7 -1
  293. package/src/runtime/plugins/first-party-registry.ts +4 -2
  294. package/src/runtime/plugins/github-adapter.ts +125 -0
  295. package/src/runtime/plugins/gmail-adapter.ts +28 -2
  296. package/src/runtime/plugins/ios/agent-device-provider.ts +1 -1
  297. package/src/runtime/plugins/ios/agent-device-typed-provider.ts +52 -9
  298. package/src/runtime/plugins/ios/remote-xpc-hid.ts +1171 -0
  299. package/src/runtime/plugins/ios-adapter.ts +106 -16
  300. package/src/runtime/plugins/ios-agent-device.ts +234 -64
  301. package/src/runtime/plugins/ios-physical-device.ts +1143 -392
  302. package/src/runtime/plugins/lightweight-action.ts +153 -0
  303. package/src/runtime/plugins/local-system-adapter.ts +125 -8
  304. package/src/runtime/plugins/macos-capability-broker.ts +55 -9
  305. package/src/runtime/plugins/plugin-action-sidecar.ts +59 -0
  306. package/src/runtime/plugins/plugin-management-adapter.ts +181 -0
  307. package/src/runtime/plugins/resend-adapter.ts +1 -1
  308. package/src/runtime/plugins/store.ts +366 -24
  309. package/src/runtime/plugins/types.ts +39 -2
  310. package/src/runtime/plugins/xiaohongshu-publish.ts +515 -0
  311. package/src/runtime/projections/git-status-sampler.ts +16 -16
  312. package/src/runtime/projections/materialized-view.ts +28 -10
  313. package/src/runtime/recovery/actions.ts +0 -10
  314. package/src/runtime/recovery/classifier.ts +0 -9
  315. package/src/runtime/recovery/maintenance-executor.ts +415 -18
  316. package/src/runtime/recovery/probe.ts +0 -1
  317. package/src/runtime/recovery/types.ts +0 -1
  318. package/src/runtime/root/mcp-transport.ts +180 -10
  319. package/src/runtime/root/package-connector-service.ts +186 -8
  320. package/src/runtime/root/package-runtime-release.ts +143 -17
  321. package/src/runtime/root/package-runtime-service.ts +430 -22
  322. package/src/runtime/root/release-manifest.ts +7 -2
  323. package/src/runtime/root/release-materialize.ts +162 -19
  324. package/src/runtime/root/release-store.ts +48 -11
  325. package/src/runtime/root/runtime.ts +23 -5
  326. package/src/runtime/root/service.ts +100 -27
  327. package/src/runtime/root/types.ts +2 -0
  328. package/src/runtime/safe-tooling/index.ts +0 -1
  329. package/src/runtime/safe-tooling/ios-development.ts +321 -63
  330. package/src/runtime/safe-tooling/types.ts +0 -30
  331. package/src/runtime/shared/trusted-executable.ts +62 -0
  332. package/src/runtime/shared/trusted-node-executable.ts +48 -0
  333. package/src/runtime/standalone-recovery/core.ts +702 -94
  334. package/src/runtime/standalone-recovery/entry.ts +99 -23
  335. package/src/runtime/standalone-recovery/installer.ts +19 -3
  336. package/src/runtime/standalone-recovery/service-labels.ts +2 -0
  337. package/src/runtime/standalone-recovery/watchdog-heartbeat.ts +160 -0
  338. package/src/runtime/workflow/schedules/browser-probe.ts +258 -0
  339. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +40 -0
  340. package/src/runtime/workflow/schedules/engine.ts +509 -80
  341. package/src/runtime/workflow/schedules/github-issue-watch.ts +158 -0
  342. package/src/runtime/workflow/schedules/settlement.ts +48 -4
  343. package/src/runtime/workflow/schedules/store.ts +131 -10
  344. package/src/runtime/workflow/schedules/types.ts +6 -3
  345. package/src/runtime/workflow/schedules/work-continuation.ts +120 -34
  346. package/src/testing/test-governance.ts +8 -40
  347. package/assets/reference-configs/heartbeat-triage.md +0 -57
  348. package/assets/templates/helpers/harness-trace-grade.sh +0 -150
  349. package/assets/templates/helpers/heartbeat-triage.sh +0 -361
  350. package/assets/templates/helpers/maintenance-triage.sh +0 -167
  351. package/bin/forge-desktop-helper.mjs +0 -204
  352. package/docs/architecture/current/approved-target-architecture.zh-CN.md +0 -1501
  353. package/docs/architecture/current/architecture-invariants.md +0 -331
  354. package/docs/architecture/current/automation-and-schedule-engine.md +0 -453
  355. package/docs/architecture/current/codegraph-context-provider.md +0 -226
  356. package/docs/architecture/current/control-plane-authority-inventory.md +0 -94
  357. package/docs/architecture/current/controller-repository-decoupling.md +0 -148
  358. package/docs/architecture/current/dispatch-and-agent-strategy.md +0 -354
  359. package/docs/architecture/current/entity-model.md +0 -592
  360. package/docs/architecture/current/external-plugin-governance.md +0 -396
  361. package/docs/architecture/current/failure-recovery.md +0 -447
  362. package/docs/architecture/current/governance.md +0 -213
  363. package/docs/architecture/current/human-interaction-plane.md +0 -87
  364. package/docs/architecture/current/implementation-status.md +0 -128
  365. package/docs/architecture/current/job-and-run-lifecycle.md +0 -413
  366. package/docs/architecture/current/legacy-governance.md +0 -58
  367. package/docs/architecture/current/local-workspace-target-grants.md +0 -70
  368. package/docs/architecture/current/migration-roadmap.md +0 -105
  369. package/docs/architecture/current/multi-repository-execution.md +0 -371
  370. package/docs/architecture/current/operations-runbook.md +0 -45
  371. package/docs/architecture/current/peer-supercontroller-runtime.md +0 -32
  372. package/docs/architecture/current/personal-assistant-plugin-baseline.md +0 -206
  373. package/docs/architecture/current/repository-access-modes.md +0 -102
  374. package/docs/architecture/current/route-policy.md +0 -15
  375. package/docs/architecture/current/route-session-concurrency.md +0 -85
  376. package/docs/architecture/current/runtime-architecture-simplification.md +0 -214
  377. package/docs/architecture/current/runtime-directory-map.md +0 -49
  378. package/docs/architecture/current/runtime-health-and-resource-lifecycle.md +0 -85
  379. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -422
  380. package/docs/architecture/current/session-aware-execution-and-authorization.md +0 -77
  381. package/docs/architecture/current/system-overview.md +0 -144
  382. package/docs/architecture/current/target-requirements-traceability.md +0 -202
  383. package/docs/architecture/current/thin-harness-governance-policy.md +0 -32
  384. package/docs/architecture/current/thin-harness-v1.md +0 -311
  385. package/docs/architecture/current/verification-and-release-gates.md +0 -391
  386. package/docs/forge-autonomous-goal-loop.md +0 -9
  387. package/docs/forge-provider-configuration.md +0 -7
  388. package/docs/operations/chatgpt-personal-assistant-runtime.md +0 -83
  389. package/docs/operations/controller-desktop-plugin.md +0 -112
  390. package/docs/operations/gmail-assistant-routines.md +0 -186
  391. package/docs/operations/personal-assistant-operator-guide.md +0 -61
  392. package/docs/operations/provider-configuration.md +0 -31
  393. package/scripts/harness-trace-grade.sh +0 -150
  394. package/scripts/heartbeat-triage.sh +0 -361
  395. package/scripts/maintenance-triage.sh +0 -167
  396. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +0 -2
  397. package/src/cli/repositories/goal-registry.ts +0 -239
  398. package/src/runtime/assistant/action-proposals.ts +0 -279
  399. package/src/runtime/assistant/intent.ts +0 -395
  400. package/src/runtime/assistant/model-provider.ts +0 -460
  401. package/src/runtime/assistant/openapi.ts +0 -223
  402. package/src/runtime/assistant/policy.ts +0 -89
  403. package/src/runtime/assistant/readiness.ts +0 -258
  404. package/src/runtime/assistant/routine-runtime.ts +0 -477
  405. package/src/runtime/assistant/schedule-binding.ts +0 -171
  406. package/src/runtime/assistant/standing-grants.ts +0 -375
  407. package/src/runtime/assistant/store.ts +0 -243
  408. package/src/runtime/assistant/types.ts +0 -135
  409. package/src/runtime/control-plane/goal-loop/config-facade.ts +0 -700
  410. package/src/runtime/control-plane/goal-loop/config-store.ts +0 -570
  411. package/src/runtime/control-plane/goal-loop/config-types.ts +0 -213
  412. package/src/runtime/control-plane/goal-loop/executor-router.ts +0 -109
  413. package/src/runtime/control-plane/goal-loop/failure-taxonomy.ts +0 -149
  414. package/src/runtime/control-plane/goal-loop/goal-contract-store.ts +0 -304
  415. package/src/runtime/control-plane/goal-loop/goal-loop-engine.ts +0 -1001
  416. package/src/runtime/control-plane/goal-loop/handoff-packet.ts +0 -160
  417. package/src/runtime/control-plane/goal-loop/index.ts +0 -13
  418. package/src/runtime/control-plane/goal-loop/local-tools.ts +0 -290
  419. package/src/runtime/control-plane/goal-loop/policy-gates.ts +0 -203
  420. package/src/runtime/control-plane/goal-loop/provider-dispatch.ts +0 -93
  421. package/src/runtime/control-plane/goal-loop/provider-registry.ts +0 -545
  422. package/src/runtime/control-plane/goal-loop/types.ts +0 -378
  423. package/src/runtime/control-plane/startup-recovery.ts +0 -198
  424. package/src/runtime/gateway/mcp/resource-policy.ts +0 -186
  425. package/src/runtime/personal-assistant/gmail-triage-manager.ts +0 -231
  426. package/src/runtime/personal-assistant/reporting-runtime.ts +0 -344
  427. package/src/runtime/personal-assistant/triage-runtime.ts +0 -748
  428. package/src/runtime/plugins/desktop-adapter.ts +0 -374
  429. package/src/runtime/safe-tooling/browser-safe.ts +0 -133
  430. package/src/runtime/workflow/findings/store.ts +0 -204
  431. package/src/runtime/workflow/findings/types.ts +0 -58
  432. package/src/runtime/workflow/portfolio/engine.ts +0 -20
  433. package/src/runtime/workflow/portfolio/store.ts +0 -185
  434. package/src/runtime/workflow/portfolio/types.ts +0 -29
@@ -0,0 +1,108 @@
1
+ import { spawn, type ChildProcess } from 'child_process';
2
+ import { capProcessOutput, redactProcessOutput } from '../../effects/process-runner';
3
+ import { repositoryChildProcessEnvironment } from '../../runtime/shared/process-environment';
4
+ import { terminateProcessTree } from '../../runtime/shared/process-tree';
5
+ import type { CanonicalRepositoryCommand } from './command-normalization';
6
+
7
+ export interface SpawnCommandResult {
8
+ ok: boolean;
9
+ exitCode: number;
10
+ timedOut: boolean;
11
+ cancelled: boolean;
12
+ stdout: string;
13
+ stderr: string;
14
+ }
15
+ export interface RepositoryCommandAsyncHooks {
16
+ onSpawn?: (pid: number) => void;
17
+ onStdout?: (chunk: string) => void;
18
+ onStderr?: (chunk: string) => void;
19
+ /** Observe the exact child exit result before repository post-processing begins. */
20
+ onExit?: (result: SpawnCommandResult) => void;
21
+ signal?: AbortSignal;
22
+ }
23
+
24
+ export function commandEnvironment(): NodeJS.ProcessEnv {
25
+ const allowed = ['PATH', 'HOME', 'USER', 'LOGNAME', 'TMPDIR', 'TMP', 'TEMP', 'SHELL', 'LANG', 'LC_ALL', 'TERM', 'SSH_AUTH_SOCK', 'GPG_TTY', 'XDG_CONFIG_HOME'];
26
+ const env: NodeJS.ProcessEnv = {};
27
+ for (const key of allowed) if (process.env[key] !== undefined) env[key] = process.env[key];
28
+ env.GIT_TERMINAL_PROMPT = '0';
29
+ env.CI = '1';
30
+ return repositoryChildProcessEnvironment(env);
31
+ }
32
+
33
+ export function collectOutput(maxOutputBytes: number): { write(chunk: string | Buffer): void; complete(): string } {
34
+ const chunks: Buffer[] = [];
35
+ let totalBytes = 0;
36
+ let truncated = false;
37
+ return {
38
+ write(chunk) {
39
+ const buffer = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
40
+ if (buffer.length === 0) return;
41
+ if (totalBytes >= maxOutputBytes) { truncated = true; return; }
42
+ const remaining = maxOutputBytes - totalBytes;
43
+ chunks.push(buffer.length <= remaining ? buffer : buffer.subarray(0, remaining));
44
+ totalBytes += Math.min(buffer.length, remaining);
45
+ if (buffer.length > remaining) truncated = true;
46
+ },
47
+ complete() {
48
+ const redacted = redactProcessOutput(Buffer.concat(chunks).toString('utf8'));
49
+ return truncated ? capProcessOutput(redacted, maxOutputBytes) : redacted;
50
+ },
51
+ };
52
+ }
53
+
54
+ async function killCommandTree(child: ChildProcess): Promise<void> {
55
+ const pid = child.pid;
56
+ if (!pid) return;
57
+ try {
58
+ if (process.platform !== 'win32') {
59
+ try { process.kill(-pid, 'SIGTERM'); } catch { child.kill('SIGTERM'); }
60
+ } else child.kill();
61
+ } catch { /* already exited */ }
62
+ await terminateProcessTree(pid, { gracePeriodMs: 200, killAfterMs: 1_000, pollIntervalMs: 25 });
63
+ }
64
+
65
+ export async function runCanonicalCommand(
66
+ command: CanonicalRepositoryCommand,
67
+ cwd: string,
68
+ timeoutMs: number,
69
+ maxOutputBytes: number,
70
+ hooks: RepositoryCommandAsyncHooks = {},
71
+ ): Promise<SpawnCommandResult> {
72
+ if (hooks.signal?.aborted) return { ok: false, exitCode: 1, timedOut: false, cancelled: true, stdout: '', stderr: 'cancelled before spawn' };
73
+ const executable = command.kind === 'argv' ? command.executable! : process.platform === 'win32' ? 'cmd.exe' : '/bin/sh';
74
+ const args = command.kind === 'argv' ? [...(command.args ?? [])] : process.platform === 'win32' ? ['/d', '/s', '/c', command.shellCommand!] : ['-c', command.shellCommand!];
75
+ const display = typeof command.value === 'string' ? command.value : JSON.stringify(command.value);
76
+ const stdout = collectOutput(maxOutputBytes);
77
+ const stderr = collectOutput(maxOutputBytes);
78
+ return await new Promise<SpawnCommandResult>((resolve) => {
79
+ const child = spawn(executable, args, { cwd, env: commandEnvironment(), stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
80
+ if (child.pid) hooks.onSpawn?.(child.pid);
81
+ let settled = false;
82
+ let timedOut = false;
83
+ let cancelled = false;
84
+ let spawnError = '';
85
+ let timeoutHandle: NodeJS.Timeout | undefined;
86
+ const finish = (exitCode: number) => {
87
+ if (settled) return;
88
+ settled = true;
89
+ if (timeoutHandle) clearTimeout(timeoutHandle);
90
+ hooks.signal?.removeEventListener('abort', onAbort);
91
+ const errors = [stderr.complete()];
92
+ if (timedOut) errors.push(`process timed out after ${timeoutMs}ms: ${redactProcessOutput(display)}`);
93
+ if (cancelled) errors.push('process cancelled');
94
+ if (spawnError) errors.push(redactProcessOutput(spawnError));
95
+ const result = { ok: exitCode === 0 && !timedOut && !cancelled && !spawnError, exitCode, timedOut, cancelled, stdout: stdout.complete(), stderr: capProcessOutput(errors.filter(Boolean).join('\n'), maxOutputBytes) };
96
+ try { hooks.onExit?.(result); } catch { /* observation hooks must never change the command outcome */ }
97
+ resolve(result);
98
+ };
99
+ const onAbort = () => { cancelled = true; void killCommandTree(child).finally(() => finish(1)); };
100
+ timeoutHandle = setTimeout(() => { timedOut = true; void killCommandTree(child).finally(() => finish(1)); }, timeoutMs);
101
+ timeoutHandle.unref();
102
+ hooks.signal?.addEventListener('abort', onAbort, { once: true });
103
+ child.stdout?.on('data', (chunk) => { stdout.write(chunk); hooks.onStdout?.(chunk.toString()); });
104
+ child.stderr?.on('data', (chunk) => { stderr.write(chunk); hooks.onStderr?.(chunk.toString()); });
105
+ child.on('error', (error) => { spawnError = error.message; });
106
+ child.on('close', (code) => finish(code ?? 1));
107
+ });
108
+ }
@@ -50,64 +50,122 @@ export function resolveRepositoryCommandCwd(
50
50
  return { root, cwd, relativeCwd };
51
51
  }
52
52
 
53
- export function assertRepositoryCommandAllowed(command: string): string {
53
+ interface RepositoryCommandInputPolicyOptions {
54
+ /** Internal lane hint: preserve repository policy while allowing an already-classified local script shape. */
55
+ allowOpaqueLocalScript?: boolean;
56
+ }
57
+
58
+ export function assertRepositoryCommandAllowed(
59
+ command: string,
60
+ _options: RepositoryCommandInputPolicyOptions = {},
61
+ ): string {
54
62
  const normalized = command.trim();
55
63
  if (!normalized) throw new Error('COMMAND_INVALID: command is required');
56
64
  if (normalized.length > MAX_COMMAND_LENGTH) {
57
65
  throw new Error(`COMMAND_INVALID: command exceeds ${MAX_COMMAND_LENGTH} characters`);
58
66
  }
59
67
  if (normalized.includes('\0')) throw new Error('COMMAND_INVALID: command contains a null byte');
60
-
61
- const denied: Array<[RegExp, string]> = [
62
- [/\$\(|`/, 'nested command substitution is not allowed'],
63
- [/(?:^|[;&|]\s*)(?:eval|source)\b|(?:^|[;&|]\s*)\.\s+[^/]/i, 'dynamic shell evaluation is not allowed'],
64
- [/\b(?:bash|sh|zsh|fish|dash|cmd|powershell|pwsh)\b\s+(?:-[^\s]*c\b|\/c\b)/i, 'nested shell execution is not allowed'],
65
- [/\b(?:python\d*|node|ruby|perl)\b\s+(?:-[ce]\b|--eval\b)/i, 'inline interpreter execution is not allowed'],
66
- [/(?:^|[\s'"=])(?:\.\.(?:\/|\\)|~(?:\/|\\)|[A-Za-z]:[\\/])/, 'ambiguous parent/home/drive paths are not allowed; use repo-relative paths or an authorized absolute external path'],
67
- [/(?:^|[;&|]\s*)cd(?:\s|$)/i, 'use the cwd argument instead of cd'],
68
- [/\b(?:env|printenv)\b|\bgh\s+auth\s+token\b|\bgit\s+credential\b|\bsecurity\s+find-(?:generic|internet)-password\b/i, 'credential or environment inspection is not allowed'],
69
- [/(?:^|[\s'"/])(?:\.ssh|\.aws|\.gnupg|Library\/Keychains|login\.keychain|\.config\/gh\/hosts\.yml)(?:[\s'"/]|$)/i, 'sensitive credential paths are not allowed'],
70
- [/(?:^|[;&|]\s*)(?:curl|wget|scp|sftp|ftp|nc|ncat|netcat|socat|ssh)(?:\s|$)/i, 'direct network or exfiltration utilities are not allowed'],
71
- [/\bgit\s+(?:--git-dir|--work-tree|-C)\b|\b(?:GIT_DIR|GIT_WORK_TREE)\s*=/i, 'Git repository scope overrides are not allowed'],
72
- [/\bgit\s+config\b[^\n]*(?:--global|--system)\b/i, 'global or system Git configuration changes are not allowed'],
73
- ];
74
- for (const [pattern, reason] of denied) {
75
- if (pattern.test(normalized)) throw new Error(`COMMAND_POLICY_DENIED: ${reason}`);
76
- }
68
+ // Repository commands are an execution surface, not a shell-language policy
69
+ // engine. Do not maintain an enumerated command/program denylist here: it is
70
+ // necessarily incomplete and turns new CLI/subcommand shapes into false
71
+ // negatives. Structural repository/cwd/external-path authority is enforced
72
+ // separately after normalization.
77
73
  return normalized;
78
74
  }
79
75
 
80
- export function assertRepositoryCommandInputAllowed(input: unknown): CanonicalRepositoryCommand {
81
- const command = normalizeRepositoryCommand(input);
76
+ function macOsTccSensitiveShellTool(command: string): string | undefined {
77
+ const match = command.match(/(?:^|[\n;&|])\s*(?:[A-Za-z_][A-Za-z0-9_]*=[^\s]+\s+)*(?:(?:command|exec|sudo|nohup)\s+)?(?:\/usr\/(?:bin|sbin)\/)?(osascript|screencapture)(?=\s|$)/i);
78
+ return match?.[1]?.toLowerCase();
79
+ }
80
+
81
+ function macOsTccSensitiveArgvTool(command: CanonicalRepositoryCommand): string | undefined {
82
+ if (command.kind !== 'argv') return undefined;
83
+ const executableName = command.executable!.split(/[\\/]/).at(-1)?.toLowerCase() ?? '';
84
+ if (executableName === 'osascript' || executableName === 'screencapture') return executableName;
85
+ if (!['bash', 'sh', 'zsh', 'fish', 'dash', 'cmd', 'powershell', 'pwsh'].includes(executableName)) return undefined;
86
+ const args = command.args ?? [];
87
+ const commandFlagIndex = args.findIndex((arg) => /^(?:-[^-]*c[^-]*|\/c|-Command)$/i.test(arg));
88
+ if (commandFlagIndex < 0) return undefined;
89
+ const shellPayload = args[commandFlagIndex + 1];
90
+ return typeof shellPayload === 'string' ? macOsTccSensitiveShellTool(shellPayload) : undefined;
91
+ }
92
+
93
+ const INLINE_INTERPRETERS = new Set(['node', 'nodejs', 'bun', 'deno']);
94
+ const SHELL_INTERPRETERS = new Set(['bash', 'sh', 'zsh', 'fish', 'dash']);
95
+ const FORGE_PLUGIN_ACTION_EXECUTOR = /\b(?:submitAssistantPluginAction|executeAssistantPluginAction|executeBrowserPluginAction)\b/;
96
+ const FORGE_RUNTIME_PLUGIN_IMPORT = /(?:\bfrom\s*|\bimport\s*\(|\brequire\s*\()\s*['"][^'"]*(?:^|[\\/])(?:src[\\/])?runtime[\\/]plugins[\\/][^'"]+['"]/m;
97
+
98
+ function inlineEvalPayload(command: CanonicalRepositoryCommand): string | undefined {
82
99
  if (command.kind === 'shell') {
83
- assertRepositoryCommandAllowed(command.shellCommand!);
84
- return command;
100
+ const shell = command.shellCommand ?? '';
101
+ return /(?:^|[;&|]\s*|\s)(?:node|nodejs|bun|deno)(?:\s+[^\n;&|]*)?\s(?:-e|--eval)(?:\s|=)/i.test(shell)
102
+ ? shell
103
+ : undefined;
85
104
  }
86
105
  const executableName = command.executable!.split(/[\\/]/).at(-1)?.toLowerCase() ?? '';
87
106
  const args = command.args ?? [];
88
- const shellFlag = args.some((arg) => ['-c', '/c', '-Command'].includes(arg));
89
- if (['bash', 'sh', 'zsh', 'fish', 'dash', 'cmd', 'powershell', 'pwsh'].includes(executableName) && shellFlag) {
90
- throw new Error('COMMAND_POLICY_DENIED: nested shell execution is not allowed');
91
- }
92
- if (['node', 'nodejs', 'bun', 'deno', 'ruby', 'perl', 'python', 'python3'].includes(executableName)
93
- && args.some((arg) => ['-c', '-e', '--eval'].includes(arg))) {
94
- throw new Error('COMMAND_POLICY_DENIED: inline interpreter execution is not allowed');
107
+ if (SHELL_INTERPRETERS.has(executableName)) {
108
+ const commandFlagIndex = args.findIndex((arg) => /^(?:-[^-]*c[^-]*)$/i.test(arg));
109
+ const payload = commandFlagIndex >= 0 ? args[commandFlagIndex + 1] : undefined;
110
+ return typeof payload === 'string'
111
+ && /(?:^|[;&|]\s*|\s)(?:node|nodejs|bun|deno)(?:\s+[^\n;&|]*)?\s(?:-e|--eval)(?:\s|=)/i.test(payload)
112
+ ? payload
113
+ : undefined;
95
114
  }
96
- if (args.some((arg) => /(?:^|[\\/])\.\.(?:[\\/]|$)|^(?:~|[A-Za-z]:[\\/])/.test(arg))) {
97
- throw new Error('COMMAND_POLICY_DENIED: ambiguous parent/home/drive paths are not allowed; use repo-relative paths or an authorized absolute external path');
98
- }
99
- if (args.some((arg) => /(?:^|[\\/])(?:\.ssh|\.aws|\.gnupg|Library[\\/]Keychains|login\.keychain|\.config[\\/]gh[\\/]hosts\.yml)(?:[\\/]|$)/i.test(arg))) {
100
- throw new Error('COMMAND_POLICY_DENIED: sensitive credential paths are not allowed');
101
- }
102
- if (executableName === 'git' && args.some((arg) => ['--git-dir', '--work-tree', '-C'].includes(arg))) {
103
- throw new Error('COMMAND_POLICY_DENIED: Git repository scope overrides are not allowed');
115
+ if (!INLINE_INTERPRETERS.has(executableName)) return undefined;
116
+ const evalIndex = args.findIndex((arg) => arg === '-e' || arg === '--eval');
117
+ if (evalIndex >= 0) return args[evalIndex + 1];
118
+ const inline = args.find((arg) => arg.startsWith('--eval='));
119
+ return inline?.slice('--eval='.length);
120
+ }
121
+
122
+ /**
123
+ * Process Runtime deliberately supports bounded direct argv eval for diagnostics
124
+ * and test helpers. What it must not support is using that escape hatch as a
125
+ * second plugin executor: typed plugin actions own authorization, resource
126
+ * claims, receipts, and recovery semantics through plugin_action_execute.
127
+ */
128
+ export function assertRepositoryCommandNoPluginExecutionBypass(input: unknown): CanonicalRepositoryCommand {
129
+ const command = normalizeRepositoryCommand(input);
130
+ const payload = inlineEvalPayload(command);
131
+ if (payload && FORGE_PLUGIN_ACTION_EXECUTOR.test(payload) && FORGE_RUNTIME_PLUGIN_IMPORT.test(payload)) {
132
+ throw new Error(
133
+ 'PLUGIN_ACTION_EXECUTION_REQUIRES_TYPED_PLUGIN_TOOL: repository commands may not invoke Forge runtime plugin action internals; use plugin_action_execute so typed authorization, resource claims, receipts, and recovery remain authoritative',
134
+ );
104
135
  }
105
- if (executableName === 'git' && args[0] === 'config' && args.some((arg) => ['--global', '--system'].includes(arg))) {
106
- throw new Error('COMMAND_POLICY_DENIED: global or system Git configuration changes are not allowed');
136
+ return command;
137
+ }
138
+
139
+ /**
140
+ * Runtime releases live at release-specific executable paths. Never let those
141
+ * binaries become macOS TCC principals: GUI automation and screen capture must
142
+ * be owned by the stable Forge Desktop Operator / typed browser provider.
143
+ */
144
+ export function assertRepositoryCommandStableHostIdentity(input: unknown): CanonicalRepositoryCommand {
145
+ const command = normalizeRepositoryCommand(input);
146
+ const tool = command.kind === 'shell'
147
+ ? macOsTccSensitiveShellTool(command.shellCommand!)
148
+ : macOsTccSensitiveArgvTool(command);
149
+ if (tool) {
150
+ throw new Error(
151
+ `COMMAND_POLICY_DENIED: ${tool} is macOS TCC-sensitive and must run through the stable Forge Desktop Operator/browser capability; forge-runtime must not become the permission identity`,
152
+ );
107
153
  }
108
- if (['env', 'printenv', 'curl', 'wget', 'scp', 'sftp', 'ftp', 'nc', 'ncat', 'netcat', 'socat', 'ssh'].includes(executableName)) {
109
- throw new Error('COMMAND_POLICY_DENIED: credential or network inspection is not allowed');
154
+ return command;
155
+ }
156
+
157
+ export function assertRepositoryCommandInputAllowed(
158
+ input: unknown,
159
+ options: RepositoryCommandInputPolicyOptions = {},
160
+ ): CanonicalRepositoryCommand {
161
+ const command = assertRepositoryCommandStableHostIdentity(input);
162
+ if (command.kind === 'shell') {
163
+ assertRepositoryCommandAllowed(command.shellCommand!, options);
110
164
  }
165
+ // Do not infer execution permission from executable names, shell flags, inline
166
+ // interpreter flags, network utilities, or Git subcommands. Those rules are
167
+ // not complete enough to be an authority boundary. Real repository and
168
+ // external-filesystem scope is validated by assertCommandPathOperandsStayInRepository.
111
169
  return command;
112
170
  }
113
171
 
@@ -12,20 +12,19 @@ export function resolveControllerHome(explicit?: string): string {
12
12
  }
13
13
 
14
14
  /**
15
- * Prefer env, then repo-local self-host layout (`_ops/controller-home`) used by
16
- * `forge runtime service install`, then the user-global default.
15
+ * Resolve the one installed Controller Home. A repository containing a retired
16
+ * `_ops/controller-home` is evidence for an explicit migration only; its mere
17
+ * presence must never redirect a package Runtime, CLI, or MCP caller away from
18
+ * the user-level authority.
19
+ *
20
+ * Repo-local storage remains available only through an explicit
21
+ * `--controller-home` argument or `FORGE_CONTROLLER_HOME` configuration.
17
22
  */
18
23
  export function resolveRepoPreferredControllerHome(repoRoot?: string, explicit?: string): string {
19
24
  const trimmedExplicit = explicit?.trim();
20
25
  if (trimmedExplicit) return resolveControllerHome(trimmedExplicit);
21
26
  const configured = process.env.FORGE_CONTROLLER_HOME?.trim();
22
27
  if (configured) return resolveControllerHome(configured);
23
- if (repoRoot?.trim()) {
24
- const opsHome = join(resolve(repoRoot.trim()), '_ops', 'controller-home');
25
- if (existsSync(join(opsHome, 'mcp', 'mcp.local.json')) || existsSync(opsHome)) {
26
- return resolve(opsHome);
27
- }
28
- }
29
28
  return resolveControllerHome();
30
29
  }
31
30
 
@@ -9,6 +9,7 @@ import { resolveRepositoryCommandCwd } from './command-scope';
9
9
  import type { RepositoryRecord } from './types';
10
10
  import { assertResolvedAuthorization, decideAuthorization, type AuthorizationDecision } from '../../runtime/control-plane/governance/authorization';
11
11
  import { readRepositoryAccessPolicy } from '../../runtime/control-plane/governance/access-policy';
12
+ import { invalidateRepositoryReadCaches } from '../repository/inspector';
12
13
 
13
14
  export interface ExecuteRepositoryGitCommandInput {
14
15
  args: string[];
@@ -296,6 +297,7 @@ export function executeRepositoryGitCommand(
296
297
  after,
297
298
  repositoryChanged: snapshotChanged(before, after),
298
299
  };
300
+ if (execution.repositoryChanged) invalidateRepositoryReadCaches(root);
299
301
  auditExecution(controllerHome, repository, execution);
300
302
  return execution;
301
303
  }
@@ -1,4 +1,4 @@
1
- import { closeSync, mkdirSync, openSync, readFileSync, rmSync, statSync, writeFileSync } from 'fs';
1
+ import { closeSync, mkdirSync, openSync, readFileSync, rmSync, writeFileSync } from 'fs';
2
2
  import { dirname, join } from 'path';
3
3
  import { ensureControllerHome, repositoryControllerRoot } from './controller-home';
4
4
  import { recordGatewayLatency } from '../../runtime/observability/gateway-contention-metrics';
@@ -39,7 +39,6 @@ export type ControllerLockTryResult =
39
39
  const DEFAULT_ASYNC_LOCK_WAIT_MS = 500;
40
40
  const MAX_ASYNC_LOCK_WAIT_MS = 5_000;
41
41
  const LOCK_POLL_INTERVAL_MS = 10;
42
- const CORRUPT_LOCK_GRACE_MS = 1_000;
43
42
 
44
43
  function safe(value: string): string {
45
44
  return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'unknown';
@@ -96,23 +95,59 @@ function isPidAlive(pid: number | undefined): boolean {
96
95
  }
97
96
  }
98
97
 
99
- export function readControllerLock(controllerHome: string, key: ControllerLockKey): ControllerLockRecord | undefined {
98
+ function readControllerLockRecord(path: string): ControllerLockRecord | undefined {
99
+ try {
100
+ return JSON.parse(readFileSync(path, 'utf-8')) as ControllerLockRecord;
101
+ } catch {
102
+ return undefined;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Reap only the exact stale lock identity that the caller observed. The
108
+ * per-lockId guard serializes competing reapers, then the canonical path is
109
+ * re-read before unlink so an owner that replaced the stale record is never
110
+ * deleted from an old observation.
111
+ */
112
+ export function reapControllerLockIfUnchanged(
113
+ controllerHome: string,
114
+ key: ControllerLockKey,
115
+ observed: ControllerLockRecord,
116
+ ): boolean {
100
117
  const path = controllerLockPath(controllerHome, key);
118
+ const guardPath = `${path}.reap-${safe(observed.lockId)}.guard`;
119
+ let guardFd: number | undefined;
101
120
  try {
102
- const record = JSON.parse(readFileSync(path, 'utf-8')) as ControllerLockRecord;
103
- if (isExpired(record) || !isPidAlive(record.pid)) {
104
- rmSync(path, { force: true });
105
- return undefined;
106
- }
107
- return record;
121
+ guardFd = openSync(guardPath, 'wx');
108
122
  } catch {
109
- try {
110
- if (Date.now() - statSync(path).mtimeMs >= CORRUPT_LOCK_GRACE_MS) rmSync(path, { force: true });
111
- } catch {
112
- /* a concurrent owner may still be creating or releasing the lock */
113
- }
123
+ return false;
124
+ }
125
+ try {
126
+ const current = readControllerLockRecord(path);
127
+ if (!current || current.lockId !== observed.lockId) return false;
128
+ if (!isExpired(current) && isPidAlive(current.pid)) return false;
129
+ rmSync(path, { force: true });
130
+ return true;
131
+ } finally {
132
+ if (guardFd !== undefined) closeSync(guardFd);
133
+ rmSync(guardPath, { force: true });
134
+ }
135
+ }
136
+
137
+ export function readControllerLock(controllerHome: string, key: ControllerLockKey): ControllerLockRecord | undefined {
138
+ const path = controllerLockPath(controllerHome, key);
139
+ const record = readControllerLockRecord(path);
140
+ if (!record) {
141
+ // Canonical lock publication is atomic. A malformed file therefore means
142
+ // legacy/corrupt state; fail closed instead of deleting an identity that
143
+ // cannot be compared safely.
144
+ return undefined;
145
+ }
146
+ if (isExpired(record) || !isPidAlive(record.pid)) {
147
+ reapControllerLockIfUnchanged(controllerHome, key, record);
114
148
  return undefined;
115
149
  }
150
+ return record;
116
151
  }
117
152
 
118
153
  function metric(key: ControllerLockKey, waitedMs: number, outcome: 'success' | 'contention' | 'timeout'): void {
@@ -157,9 +192,11 @@ export function tryAcquireControllerLock(
157
192
  };
158
193
  let fd: number | undefined;
159
194
  try {
195
+ // The exclusive create is the ownership primitive. A concurrent reader may
196
+ // briefly observe incomplete JSON, but malformed state is never deleted;
197
+ // competing open(..., 'wx') attempts therefore still fail closed.
160
198
  fd = openSync(path, 'wx');
161
- writeFileSync(fd, `${JSON.stringify(record, null, 2)}
162
- `, 'utf-8');
199
+ writeFileSync(fd, `${JSON.stringify(record, null, 2)}\n`, 'utf-8');
163
200
  const waitedMs = performance.now() - started;
164
201
  if (recordMetrics) metric(key, waitedMs, 'success');
165
202
  return { acquired: true, lock: record, waitedMs };
@@ -0,0 +1,121 @@
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, lstatSync, readFileSync, readlinkSync } from 'fs';
3
+ import { resolve } from 'path';
4
+ import { spawn, spawnSync } from 'child_process';
5
+ import { capProcessOutput, redactProcessOutput } from '../../effects/process-runner';
6
+ import { commandEnvironment, collectOutput } from './command-process';
7
+
8
+ export interface RepositoryCommandSnapshot {
9
+ head: string | null;
10
+ branch: string | null;
11
+ status: string;
12
+ dirty: boolean;
13
+ refsHash: string;
14
+ paths: string[];
15
+ pathFingerprints: Record<string, string>;
16
+ }
17
+
18
+ export function emptyWorkspaceSnapshot(): RepositoryCommandSnapshot {
19
+ return { head: null, branch: null, status: '', dirty: false, refsHash: createHash('sha256').update('').digest('hex'), paths: [], pathFingerprints: {} };
20
+ }
21
+
22
+ function commandOutput(command: string, args: string[], cwd: string, maxOutputBytes: number): { ok: boolean; stdout: string } {
23
+ const result = spawnSync(command, args, { cwd, env: commandEnvironment(), encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000, maxBuffer: Math.max(maxOutputBytes, 1024 * 1024) });
24
+ return { ok: result.status === 0 && !result.error, stdout: capProcessOutput(redactProcessOutput(typeof result.stdout === 'string' ? result.stdout : ''), maxOutputBytes) };
25
+ }
26
+ function gitText(root: string, args: string[]): string {
27
+ const output = commandOutput('git', ['-C', root, ...args], root, 256 * 1024);
28
+ return output.ok ? output.stdout.trim() : '';
29
+ }
30
+ const SNAPSHOT_PATHS = ['.', ':(exclude).ai/harness/**', ':(exclude)_ops/controller-home/**'];
31
+ function statusPath(line: string): string | undefined {
32
+ if (!line.trim() || line.startsWith('##')) return undefined;
33
+ const raw = line.length > 3 ? line.slice(3) : '';
34
+ const path = raw.includes(' -> ') ? raw.split(' -> ').at(-1) : raw;
35
+ return path?.replace(/^"|"$/g, '');
36
+ }
37
+ function pathFingerprint(root: string, relativePath: string, statusLines: string[]): string {
38
+ const hash = createHash('sha256').update(statusLines.join('\n'));
39
+ const absolute = resolve(root, relativePath);
40
+ if (!existsSync(absolute)) return hash.update('\nmissing').digest('hex');
41
+ try {
42
+ const stat = lstatSync(absolute);
43
+ if (stat.isSymbolicLink()) hash.update(`\nsymlink:${readlinkSync(absolute)}`);
44
+ else if (stat.isFile()) hash.update('\nfile:').update(readFileSync(absolute));
45
+ else hash.update(`\nmode:${stat.mode}:size:${stat.size}`);
46
+ } catch (error) { hash.update(`\nunreadable:${error instanceof Error ? error.message : String(error)}`); }
47
+ return hash.digest('hex');
48
+ }
49
+ function buildSnapshot(root: string, head: string | null, branch: string | null, status: string, refs: string): RepositoryCommandSnapshot {
50
+ const lines = status.split(/\r?\n/).filter((line) => line && !line.startsWith('##'));
51
+ const byPath = new Map<string, string[]>();
52
+ for (const line of lines) {
53
+ const path = statusPath(line); if (!path) continue;
54
+ byPath.set(path, [...(byPath.get(path) ?? []), line]);
55
+ }
56
+ const paths = [...byPath.keys()].sort();
57
+ return { head, branch, status, dirty: paths.length > 0, refsHash: createHash('sha256').update(refs).digest('hex'), paths, pathFingerprints: Object.fromEntries(paths.map((path) => [path, pathFingerprint(root, path, byPath.get(path) ?? [])])) };
58
+ }
59
+
60
+ export function repositorySnapshot(root: string): RepositoryCommandSnapshot {
61
+ return buildSnapshot(root, gitText(root, ['rev-parse', '--verify', 'HEAD']) || null, gitText(root, ['branch', '--show-current']) || null, gitText(root, ['status', '--porcelain=v1', '--branch', '--untracked-files=all', '--', ...SNAPSHOT_PATHS]), gitText(root, ['show-ref']));
62
+ }
63
+
64
+ interface GitResult { ok: boolean; exitCode: number; stdout: string; stderr: string; timedOut: boolean; cancelled: boolean }
65
+ async function gitAsync(root: string, args: string[], signal?: AbortSignal): Promise<GitResult> {
66
+ if (signal?.aborted) return { ok: false, exitCode: 1, stdout: '', stderr: 'cancelled', timedOut: false, cancelled: true };
67
+ return await new Promise((resolveResult) => {
68
+ const child = spawn('git', ['-C', root, ...args], { cwd: root, env: commandEnvironment(), stdio: ['ignore', 'pipe', 'pipe'], detached: process.platform !== 'win32' });
69
+ const stdout = collectOutput(256 * 1024); const stderr = collectOutput(64 * 1024);
70
+ let settled = false; let timedOut = false; let cancelled = false;
71
+ const finish = (exitCode: number, forcedError?: string) => {
72
+ if (settled) return; settled = true; clearTimeout(timeout); signal?.removeEventListener('abort', abort);
73
+ resolveResult({ ok: exitCode === 0 && !timedOut && !cancelled, exitCode, stdout: stdout.complete().trim(), stderr: forcedError ?? stderr.complete(), timedOut, cancelled });
74
+ };
75
+ const timeout = setTimeout(() => { timedOut = true; try { child.kill(); } catch {} finish(1, `git ${args.join(' ')} timed out`); }, 10_000); timeout.unref();
76
+ const abort = () => { cancelled = true; try { child.kill(); } catch {} finish(1, 'cancelled'); };
77
+ signal?.addEventListener('abort', abort, { once: true });
78
+ child.stdout?.on('data', (chunk) => stdout.write(chunk)); child.stderr?.on('data', (chunk) => stderr.write(chunk));
79
+ child.on('error', (error) => finish(1, error.message)); child.on('close', (code) => finish(code ?? 1));
80
+ });
81
+ }
82
+
83
+ const MAX_DIRTY_PATHS = 200;
84
+ export async function repositorySnapshotAsync(
85
+ root: string,
86
+ signal?: AbortSignal,
87
+ options: { pathFingerprints?: boolean } = {},
88
+ ): Promise<RepositoryCommandSnapshot> {
89
+ const [headResult, branchResult, statusResult, refsResult] = await Promise.all([
90
+ gitAsync(root, ['rev-parse', '--verify', 'HEAD'], signal), gitAsync(root, ['branch', '--show-current'], signal),
91
+ gitAsync(root, ['status', '--porcelain=v1', '--branch', '--untracked-files=all', '--', ...SNAPSHOT_PATHS], signal), gitAsync(root, ['show-ref'], signal),
92
+ ]);
93
+ if ([headResult, branchResult, statusResult, refsResult].some((result) => result.cancelled)) throw new Error('CANCELLED: repository snapshot aborted');
94
+ if (statusResult.timedOut || headResult.timedOut) throw new Error('SNAPSHOT_TIMEOUT: git snapshot timed out');
95
+ if (!statusResult.ok) throw new Error(`SNAPSHOT_FAILED: git status exit ${statusResult.exitCode}: ${statusResult.stderr}`);
96
+ let head: string | null = null;
97
+ if (headResult.ok) head = headResult.stdout || null;
98
+ else if (!(headResult.exitCode === 128 || /unknown revision|bad revision|Needed a single revision|ambiguous argument|not a valid object name/i.test(headResult.stderr))) throw new Error(`SNAPSHOT_FAILED: git rev-parse HEAD exit ${headResult.exitCode}: ${headResult.stderr}`);
99
+ const refs = refsResult.ok || (refsResult.exitCode === 1 && (!refsResult.stderr.trim() || /expected|no match|no references/i.test(refsResult.stderr))) ? refsResult.stdout || '' : (() => { throw new Error(`SNAPSHOT_FAILED: git show-ref exit ${refsResult.exitCode}: ${refsResult.stderr}`); })();
100
+ if (signal?.aborted) throw new Error('CANCELLED: repository snapshot aborted');
101
+ const lines = statusResult.stdout.split(/\r?\n/).filter((line) => line && !line.startsWith('##'));
102
+ const includePathFingerprints = options.pathFingerprints !== false;
103
+ if (includePathFingerprints && lines.length > MAX_DIRTY_PATHS) throw new Error(`SNAPSHOT_TOO_DIRTY: ${lines.length} dirty paths exceeds Fast Path cap ${MAX_DIRTY_PATHS}`);
104
+ const byPath = new Map<string, string[]>();
105
+ for (const line of lines) { const path = statusPath(line); if (path) byPath.set(path, [...(byPath.get(path) ?? []), line]); }
106
+ const paths = [...byPath.keys()].sort();
107
+ let pathFingerprints: Record<string, string> = {};
108
+ if (includePathFingerprints) {
109
+ const { computePathFingerprintsAsync } = await import('../../runtime/execution/thin-harness/fingerprint-worker');
110
+ const fingerprint = await computePathFingerprintsAsync({ root, paths, statusByPath: Object.fromEntries(byPath), maxFileBytes: 256 * 1024, maxTotalBytes: 8 * 1024 * 1024, maxPaths: MAX_DIRTY_PATHS }, { signal, timeoutMs: 5_000 });
111
+ pathFingerprints = fingerprint.pathFingerprints;
112
+ }
113
+ return { head, branch: branchResult.ok ? branchResult.stdout || null : null, status: statusResult.stdout, dirty: paths.length > 0, refsHash: createHash('sha256').update(refs).digest('hex'), paths, pathFingerprints };
114
+ }
115
+
116
+ export function changedSnapshotPaths(before: RepositoryCommandSnapshot, after: RepositoryCommandSnapshot): string[] {
117
+ return [...new Set([...before.paths, ...after.paths])].filter((path) => before.pathFingerprints[path] !== after.pathFingerprints[path]).sort();
118
+ }
119
+ export function snapshotChanged(before: RepositoryCommandSnapshot, after: RepositoryCommandSnapshot): boolean {
120
+ return before.head !== after.head || before.branch !== after.branch || before.refsHash !== after.refsHash || changedSnapshotPaths(before, after).length > 0;
121
+ }
@@ -7,6 +7,7 @@ import {
7
7
  getEditSession,
8
8
  type EditOperation,
9
9
  type EditSession,
10
+ type EditSessionBinding,
10
11
  type EditSessionPatchError,
11
12
  } from '../editing/edit-session';
12
13
  import { getMcpPolicy } from '../mcp/policy';
@@ -345,6 +346,7 @@ export function applySafePatch(repository: RepositoryRecord, input: {
345
346
  continueOnError?: unknown;
346
347
  refreshFingerprints?: unknown;
347
348
  recoverStaleSession?: unknown;
349
+ binding?: EditSessionBinding;
348
350
  }): SafePatchApplyResult {
349
351
  const operations = normalizedSafePatchOperations(input.operations);
350
352
  if (operations.length === 0) throw new Error('SAFE_PATCH_OPERATIONS_REQUIRED: provide at least one operation');
@@ -356,7 +358,7 @@ export function applySafePatch(repository: RepositoryRecord, input: {
356
358
  const allowedPaths = Array.isArray(input.allowedPaths) ? input.allowedPaths.map((item) => String(item).trim()).filter(Boolean) : undefined;
357
359
  let session = sessionId
358
360
  ? getEditSession(repository.canonicalRoot, sessionId)
359
- : beginEditSession(repository.canonicalRoot, { purpose: String(input.purpose ?? 'Safe repository patch').trim() || 'Safe repository patch', allowedPaths });
361
+ : beginEditSession(repository.canonicalRoot, { purpose: String(input.purpose ?? 'Safe repository patch').trim() || 'Safe repository patch', allowedPaths, binding: input.binding });
360
362
  const createdSession = !sessionId;
361
363
  let recoveredSession: SafePatchRecoveredSession | undefined;
362
364
  const appliedChunks: SafePatchAppliedChunk[] = [];
@@ -376,17 +378,17 @@ export function applySafePatch(repository: RepositoryRecord, input: {
376
378
  continue;
377
379
  }
378
380
  try {
379
- session = applyEditOperations(repository.canonicalRoot, policy, session.sessionId, preflight.operations.map(stripInternalIndex), { expectedRevision, maxBatchOperations: MAX_SAFE_PATCH_CHUNK_SIZE });
381
+ session = applyEditOperations(repository.canonicalRoot, policy, session.sessionId, preflight.operations.map(stripInternalIndex), { expectedRevision, maxBatchOperations: MAX_SAFE_PATCH_CHUNK_SIZE, binding: input.binding });
380
382
  } catch (error) {
381
383
  const message = error instanceof Error ? error.message : String(error);
382
384
  if (!sessionId && input.recoverStaleSession !== false && message.includes('EDIT_SESSION_FINGERPRINT_STALE')) {
383
385
  const previousSessionId = session.sessionId;
384
- session = beginEditSession(repository.canonicalRoot, { purpose: `${String(input.purpose ?? 'Safe repository patch').trim() || 'Safe repository patch'} (recovered)`, allowedPaths });
386
+ session = beginEditSession(repository.canonicalRoot, { purpose: `${String(input.purpose ?? 'Safe repository patch').trim() || 'Safe repository patch'} (recovered)`, allowedPaths, binding: input.binding });
385
387
  expectedRevision = session.currentRevision;
386
388
  recoveredSession = { reason: 'Previous edit session fingerprints were stale; remaining chunks were moved into a fresh session.', previousSessionId, newSessionId: session.sessionId };
387
389
  const retry = preflightChunk(repository, group, chunkIndex, refresh);
388
390
  if (retry.failures.length === 0) {
389
- session = applyEditOperations(repository.canonicalRoot, policy, session.sessionId, retry.operations.map(stripInternalIndex), { expectedRevision, maxBatchOperations: MAX_SAFE_PATCH_CHUNK_SIZE });
391
+ session = applyEditOperations(repository.canonicalRoot, policy, session.sessionId, retry.operations.map(stripInternalIndex), { expectedRevision, maxBatchOperations: MAX_SAFE_PATCH_CHUNK_SIZE, binding: input.binding });
390
392
  } else {
391
393
  failures.push(...retry.failures);
392
394
  failedChunk = chunkIndex;
@@ -4,6 +4,7 @@ import { join, relative, resolve } from 'path';
4
4
  import { capProcessOutput, redactProcessOutput } from '../../effects/process-runner';
5
5
  import { buildPatchHandoffArtifact } from '../../runtime/recovery/patch-handoff';
6
6
  import { executeRepositoryGitCommand, type RepositoryGitExecution } from './git-command-executor';
7
+ import { withControllerLock } from './locks';
7
8
  import type { RepositoryRecord } from './types';
8
9
 
9
10
  const DEFAULT_MAX_OUTPUT_BYTES = 128 * 1024;
@@ -254,6 +255,11 @@ export function commitSelectedPaths(
254
255
  const message = String(input.message ?? '').trim();
255
256
  if (!message) throw new Error('COMMIT_MESSAGE_REQUIRED: message is required');
256
257
 
258
+ return withControllerLock(
259
+ controllerHome,
260
+ { scope: 'worktree', repoId: repository.repoId, worktreeId: repository.activeCheckoutId },
261
+ `selected-path-commit:${repository.activeCheckoutId}`,
262
+ () => {
257
263
  const stage = executeRepositoryGitCommand(controllerHome, repository, {
258
264
  args: ['add', '--all', '--', ...paths],
259
265
  authorization: 'explicit_user_request',
@@ -281,15 +287,17 @@ export function commitSelectedPaths(
281
287
  args: ['commit', '--only', '-m', message, '--', ...paths],
282
288
  authorization: 'explicit_user_request',
283
289
  });
284
- return {
285
- paths,
286
- stage,
287
- stagedPaths: staged,
288
- commit,
289
- ...(commit.status !== 'executed' || commit.ok !== true
290
- ? { error: { code: 'SELECTED_PATH_COMMIT_FAILED', message: commit.stderr || 'git commit failed for selected paths' } }
291
- : {}),
292
- };
290
+ return {
291
+ paths,
292
+ stage,
293
+ stagedPaths: staged,
294
+ commit,
295
+ ...(commit.status !== 'executed' || commit.ok !== true
296
+ ? { error: { code: 'SELECTED_PATH_COMMIT_FAILED', message: commit.stderr || 'git commit failed for selected paths' } }
297
+ : {}),
298
+ };
299
+ },
300
+ );
293
301
  }
294
302
 
295
303
  export function prepareTransferArtifacts(