@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
@@ -1,4 +1,5 @@
1
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'fs';
1
+ import { createHash } from 'crypto';
2
+ import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'fs';
2
3
  import { basename, dirname, join, relative, resolve, sep } from 'path';
3
4
  import { spawnSync } from 'child_process';
4
5
  import type { RepositoryRecord } from '../../cli/repositories/types';
@@ -19,32 +20,40 @@ export interface IosDevelopmentHooks {
19
20
  }
20
21
 
21
22
  let hooks: IosDevelopmentHooks = {};
23
+ let artifactSequence = 0;
22
24
 
23
25
  /** Host Xcode/simctl probes are expensive (spawnSync). Cache them for hot MCP reads. */
24
26
  const XCODE_STATUS_TTL_MS = 60_000;
25
27
  const XCODE_STATUS_DEGRADED_TTL_MS = 10_000;
26
28
  const PROJECT_DISCOVER_TTL_MS = 30_000;
29
+ const SCHEME_LIST_TTL_MS = 30_000;
27
30
  let xcodeStatusCache:
28
31
  | { expiresAt: number; value: ReturnType<typeof probeIosXcodeStatus> }
29
32
  | undefined;
30
33
  const projectDiscoverCache = new Map<string, { expiresAt: number; value: ReturnType<typeof probeIosProjectDiscover> }>();
34
+ const schemeListCache = new Map<string, { expiresAt: number; value: ReturnType<typeof probeIosSchemesList> }>();
31
35
 
32
36
  export function setIosDevelopmentHooksForTest(next: IosDevelopmentHooks): void {
33
37
  hooks = next;
38
+ artifactSequence = 0;
34
39
  xcodeStatusCache = undefined;
35
40
  projectDiscoverCache.clear();
41
+ schemeListCache.clear();
36
42
  }
37
43
 
38
44
  export function resetIosDevelopmentHooksForTest(): void {
39
45
  hooks = {};
46
+ artifactSequence = 0;
40
47
  xcodeStatusCache = undefined;
41
48
  projectDiscoverCache.clear();
49
+ schemeListCache.clear();
42
50
  }
43
51
 
44
52
  /** Test helper: drop cached Xcode probe results without clearing hooks. */
45
53
  export function clearIosXcodeStatusCacheForTest(): void {
46
54
  xcodeStatusCache = undefined;
47
55
  projectDiscoverCache.clear();
56
+ schemeListCache.clear();
48
57
  }
49
58
 
50
59
  export function iosDevelopmentPlatform(): NodeJS.Platform {
@@ -113,7 +122,7 @@ function safeRepoRelativePath(repoRoot: string, value: string | undefined): stri
113
122
 
114
123
  function safeArtifactDir(
115
124
  repository: RepositoryRecord,
116
- leaf: 'screenshots' | 'logs' | 'build-reports' | 'DerivedData',
125
+ leaf: 'screenshots' | 'logs' | 'build-reports' | 'test-results' | 'DerivedData',
117
126
  overrideRoot?: string,
118
127
  ): string {
119
128
  const root = overrideRoot
@@ -135,10 +144,24 @@ function timestamp(): string {
135
144
  return now().toISOString().replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
136
145
  }
137
146
 
147
+ function artifactStamp(): string {
148
+ artifactSequence = (artifactSequence + 1) % Number.MAX_SAFE_INTEGER;
149
+ return `${timestamp()}-${process.pid}-${artifactSequence}`;
150
+ }
151
+
138
152
  function sanitize(value: string): string {
139
153
  return value.replace(/[^a-zA-Z0-9._-]+/g, '-').replace(/^-+|-+$/g, '') || 'ios';
140
154
  }
141
155
 
156
+ function stableBuildDerivedDataPath(
157
+ repository: RepositoryRecord,
158
+ input: { workspace?: string; project?: string; scheme: string; configuration: string; destination: string },
159
+ ): string {
160
+ const identity = JSON.stringify([input.workspace ?? '', input.project ?? '', input.scheme, input.configuration, input.destination]);
161
+ const digest = createHash('sha256').update(identity).digest('hex').slice(0, 16);
162
+ return join(safeArtifactDir(repository, 'DerivedData'), `build-${sanitize(input.scheme)}-${digest}`);
163
+ }
164
+
142
165
  function boundedText(value: string, maxBytes = 20_000): { content: string; truncated: boolean } {
143
166
  const buffer = Buffer.from(value, 'utf-8');
144
167
  if (buffer.byteLength <= maxBytes) return { content: value, truncated: false };
@@ -172,6 +195,28 @@ function probeIosXcodeStatus() {
172
195
  };
173
196
  }
174
197
 
198
+ export function iosXcodeStatusSnapshot() {
199
+ const unsupported = assertDarwin();
200
+ if (unsupported) return { status: unsupported, probed: true, cached: false };
201
+ const currentMs = currentTimeMs();
202
+ if (xcodeStatusCache && xcodeStatusCache.expiresAt > currentMs) {
203
+ return { status: xcodeStatusCache.value, probed: true, cached: true };
204
+ }
205
+ return {
206
+ status: {
207
+ ready: false,
208
+ platform: platform(),
209
+ simctlAvailable: false,
210
+ problems: [{
211
+ code: 'XCODE_STATUS_NOT_PROBED',
212
+ message: 'Xcode readiness has not been probed asynchronously or explicitly in this runtime yet.',
213
+ }],
214
+ },
215
+ probed: false,
216
+ cached: false,
217
+ };
218
+ }
219
+
175
220
  export function iosXcodeStatus(options: { forceRefresh?: boolean } = {}) {
176
221
  const currentMs = currentTimeMs();
177
222
  if (!options.forceRefresh && xcodeStatusCache && xcodeStatusCache.expiresAt > currentMs) {
@@ -205,15 +250,16 @@ export function iosSimulatorsList(input: { runtime?: string; name?: string } = {
205
250
  return { ready: true, devices };
206
251
  }
207
252
 
208
- function findFiles(root: string, suffix: string, maxDepth = 3): string[] {
253
+ function findFiles(root: string, suffix: string, maxDepth = 6): string[] {
209
254
  const results: string[] = [];
255
+ const skippedDirectories = new Set(['node_modules', '.git', '.forge', '.ai', 'DerivedData', 'Pods', '.build', 'build', 'dist', '.cache', 'Carthage']);
210
256
  function visit(dir: string, depth: number) {
211
257
  if (depth > maxDepth || results.length > 50) return;
212
258
  let entries;
213
259
  try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return; }
214
260
  for (const entry of entries) {
215
261
  const entryName = String(entry.name);
216
- if (entryName === 'node_modules' || entryName === '.git' || entryName === '.forge' || entryName === '.ai') continue;
262
+ if (skippedDirectories.has(entryName)) continue;
217
263
  const path = join(dir, entryName);
218
264
  if (entry.isDirectory() && entryName.endsWith(suffix)) results.push(path);
219
265
  else if (entry.isDirectory()) visit(path, depth + 1);
@@ -253,12 +299,11 @@ export function iosProjectDiscover(repository: RepositoryRecord, options: { forc
253
299
  return value;
254
300
  }
255
301
 
256
- export function iosSchemesList(repository: RepositoryRecord, input: { workspace?: string; project?: string } = {}) {
302
+ function probeIosSchemesList(repository: RepositoryRecord, input: { workspace?: string; project?: string } = {}) {
257
303
  const unsupported = assertDarwin();
258
304
  if (unsupported) return unsupported;
259
- const discovered = iosProjectDiscover(repository);
260
- const workspace = safeRepoRelativePath(repository.canonicalRoot, input.workspace ?? discovered.workspace);
261
- const project = safeRepoRelativePath(repository.canonicalRoot, input.project ?? discovered.project);
305
+ const workspace = safeRepoRelativePath(repository.canonicalRoot, input.workspace);
306
+ const project = safeRepoRelativePath(repository.canonicalRoot, input.project);
262
307
  const args = workspace
263
308
  ? ['-list', '-json', '-workspace', workspace]
264
309
  : project
@@ -274,6 +319,21 @@ export function iosSchemesList(repository: RepositoryRecord, input: { workspace?
274
319
  }
275
320
  }
276
321
 
322
+ export function iosSchemesList(repository: RepositoryRecord, input: { workspace?: string; project?: string } = {}) {
323
+ const unsupported = assertDarwin();
324
+ if (unsupported) return unsupported;
325
+ const discovered = iosProjectDiscover(repository);
326
+ const workspace = safeRepoRelativePath(repository.canonicalRoot, input.workspace ?? discovered.workspace);
327
+ const project = safeRepoRelativePath(repository.canonicalRoot, input.project ?? discovered.project);
328
+ const cacheKey = JSON.stringify([resolve(repository.canonicalRoot), workspace ?? '', project ?? '']);
329
+ const currentMs = currentTimeMs();
330
+ const cached = schemeListCache.get(cacheKey);
331
+ if (cached && cached.expiresAt > currentMs) return cached.value;
332
+ const value = probeIosSchemesList(repository, { workspace, project });
333
+ if (value.ready) schemeListCache.set(cacheKey, { expiresAt: currentMs + SCHEME_LIST_TTL_MS, value });
334
+ return value;
335
+ }
336
+
277
337
  export function iosSimulatorBoot(input: { udid: string; openSimulator?: boolean; timeoutMs?: number }) {
278
338
  const unsupported = assertDarwin();
279
339
  if (unsupported) return unsupported;
@@ -281,15 +341,23 @@ export function iosSimulatorBoot(input: { udid: string; openSimulator?: boolean;
281
341
  if (!udid) throw new Error('IOS_SIMULATOR_UDID_REQUIRED');
282
342
  const timeoutMs = input.timeoutMs ?? 60_000;
283
343
  const boot = runCommand('xcrun', ['simctl', 'boot', udid], { timeoutMs });
284
- const alreadyBooted = /Unable to boot|current state: Booted|already booted/i.test(boot.stderr + boot.stdout);
344
+ const alreadyBooted = /current state:\s*Booted|already booted/i.test(boot.stderr + boot.stdout);
285
345
  if (!boot.ok && !alreadyBooted) {
286
- return { ready: false, booted: false, error: { code: 'SIMULATOR_BOOT_FAILED', message: boot.stderr || boot.stdout }, command: boot.command };
346
+ return {
347
+ ready: false,
348
+ booted: false,
349
+ udid,
350
+ alreadyBooted: false,
351
+ error: { code: 'SIMULATOR_BOOT_FAILED', message: boot.stderr || boot.stdout },
352
+ command: boot.command,
353
+ };
287
354
  }
288
355
  const readiness = runCommand('xcrun', ['simctl', 'bootstatus', udid, '-b'], { timeoutMs });
289
356
  if (!readiness.ok) {
290
357
  return {
291
358
  ready: false,
292
- booted: true,
359
+ booted: false,
360
+ bootRequested: boot.ok || alreadyBooted,
293
361
  udid,
294
362
  alreadyBooted,
295
363
  ownership: alreadyBooted ? 'reused' : 'started_by_work',
@@ -316,7 +384,7 @@ export function iosSimulatorShutdown(input: { udid: string; timeoutMs?: number }
316
384
  const udid = String(input.udid ?? '').trim();
317
385
  if (!udid) throw new Error('IOS_SIMULATOR_UDID_REQUIRED');
318
386
  const result = runCommand('xcrun', ['simctl', 'shutdown', udid], { timeoutMs: input.timeoutMs ?? 60_000 });
319
- const alreadyShutdown = /current state: Shutdown|already shutdown|Unable to shutdown/i.test(result.stderr + result.stdout);
387
+ const alreadyShutdown = /current state:\s*Shutdown|already shutdown/i.test(result.stderr + result.stdout);
320
388
  return {
321
389
  ready: result.ok || alreadyShutdown,
322
390
  shutdown: result.ok || alreadyShutdown,
@@ -327,7 +395,20 @@ export function iosSimulatorShutdown(input: { udid: string; timeoutMs?: number }
327
395
  };
328
396
  }
329
397
 
330
- export function iosAppBuild(repository: RepositoryRecord, input: { scheme?: string; udid?: string; workspace?: string; project?: string; configuration?: string; timeoutMs?: number; simulatorName?: string }) {
398
+ export function iosAppBuild(repository: RepositoryRecord, input: {
399
+ scheme?: string;
400
+ udid?: string;
401
+ workspace?: string;
402
+ project?: string;
403
+ configuration?: string;
404
+ timeoutMs?: number;
405
+ simulatorName?: string;
406
+ destinationPlatform?: 'ios_simulator' | 'ios';
407
+ provisioning?: {
408
+ allowUpdates: boolean;
409
+ authentication?: { privateKeyPath: string; keyId: string; issuerId: string };
410
+ };
411
+ }) {
331
412
  const unsupported = assertDarwin();
332
413
  if (unsupported) return unsupported;
333
414
  const listed = iosSchemesList(repository, { workspace: input.workspace, project: input.project });
@@ -337,34 +418,136 @@ export function iosAppBuild(repository: RepositoryRecord, input: { scheme?: stri
337
418
  if (!scheme) throw new Error('IOS_SCHEME_REQUIRED: provide scheme or share at least one Xcode scheme');
338
419
  const workspace = safeRepoRelativePath(repository.canonicalRoot, input.workspace ?? listedReady.workspace);
339
420
  const project = safeRepoRelativePath(repository.canonicalRoot, input.project ?? listedReady.project);
340
- const derivedDataRoot = safeArtifactDir(repository, 'DerivedData');
341
- const derivedDataPath = join(derivedDataRoot, `${timestamp()}-${sanitize(scheme)}`);
421
+ const configuration = input.configuration ?? 'Debug';
422
+ const destinationPlatform = input.destinationPlatform ?? 'ios_simulator';
423
+ const udid = String(input.udid ?? '').trim();
424
+ if (destinationPlatform === 'ios' && !udid) throw new Error('IOS_PHYSICAL_DEVICE_UDID_REQUIRED');
425
+ const destination = destinationPlatform === 'ios'
426
+ ? `platform=iOS,id=${udid}`
427
+ : udid
428
+ ? `platform=iOS Simulator,id=${udid}`
429
+ : `platform=iOS Simulator,name=${input.simulatorName ?? 'iPhone 16 Pro'}`;
430
+ const derivedDataPath = stableBuildDerivedDataPath(repository, { workspace, project, scheme, configuration, destination });
431
+ const derivedDataReused = existsSync(derivedDataPath);
342
432
  mkdirSync(derivedDataPath, { recursive: true });
433
+ const priorBuiltApps = derivedDataReused ? findBuiltApps(repository, derivedDataPath) : [];
343
434
  const args = ['build'];
344
435
  if (workspace) args.push('-workspace', workspace);
345
436
  else if (project) args.push('-project', project);
346
- args.push('-scheme', scheme, '-configuration', input.configuration ?? 'Debug');
347
- if (input.udid) args.push('-destination', `platform=iOS Simulator,id=${String(input.udid).trim()}`);
348
- else args.push('-destination', 'platform=iOS Simulator,name=' + (input.simulatorName ?? 'iPhone 16 Pro'));
437
+ args.push('-scheme', scheme, '-configuration', configuration, '-destination', destination);
349
438
  args.push('-derivedDataPath', derivedDataPath);
439
+ const authentication = input.provisioning?.authentication;
440
+ if (input.provisioning?.allowUpdates) args.push('-allowProvisioningUpdates');
441
+ if (authentication) {
442
+ args.push(
443
+ '-authenticationKeyPath', authentication.privateKeyPath,
444
+ '-authenticationKeyID', authentication.keyId,
445
+ '-authenticationKeyIssuerID', authentication.issuerId,
446
+ );
447
+ }
350
448
  const result = runCommand('xcodebuild', args, { cwd: repository.canonicalRoot, timeoutMs: input.timeoutMs ?? 10 * 60_000 });
449
+ const sensitiveValues = authentication
450
+ ? [authentication.privateKeyPath, authentication.keyId, authentication.issuerId]
451
+ : [];
452
+ const redactSensitiveText = (value: string): string => sensitiveValues.reduce(
453
+ (current, secret) => secret ? current.split(secret).join('[REDACTED]') : current,
454
+ value,
455
+ );
456
+ const redactBoundedText = (value: string) => {
457
+ const bounded = boundedText(value);
458
+ return { ...bounded, content: redactSensitiveText(bounded.content) };
459
+ };
460
+ const sensitiveFlags = new Set(['-authenticationKeyPath', '-authenticationKeyID', '-authenticationKeyIssuerID']);
461
+ const safeCommand = result.command.map((entry, index, command) => (
462
+ index > 0 && sensitiveFlags.has(command[index - 1] ?? '') ? '[REDACTED]' : entry
463
+ ));
464
+ const safeStdout = redactBoundedText(result.stdout);
465
+ const safeStderr = redactBoundedText(result.stderr);
351
466
  const builtApps = findBuiltApps(repository, derivedDataPath);
352
- const selected = selectBuiltAppProduct(scheme, builtApps);
467
+ const priorSet = new Set(priorBuiltApps);
468
+ const newBuiltApps = builtApps.filter((candidate) => !priorSet.has(candidate));
469
+ const selected = selectBuiltAppProduct(scheme, newBuiltApps.length > 0 ? newBuiltApps : builtApps);
353
470
  const productError = result.ok ? selected.error : undefined;
354
471
  return {
355
472
  ready: result.ok && Boolean(selected.appPath),
356
473
  ok: result.ok && Boolean(selected.appPath),
357
474
  scheme,
358
- configuration: input.configuration ?? 'Debug',
475
+ configuration,
476
+ destination,
359
477
  derivedDataPath: relative(repository.canonicalRoot, derivedDataPath),
478
+ derivedDataReused,
360
479
  appPath: selected.appPath,
361
480
  builtApps,
362
- command: result.command,
363
- stdout: boundedText(result.stdout),
364
- stderr: boundedText(result.stderr),
481
+ newBuiltApps,
482
+ command: safeCommand,
483
+ stdout: safeStdout,
484
+ stderr: safeStderr,
365
485
  error: result.ok
366
486
  ? productError
367
- : { code: 'IOS_BUILD_FAILED', message: (result.stderr || result.stdout).slice(0, 2000) },
487
+ : { code: 'IOS_BUILD_FAILED', message: redactSensitiveText(result.stderr || result.stdout).slice(0, 2000) },
488
+ };
489
+ }
490
+
491
+ export function iosXcodeTest(repository: RepositoryRecord, input: {
492
+ scheme?: string;
493
+ udid?: string;
494
+ simulatorName?: string;
495
+ workspace?: string;
496
+ project?: string;
497
+ configuration?: string;
498
+ onlyTesting?: string[];
499
+ parallelTesting?: boolean;
500
+ maxParallelTestingWorkers?: number;
501
+ timeoutMs?: number;
502
+ }) {
503
+ const unsupported = assertDarwin();
504
+ if (unsupported) return unsupported;
505
+ const listed = iosSchemesList(repository, { workspace: input.workspace, project: input.project });
506
+ if (!listed.ready) return listed;
507
+ const listedReady = listed as { ready: true; workspace?: string; project?: string; schemes: string[] };
508
+ const scheme = String(input.scheme ?? listedReady.schemes?.[0] ?? '').trim();
509
+ if (!scheme) throw new Error('IOS_SCHEME_REQUIRED: provide scheme or share at least one Xcode scheme');
510
+ const workspace = safeRepoRelativePath(repository.canonicalRoot, input.workspace ?? listedReady.workspace);
511
+ const project = safeRepoRelativePath(repository.canonicalRoot, input.project ?? listedReady.project);
512
+ const derivedDataPath = join(safeArtifactDir(repository, 'DerivedData'), `tests-${sanitize(scheme)}`);
513
+ const resultBundlePath = join(safeArtifactDir(repository, 'test-results'), `${timestamp()}-${sanitize(scheme)}.xcresult`);
514
+ mkdirSync(derivedDataPath, { recursive: true });
515
+ const args = ['test'];
516
+ if (workspace) args.push('-workspace', workspace);
517
+ else if (project) args.push('-project', project);
518
+ args.push('-scheme', scheme, '-configuration', input.configuration ?? 'Debug');
519
+ if (input.udid) args.push('-destination', `platform=iOS Simulator,id=${String(input.udid).trim()}`);
520
+ else args.push('-destination', 'platform=iOS Simulator,name=' + (input.simulatorName ?? 'iPhone 16 Pro'));
521
+ const parallelTesting = input.parallelTesting === true;
522
+ const maxParallelTestingWorkers = parallelTesting
523
+ ? Math.max(1, Math.min(8, Math.trunc(input.maxParallelTestingWorkers ?? 2)))
524
+ : 1;
525
+ args.push(
526
+ '-parallel-testing-enabled', parallelTesting ? 'YES' : 'NO',
527
+ '-maximum-parallel-testing-workers', String(maxParallelTestingWorkers),
528
+ );
529
+ for (const selector of (input.onlyTesting ?? []).slice(0, 50)) {
530
+ const normalized = String(selector).trim();
531
+ if (normalized) args.push(`-only-testing:${normalized}`);
532
+ }
533
+ args.push('-derivedDataPath', derivedDataPath, '-resultBundlePath', resultBundlePath);
534
+ const result = runCommand('xcodebuild', args, { cwd: repository.canonicalRoot, timeoutMs: input.timeoutMs ?? 15 * 60_000 });
535
+ return {
536
+ ready: result.ok,
537
+ ok: result.ok,
538
+ scheme,
539
+ configuration: input.configuration ?? 'Debug',
540
+ onlyTesting: (input.onlyTesting ?? []).map((entry) => String(entry).trim()).filter(Boolean).slice(0, 50),
541
+ parallelTesting,
542
+ maxParallelTestingWorkers,
543
+ derivedDataPath: artifactRelativePath(repository, derivedDataPath),
544
+ resultBundlePath: artifactRelativePath(repository, resultBundlePath),
545
+ command: result.command,
546
+ stdout: boundedText(result.stdout, 64 * 1024),
547
+ stderr: boundedText(result.stderr, 32 * 1024),
548
+ error: result.ok
549
+ ? undefined
550
+ : { code: 'IOS_TEST_FAILED', message: (result.stderr || result.stdout).slice(0, 4000) },
368
551
  };
369
552
  }
370
553
 
@@ -398,7 +581,7 @@ function selectBuiltAppProduct(scheme: string, builtApps: string[]): {
398
581
  return !/(?:tests?|uitests?|xctrunner|runner|testhost)\.app$/i.test(name);
399
582
  });
400
583
  if (productionCandidates.length === 0) {
401
- return { error: { code: 'IOS_BUILD_PRODUCT_MISSING', message: 'Build succeeded but no application product was found in this build-specific DerivedData directory.' } };
584
+ return { error: { code: 'IOS_BUILD_PRODUCT_MISSING', message: 'Build succeeded but no application product was found in this DerivedData cache bucket.' } };
402
585
  }
403
586
  const normalizedScheme = normalizedProductName(scheme);
404
587
  const exact = productionCandidates.filter((candidate) => normalizedProductName(basename(candidate)) === normalizedScheme);
@@ -413,46 +596,96 @@ function selectBuiltAppProduct(scheme: string, builtApps: string[]): {
413
596
  };
414
597
  }
415
598
 
416
- function readBuiltAppBundleId(repository: RepositoryRecord, appPath: string): string | undefined {
417
- const appRoot = assertRepoBoundedAppPath(repository, appPath);
599
+ function readBuiltAppPlistValue(repository: RepositoryRecord, appPath: string, key: string, artifactRoot?: string): string | undefined {
600
+ const appRoot = assertBoundedAppPath(repository, appPath, artifactRoot);
418
601
  const plist = join(appRoot, 'Info.plist');
419
602
  if (!existsSync(plist)) return undefined;
420
- const plutil = runCommand('plutil', ['-extract', 'CFBundleIdentifier', 'raw', '-o', '-', plist]);
603
+ const plutil = runCommand('plutil', ['-extract', key, 'raw', '-o', '-', plist]);
421
604
  return plutil.ok && plutil.stdout.trim() ? plutil.stdout.trim() : undefined;
422
605
  }
423
606
 
607
+ function readBuiltAppBundleId(repository: RepositoryRecord, appPath: string, artifactRoot?: string): string | undefined {
608
+ return readBuiltAppPlistValue(repository, appPath, 'CFBundleIdentifier', artifactRoot);
609
+ }
610
+
611
+ function readBuiltAppExecutable(repository: RepositoryRecord, appPath: string, artifactRoot?: string): string | undefined {
612
+ return readBuiltAppPlistValue(repository, appPath, 'CFBundleExecutable', artifactRoot);
613
+ }
614
+
615
+ function compareSimulatorCandidates(
616
+ left: { runtime: string; name: string; state: string; udid: string },
617
+ right: { runtime: string; name: string; state: string; udid: string },
618
+ ): number {
619
+ const leftBooted = left.state === 'Booted' ? 1 : 0;
620
+ const rightBooted = right.state === 'Booted' ? 1 : 0;
621
+ if (leftBooted !== rightBooted) return rightBooted - leftBooted;
622
+ const runtimeOrder = right.runtime.localeCompare(left.runtime, undefined, { numeric: true, sensitivity: 'base' });
623
+ if (runtimeOrder !== 0) return runtimeOrder;
624
+ return left.name.localeCompare(right.name, undefined, { numeric: true, sensitivity: 'base' });
625
+ }
626
+
424
627
  function chooseSimulatorUdid(input: { udid?: string; simulatorName?: string }): string {
425
628
  const requested = String(input.udid ?? '').trim();
426
629
  if (requested) return requested;
427
- const preferred = iosSimulatorsList({ name: input.simulatorName ?? 'iPhone 16 Pro' });
428
- if (preferred.ready) {
429
- const preferredReady = preferred as { ready: true; devices: Array<{ udid: string }> };
430
- if (preferredReady.devices[0]?.udid) return preferredReady.devices[0].udid;
431
- }
432
- const fallback = iosSimulatorsList();
433
- if (fallback.ready) {
434
- const fallbackReady = fallback as { ready: true; devices: Array<{ udid: string }> };
435
- if (fallbackReady.devices[0]?.udid) return fallbackReady.devices[0].udid;
630
+ const inventory = iosSimulatorsList();
631
+ if (!inventory.ready) throw new Error('IOS_SIMULATOR_UNAVAILABLE: simulator inventory could not be read');
632
+ const devices = (inventory as { ready: true; devices: Array<{ runtime: string; name: string; state: string; udid: string }> }).devices
633
+ .filter((device) => /SimRuntime\.iOS-/i.test(device.runtime));
634
+ const requestedName = String(input.simulatorName ?? '').trim();
635
+ const matches = requestedName
636
+ ? devices.filter((device) => device.name.includes(requestedName)).sort(compareSimulatorCandidates)
637
+ : [];
638
+ if (matches[0]?.udid) return matches[0].udid;
639
+ if (!requestedName) {
640
+ const booted = devices.filter((device) => device.state === 'Booted').sort(compareSimulatorCandidates);
641
+ if (booted[0]?.udid) return booted[0].udid;
642
+ const preferred = devices.filter((device) => device.name.includes('iPhone 16 Pro')).sort(compareSimulatorCandidates);
643
+ if (preferred[0]?.udid) return preferred[0].udid;
436
644
  }
645
+ const fallback = [...devices].sort(compareSimulatorCandidates);
646
+ if (fallback[0]?.udid) return fallback[0].udid;
437
647
  throw new Error('IOS_SIMULATOR_UNAVAILABLE: no available simulator was found');
438
648
  }
439
649
 
440
- function assertRepoBoundedAppPath(repository: RepositoryRecord, appPath: string): string {
441
- const resolved = resolve(repository.canonicalRoot, appPath);
442
- const allowedRoot = resolve(repository.canonicalRoot, '.forge', 'ios', 'DerivedData');
443
- const rel = relative(allowedRoot, resolved);
444
- if (rel === '' || rel === '..' || rel.startsWith(`..${sep}`)) throw new Error('IOS_APP_PATH_NOT_BOUNDED: app_path must be under .forge/ios/DerivedData');
650
+ function pathIsWithin(root: string, target: string): boolean {
651
+ const canonicalRoot = existsSync(root) ? realpathSync(root) : resolve(root);
652
+ const canonicalTarget = existsSync(target) ? realpathSync(target) : resolve(target);
653
+ const rel = relative(canonicalRoot, canonicalTarget);
654
+ return Boolean(rel) && rel !== '..' && !rel.startsWith(`..${sep}`);
655
+ }
656
+
657
+ function assertBoundedAppPath(repository: RepositoryRecord, appPath: string, artifactRoot?: string): string {
658
+ const controllerPrefix = 'controller-artifacts/ios/';
659
+ const resolved = artifactRoot && appPath.startsWith(controllerPrefix)
660
+ ? resolve(artifactRoot, appPath.slice(controllerPrefix.length))
661
+ : resolve(repository.canonicalRoot, appPath);
445
662
  if (!resolved.endsWith('.app')) throw new Error('IOS_APP_PATH_INVALID: app_path must end with .app');
663
+ const derivedDataRoot = resolve(repository.canonicalRoot, '.forge', 'ios', 'DerivedData');
664
+ const controllerArtifactRoot = artifactRoot ? resolve(artifactRoot) : undefined;
665
+ if (!pathIsWithin(derivedDataRoot, resolved) && !(controllerArtifactRoot && pathIsWithin(controllerArtifactRoot, resolved))) {
666
+ throw new Error(
667
+ controllerArtifactRoot
668
+ ? 'IOS_APP_PATH_NOT_BOUNDED: app_path must be under .forge/ios/DerivedData or the Controller iOS artifact root'
669
+ : 'IOS_APP_PATH_NOT_BOUNDED: app_path must be under .forge/ios/DerivedData',
670
+ );
671
+ }
446
672
  return resolved;
447
673
  }
448
674
 
449
- export function iosAppInstall(repository: RepositoryRecord, input: { udid: string; appPath: string }) {
675
+ export function iosAppInstall(repository: RepositoryRecord, input: { udid: string; appPath: string; artifactRoot?: string }) {
450
676
  const unsupported = assertDarwin();
451
677
  if (unsupported) return unsupported;
452
678
  const udid = String(input.udid ?? '').trim();
453
- const appPath = assertRepoBoundedAppPath(repository, String(input.appPath ?? ''));
679
+ const appPath = assertBoundedAppPath(repository, String(input.appPath ?? ''), input.artifactRoot);
454
680
  const result = runCommand('xcrun', ['simctl', 'install', udid, appPath], { timeoutMs: 120_000 });
455
- return { ready: result.ok, installed: result.ok, udid, appPath: relative(repository.canonicalRoot, appPath), error: result.ok ? undefined : { code: 'IOS_INSTALL_FAILED', message: result.stderr || result.stdout } };
681
+ return {
682
+ ready: result.ok,
683
+ installed: result.ok,
684
+ udid,
685
+ appPath: artifactRelativePath(repository, appPath, input.artifactRoot),
686
+ absolutePath: appPath,
687
+ error: result.ok ? undefined : { code: 'IOS_INSTALL_FAILED', message: result.stderr || result.stdout },
688
+ };
456
689
  }
457
690
 
458
691
  export function iosAppLaunch(input: { udid: string; bundleId: string; arguments?: string[]; waitMs?: number }) {
@@ -465,14 +698,32 @@ export function iosAppLaunch(input: { udid: string; bundleId: string; arguments?
465
698
  const result = runCommand('xcrun', ['simctl', 'launch', udid, bundleId, ...launchArgs], { timeoutMs: 60_000 });
466
699
  const waitMs = Math.max(0, Math.min(Math.trunc(input.waitMs ?? 1_500), 15_000));
467
700
  if (result.ok) sleep(waitMs);
701
+ const pidMatch = result.ok ? result.stdout.match(/:\s*(\d+)\s*$/m) : undefined;
702
+ const pid = pidMatch?.[1] ? Number(pidMatch[1]) : undefined;
703
+ const processCheck = result.ok && Number.isFinite(pid)
704
+ ? runCommand('xcrun', ['simctl', 'spawn', udid, 'launchctl', 'procinfo', String(pid)], { timeoutMs: 10_000 })
705
+ : undefined;
706
+ const processCheckText = processCheck ? `${processCheck.stdout}\n${processCheck.stderr}` : '';
707
+ const alive = processCheck
708
+ ? processCheck.ok && !/No such process|could not resolve path/i.test(processCheckText)
709
+ : undefined;
710
+ const ready = result.ok && alive !== false;
468
711
  return {
469
- ready: result.ok,
712
+ ready,
470
713
  launched: result.ok,
714
+ alive,
715
+ pid,
471
716
  udid,
472
717
  bundleId,
473
718
  waitMs,
474
719
  stdout: result.stdout.trim(),
475
- error: result.ok ? undefined : { code: 'IOS_LAUNCH_FAILED', message: result.stderr || result.stdout },
720
+ command: result.command,
721
+ processCheckCommand: processCheck?.command,
722
+ error: !result.ok
723
+ ? { code: 'IOS_LAUNCH_FAILED', message: result.stderr || result.stdout }
724
+ : alive === false
725
+ ? { code: 'IOS_LAUNCH_PROCESS_EXITED', message: processCheck?.stderr || processCheck?.stdout || `Process ${pid} exited during post-launch verification.` }
726
+ : undefined,
476
727
  };
477
728
  }
478
729
 
@@ -482,7 +733,7 @@ export function iosSimulatorScreenshot(repository: RepositoryRecord, input: { ud
482
733
  const udid = String(input.udid ?? '').trim();
483
734
  if (!udid) throw new Error('IOS_SIMULATOR_UDID_REQUIRED');
484
735
  const dir = safeArtifactDir(repository, 'screenshots', input.artifactRoot);
485
- const file = join(dir, `${timestamp()}-${sanitize(input.label ?? udid)}.png`);
736
+ const file = join(dir, `${artifactStamp()}-${sanitize(input.label ?? udid)}.png`);
486
737
  const result = runCommand('xcrun', ['simctl', 'io', udid, 'screenshot', file], { timeoutMs: 60_000 });
487
738
  return {
488
739
  ready: result.ok,
@@ -514,23 +765,27 @@ export function iosSimulatorLogTail(repository: RepositoryRecord, input: { udid:
514
765
  ['show', '--style', 'compact', '--last', last, '--predicate', predicate],
515
766
  { timeoutMs: 30_000 },
516
767
  );
517
- const result = hostResult?.ok ? hostResult : simulatorResult;
518
- const text = boundedText(result.stdout || result.stderr, input.maxBytes ?? 20_000);
768
+ const diagnosticResult = simulatorResult.ok ? simulatorResult : hostResult?.ok ? hostResult : simulatorResult;
769
+ const text = boundedText(diagnosticResult.stdout || diagnosticResult.stderr, input.maxBytes ?? 20_000);
519
770
  const dir = safeArtifactDir(repository, 'logs', input.artifactRoot);
520
- const file = join(dir, `${timestamp()}-${sanitize(input.process ?? udid)}.log`);
771
+ const file = join(dir, `${artifactStamp()}-${sanitize(input.process ?? udid)}.log`);
521
772
  writeFileSync(file, text.content, 'utf-8');
522
773
  return {
523
- ready: result.ok,
774
+ ready: simulatorResult.ok,
524
775
  path: artifactRelativePath(repository, file, input.artifactRoot),
525
776
  absolutePath: file,
526
777
  content: text.content,
527
778
  truncated: text.truncated,
528
- command: result.command,
779
+ command: simulatorResult.command,
529
780
  attempts: [simulatorResult.command, ...(hostResult ? [hostResult.command] : [])],
530
- source: hostResult?.ok ? 'host_unified_log_fallback' : 'simulator_unified_log',
531
- error: result.ok ? undefined : {
781
+ source: simulatorResult.ok ? 'simulator_unified_log' : hostResult?.ok ? 'host_unified_log_diagnostic_fallback' : 'simulator_unified_log',
782
+ targetProcess: input.process,
783
+ error: simulatorResult.ok ? undefined : {
532
784
  code: 'IOS_LOG_TAIL_FAILED',
533
- message: [simulatorResult.stderr || simulatorResult.stdout, hostResult?.stderr || hostResult?.stdout]
785
+ message: [
786
+ simulatorResult.stderr || simulatorResult.stdout,
787
+ hostResult?.ok ? 'Host unified log fallback was captured for diagnostics only.' : hostResult?.stderr || hostResult?.stdout,
788
+ ]
534
789
  .filter(Boolean)
535
790
  .join('\n--- fallback ---\n'),
536
791
  },
@@ -860,7 +1115,7 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
860
1115
  }));
861
1116
 
862
1117
  // 5. install
863
- const install = iosAppInstall(repository, { udid, appPath });
1118
+ const install = iosAppInstall(repository, { udid, appPath, artifactRoot: input.artifactRoot });
864
1119
  if (!install.ready) {
865
1120
  overallStatus = 'failed';
866
1121
  blockedStage = 'install';
@@ -883,7 +1138,7 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
883
1138
  }));
884
1139
 
885
1140
  // 6. launch
886
- const bundleId = String(input.bundleId ?? readBuiltAppBundleId(repository, appPath) ?? '').trim();
1141
+ const bundleId = String(input.bundleId ?? readBuiltAppBundleId(repository, appPath, input.artifactRoot) ?? '').trim();
887
1142
  if (!bundleId) {
888
1143
  overallStatus = 'failed';
889
1144
  blockedStage = 'launch';
@@ -901,6 +1156,7 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
901
1156
  simulatorOwnership, cleanupPolicy, simulatorCleanup,
902
1157
  };
903
1158
  }
1159
+ const appExecutable = readBuiltAppExecutable(repository, appPath, input.artifactRoot);
904
1160
  const launch = iosAppLaunch({ udid, bundleId, waitMs: input.launchWaitMs });
905
1161
  if (!launch.ready) {
906
1162
  overallStatus = 'failed';
@@ -946,8 +1202,7 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
946
1202
  'Ensure the simulator is booted and simctl io screenshot works for the selected udid.',
947
1203
  { command: screenshot.command, evidence: screenshot as unknown as Record<string, unknown> },
948
1204
  ));
949
- failRemaining(7, 'Skipped because screenshot failed.');
950
- // still attempt logs for diagnosis
1205
+ // Continue to logs for diagnosis; do not add a skipped logs stage before the real attempt.
951
1206
  } else {
952
1207
  stages.push(stagePassed('screenshot', `Captured screenshot ${screenshot.screenshot}.`, {
953
1208
  command: screenshot.command,
@@ -959,7 +1214,7 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
959
1214
  // 8. logs
960
1215
  const logs = iosSimulatorLogTail(repository, {
961
1216
  udid,
962
- process: selectedScheme,
1217
+ process: appExecutable,
963
1218
  maxBytes: 12_000,
964
1219
  artifactRoot: input.artifactRoot,
965
1220
  }) as {
@@ -969,6 +1224,8 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
969
1224
  truncated?: boolean;
970
1225
  command?: string[];
971
1226
  error?: { message?: string };
1227
+ source?: string;
1228
+ targetProcess?: string;
972
1229
  };
973
1230
  if (Array.isArray(logs.command)) commands.push(logs.command);
974
1231
  if (logs.path) artifacts.push(logs.path);
@@ -981,12 +1238,12 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
981
1238
  'logs',
982
1239
  logs.error?.message ?? 'Log collection failed',
983
1240
  'Simulator may still be booting; retry log_tail after a few seconds.',
984
- { command: logs.command, evidence: { path: logs.path, truncated: logs.truncated }, artifacts: logs.path ? [logs.path] : undefined },
1241
+ { command: logs.command, evidence: { path: logs.path, truncated: logs.truncated, process: appExecutable, source: logs.source }, artifacts: logs.path ? [logs.path] : undefined },
985
1242
  ));
986
1243
  } else {
987
1244
  stages.push(stagePassed('logs', `Collected logs at ${logs.path}.`, {
988
1245
  command: logs.command,
989
- evidence: { path: logs.path, truncated: logs.truncated, preview: logs.content?.slice(0, 500) },
1246
+ evidence: { path: logs.path, truncated: logs.truncated, process: appExecutable, source: logs.source, preview: logs.content?.slice(0, 500) },
990
1247
  artifacts: logs.path ? [logs.path] : undefined,
991
1248
  }));
992
1249
  }
@@ -1005,6 +1262,7 @@ export function iosSmokeReview(repository: RepositoryRecord, input: IosSmokeRevi
1005
1262
  schemes: schemesList,
1006
1263
  scheme: selectedScheme,
1007
1264
  bundleId,
1265
+ appExecutable,
1008
1266
  udid,
1009
1267
  build: buildResult,
1010
1268
  boot,