@moretea-labs/forge 1.8.1 → 1.8.2

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 (328) hide show
  1. package/AGENTS.md +5 -2
  2. package/CHANGELOG.md +20 -0
  3. package/README.en.md +24 -3
  4. package/README.md +10 -6
  5. package/README.zh-CN.md +5 -3
  6. package/THIRD_PARTY_NOTICES.md +1 -2
  7. package/adapters/chatgpt/browser-delivery-runtime.ts +5 -3
  8. package/adapters/chatgpt/provider-delivery.ts +1 -1
  9. package/adapters/chatgpt/work-conversation-binding-store.ts +20 -1
  10. package/adapters/computer/chatgpt-extension-target.ts +295 -50
  11. package/adapters/computer/chrome-extension/background.js +227 -71
  12. package/adapters/computer/chrome-extension/content.js +181 -157
  13. package/adapters/computer/chrome-extension/manifest.json +3 -1
  14. package/adapters/computer/desktop-operator-provider.ts +18 -6
  15. package/adapters/computer/interaction-target-authority.ts +39 -9
  16. package/adapters/mcp/access-mode.ts +18 -39
  17. package/adapters/mcp/auth.ts +2 -6
  18. package/adapters/mcp/instructions.ts +2 -2
  19. package/adapters/mcp/multi-repository.ts +7 -28
  20. package/adapters/mcp/runtime-gateway/context-adapter.ts +79 -40
  21. package/adapters/mcp/runtime-gateway/context-pack-sidecar.ts +7 -15
  22. package/adapters/mcp/runtime-gateway/execution-tools.ts +7 -13
  23. package/adapters/mcp/runtime-gateway/plugin-adapter.ts +16 -3
  24. package/adapters/mcp/runtime-gateway/process-tools.ts +3 -21
  25. package/adapters/mcp/runtime-gateway/router.ts +3 -5
  26. package/adapters/mcp/runtime-gateway/routing-policy.ts +17 -10
  27. package/adapters/mcp/runtime-gateway/runtime-observation-adapter.ts +1 -1
  28. package/adapters/mcp/runtime-gateway/runtime-tool-definitions.ts +14 -9
  29. package/adapters/mcp/runtime-gateway/runtime-tools.ts +1 -5
  30. package/adapters/mcp/runtime-gateway/scheduler-adapter.ts +36 -1
  31. package/adapters/mcp/runtime-gateway/status-inbox-adapter.ts +58 -4
  32. package/adapters/mcp/runtime-gateway/typescript-navigation-sidecar.ts +7 -3
  33. package/adapters/mcp/runtime-gateway/work-adapter.ts +16 -16
  34. package/adapters/mcp/runtime-gateway/work-input-wire-migration.ts +0 -17
  35. package/adapters/mcp/runtime-gateway/work-semantic-operations.ts +69 -12
  36. package/adapters/mcp/runtime-gateway/workflow-supervisor-adapter.ts +26 -2
  37. package/adapters/mcp/server.ts +24 -3
  38. package/adapters/mcp/setup.ts +4 -10
  39. package/adapters/mcp/tool-mapping/access-tools.ts +0 -6
  40. package/adapters/mcp/tool-mapping/legacy-tool-service.ts +672 -2935
  41. package/adapters/mcp/tool-mapping/post-finalize-work-attribution.ts +1 -1
  42. package/adapters/mcp/tool-mapping/repository-tools.ts +52 -86
  43. package/adapters/mcp/toolset-names.ts +11 -42
  44. package/adapters/mcp/toolset.ts +16 -90
  45. package/{src/cli → adapters}/mcp/transports/AGENTS.md +5 -5
  46. package/{src/cli → adapters}/mcp/transports/CLAUDE.md +5 -5
  47. package/adapters/mcp/transports/http-observation.ts +18 -6
  48. package/adapters/mcp/transports/http-post-lifecycle.ts +19 -0
  49. package/adapters/mcp/transports/http.ts +54 -14
  50. package/adapters/mcp/types.ts +0 -6
  51. package/assets/hooks/lib/session-state.sh +0 -38
  52. package/assets/hooks/post-bash.sh +1 -1
  53. package/assets/hooks/post-tool-observer.sh +0 -5
  54. package/assets/hooks/prompt-guard.sh +0 -55
  55. package/assets/partials/04-project-structure.partial.md +1 -1
  56. package/assets/partials/07-footer.partial.md +1 -1
  57. package/assets/partials-agents/02-operating-mode.partial.md +1 -1
  58. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  59. package/assets/reference-configs/agentic-development-flow.md +1 -1
  60. package/assets/reference-configs/external-tooling.md +0 -76
  61. package/assets/reference-configs/release-deploy.md +1 -1
  62. package/assets/templates/helpers/architecture-queue.sh +1 -1
  63. package/assets/templates/helpers/check-agent-tooling.sh +1 -327
  64. package/assets/templates/helpers/check-task-workflow.sh +1 -1
  65. package/assets/templates/helpers/ensure-task-workflow.sh +0 -14
  66. package/assets/templates/helpers/migrate-project-template.sh +2 -2
  67. package/assets/workflow-contract.v1.json +0 -2
  68. package/docs/README.md +5 -2
  69. package/docs/architecture/CURRENT.md +114 -30
  70. package/docs/architecture/decisions/20260904-ios-device-migration-baseline.md +42 -0
  71. package/docs/architecture/decisions/20260918-stable-candidate-release-lifecycle.md +15 -2
  72. package/docs/architecture/decisions/20260924-thin-semantic-working-context.md +4 -2
  73. package/docs/architecture/decisions/20261001-supervisor-turn-authority.md +109 -4
  74. package/docs/architecture/decisions/20261009-exact-code-context-discovery.md +19 -0
  75. package/docs/architecture/decisions/20261009-generated-xcode-cache-retention.md +37 -0
  76. package/docs/architecture/decisions/20261011-autonomous-continuation-convergence.md +597 -0
  77. package/docs/architecture/decisions/20261017-edit-session-finalization-reconciliation.md +17 -0
  78. package/docs/architecture/decisions/20261019-retire-host-skills.md +16 -0
  79. package/docs/architecture/diagrams/agentic-dev-plugin-architecture.html +1 -1
  80. package/docs/architecture/domains/public-surface.md +3 -5
  81. package/docs/architecture/domains/verification.md +5 -5
  82. package/docs/architecture/modules/controller-runtime/transport-lifecycle.md +2 -2
  83. package/docs/architecture/requests/2026-09-04-computer-plugin-workflow-taxonomy.md +4 -4
  84. package/docs/architecture/reviews/20261015-code-search-runtime-acceptance.md +75 -0
  85. package/docs/forge-plugin-management.md +14 -2
  86. package/docs/operations/controller-browser-plugin.md +23 -0
  87. package/docs/operations/features.md +6 -7
  88. package/docs/operations/features.zh-CN.md +1 -1
  89. package/docs/operations/github-repository.md +4 -4
  90. package/docs/operations/github-semantic-release-projection.md +20 -0
  91. package/docs/operations/homebrew.md +2 -2
  92. package/docs/operations/mcp-tool-exposure.md +13 -54
  93. package/docs/operations/runtime-performance-diagnostics.md +8 -0
  94. package/docs/operations/standalone-disaster-recovery.md +1 -1
  95. package/docs/tutorials/01-install-and-start.zh-CN.md +1 -1
  96. package/docs/tutorials/03-first-repository-task.md +2 -0
  97. package/docs/wiki/Computer-and-Plugins.md +78 -0
  98. package/docs/wiki/Computer-and-Plugins.zh-CN.md +88 -0
  99. package/docs/wiki/Home.md +16 -2
  100. package/docs/wiki/Implementation.md +24 -12
  101. package/docs/wiki/Installation.md +28 -32
  102. package/docs/wiki/Integrations.md +4 -0
  103. package/docs/wiki/Quick-Start.md +35 -22
  104. package/docs/wiki/Releases-and-Upgrades.md +15 -25
  105. package/docs/wiki/_Sidebar.md +5 -1
  106. package/package.json +1 -12
  107. package/packages/kernel/controller/infrastructure/controller-session-store.ts +63 -38
  108. package/packages/kernel/identity/infrastructure/activity-projection-store.ts +10 -0
  109. package/packages/kernel/scheduler/application/eligibility.ts +4 -1
  110. package/packages/kernel/scheduler/infrastructure/schedule-store.ts +13 -10
  111. package/packages/kernel/work/application/work-service.ts +1 -0
  112. package/packages/kernel/work/domain/repository-completion-receipt.ts +1 -1
  113. package/packages/kernel/work/domain/state-machine.ts +9 -2
  114. package/packages/kernel/work/infrastructure/work-contract-store.ts +28 -1
  115. package/packages/plugin-runtime/browser/provider-registry.ts +12 -36
  116. package/packages/plugin-runtime/computer/conversation-target.ts +17 -7
  117. package/scripts/assemble-template.ts +11 -72
  118. package/scripts/check-mcp-compatibility.ts +55 -74
  119. package/scripts/check-package-identity.mjs +0 -5
  120. package/scripts/check-runtime-architecture.mjs +305 -28
  121. package/scripts/init-project.sh +0 -46
  122. package/scripts/install-ios-hid-toolchain.sh +60 -0
  123. package/scripts/install-standalone-recovery.ts +1 -1
  124. package/scripts/lib/project-init-lib.sh +3 -3
  125. package/scripts/migrate-windows-wsl-controller-home.ts +2 -0
  126. package/scripts/open-source-audit-allowlist.txt +0 -1
  127. package/scripts/run-governed-gate.ts +2 -0
  128. package/scripts/smoke-ios-physical-device.ts +38 -10
  129. package/scripts/smoke-mcp-http-runtime.ts +7 -8
  130. package/scripts/smoke-mcp-tool-surface.ts +27 -41
  131. package/scripts/smoke-workflow-supervisor-chrome-adapter.ts +6 -3
  132. package/scripts/sync-generated-authority.mjs +0 -1
  133. package/src/cli/agent-jobs/executor-health.ts +1 -1
  134. package/src/cli/agent-jobs/integration.ts +2 -2
  135. package/src/cli/chatgpt-browser/file-policy.ts +2 -2
  136. package/src/cli/commands/doctor.ts +1 -143
  137. package/src/cli/commands/global-runtime.ts +3 -196
  138. package/src/cli/commands/init-hook.ts +6 -23
  139. package/src/cli/commands/init.ts +3 -252
  140. package/src/cli/commands/mcp.ts +0 -5
  141. package/src/cli/commands/prompt-guard-decision.ts +0 -2
  142. package/src/cli/commands/recovery.ts +2 -14
  143. package/src/cli/commands/repository.ts +17 -1
  144. package/src/cli/commands/setup-execution.ts +41 -0
  145. package/src/cli/controller/completion-orchestrator.ts +1 -1
  146. package/src/cli/controller/composite-operations.ts +1 -1
  147. package/src/cli/controller/context/instruction-context.ts +1 -1
  148. package/src/cli/controller/context/known-paths.ts +2 -2
  149. package/src/cli/controller/context/query-planning.ts +11 -4
  150. package/src/cli/controller/context/rule-resolution.ts +2 -2
  151. package/src/cli/controller/context/source-materializer.ts +2 -2
  152. package/src/cli/controller/context/structural-provider.ts +84 -0
  153. package/src/cli/controller/context/structural.ts +6 -6
  154. package/src/cli/controller/context/types.ts +8 -9
  155. package/src/cli/controller/context-pack.ts +97 -76
  156. package/src/cli/controller/operational-plan.ts +1 -1
  157. package/src/cli/controller/systemd-user.ts +4 -10
  158. package/src/cli/editing/edit-session.ts +17 -5
  159. package/src/cli/github/github.ts +4 -4
  160. package/src/cli/github/semantic-projection.ts +174 -0
  161. package/src/cli/hook/prompt-intents.ts +0 -9
  162. package/src/cli/index.ts +1 -43
  163. package/src/cli/local-bridge/connector-freshness.ts +1 -12
  164. package/src/cli/local-bridge/console-automations.ts +1 -1
  165. package/src/cli/local-bridge/console-view-models.ts +1 -1
  166. package/src/cli/local-bridge/facade-api.ts +0 -2
  167. package/src/cli/repositories/command-classifier.ts +0 -9
  168. package/src/cli/repositories/command-scope.ts +2 -2
  169. package/src/cli/repositories/locks.ts +20 -0
  170. package/src/cli/repositories/registry.ts +2 -1
  171. package/src/cli/repositories/safe-patch.ts +2 -12
  172. package/src/cli/repositories/structured-git.ts +39 -3
  173. package/src/cli/repository/inspector.ts +185 -343
  174. package/src/effects/process-runner.ts +17 -1
  175. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  176. package/src/runtime/context/semantic-navigation-contract.ts +7 -0
  177. package/src/runtime/context/semantic-navigation.ts +1 -1
  178. package/src/runtime/control-plane/cleanup-artifact-retention.ts +4 -4
  179. package/src/runtime/control-plane/execution/work-delivery-service.ts +150 -31
  180. package/src/runtime/control-plane/execution/work-handle-authority.ts +12 -4
  181. package/src/runtime/control-plane/execution/work-handle-store.ts +53 -1
  182. package/src/runtime/control-plane/execution/work-operation-service.ts +20 -10
  183. package/src/runtime/control-plane/execution/work-preparation-service.ts +108 -4
  184. package/src/runtime/control-plane/execution/work-process-evidence.ts +2 -2
  185. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +28 -13
  186. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +5 -1
  187. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +1 -1
  188. package/src/runtime/control-plane/facade/capability-registry.ts +2 -0
  189. package/src/runtime/control-plane/facade/work-continuation.ts +1 -1
  190. package/src/runtime/control-plane/generated-cache-retention.ts +114 -63
  191. package/src/runtime/control-plane/global-scheduler/maintenance.ts +20 -8
  192. package/src/runtime/control-plane/global-scheduler/reconciliation.ts +1 -11
  193. package/src/runtime/control-plane/governance/external-effects.ts +1 -1
  194. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +1 -0
  195. package/src/runtime/control-plane/runtime-cleanup.ts +12 -93
  196. package/src/runtime/evidence/sensitive-output.ts +22 -2
  197. package/src/runtime/execution/jobs/child-reference.ts +11 -0
  198. package/src/runtime/execution/jobs/receipt-recovery.ts +1 -11
  199. package/src/runtime/execution/process-runtime/command-facade.ts +3 -3
  200. package/src/runtime/execution/process-runtime/runtime.ts +20 -16
  201. package/src/runtime/execution/thin-harness/execution-router.ts +16 -16
  202. package/src/runtime/execution/thin-harness/fast-executor.ts +2 -2
  203. package/src/runtime/execution/thin-harness/search-worker.ts +2 -2
  204. package/src/runtime/execution/workers/executor.ts +1 -2
  205. package/src/runtime/maintenance/cleanup.ts +41 -5
  206. package/src/runtime/plugins/action-application.ts +12 -1
  207. package/src/runtime/plugins/app-store-connect-adapter.ts +252 -4
  208. package/src/runtime/plugins/browser-adapter.ts +381 -313
  209. package/src/runtime/plugins/browser-macos-bridge.ts +114 -112
  210. package/src/runtime/plugins/browser-manifest-surface.ts +2 -1
  211. package/src/runtime/plugins/browser-registration.ts +2 -0
  212. package/src/runtime/plugins/browser-runtime.ts +26 -35
  213. package/src/runtime/plugins/computer-chatgpt-browser-adapter-installer.ts +11 -2
  214. package/src/runtime/plugins/computer-registration.ts +76 -9
  215. package/src/runtime/plugins/github-adapter.ts +30 -3
  216. package/src/runtime/plugins/gmail-adapter.ts +166 -1
  217. package/src/runtime/plugins/google-shared.ts +10 -5
  218. package/src/runtime/plugins/ios/remote-xpc-hid.ts +70 -30
  219. package/src/runtime/plugins/ios-adapter.ts +45 -40
  220. package/src/runtime/plugins/ios-agent-device.ts +54 -4
  221. package/src/runtime/plugins/ios-device-ownership.ts +103 -0
  222. package/src/runtime/plugins/ios-physical-device.ts +497 -79
  223. package/src/runtime/plugins/local-system-adapter.ts +1 -99
  224. package/src/runtime/plugins/store.ts +80 -7
  225. package/src/runtime/plugins/types.ts +4 -0
  226. package/src/runtime/projections/git-status-sampler.ts +2 -19
  227. package/src/runtime/recovery/actions.ts +1 -1
  228. package/src/runtime/recovery/lifecycle-inventory.ts +1 -11
  229. package/src/runtime/recovery/probe.ts +2 -2
  230. package/src/runtime/release/release-coordinator.ts +20 -4
  231. package/src/runtime/root/controller-progression-composition.ts +67 -5
  232. package/src/runtime/root/package-connector-service.ts +123 -11
  233. package/src/runtime/root/package-runtime-release.ts +6 -18
  234. package/src/runtime/root/package-runtime-service.ts +7 -13
  235. package/src/runtime/root/release-execution-canary.ts +18 -5
  236. package/src/runtime/root/release-manifest.ts +0 -34
  237. package/src/runtime/root/release-materialize.ts +0 -78
  238. package/src/runtime/root/runtime.ts +1 -2
  239. package/src/runtime/root/scheduled-controller-composition.ts +2 -1
  240. package/src/runtime/root/service.ts +90 -11
  241. package/src/runtime/root/types.ts +0 -6
  242. package/src/runtime/root/workflow-supervisor-composition.ts +207 -19
  243. package/src/runtime/root/workflow-supervisor-runtime.ts +365 -10
  244. package/src/runtime/safe-tooling/google-credential-store.ts +1 -11
  245. package/src/runtime/safe-tooling/ios-development.ts +22 -8
  246. package/src/runtime/safe-tooling/job-summary.ts +1 -1
  247. package/src/runtime/safe-tooling/workspace-auth.ts +4 -1
  248. package/src/runtime/shared/atomic-text-file.ts +21 -0
  249. package/src/runtime/shared/json-files.ts +8 -3
  250. package/src/runtime/standalone-recovery/core.ts +189 -174
  251. package/src/runtime/standalone-recovery/entry.ts +62 -30
  252. package/src/runtime/standalone-recovery/operation-lock.ts +11 -1
  253. package/src/runtime/workflow/schedules/chatgpt-automation-migration.ts +1 -0
  254. package/src/runtime/workflow/schedules/engine.ts +36 -14
  255. package/src/runtime/workflow/schedules/pending-activation.ts +11 -4
  256. package/src/runtime/workflow/schedules/work-continuation.ts +51 -6
  257. package/supervisor/client.ts +31 -14
  258. package/supervisor/control-plane.ts +210 -115
  259. package/supervisor/forge-validators.ts +88 -21
  260. package/supervisor/native-browser-adapter.ts +393 -28
  261. package/supervisor/native-messaging/host.ts +21 -2
  262. package/supervisor/protocol.ts +74 -26
  263. package/supervisor/server.ts +16 -8
  264. package/supervisor/store.ts +476 -57
  265. package/supervisor/types.ts +19 -13
  266. package/SKILL.md +0 -427
  267. package/adapters/computer/chatgpt-target-router.ts +0 -103
  268. package/adapters/mcp/runtime-gateway/automation-receipt-adapter.ts +0 -87
  269. package/adapters/mcp/runtime-gateway/work-validation-reconciler.ts +0 -1
  270. package/adapters/mcp/toolset-selection.ts +0 -41
  271. package/agents/openai.yaml +0 -4
  272. package/assets/skill-hooks.json +0 -41
  273. package/assets/skill-version.json +0 -189
  274. package/assets/skills/claude-review/SKILL.md +0 -231
  275. package/assets/skills/codex-review/SKILL.md +0 -103
  276. package/assets/templates/helpers/check-skill-version.ts +0 -193
  277. package/scripts/check-skill-version.ts +0 -5
  278. package/scripts/ensure-codegraph.sh +0 -16
  279. package/scripts/run-skill-hook.ts +0 -244
  280. package/scripts/sync-codex-installed-copies.sh +0 -182
  281. package/src/cli/commands/tools.ts +0 -167
  282. package/src/cli/mcp/access-mode.ts +0 -2
  283. package/src/cli/mcp/access-tools.ts +0 -2
  284. package/src/cli/mcp/audit.ts +0 -2
  285. package/src/cli/mcp/auth.ts +0 -2
  286. package/src/cli/mcp/instructions.ts +0 -2
  287. package/src/cli/mcp/legacy-context.ts +0 -2
  288. package/src/cli/mcp/legacy-tool-service.ts +0 -2
  289. package/src/cli/mcp/multi-repository.ts +0 -2
  290. package/src/cli/mcp/oauth.ts +0 -2
  291. package/src/cli/mcp/openai-secure-tunnel.ts +0 -2
  292. package/src/cli/mcp/paths.ts +0 -2
  293. package/src/cli/mcp/policy.ts +0 -2
  294. package/src/cli/mcp/post-finalize-work-attribution.ts +0 -2
  295. package/src/cli/mcp/proxy-env.ts +0 -2
  296. package/src/cli/mcp/readiness-projection.ts +0 -2
  297. package/src/cli/mcp/redaction.ts +0 -2
  298. package/src/cli/mcp/repo.ts +0 -2
  299. package/src/cli/mcp/repository-tools.ts +0 -2
  300. package/src/cli/mcp/server.ts +0 -2
  301. package/src/cli/mcp/setup.ts +0 -2
  302. package/src/cli/mcp/tool-capability-cache.ts +0 -2
  303. package/src/cli/mcp/tool-contract.ts +0 -2
  304. package/src/cli/mcp/tools.ts +0 -2
  305. package/src/cli/mcp/toolset-names.ts +0 -2
  306. package/src/cli/mcp/toolset-selection.ts +0 -2
  307. package/src/cli/mcp/toolset.ts +0 -2
  308. package/src/cli/mcp/transports/http.ts +0 -2
  309. package/src/cli/mcp/transports/session-registry.ts +0 -2
  310. package/src/cli/mcp/transports/stdio.ts +0 -2
  311. package/src/cli/mcp/types.ts +0 -2
  312. package/src/cli/mcp/umbrella-tools.ts +0 -2
  313. package/src/cli/tools/codegraph.ts +0 -695
  314. package/src/core/adoption/helper-wrapper-plan.ts +0 -159
  315. package/src/runtime/context/codegraph-cache-boundary.ts +0 -255
  316. package/src/runtime/context/codegraph-read-provider.ts +0 -611
  317. package/src/runtime/context/codegraph-sidecar.cjs +0 -248
  318. package/src/runtime/control-plane/codegraph-cache-retention.ts +0 -164
  319. package/src/runtime/gateway/mcp/execution-tools.ts +0 -2
  320. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +0 -2
  321. package/src/runtime/gateway/mcp/persisted-check-process.ts +0 -2
  322. package/src/runtime/gateway/mcp/process-tools.ts +0 -2
  323. package/src/runtime/gateway/mcp/router.ts +0 -2
  324. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +0 -2
  325. package/src/runtime/gateway/mcp/runtime-tools.ts +0 -2
  326. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +0 -2
  327. package/src/runtime/plugins/browser-provider-registry.ts +0 -2
  328. package/src/runtime/plugins/computer-chatgpt-macos-target.ts +0 -891
@@ -33,14 +33,7 @@ import {
33
33
  iosAgentDeviceStatus,
34
34
  isIosAgentDeviceAction,
35
35
  } from './ios-agent-device';
36
- import {
37
- executeIosPhysicalDeviceAction,
38
- iosPhysicalDeviceActions,
39
- iosPhysicalDeviceCapabilities,
40
- iosPhysicalDeviceStatus,
41
- isIosPhysicalDeviceAction,
42
- resolveIosPhysicalDeviceAuthorizationContext,
43
- } from './ios-physical-device';
36
+ import { iosPhysicalDeviceStatus, isIosPhysicalDeviceAction } from './ios-physical-device';
44
37
 
45
38
  const IOS_PLUGIN_ID = 'ios';
46
39
 
@@ -99,26 +92,36 @@ function health(repoRoot: string): AssistantPluginHealth {
99
92
  } as RepositoryRecord);
100
93
  const currentPlatform = iosDevelopmentPlatform();
101
94
  const platformOk = currentPlatform === 'darwin';
102
- const toolingReady = platformOk && Boolean(xcode && 'ready' in xcode && xcode.ready);
95
+ const developmentReady = platformOk && Boolean(xcode && 'ready' in xcode && xcode.ready);
96
+ const physicalDevice = iosPhysicalDeviceStatus();
97
+ const physicalReady = platformOk && physicalDevice.coreDeviceReady;
98
+ const agentDevice = iosAgentDeviceStatus({ repoRoot });
99
+ const semanticReady = agentDevice.available === true;
100
+ const pluginReady = platformOk && (developmentReady || physicalReady || semanticReady);
103
101
  const warnings: string[] = [];
104
- if (!platformOk) warnings.push('iOS plugin requires macOS with Xcode and Simulator.');
105
- if (platformOk && !xcodeSnapshot.probed) warnings.push('Xcode/simctl readiness has not been explicitly probed in this runtime yet.');
106
- else if (platformOk && !toolingReady) warnings.push('Xcode/simctl is not fully ready on this host.');
102
+ if (!platformOk) warnings.push('iOS capabilities require a macOS host.');
103
+ if (platformOk && !developmentReady) warnings.push('Xcode/Simulator development capability is not fully ready on this host.');
104
+ if (platformOk && !physicalReady) warnings.push('CoreDevice physical-iPhone capability is not ready on this host.');
107
105
  if (!discovery.ready) warnings.push('No .xcworkspace/.xcodeproj/Package.swift discovered in this repository yet.');
108
- const status = userFacingStatusFrom(toolingReady, discovery.ready, Boolean(xcode && 'simctlAvailable' in xcode && xcode.simctlAvailable));
106
+ const status = userFacingStatusFrom(developmentReady, discovery.ready, Boolean(xcode && 'simctlAvailable' in xcode && xcode.simctlAvailable), physicalReady);
109
107
  return {
110
- state: !platformOk ? 'disabled' : toolingReady ? 'ready' : 'degraded',
108
+ state: !platformOk ? 'disabled' : pluginReady ? 'ready' : 'degraded',
111
109
  checkedAt: now(),
112
- ready: toolingReady,
113
- probed: xcodeSnapshot.probed,
110
+ ready: pluginReady,
111
+ probed: xcodeSnapshot.probed || physicalDevice.probed || Boolean(agentDevice.available),
114
112
  errors: [],
115
113
  warnings,
116
114
  details: {
117
- provider: 'local-xcode',
115
+ provider: 'local-ios',
118
116
  platform: currentPlatform,
119
117
  userFacingStatus: status,
120
118
  discoverable: discovery.ready,
121
119
  defaultContainer: discovery.defaultContainer,
120
+ capabilities: {
121
+ development: developmentReady,
122
+ physicalDevice: physicalReady,
123
+ semanticAgentDevice: semanticReady,
124
+ },
122
125
  xcode: xcode && 'ready' in xcode ? {
123
126
  ready: xcode.ready,
124
127
  xcodeSelectPath: 'xcodeSelectPath' in xcode ? xcode.xcodeSelectPath : undefined,
@@ -127,8 +130,8 @@ function health(repoRoot: string): AssistantPluginHealth {
127
130
  probed: xcodeSnapshot.probed,
128
131
  cached: xcodeSnapshot.cached,
129
132
  } : xcode,
130
- agentDevice: iosAgentDeviceStatus({ repoRoot }),
131
- physicalDevice: iosPhysicalDeviceStatus(),
133
+ agentDevice,
134
+ physicalDevice,
132
135
  artifactRoots: {
133
136
  repoLocal: '.forge/ios/',
134
137
  controller: 'controller-home/repositories/<repoId>/artifacts/ios/',
@@ -137,21 +140,22 @@ function health(repoRoot: string): AssistantPluginHealth {
137
140
  };
138
141
  }
139
142
 
140
- function userFacingStatusFrom(toolingReady: boolean, discoverable: boolean, simReady: boolean): string {
141
- if (!toolingReady && !discoverable) return 'not_configured';
142
- if (discoverable && toolingReady && simReady) return 'buildable';
143
+ function userFacingStatusFrom(toolingReady: boolean, discoverable: boolean, simReady: boolean, physicalReady: boolean): string {
144
+ if (!toolingReady && !physicalReady && !discoverable) return 'not_configured';
145
+ if (toolingReady && discoverable && simReady) return 'buildable';
146
+ if (physicalReady && !toolingReady) return 'physical_device_ready';
143
147
  if (toolingReady && simReady) return 'simulator_ready';
144
148
  if (discoverable) return 'discoverable';
145
- if (toolingReady) return 'simulator_ready';
149
+ if (physicalReady) return 'physical_device_ready';
146
150
  return 'not_configured';
147
151
  }
148
152
 
149
- function permissions(ready: boolean): AssistantPluginPermissionScope[] {
153
+ function permissions(input: { developmentReady: boolean; physicalReady: boolean }): AssistantPluginPermissionScope[] {
150
154
  return [
151
- { scope: 'ios.discover', mode: 'read', description: 'Discover Xcode projects, schemes, and simulator inventory.', granted: ready, required: true },
152
- { scope: 'ios.build', mode: 'write', description: 'Build iOS app targets into bounded DerivedData.', granted: ready, required: true },
153
- { scope: 'ios.simulator', mode: 'write', description: 'Boot simulators, install, launch, screenshot, and collect logs.', granted: ready, required: true },
154
- { scope: 'ios.device', mode: 'write', description: 'Inspect and operate an exact paired physical iPhone through CoreDevice; optional XCTest semantics are isolated in the agent-device fallback.', granted: ready, required: false },
155
+ { scope: 'ios.discover', mode: 'read', description: 'Discover iOS projects and paired physical devices.', granted: input.developmentReady || input.physicalReady, required: true },
156
+ { scope: 'ios.build', mode: 'write', description: 'Build iOS app targets into bounded DerivedData.', granted: input.developmentReady, required: true },
157
+ { scope: 'ios.simulator', mode: 'write', description: 'Boot simulators, install, launch, screenshot, and collect logs.', granted: input.developmentReady, required: true },
158
+ { scope: 'ios.device', mode: 'write', description: 'Explicit paired-iPhone development/XCTest actions; everyday CoreDevice app interaction belongs to Computer.', granted: input.physicalReady, required: false },
155
159
  ];
156
160
  }
157
161
 
@@ -172,7 +176,6 @@ function capabilities(): AssistantPluginCapability[] {
172
176
  actions: ['smoke_review', 'build', 'xcode_test', 'launch_simulator', 'capture_screenshot'],
173
177
  },
174
178
  ...iosAgentDeviceCapabilities(),
175
- ...iosPhysicalDeviceCapabilities(),
176
179
  ];
177
180
  }
178
181
 
@@ -395,7 +398,6 @@ function actions(): AssistantPluginActionDescriptor[] {
395
398
  },
396
399
  },
397
400
  ...iosAgentDeviceActions(),
398
- ...iosPhysicalDeviceActions(),
399
401
  ];
400
402
  }
401
403
 
@@ -408,7 +410,7 @@ export function buildIosPluginManifest(previousRevision = 0, previousUpdatedAt?:
408
410
  revision: Math.max(1, previousRevision || 1),
409
411
  pluginId: IOS_PLUGIN_ID,
410
412
  provider: 'apple-local',
411
- displayName: 'iOS Development Plugin',
413
+ displayName: 'iOS Development / Simulator / XCTest',
412
414
  pluginVersion: '1.0.0',
413
415
  authority: {
414
416
  strategy: 'derived',
@@ -419,13 +421,16 @@ export function buildIosPluginManifest(previousRevision = 0, previousUpdatedAt?:
419
421
  lifecycle: {
420
422
  state: !enabled ? 'disabled' : state.ready ? 'enabled' : 'degraded',
421
423
  reason: !enabled
422
- ? 'iOS plugin is only available on macOS hosts.'
424
+ ? 'iOS capabilities are only available on macOS hosts.'
423
425
  : state.ready
424
- ? 'Xcode and Simulator tooling are ready.'
425
- : state.warnings[0] ?? 'iOS tooling is partially available.',
426
+ ? 'At least one iOS development or physical-device capability is ready.'
427
+ : state.warnings[0] ?? 'iOS capabilities are partially available.',
426
428
  },
427
429
  health: state,
428
- permissions: permissions(state.ready),
430
+ permissions: permissions({
431
+ developmentReady: Boolean(state.details?.capabilities && typeof state.details.capabilities === 'object' && (state.details.capabilities as Record<string, unknown>).development === true),
432
+ physicalReady: Boolean(state.details?.capabilities && typeof state.details.capabilities === 'object' && (state.details.capabilities as Record<string, unknown>).physicalDevice === true),
433
+ }),
429
434
  capabilities: capabilities(),
430
435
  actions: actions(),
431
436
  updatedAt: previousUpdatedAt ?? now(),
@@ -434,7 +439,9 @@ export function buildIosPluginManifest(previousRevision = 0, previousUpdatedAt?:
434
439
 
435
440
  export async function executeIosPluginAction(input: AssistantPluginActionExecutionInput): Promise<Record<string, unknown>> {
436
441
  if (isIosAgentDeviceAction(input.actionId)) return executeIosAgentDeviceAction(input);
437
- if (isIosPhysicalDeviceAction(input.actionId)) return executeIosPhysicalDeviceAction(input);
442
+ if (isIosPhysicalDeviceAction(input.actionId)) {
443
+ throw new AssistantPluginError('PLUGIN_ACTION_NOT_SUPPORTED', `Physical iPhone app actions are available only through computer/${input.actionId}.`, { retryable: false });
444
+ }
438
445
  const currentPlatform = iosDevelopmentPlatform();
439
446
  if (currentPlatform !== 'darwin' && input.actionId !== 'discover_project' && input.actionId !== 'xcode_status') {
440
447
  throw new AssistantPluginError('PLUGIN_DEPENDENCY_MISSING', 'iOS plugin actions require macOS with Xcode/Simulator.', {
@@ -590,9 +597,7 @@ export async function executeIosPluginAction(input: AssistantPluginActionExecuti
590
597
  export const iosPluginAdapter = {
591
598
  pluginId: IOS_PLUGIN_ID,
592
599
  buildManifest: buildIosPluginManifest,
593
- resolveAuthorizationContext: async (input: AssistantPluginActionExecutionInput) => {
594
- if (!isIosPhysicalDeviceAction(input.actionId)) return undefined;
595
- return resolveIosPhysicalDeviceAuthorizationContext(input);
596
- },
600
+ // CoreDevice actions are public only through the Computer manifest; Xcode and XCTest stay here.
601
+ resolveAuthorizationContext: async () => undefined,
597
602
  executeAction: executeIosPluginAction,
598
603
  };
@@ -8,6 +8,7 @@ import { runBoundedProcess } from '../execution/thin-harness/async-process';
8
8
  import { readJsonFile, writeJsonAtomic } from '../shared/json-files';
9
9
  import { resolveTrustedNodeExecutable } from '../shared/trusted-node-executable';
10
10
  import { AssistantPluginError, toAssistantPluginError } from './errors';
11
+ import { ensureIosDeviceOwnership, releaseIosDeviceOwnership, withIosDeviceTransientOwnership } from './ios-device-ownership';
11
12
  import {
12
13
  interactionAutomationEngine,
13
14
  interactionMayOwnTarget,
@@ -734,6 +735,15 @@ function providerForDevice(device: AgentDeviceEntry): InteractionProvider {
734
735
  return device.kind === 'simulator' ? SIMULATOR_PROVIDER : DEVICE_PROVIDER;
735
736
  }
736
737
 
738
+ async function runPhysicalDeviceCommand<T>(
739
+ input: AssistantPluginActionExecutionInput,
740
+ device: AgentDeviceEntry,
741
+ operation: () => Promise<T>,
742
+ ): Promise<T> {
743
+ if (device.kind !== 'device') return operation();
744
+ return withIosDeviceTransientOwnership(input.controllerHome, input.repoId, input.requestId, device.id, operation);
745
+ }
746
+
737
747
  function selectTarget(input: AssistantPluginActionExecutionInput, selector?: string): AgentDeviceEntry {
738
748
  const inventory = devices(input);
739
749
  const normalizedSelector = selector?.trim().toLocaleLowerCase('en-US');
@@ -835,12 +845,26 @@ function requireRecord(input: AssistantPluginActionExecutionInput, allowTerminal
835
845
  status: 'closing',
836
846
  error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: 'The expired agent-device session could not be closed; ownership remains fenced.' },
837
847
  });
848
+ if (record.provider === DEVICE_PROVIDER && closed) releaseIosDeviceOwnership(input.controllerHome, record.owner.repoId, interactionId, record.targetId);
838
849
  throw new AssistantPluginError(
839
850
  closed ? 'AGENT_DEVICE_SESSION_EXPIRED' : 'AGENT_DEVICE_CLEANUP_FAILED',
840
851
  closed ? 'The agent-device session expired and was closed.' : 'The expired agent-device session could not be closed; retry agent_device_close.',
841
852
  { retryable: !closed },
842
853
  );
843
854
  }
855
+ if (record.provider === DEVICE_PROVIDER) {
856
+ const ownership = ensureIosDeviceOwnership(input.controllerHome, input.repoId, interactionId, record.targetId);
857
+ if (!ownership.acquired) {
858
+ throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iPhone is already owned by another active Forge interaction.', {
859
+ retryable: true,
860
+ details: { interactionId, targetId: record.targetId, blocker: ownership.blocker },
861
+ });
862
+ }
863
+ const active = patchInteractionSession(input.repoRoot, record.provider, interactionId, {
864
+ expiresAt: new Date(hooks.now().getTime() + SESSION_EXPIRY_MS).toISOString(),
865
+ });
866
+ if (active) return active;
867
+ }
844
868
  return record;
845
869
  }
846
870
 
@@ -915,6 +939,7 @@ function reconcileExpiredSessions(input: AssistantPluginActionExecutionInput): v
915
939
  status: 'closing',
916
940
  error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: 'The expired agent-device session could not be closed; ownership remains fenced.' },
917
941
  });
942
+ if (record.provider === DEVICE_PROVIDER && closed) releaseIosDeviceOwnership(input.controllerHome, record.owner.repoId, record.interactionId, record.targetId);
918
943
  }
919
944
  }
920
945
 
@@ -934,6 +959,7 @@ function failSession(input: AssistantPluginActionExecutionInput, record: Interac
934
959
  status: 'closing',
935
960
  error: { code: 'AGENT_DEVICE_CLEANUP_FAILED', message: `${normalized.message}; cleanup failed and ownership remains fenced.` },
936
961
  });
962
+ if (closed && record.provider === DEVICE_PROVIDER) releaseIosDeviceOwnership(input.controllerHome, record.owner.repoId, record.interactionId, record.targetId);
937
963
  if (!closed) {
938
964
  throw new AssistantPluginError('AGENT_DEVICE_CLEANUP_FAILED', 'The agent-device action failed and its session could not be closed; retry agent_device_close.', {
939
965
  retryable: true,
@@ -2258,12 +2284,12 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
2258
2284
  contractFingerprint: dependency.capabilityProfile.contractFingerprint,
2259
2285
  device: selected,
2260
2286
  physicalDeviceSupported: selected.kind === 'device',
2261
- result: bounded(await runJsonAsync(input, args, {
2287
+ result: bounded(await runPhysicalDeviceCommand(input, selected, () => runJsonAsync(input, args, {
2262
2288
  signing: signingFromArgs(input.args),
2263
2289
  stateDir: targetRuntimeStateDir(input, selected),
2264
2290
  failureCode: 'AGENT_DEVICE_DOCTOR_FAILED',
2265
2291
  timeoutMs: 4 * 60_000,
2266
- })),
2292
+ }))),
2267
2293
  };
2268
2294
  }
2269
2295
 
@@ -2276,14 +2302,14 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
2276
2302
  contractFingerprint: dependency.capabilityProfile.contractFingerprint,
2277
2303
  device: selected,
2278
2304
  physicalDeviceSupported: selected.kind === 'device',
2279
- result: bounded(await runJsonAsync(input, [
2305
+ result: bounded(await runPhysicalDeviceCommand(input, selected, () => runJsonAsync(input, [
2280
2306
  'prepare', 'ios-runner', '--platform', 'ios', '--device', selected.name, '--timeout', '600000', '--json',
2281
2307
  ], {
2282
2308
  signing,
2283
2309
  stateDir: targetRuntimeStateDir(input, selected),
2284
2310
  failureCode: 'AGENT_DEVICE_PREPARE_FAILED',
2285
2311
  timeoutMs: 10 * 60_000,
2286
- })),
2312
+ }))),
2287
2313
  };
2288
2314
  }
2289
2315
 
@@ -2298,6 +2324,19 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
2298
2324
  const conflict = listInteractionSessions(input.repoRoot, provider).find((entry) =>
2299
2325
  isInteractionSessionActive(entry.status) && interactionMayOwnTarget(entry, selected.aliases));
2300
2326
  if (conflict) {
2327
+ if (selected.kind === 'device' && provider === DEVICE_PROVIDER && conflict.instructions === app && input.args.relaunch !== true) {
2328
+ const ownership = ensureIosDeviceOwnership(input.controllerHome, input.repoId, conflict.interactionId, selected.id);
2329
+ if (!ownership.acquired) {
2330
+ throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iPhone is already owned by another active Forge interaction.', {
2331
+ retryable: true,
2332
+ details: { interactionId: conflict.interactionId, targetId: conflict.targetId, blocker: ownership.blocker },
2333
+ });
2334
+ }
2335
+ const active = patchInteractionSession(input.repoRoot, provider, conflict.interactionId, {
2336
+ expiresAt: new Date(hooks.now().getTime() + SESSION_EXPIRY_MS).toISOString(),
2337
+ }) ?? conflict;
2338
+ return { provider: 'agent-device', version: dependency.detectedVersion ?? IOS_AGENT_DEVICE_VERSION, contractFingerprint: dependency.capabilityProfile.contractFingerprint, interaction: active, device: selected, physicalDeviceSupported: true, reusedSession: true };
2339
+ }
2301
2340
  throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iOS target already has an active interaction.', {
2302
2341
  retryable: true,
2303
2342
  details: {
@@ -2310,6 +2349,15 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
2310
2349
  }
2311
2350
  pruneInteractionSessions(input.repoRoot, provider, 100);
2312
2351
  const interactionId = `ios_agent_device_${randomUUID()}`;
2352
+ if (selected.kind === 'device') {
2353
+ const ownership = ensureIosDeviceOwnership(input.controllerHome, input.repoId, interactionId, selected.id);
2354
+ if (!ownership.acquired) {
2355
+ throw new AssistantPluginError('PLUGIN_RESOURCE_BUSY', 'The selected iPhone is already owned by another active Forge interaction.', {
2356
+ retryable: true,
2357
+ details: { interactionId, targetId: selected.id, blocker: ownership.blocker },
2358
+ });
2359
+ }
2360
+ }
2313
2361
  const createdAt = timestamp();
2314
2362
  const record: InteractionSessionRecord = {
2315
2363
  schemaVersion: 1,
@@ -2348,6 +2396,7 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
2348
2396
  result: bounded(result),
2349
2397
  };
2350
2398
  } catch (error) {
2399
+ if (selected.kind === 'device') releaseIosDeviceOwnership(input.controllerHome, input.repoId, interactionId, selected.id);
2351
2400
  return failSession(input, record, error);
2352
2401
  }
2353
2402
  }
@@ -2464,6 +2513,7 @@ export async function executeIosAgentDeviceAction(input: AssistantPluginActionEx
2464
2513
  try {
2465
2514
  const closedProvider = await closeProviderSession(input, current, args);
2466
2515
  const closed = patchInteractionSession(input.repoRoot, current.provider, current.interactionId, { status: 'closed' }) ?? current;
2516
+ if (current.provider === DEVICE_PROVIDER) releaseIosDeviceOwnership(input.controllerHome, current.owner.repoId, current.interactionId, current.targetId);
2467
2517
  return {
2468
2518
  provider: 'agent-device',
2469
2519
  interaction: closed,
@@ -0,0 +1,103 @@
1
+ import {
2
+ acquireControllerLock,
3
+ ControllerLockContentionError,
4
+ readControllerLock,
5
+ releaseControllerLock,
6
+ renewControllerLock,
7
+ type ControllerLockRecord,
8
+ } from '../../cli/repositories/locks';
9
+
10
+ const IOS_DEVICE_LOCK_PREFIX = 'ios-device:';
11
+ const IOS_DEVICE_LOCK_TTL_MS = 2 * 60 * 60_000;
12
+
13
+ export interface IosDeviceOwnershipResult {
14
+ acquired: boolean;
15
+ lock?: ControllerLockRecord;
16
+ blocker?: Pick<ControllerLockRecord, 'lockId' | 'owner' | 'pid' | 'acquiredAt' | 'heartbeatAt' | 'expiresAt'>;
17
+ }
18
+
19
+ function deviceResourceKey(deviceIdentifier: string): string {
20
+ const normalized = deviceIdentifier.trim().toUpperCase();
21
+ if (!normalized) throw new Error('IOS_DEVICE_IDENTIFIER_REQUIRED');
22
+ return `${IOS_DEVICE_LOCK_PREFIX}${normalized}`;
23
+ }
24
+
25
+ function sessionOwner(repoId: string, interactionId: string): string {
26
+ const repo = repoId.trim();
27
+ const interaction = interactionId.trim();
28
+ if (!repo || !interaction) throw new Error('IOS_DEVICE_OWNERSHIP_ID_REQUIRED');
29
+ return `ios-device-session:${repo}:${interaction}`;
30
+ }
31
+
32
+ function blocker(lock: ControllerLockRecord) {
33
+ return {
34
+ lockId: lock.lockId,
35
+ owner: lock.owner,
36
+ pid: lock.pid,
37
+ acquiredAt: lock.acquiredAt,
38
+ heartbeatAt: lock.heartbeatAt,
39
+ ...(lock.expiresAt ? { expiresAt: lock.expiresAt } : {}),
40
+ } satisfies IosDeviceOwnershipResult['blocker'];
41
+ }
42
+
43
+ // Use the hardware UDID as the single physical iPhone identity shared with XCTest.
44
+ // CoreDevice identifier is a transport-local locator, not the agent-device device id.
45
+ export function iosCoreDeviceOwnershipId(device: { identifier: string; udid?: string }): string {
46
+ const udid = device.udid?.trim();
47
+ if (!udid) throw new Error('IOS_DEVICE_UDID_REQUIRED');
48
+ return udid;
49
+ }
50
+
51
+ export function ensureIosDeviceOwnership(
52
+ controllerHome: string,
53
+ repoId: string,
54
+ interactionId: string,
55
+ deviceIdentifier: string,
56
+ ): IosDeviceOwnershipResult {
57
+ const key = { scope: 'global' as const, resource: deviceResourceKey(deviceIdentifier) };
58
+ const owner = sessionOwner(repoId, interactionId);
59
+ const current = readControllerLock(controllerHome, key);
60
+ if (current) {
61
+ if (current.owner !== owner) return { acquired: false, blocker: blocker(current) };
62
+ return { acquired: true, lock: renewControllerLock(controllerHome, key, current.lockId, IOS_DEVICE_LOCK_TTL_MS) };
63
+ }
64
+ try {
65
+ return { acquired: true, lock: acquireControllerLock(controllerHome, key, owner, IOS_DEVICE_LOCK_TTL_MS) };
66
+ } catch (error) {
67
+ if (!(error instanceof ControllerLockContentionError)) throw error;
68
+ const raced = readControllerLock(controllerHome, key);
69
+ if (!raced) return { acquired: false };
70
+ if (raced.owner !== owner) return { acquired: false, blocker: blocker(raced) };
71
+ return { acquired: true, lock: renewControllerLock(controllerHome, key, raced.lockId, IOS_DEVICE_LOCK_TTL_MS) };
72
+ }
73
+ }
74
+
75
+ export function releaseIosDeviceOwnership(
76
+ controllerHome: string,
77
+ repoId: string,
78
+ interactionId: string,
79
+ deviceIdentifier: string,
80
+ ): boolean {
81
+ const key = { scope: 'global' as const, resource: deviceResourceKey(deviceIdentifier) };
82
+ const current = readControllerLock(controllerHome, key);
83
+ if (!current || current.owner !== sessionOwner(repoId, interactionId)) return false;
84
+ releaseControllerLock(controllerHome, key, current.lockId);
85
+ return true;
86
+ }
87
+
88
+ export async function withIosDeviceTransientOwnership<T>(
89
+ controllerHome: string,
90
+ repoId: string,
91
+ requestId: string,
92
+ deviceIdentifier: string,
93
+ operation: () => T | Promise<T>,
94
+ ): Promise<T> {
95
+ const key = { scope: 'global' as const, resource: deviceResourceKey(deviceIdentifier) };
96
+ const owner = `ios-device-transient:${repoId}:${requestId}`;
97
+ const lock = acquireControllerLock(controllerHome, key, owner, undefined);
98
+ try {
99
+ return await operation();
100
+ } finally {
101
+ releaseControllerLock(controllerHome, key, lock.lockId);
102
+ }
103
+ }