@moretea-labs/forge 1.7.0 → 1.7.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 (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +26 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +125 -101
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +69 -29
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +150 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +815 -1015
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +3 -2
  166. package/src/runtime/root/package-runtime-service.ts +48 -3
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -248,13 +248,23 @@ function desktopSession(status: Record<string, unknown>, interactionId: string):
248
248
  return recordArray(status.sessions).find((session) => firstString(session, 'interactionId', 'interaction_id') === interactionId);
249
249
  }
250
250
 
251
- function desktopApplicationIsActive(status: Record<string, unknown>, bundleId: string, appName: string): boolean {
252
- return recordArray(status.applications).some((application) => {
253
- if (application.active !== true || application.terminated === true) return false;
254
- const candidateBundleId = firstString(application, 'bundle_id', 'bundleIdentifier');
255
- const candidateName = firstString(application, 'name', 'appName');
256
- return bundleId ? candidateBundleId === bundleId : Boolean(appName) && candidateName === appName;
257
- });
251
+ async function independentlyEnsureExactFrontmostDesktopApplication(
252
+ bundleId: string,
253
+ appName: string,
254
+ dependencies: ExternalPluginAdapterDependencies,
255
+ ): Promise<void> {
256
+ const activateAndVerify = dependencies.activateAndVerifyFrontmostApplication ?? activateAndVerifyFrontmostApplication;
257
+ const proof = await activateAndVerify({ bundleId: bundleId || undefined, appName: appName || undefined });
258
+ const proofBundleId = typeof proof.bundleId === 'string' ? proof.bundleId.trim() : '';
259
+ const proofAppName = typeof proof.appName === 'string' ? proof.appName.trim() : '';
260
+ const proofMatches = bundleId ? proofBundleId === bundleId : proofAppName === appName;
261
+ if (proof.activated !== true || proof.verified !== true || !proofMatches) {
262
+ throw new AssistantPluginError(
263
+ 'DESKTOP_ACTIVATION_FALLBACK_IDENTITY_MISMATCH',
264
+ `Forge did not prove the exact requested application ${bundleId || appName} as frontmost.`,
265
+ { retryable: true, details: { target: { bundleId, appName }, observed: { bundleId: proofBundleId, appName: proofAppName } } },
266
+ );
267
+ }
258
268
  }
259
269
 
260
270
  async function openVerifiedDesktopSession(
@@ -272,7 +282,7 @@ async function openVerifiedDesktopSession(
272
282
  }
273
283
 
274
284
  let result: Record<string, unknown>;
275
- let fallbackVerified = false;
285
+ let providerActivationError: AssistantPluginError | undefined;
276
286
  try {
277
287
  result = await callProvider(
278
288
  registration,
@@ -285,7 +295,7 @@ async function openVerifiedDesktopSession(
285
295
  );
286
296
  } catch (error) {
287
297
  if (!(error instanceof AssistantPluginError) || error.code !== 'APP_ACTIVATION_FAILED') throw error;
288
-
298
+ providerActivationError = error;
289
299
  result = await callProvider(
290
300
  registration,
291
301
  `${requestId}:fallback-session`,
@@ -295,59 +305,36 @@ async function openVerifiedDesktopSession(
295
305
  signal,
296
306
  dependencies,
297
307
  );
298
-
299
- const activateFallback = dependencies.activateAndVerifyFrontmostApplication ?? activateAndVerifyFrontmostApplication;
300
- try {
301
- const fallbackProof = await activateFallback({ bundleId: bundleId || undefined, appName: appName || undefined });
302
- const proofBundleId = typeof fallbackProof.bundleId === 'string' ? fallbackProof.bundleId.trim() : '';
303
- const proofAppName = typeof fallbackProof.appName === 'string' ? fallbackProof.appName.trim() : '';
304
- const proofMatches = bundleId ? proofBundleId === bundleId : proofAppName === appName;
305
- if (fallbackProof.activated !== true || fallbackProof.verified !== true || !proofMatches) {
306
- throw new AssistantPluginError(
307
- 'DESKTOP_ACTIVATION_FALLBACK_IDENTITY_MISMATCH',
308
- `Forge fallback did not prove the exact requested application ${bundleId || appName} as frontmost.`,
309
- { retryable: true, details: { target: { bundleId, appName }, observed: { bundleId: proofBundleId, appName: proofAppName } } },
310
- );
311
- }
312
- fallbackVerified = true;
313
- } catch (fallbackError) {
314
- if (fallbackError instanceof AssistantPluginError) {
315
- throw new AssistantPluginError(
316
- 'DESKTOP_ACTIVATION_FALLBACK_FAILED',
317
- `Desktop Operator activation failed and Forge could not independently verify ${bundleId || appName} as frontmost.`,
318
- {
319
- retryable: fallbackError.retryable,
320
- details: {
321
- providerErrorCode: error.code,
322
- fallbackErrorCode: fallbackError.code,
323
- fallbackErrorMessage: fallbackError.message,
324
- target: bundleId || appName,
325
- },
326
- },
327
- );
328
- }
329
- throw new AssistantPluginError(
330
- 'DESKTOP_ACTIVATION_FALLBACK_FAILED',
331
- `Desktop Operator activation failed and Forge fallback failed for ${bundleId || appName}.`,
332
- { retryable: true, details: { providerErrorCode: error.code, fallbackErrorMessage: String(fallbackError), target: bundleId || appName } },
333
- );
334
- }
335
308
  }
336
309
 
337
- if (!fallbackVerified) {
338
- const activeStatus = await callProvider(
339
- registration,
340
- `${requestId}:active-status`,
341
- 'desktop_status',
342
- { limit: 500 },
343
- timeoutMs,
344
- signal,
345
- dependencies,
346
- );
347
- if (!desktopApplicationIsActive(activeStatus, bundleId, appName)) {
348
- throw providerError('DESKTOP_ACTIVATION_NOT_CONFIRMED', `Desktop Operator did not confirm ${bundleId || appName} as the frontmost application after activation.`, true);
310
+ try {
311
+ await independentlyEnsureExactFrontmostDesktopApplication(bundleId, appName, dependencies);
312
+ } catch (verificationError) {
313
+ const wrappedCode = providerActivationError ? 'DESKTOP_ACTIVATION_FALLBACK_FAILED' : 'DESKTOP_ACTIVATION_NOT_CONFIRMED';
314
+ const wrappedMessage = providerActivationError
315
+ ? `Desktop Operator activation failed and Forge could not independently verify ${bundleId || appName} as frontmost.`
316
+ : `Desktop Operator returned an activated session, but Forge could not independently verify ${bundleId || appName} as frontmost.`;
317
+ if (verificationError instanceof AssistantPluginError) {
318
+ throw new AssistantPluginError(wrappedCode, wrappedMessage, {
319
+ retryable: verificationError.retryable,
320
+ details: {
321
+ ...(providerActivationError ? { providerErrorCode: providerActivationError.code } : {}),
322
+ verificationErrorCode: verificationError.code,
323
+ verificationErrorMessage: verificationError.message,
324
+ target: bundleId || appName,
325
+ },
326
+ });
349
327
  }
328
+ throw new AssistantPluginError(wrappedCode, wrappedMessage, {
329
+ retryable: true,
330
+ details: {
331
+ ...(providerActivationError ? { providerErrorCode: providerActivationError.code } : {}),
332
+ verificationErrorMessage: String(verificationError),
333
+ target: bundleId || appName,
334
+ },
335
+ });
350
336
  }
337
+
351
338
  return result;
352
339
  }
353
340
 
@@ -616,6 +603,53 @@ export function createExternalPluginAdapter(
616
603
  };
617
604
  }
618
605
 
606
+ if (registration.pluginId === 'desktop_operator' && input.actionId === 'desktop_key') {
607
+ const sourceInteractionId = typeof input.args.interaction_id === 'string' ? input.args.interaction_id.trim() : '';
608
+ const keys = Array.isArray(input.args.keys) ? input.args.keys : [];
609
+ if (!sourceInteractionId || keys.length === 0) {
610
+ throw providerError('DESKTOP_FOREGROUND_KEY_ARGUMENT_INVALID', 'desktop_key requires interaction_id and at least one key.');
611
+ }
612
+ const sourceStatus = await callProvider(
613
+ registration,
614
+ `${input.requestId}:source-status`,
615
+ 'desktop_status',
616
+ { limit: 500 },
617
+ input.timeoutMs,
618
+ input.signal,
619
+ dependencies,
620
+ );
621
+ const sourceSession = desktopSession(sourceStatus, sourceInteractionId);
622
+ if (!sourceSession) {
623
+ throw providerError('DESKTOP_FOREGROUND_KEY_SESSION_NOT_FOUND', `Desktop session ${sourceInteractionId} is no longer available.`, true);
624
+ }
625
+ const bundleId = firstString(sourceSession, 'bundleIdentifier', 'bundle_id');
626
+ const appName = firstString(sourceSession, 'appName', 'app_name');
627
+ if (!bundleId && !appName) {
628
+ throw providerError('DESKTOP_FOREGROUND_KEY_TARGET_UNAVAILABLE', `Desktop session ${sourceInteractionId} has no stable application identity.`);
629
+ }
630
+ const activation = await openVerifiedDesktopSession(
631
+ registration,
632
+ `${input.requestId}:activate`,
633
+ { ...(bundleId ? { bundle_id: bundleId } : { app_name: appName }), launch: false, activate: true },
634
+ input.timeoutMs,
635
+ input.signal,
636
+ dependencies,
637
+ );
638
+ const activationInteractionId = firstString(activation, 'interactionId', 'interaction_id');
639
+ if (!activationInteractionId) {
640
+ throw providerError('DESKTOP_ACTIVATION_SESSION_MISSING', 'Desktop Operator activated the application without returning a bound interaction session.', true);
641
+ }
642
+ return await callProvider(
643
+ registration,
644
+ `${input.requestId}:key`,
645
+ 'desktop_key',
646
+ { interaction_id: activationInteractionId, keys },
647
+ input.timeoutMs,
648
+ input.signal,
649
+ dependencies,
650
+ );
651
+ }
652
+
619
653
  if (registration.pluginId === 'desktop_operator' && input.actionId === 'desktop_session_open' && input.args.activate === true) {
620
654
  return await openVerifiedDesktopSession(
621
655
  registration,
@@ -54,14 +54,6 @@ export function validateMacOsCapabilityBrokerHandshake(
54
54
  ? handshake.browserAutomationActions.filter((entry): entry is string => typeof entry === 'string')
55
55
  : [];
56
56
  const browserProtocolVersion = handshake.browserAutomationProtocolVersion;
57
- const declaresBrowserMetadata = Array.isArray(handshake.internalCapabilities)
58
- || Array.isArray(handshake.browserAutomationActions)
59
- || browserProtocolVersion !== undefined;
60
- // Desktop Operator v0.2.1 predates optional browser capability metadata in its handshake,
61
- // but it implements the separately routed macos_browser_automation method. In that legacy
62
- // shape, let the real method/action call prove support. Unsupported methods/actions still
63
- // fail closed at the provider boundary.
64
- if (!declaresBrowserMetadata) return;
65
57
  if (!capabilities.includes(BROWSER_AUTOMATION_CAPABILITY)
66
58
  || browserProtocolVersion !== BROWSER_AUTOMATION_PROTOCOL_VERSION
67
59
  || (requestedAction && !actions.includes(requestedAction))) {
@@ -598,6 +598,8 @@ export interface PluginActionReceipt {
598
598
  receiptId: string;
599
599
  requestId: string;
600
600
  repoId: string;
601
+ /** Work authority repository when it differs from controller/provider receipt scope. */
602
+ workRepoId?: string;
601
603
  pluginId: string;
602
604
  actionId: string;
603
605
  semanticKey: string;
@@ -613,6 +615,7 @@ export interface PluginActionReceipt {
613
615
  interface PluginActionRequestIndex {
614
616
  requestId: string;
615
617
  repoId: string;
618
+ workRepoId?: string;
616
619
  receiptId: string;
617
620
  semanticKey: string;
618
621
  createdAt: string;
@@ -733,6 +736,15 @@ function localEffectTarget(
733
736
  };
734
737
  }
735
738
 
739
+ function workAttributionRepoId(repository: RepositoryRecord, request: AssistantPluginActionRequest): string {
740
+ const explicit = request.workRepoId?.trim();
741
+ if (!request.workId?.trim()) {
742
+ if (explicit) throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_WITHOUT_WORK: ${explicit}`);
743
+ return repository.repoId;
744
+ }
745
+ return explicit || repository.repoId;
746
+ }
747
+
736
748
  function remoteEffectWorkForPluginAction(
737
749
  controllerHome: string,
738
750
  repository: RepositoryRecord,
@@ -745,7 +757,8 @@ function remoteEffectWorkForPluginAction(
745
757
  if (action.risk !== 'remote_write') {
746
758
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_REQUIRES_REMOTE_WRITE: ${action.actionId}`);
747
759
  }
748
- const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
760
+ const workRepoId = workAttributionRepoId(repository, request);
761
+ const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
749
762
  if (!work) throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_NOT_FOUND: ${workId}`);
750
763
  if (work.workKind !== 'remote_effect') {
751
764
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_KIND_MISMATCH: ${workId} is ${work.workKind}, expected remote_effect`);
@@ -772,14 +785,38 @@ function attributedWorkForPluginAction(
772
785
  ): WorkContract | undefined {
773
786
  const workId = request.workId?.trim();
774
787
  if (!workId || action.risk === 'remote_write') return undefined;
775
- const work = getWorkContract({ controllerHome, repoId: repository.repoId }, workId);
776
- if (!work) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workId}`);
788
+ const workRepoId = workAttributionRepoId(repository, request);
789
+ const work = getWorkContract({ controllerHome, repoId: workRepoId }, workId);
790
+ if (!work) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${workId}`);
777
791
  if (isTerminalWorkContractStatus(work.status)) {
778
792
  throw new Error(`WORK_PLUGIN_ATTRIBUTION_TERMINAL: ${workId} is ${work.status}`);
779
793
  }
780
794
  return work;
781
795
  }
782
796
 
797
+ function bindLocalEffectReceiptToAttributedWork(
798
+ controllerHome: string,
799
+ repository: RepositoryRecord,
800
+ action: AssistantPluginActionDescriptor,
801
+ request: AssistantPluginActionRequest,
802
+ receipt: PluginActionReceipt,
803
+ work?: WorkContract,
804
+ ): WorkContract | undefined {
805
+ if (!request.workId || receipt.status !== 'succeeded') return work;
806
+ const workRepoId = workAttributionRepoId(repository, request);
807
+ const attributed = work ?? getWorkContract({ controllerHome, repoId: workRepoId }, request.workId.trim());
808
+ if (!attributed) throw new Error(`WORK_PLUGIN_ATTRIBUTION_NOT_FOUND: ${workRepoId}:${request.workId.trim()}`);
809
+ if (attributed.workKind !== 'local_effect' || action.readOnly || action.risk === 'readonly') return attributed;
810
+ if (attributed.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) return attributed;
811
+ if (isTerminalWorkContractStatus(attributed.status)) return attributed;
812
+ return appendWorkEvidence({ controllerHome, repoId: workRepoId }, attributed.workId, {
813
+ evidenceId: receipt.receiptId,
814
+ title: 'typed local plugin effect completed',
815
+ summary: `${receipt.pluginId}/${receipt.actionId} completed with durable receipt ${receipt.receiptId}.`,
816
+ detailLevel: 'summary',
817
+ });
818
+ }
819
+
783
820
  export function finalizeRemoteEffectWorkFromActionReceipt(
784
821
  controllerHome: string,
785
822
  repoId: string,
@@ -795,7 +832,11 @@ export function finalizeRemoteEffectWorkFromActionReceipt(
795
832
  throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_TERMINAL: ${workId} is ${work.status}`);
796
833
  }
797
834
  const receipt = work.evidenceRefs
798
- .map((evidence) => evidence.evidenceId ? readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId) : undefined)
835
+ .map((evidence) => {
836
+ if (!evidence.evidenceId) return undefined;
837
+ return readPluginActionReceipt(controllerHome, repoId, evidence.evidenceId)
838
+ ?? readPluginActionReceipt(controllerHome, CONTROLLER_SCOPE_REPO_ID, evidence.evidenceId);
839
+ })
799
840
  .find((candidate) => candidate?.workId === workId && candidate.status === 'succeeded');
800
841
  if (!receipt) {
801
842
  throw new Error(`WORK_REMOTE_EFFECT_FINALIZE_RECEIPT_REQUIRED: ${workId} has no successful durable plugin action receipt`);
@@ -833,9 +874,10 @@ function bindRemoteEffectReceiptToWork(
833
874
  const work = remoteEffectWorkForPluginAction(controllerHome, repository, action, request, receipt.receiptId);
834
875
  if (!work) return undefined;
835
876
  if (work.status === 'completed') return work;
877
+ const workRepoId = workAttributionRepoId(repository, request);
836
878
  let updated = work;
837
879
  if (!work.evidenceRefs.some((evidence) => evidence.evidenceId === receipt.receiptId)) {
838
- updated = appendWorkEvidence({ controllerHome, repoId: repository.repoId }, work.workId, {
880
+ updated = appendWorkEvidence({ controllerHome, repoId: workRepoId }, work.workId, {
839
881
  evidenceId: receipt.receiptId,
840
882
  title: 'typed remote plugin effect completed',
841
883
  summary: `${receipt.pluginId}/${receipt.actionId} completed with durable receipt ${receipt.receiptId}.`,
@@ -856,7 +898,7 @@ function bindRemoteEffectReceiptToWork(
856
898
  recordedAt: new Date().toISOString(),
857
899
  };
858
900
  return recordWorkCompletionReceipt(
859
- { controllerHome, repoId: repository.repoId },
901
+ { controllerHome, repoId: workRepoId },
860
902
  work.workId,
861
903
  remoteReceipt,
862
904
  'completed_remote',
@@ -940,7 +982,15 @@ export async function submitAssistantPluginAction(
940
982
  if (request.workId && existing.workId !== request.workId) {
941
983
  throw new Error(`WORK_PLUGIN_RECEIPT_BINDING_CONFLICT: ${request.requestId} already belongs to Work ${existing.workId ?? 'none'}`);
942
984
  }
943
- if (request.workId && action.risk === 'remote_write') bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, existing);
985
+ const requestedWorkRepoId = request.workId ? workAttributionRepoId(repository, request) : undefined;
986
+ if (requestedWorkRepoId && (index.workRepoId ?? index.repoId) !== requestedWorkRepoId) {
987
+ throw new Error(`WORK_PLUGIN_ATTRIBUTION_REPO_CONFLICT: ${request.requestId} already belongs to Work repository ${index.workRepoId ?? index.repoId}`);
988
+ }
989
+ if (request.workId && action.risk === 'remote_write') {
990
+ bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, existing);
991
+ } else if (request.workId) {
992
+ bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, existing);
993
+ }
944
994
  return {
945
995
  manifest,
946
996
  action,
@@ -1181,6 +1231,7 @@ export async function submitAssistantPluginAction(
1181
1231
  receiptId,
1182
1232
  requestId: request.requestId,
1183
1233
  repoId: repository.repoId,
1234
+ ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1184
1235
  pluginId: request.pluginId,
1185
1236
  actionId: request.actionId,
1186
1237
  semanticKey: key,
@@ -1201,11 +1252,16 @@ export async function submitAssistantPluginAction(
1201
1252
  writeJsonAtomic(requestPath, {
1202
1253
  requestId: request.requestId,
1203
1254
  repoId: repository.repoId,
1255
+ ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1204
1256
  receiptId,
1205
1257
  semanticKey: key,
1206
1258
  createdAt,
1207
1259
  } satisfies PluginActionRequestIndex);
1208
- if (boundRemoteWork) bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, receipt);
1260
+ if (boundRemoteWork) {
1261
+ bindRemoteEffectReceiptToWork(controllerHome, repository, action, request, receipt);
1262
+ } else if (attributedWork) {
1263
+ bindLocalEffectReceiptToAttributedWork(controllerHome, repository, action, request, receipt, attributedWork);
1264
+ }
1209
1265
  const nextManifest = getAssistantPluginManifest(controllerHome, repository, request.pluginId);
1210
1266
  return {
1211
1267
  manifest: nextManifest,
@@ -1221,7 +1277,7 @@ export async function submitAssistantPluginAction(
1221
1277
  const message = error instanceof Error ? error.message : String(error);
1222
1278
  const code = /^([A-Z][A-Z0-9_]+)/.exec(message)?.[1] ?? 'PLUGIN_ACTION_FAILED';
1223
1279
  if (boundRemoteWork) {
1224
- appendWorkEvidence({ controllerHome, repoId: repository.repoId }, boundRemoteWork.workId, {
1280
+ appendWorkEvidence({ controllerHome, repoId: workAttributionRepoId(repository, request) }, boundRemoteWork.workId, {
1225
1281
  title: 'typed remote plugin effect failed',
1226
1282
  summary: `${request.pluginId}/${request.actionId}: ${message}`.slice(0, 1_000),
1227
1283
  detailLevel: 'summary',
@@ -1246,6 +1302,7 @@ export async function submitAssistantPluginAction(
1246
1302
  receiptId,
1247
1303
  requestId: request.requestId,
1248
1304
  repoId: repository.repoId,
1305
+ ...(request.workId ? { workRepoId: workAttributionRepoId(repository, request) } : {}),
1249
1306
  pluginId: request.pluginId,
1250
1307
  actionId: request.actionId,
1251
1308
  semanticKey: key,
@@ -124,8 +124,10 @@ export interface AssistantPluginActionRequest {
124
124
  pluginId: string;
125
125
  actionId: string;
126
126
  requestId: string;
127
- /** Optional existing external-effect Work that owns this typed action receipt. */
127
+ /** Optional existing Work that owns this typed action receipt. */
128
128
  workId?: string;
129
+ /** Durable repository authority for workId. May differ from provider scope for controller-scoped plugins. */
130
+ workRepoId?: string;
129
131
  args: Record<string, unknown>;
130
132
  timeoutMs?: number;
131
133
  signal?: AbortSignal;
@@ -91,7 +91,7 @@ export interface RuntimeMaintenanceCandidate {
91
91
  sourceControllerHome?: string;
92
92
  objective?: string;
93
93
  continuationPrompt?: string;
94
- disposition?: 'resume_or_supersede_review' | 'completion_reconciliation' | 'ownership_reconciliation' | 'source_preservation_required';
94
+ disposition?: 'resume_or_supersede_review' | 'completion_reconciliation' | 'ownership_reconciliation' | 'source_preservation_required' | 'semantic_completion_required';
95
95
  sourceState?: 'no_managed_source' | 'clean_integrated' | 'dirty_worktree' | 'unintegrated_commits' | 'source_state_unknown';
96
96
  }
97
97
 
@@ -584,10 +584,20 @@ function inspectStaleWorkRepositorySource(
584
584
  };
585
585
  }
586
586
 
587
- function staleWorkCandidateReason(source: StaleWorkSourceInspection): string {
588
- return source.safeToCancel
589
- ? `Nonterminal WorkContract exceeded the explicit maintenance age threshold and has no active Plan, Requirement, Schedule, or Controller authority. ${source.detail} Explicit full maintenance may cancel Work metadata while retaining durable evidence.`
590
- : `Protected stale Work: no active lifecycle authority remains, but repository source preservation is required. ${source.detail} The Work must remain nonterminal until source is explicitly adopted, delivered, or cleanup is semantically authorized.`;
587
+ function staleWorkSemanticTerminalizationReady(contract: WorkContract): boolean {
588
+ if (contract.phase !== 'cleanup') return false;
589
+ return (['implementation', 'verification', 'delivery'] as const).every((phase) =>
590
+ ['satisfied', 'skipped'].includes(contract.phaseEvidence[phase].state));
591
+ }
592
+
593
+ function staleWorkCandidateReason(source: StaleWorkSourceInspection, semanticReady: boolean): string {
594
+ if (!source.safeToCancel) {
595
+ return `Protected stale Work: no active lifecycle authority remains, but repository source preservation is required. ${source.detail} The Work must remain nonterminal until source is explicitly adopted, delivered, or cleanup is semantically authorized.`;
596
+ }
597
+ if (!semanticReady) {
598
+ return `Protected stale Work: repository source has no unique live changes, but the Work semantic lifecycle has not reached cleanup with implementation, verification, and delivery explicitly satisfied or skipped. ${source.detail} Maintenance may clean source debt, but it cannot infer that the objective or acceptance criteria are complete.`;
599
+ }
600
+ return `Cleanup-ready WorkContract exceeded the explicit maintenance age threshold and has no active Plan, Requirement, Schedule, or Controller authority. ${source.detail} Explicit full maintenance may finish cleanup while retaining durable evidence.`;
591
601
  }
592
602
 
593
603
  function scanRetainedWorktreeCandidates(
@@ -738,19 +748,26 @@ function scanStaleWorkContractCandidates(
738
748
  })
739
749
  .sort((left, right) => right.ageMinutes - left.ageMinutes)
740
750
  .slice(0, options.maxCandidates)
741
- .map(({ contract, ageMinutes, source }) => ({
742
- kind: 'stale_work_contract' as const,
743
- id: contract.workId,
744
- path: source.path,
745
- status: contract.status,
746
- safe: source.safeToCancel,
747
- reason: staleWorkCandidateReason(source),
748
- ageMinutes,
749
- suggestedAction: 'full_maintenance_pass' as const,
750
- ownershipStatus: 'explicit' as const,
751
- sourceState: source.state,
752
- disposition: source.safeToCancel ? undefined : ('source_preservation_required' as const),
753
- }));
751
+ .map(({ contract, ageMinutes, source }) => {
752
+ const semanticReady = staleWorkSemanticTerminalizationReady(contract);
753
+ return {
754
+ kind: 'stale_work_contract' as const,
755
+ id: contract.workId,
756
+ path: source.path,
757
+ status: contract.status,
758
+ safe: source.safeToCancel && semanticReady,
759
+ reason: staleWorkCandidateReason(source, semanticReady),
760
+ ageMinutes,
761
+ suggestedAction: 'full_maintenance_pass' as const,
762
+ ownershipStatus: 'explicit' as const,
763
+ sourceState: source.state,
764
+ disposition: !source.safeToCancel
765
+ ? ('source_preservation_required' as const)
766
+ : !semanticReady
767
+ ? ('semantic_completion_required' as const)
768
+ : undefined,
769
+ };
770
+ });
754
771
  }
755
772
 
756
773
  export function applyStaleWorkContractMaintenanceCandidate(
@@ -794,25 +811,38 @@ export function applyStaleWorkContractMaintenanceCandidate(
794
811
  ...candidate,
795
812
  path: source.path ?? candidate.path,
796
813
  safe: false,
797
- reason: staleWorkCandidateReason(source),
814
+ reason: staleWorkCandidateReason(source, staleWorkSemanticTerminalizationReady(current)),
798
815
  sourceState: source.state,
799
816
  disposition: 'source_preservation_required' as const,
800
817
  applied: false,
801
818
  result: `work_source_preserved:${source.state}`,
802
819
  };
803
820
  }
821
+ const semanticReady = staleWorkSemanticTerminalizationReady(current);
822
+ if (!semanticReady) {
823
+ return {
824
+ ...candidate,
825
+ path: source.path ?? candidate.path,
826
+ safe: false,
827
+ reason: staleWorkCandidateReason(source, false),
828
+ sourceState: source.state,
829
+ disposition: 'semantic_completion_required' as const,
830
+ applied: false,
831
+ result: 'work_semantic_completion_required',
832
+ };
833
+ }
804
834
  transitionWorkContractPhase({ controllerHome, repoId: repository.repoId }, current.workId, {
805
835
  phase: 'cleanup',
806
836
  status: 'cancelled',
807
837
  state: 'skipped',
808
- summary: 'Cancelled by explicit full maintenance stale Work cleanup after proving no unique live repository source; durable evidence retained.',
838
+ summary: 'Cancelled by explicit full maintenance after the Work had already reached cleanup with prior semantic phases satisfied and no unique live repository source remained; durable evidence retained.',
809
839
  evidenceRefs: current.evidenceRefs,
810
840
  });
811
841
  return {
812
842
  ...candidate,
813
843
  path: source.path ?? candidate.path,
814
844
  safe: true,
815
- reason: staleWorkCandidateReason(source),
845
+ reason: staleWorkCandidateReason(source, true),
816
846
  sourceState: source.state,
817
847
  disposition: undefined,
818
848
  applied: true,
@@ -856,14 +886,20 @@ function scanStaleEditSessionCandidates(
856
886
  .filter(({ ageMinutes }) => ageMinutes >= options.minAgeMinutes)
857
887
  .sort((left, right) => right.ageMinutes - left.ageMinutes)
858
888
  .slice(0, options.maxCandidates)
859
- .map(({ session, ageMinutes }) => {
889
+ .flatMap(({ session, ageMinutes }) => {
860
890
  const work = session.workId
861
891
  ? getWorkContract({ controllerHome, repoId: repository.repoId }, session.workId)
862
892
  : undefined;
863
893
  const terminalWork = Boolean(work && isTerminalWorkContractStatus(work.status));
894
+ if (work && !terminalWork && activeWorkAuthorityRefs(repository, controllerHome, work).length > 0) {
895
+ // The Edit Session inherits the live Work lifecycle authority. Reporting it
896
+ // as stale maintenance debt while that authority is active would duplicate
897
+ // ownership and can permanently block release readiness for valid long work.
898
+ return [];
899
+ }
864
900
  const contractFreeDirect = !session.workId;
865
901
  const safeToReconcile = terminalWork || contractFreeDirect;
866
- return {
902
+ return [{
867
903
  kind: 'stale_edit_session' as const,
868
904
  id: session.sessionId,
869
905
  status: session.status,
@@ -872,11 +908,11 @@ function scanStaleEditSessionCandidates(
872
908
  ? 'Edit Session belongs to a terminal WorkContract; cleanup may reconcile metadata only and must refuse unique uncommitted source changes.'
873
909
  : contractFreeDirect
874
910
  ? 'Contract-free Direct Edit Session has no WorkContract by design; cleanup may reconcile metadata only and must refuse unique uncommitted source changes.'
875
- : 'Edit Session WorkContract is missing or still nonterminal; active-session ownership must remain fenced.',
911
+ : 'Edit Session WorkContract is missing or still nonterminal without live lifecycle authority; active-session ownership must remain fenced.',
876
912
  ageMinutes,
877
913
  suggestedAction: 'full_maintenance_pass' as const,
878
914
  ownershipStatus: safeToReconcile ? 'explicit' as const : 'unknown' as const,
879
- };
915
+ }];
880
916
  });
881
917
  }
882
918
 
@@ -122,13 +122,40 @@ async function defaultConnectorEndpointProbe(endpoint: string): Promise<boolean>
122
122
  }
123
123
  }
124
124
 
125
+ export async function waitForPackageConnectorEndpointReady(
126
+ endpoint: string,
127
+ options: {
128
+ timeoutMs?: number;
129
+ pollIntervalMs?: number;
130
+ probeEndpoint?: (endpoint: string) => Promise<boolean>;
131
+ wait?: (ms: number) => Promise<void>;
132
+ now?: () => number;
133
+ } = {},
134
+ ): Promise<boolean> {
135
+ const timeoutMs = Math.max(0, options.timeoutMs ?? 15_000);
136
+ const pollIntervalMs = Math.max(10, options.pollIntervalMs ?? 100);
137
+ const probeEndpoint = options.probeEndpoint ?? defaultConnectorEndpointProbe;
138
+ const wait = options.wait ?? ((ms: number) => new Promise<void>((resolveWait) => setTimeout(resolveWait, ms)));
139
+ const now = options.now ?? Date.now;
140
+ const deadline = now() + timeoutMs;
141
+ do {
142
+ if (await probeEndpoint(endpoint)) return true;
143
+ if (now() >= deadline) return false;
144
+ await wait(Math.min(pollIntervalMs, Math.max(1, deadline - now())));
145
+ } while (now() <= deadline);
146
+ return false;
147
+ }
148
+
125
149
  export function packageConnectorLaunchSpec(input: { release: PackageConnectorReleaseBinding; controllerHome: string; endpoint: string; executable?: string }): { executable: string; args: string[]; environment: Record<string, string>; port: number } {
126
150
  const parsed = new URL(input.endpoint);
127
151
  const port = Number(parsed.port);
128
152
  if (parsed.protocol !== 'http:' || parsed.hostname !== '127.0.0.1' || parsed.pathname !== '/mcp' || !Number.isInteger(port) || port < 1 || port > 65_535) {
129
153
  throw new Error('FORGE_PACKAGE_CONNECTOR_ENDPOINT_INVALID');
130
154
  }
131
- const executable = resolve(input.executable ?? process.execPath);
155
+ const executable = resolve(input.executable ?? process.env.FORGE_CONNECTOR_EXECUTABLE ?? process.execPath);
156
+ if (/^forge-recovery-(?:gateway|watchdog)$/i.test(basename(executable))) {
157
+ throw new Error('FORGE_PACKAGE_CONNECTOR_EXECUTABLE_INVALID');
158
+ }
132
159
  const packageRoot = resolve(input.release.packageRoot);
133
160
  const cliEntry = join(packageRoot, 'src', 'cli', 'index.ts');
134
161
  const nodeLoader = join(packageRoot, 'src', 'runtime', 'shared', 'node-ts-loader.mjs');
@@ -168,6 +195,7 @@ export function packageConnectorServiceMatchesRelease(input: {
168
195
  endpoint: string;
169
196
  platform: NodeJS.Platform;
170
197
  env?: NodeJS.ProcessEnv;
198
+ executable?: string;
171
199
  }): boolean {
172
200
  try {
173
201
  if (
@@ -176,7 +204,12 @@ export function packageConnectorServiceMatchesRelease(input: {
176
204
  || resolve(input.authority.releaseRoot) !== resolve(input.release.releaseRoot)
177
205
  || resolve(input.authority.packageRoot) !== resolve(input.release.packageRoot)
178
206
  ) return false;
179
- const launch = packageConnectorLaunchSpec({ release: input.release, controllerHome: input.controllerHome, endpoint: input.endpoint });
207
+ const launch = packageConnectorLaunchSpec({
208
+ release: input.release,
209
+ controllerHome: input.controllerHome,
210
+ endpoint: input.endpoint,
211
+ executable: input.executable,
212
+ });
180
213
  if (input.platform === 'darwin') {
181
214
  const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
182
215
  if (input.authority.mode !== 'launchd' || !input.authority.servicePath) return false;
@@ -226,10 +259,15 @@ function startPortable(paths: PackageConnectorServicePaths, launch: ReturnType<t
226
259
  } finally { closeSync(stdout); closeSync(stderr); }
227
260
  }
228
261
 
229
- export async function installPackageConnectorService(input: { release: PackageConnectorReleaseBinding; controllerHome: string; endpoint: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean }): Promise<PackageConnectorServiceResult> {
262
+ export async function installPackageConnectorService(input: { release: PackageConnectorReleaseBinding; controllerHome: string; endpoint: string; executable?: string; platform?: NodeJS.Platform; env?: NodeJS.ProcessEnv; forcePortable?: boolean; probeEndpoint?: (endpoint: string) => Promise<boolean> }): Promise<PackageConnectorServiceResult> {
230
263
  const paths = packageConnectorServicePaths(input.controllerHome, input.env?.HOME);
231
264
  mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
232
- const launch = packageConnectorLaunchSpec({ release: input.release, controllerHome: input.controllerHome, endpoint: input.endpoint });
265
+ const launch = packageConnectorLaunchSpec({
266
+ release: input.release,
267
+ controllerHome: input.controllerHome,
268
+ endpoint: input.endpoint,
269
+ executable: input.executable,
270
+ });
233
271
  const platform = input.platform ?? process.platform, env = input.env ?? process.env;
234
272
  if (!input.forcePortable && platform === 'darwin') {
235
273
  await retireConflictingForgeLaunchAgents({
@@ -243,6 +281,8 @@ export async function installPackageConnectorService(input: { release: PackageCo
243
281
  installLaunchAgent(paths.sourcePlistPath, paths.label);
244
282
  const result = await bootstrapLaunchAgentWithRetryV2({ label: paths.label, plistPath: paths.installedPlistPath });
245
283
  if (!result.ok) throw new Error(`FORGE_PACKAGE_CONNECTOR_LAUNCHD_INSTALL_FAILED: ${result.diagnostics.join('; ')}`);
284
+ const endpointReady = await waitForPackageConnectorEndpointReady(input.endpoint, { probeEndpoint: input.probeEndpoint });
285
+ if (!endpointReady) throw new Error(`FORGE_PACKAGE_CONNECTOR_ENDPOINT_NOT_READY: ${input.endpoint}`);
246
286
  const installed = { endpoint: input.endpoint, mode: 'launchd' as const, persistent: true, servicePath: paths.installedPlistPath, releaseId: input.release.releaseId, releaseRoot: input.release.releaseRoot };
247
287
  writePackageConnectorServiceAuthority({ release: input.release, controllerHome: input.controllerHome, result: installed });
248
288
  return installed;
@@ -264,6 +304,7 @@ export async function ensurePackageConnectorService(input: {
264
304
  release: PackageConnectorReleaseBinding;
265
305
  controllerHome: string;
266
306
  endpoint: string;
307
+ executable?: string;
267
308
  platform?: NodeJS.Platform;
268
309
  env?: NodeJS.ProcessEnv;
269
310
  forcePortable?: boolean;
@@ -296,6 +337,7 @@ export async function ensurePackageConnectorService(input: {
296
337
  endpoint: input.endpoint,
297
338
  platform,
298
339
  env: input.env,
340
+ executable: input.executable,
299
341
  })
300
342
  && await (input.probeEndpoint ?? defaultConnectorEndpointProbe)(input.endpoint)
301
343
  ) {