@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
@@ -107,7 +107,7 @@ function assertPackageRuntimeSnapshot(snapshotRoot: string, records: PackageRunt
107
107
  }
108
108
  }
109
109
 
110
- function stagePackageRuntimeSnapshot(sourceRoot: string, snapshotRoot: string, records: PackageRuntimeFileRecord[]): void {
110
+ export function stagePackageRuntimeSnapshot(sourceRoot: string, snapshotRoot: string, records: PackageRuntimeFileRecord[]): void {
111
111
  mkdirSync(snapshotRoot, { recursive: false, mode: 0o700 });
112
112
  for (const record of records) {
113
113
  const source = join(sourceRoot, record.path);
@@ -156,7 +156,8 @@ function launcherSource(input: { packageRoot: string; indexPath: string; indexSh
156
156
  + `const loader=join(packageRoot,'src','runtime','shared','node-ts-loader.mjs');\n`
157
157
  + `const args=process.versions?.bun?[entry,...process.argv.slice(2)]:['--loader',loader,entry,...process.argv.slice(2)];\n`
158
158
  + `const child=spawn(process.execPath,args,{stdio:'inherit',env:process.env});\n`
159
- + `for(const signal of ['SIGINT','SIGTERM','SIGHUP']){try{process.on(signal,()=>child.kill(signal));}catch{}}\n`
159
+ + `const managerSignalsProcessGroup=process.platform==='linux'&&Boolean(process.env.INVOCATION_ID);\n`
160
+ + `for(const signal of ['SIGINT','SIGTERM','SIGHUP']){try{process.on(signal,()=>{if(!managerSignalsProcessGroup)child.kill(signal);});}catch{}}\n`
160
161
  + `child.on('error',(error)=>{console.error('FORGE_PACKAGE_RUNTIME_LAUNCH_FAILED: '+error.message);process.exit(1);});\n`
161
162
  + `child.on('exit',(code)=>process.exit(code??1));\n`;
162
163
  }
@@ -176,6 +176,20 @@ function cleanRuntimeInstallerEnvironment(env: NodeJS.ProcessEnv, releaseEnviron
176
176
  return { ...next, ...releaseEnvironment };
177
177
  }
178
178
 
179
+ export function systemdRuntimeInstallCommands(unitName: string): string[][] {
180
+ // `enable --now` only starts an inactive unit. When Forge is already running,
181
+ // replacing the unit file and calling enable --now leaves the old ExecStart
182
+ // process alive even though release authority has advanced, fencing that
183
+ // process from writes. Enable and restart are intentionally separate so both
184
+ // first install and in-place immutable-release activation converge on the
185
+ // just-written unit while systemd remains the sole lifecycle owner.
186
+ return [
187
+ ['--user', 'daemon-reload'],
188
+ ['--user', 'enable', unitName],
189
+ ['--user', 'restart', unitName],
190
+ ];
191
+ }
192
+
179
193
  function installSystemdUserService(controllerHome: string, env: NodeJS.ProcessEnv): string {
180
194
  const entrypoint = activeRuntimeEntrypoint(controllerHome);
181
195
  const launch = activeRuntimeLaunchSpec(controllerHome);
@@ -184,7 +198,7 @@ function installSystemdUserService(controllerHome: string, env: NodeJS.ProcessEn
184
198
  const unitName = `${label}.service`;
185
199
  const unitPath = join(env.HOME ?? homedir(), '.config', 'systemd', 'user', unitName);
186
200
  atomicWrite(unitPath, renderForgeRuntimeSystemdUserUnit({ executable: entrypoint, args: launch.args, environment: launch.environment }), 0o644);
187
- for (const args of [['--user', 'daemon-reload'], ['--user', 'enable', '--now', unitName]]) {
201
+ for (const args of systemdRuntimeInstallCommands(unitName)) {
188
202
  const result = commandSucceeded('systemctl', args, env);
189
203
  if (result.status !== 0) throw new Error(`FORGE_RUNTIME_SYSTEMD_INSTALL_FAILED: systemctl ${args.join(' ')}: ${(result.stderr || result.stdout || '').trim()}`);
190
204
  }
@@ -357,6 +371,10 @@ async function cleanupActivationHelper(request: PackageRuntimeActivationRequest)
357
371
  async function rollbackPackageRuntimeActivation(
358
372
  request: PackageRuntimeActivationRequest,
359
373
  installDarwinService: typeof installForgeRuntimeService,
374
+ options: {
375
+ restoreConnector?: boolean;
376
+ ensureConnectorService?: typeof ensurePackageConnectorService;
377
+ } = {},
360
378
  ): Promise<string[]> {
361
379
  const errors: string[] = [];
362
380
  const servicePaths = forgeRuntimeServicePaths(request.controllerHome);
@@ -391,6 +409,29 @@ async function rollbackPackageRuntimeActivation(
391
409
  errors.push(error instanceof Error ? error.message : String(error));
392
410
  }
393
411
  }
412
+ if (options.restoreConnector && request.connectorEndpoint && request.priorAuthorityPresent) {
413
+ try {
414
+ const restored = readRuntimeReleaseAuthority(request.controllerHome);
415
+ if (!restored) throw new Error('FORGE_PACKAGE_CONNECTOR_ROLLBACK_AUTHORITY_MISSING');
416
+ const releaseRoot = dirname(resolve(restored.active.manifestPath));
417
+ const packageRoot = join(releaseRoot, 'package');
418
+ const ensureConnectorService = options.ensureConnectorService ?? ensurePackageConnectorService;
419
+ await ensureConnectorService({
420
+ release: {
421
+ releaseId: restored.active.releaseId,
422
+ releaseRoot,
423
+ packageRoot,
424
+ },
425
+ controllerHome: request.controllerHome,
426
+ endpoint: request.connectorEndpoint,
427
+ executable: request.nodeExecutable,
428
+ platform: 'darwin',
429
+ refresh: true,
430
+ });
431
+ } catch (error) {
432
+ errors.push(`primary Connector rollback failed: ${error instanceof Error ? error.message : String(error)}`);
433
+ }
434
+ }
394
435
  return errors;
395
436
  }
396
437
 
@@ -421,6 +462,7 @@ export async function activateScheduledPackageRuntimeService(
421
462
  release: request.release,
422
463
  controllerHome: request.controllerHome,
423
464
  endpoint: request.connectorEndpoint,
465
+ executable: request.nodeExecutable,
424
466
  platform: 'darwin',
425
467
  refresh: request.refreshConnector === true,
426
468
  });
@@ -428,7 +470,10 @@ export async function activateScheduledPackageRuntimeService(
428
470
  return writeActivationReceipt(request, 'activated', { servicePath: paths.installedPlistPath });
429
471
  } catch (error) {
430
472
  const detail = error instanceof Error ? error.message : String(error);
431
- const rollbackErrors = await rollbackPackageRuntimeActivation(request, installDarwinService);
473
+ const rollbackErrors = await rollbackPackageRuntimeActivation(request, installDarwinService, {
474
+ restoreConnector: Boolean(request.connectorEndpoint),
475
+ ensureConnectorService,
476
+ });
432
477
  writeActivationReceipt(request, 'failed+rollback', {
433
478
  error: detail,
434
479
  rollbackSucceeded: rollbackErrors.length === 0,
@@ -587,7 +632,7 @@ export async function installPackageRuntimeService(
587
632
  if (!connectorEndpoint || base.status === 'activation_scheduled') return base;
588
633
  const ensureConnectorService = dependencies.ensureConnectorService ?? ensurePackageConnectorService;
589
634
  const connector = await ensureConnectorService({
590
- release, controllerHome, endpoint: connectorEndpoint, platform, env, forcePortable: options.forcePortable === true, refresh: options.refreshConnector === true,
635
+ release, controllerHome, endpoint: connectorEndpoint, executable: process.execPath, platform, env, forcePortable: options.forcePortable === true, refresh: options.refreshConnector === true,
591
636
  });
592
637
  return { ...base, connector };
593
638
  }
@@ -13,6 +13,77 @@ function canonicalExistingPathIdentity(path: string): string {
13
13
  return existsSync(resolved) ? realpathSync(resolved) : resolved;
14
14
  }
15
15
 
16
+ function requireSha256Identity(value: unknown, field: string): string {
17
+ const identity = requireString(value, field);
18
+ if (!/^sha256:[a-f0-9]{64}$/i.test(identity)) {
19
+ throw new Error(`RELEASE_MANIFEST_INVALID: ${field} must be sha256:<64 hex>`);
20
+ }
21
+ return identity;
22
+ }
23
+
24
+ type RuntimeComponentPair<KEntry extends keyof RuntimeReleaseManifest, KIdentity extends keyof RuntimeReleaseManifest> = Pick<
25
+ RuntimeReleaseManifest,
26
+ KEntry | KIdentity
27
+ >;
28
+
29
+ function optionalRuntimeComponent<
30
+ KEntry extends keyof RuntimeReleaseManifest,
31
+ KIdentity extends keyof RuntimeReleaseManifest,
32
+ >(input: {
33
+ value: Record<string, unknown>;
34
+ entryField: KEntry;
35
+ identityField: KIdentity;
36
+ canonicalEntry: RuntimeReleaseManifest[KEntry];
37
+ }): RuntimeComponentPair<KEntry, KIdentity> | undefined {
38
+ const rawEntry = input.value[input.entryField as string];
39
+ const rawIdentity = input.value[input.identityField as string];
40
+ if (rawEntry === undefined && rawIdentity === undefined) return undefined;
41
+ const entry = requireString(rawEntry, input.entryField as string);
42
+ if (entry !== input.canonicalEntry) {
43
+ throw new Error(`RELEASE_MANIFEST_INVALID: ${String(input.entryField)} must be ${String(input.canonicalEntry)}, got ${entry}`);
44
+ }
45
+ const identity = requireSha256Identity(rawIdentity, input.identityField as string);
46
+ return {
47
+ [input.entryField]: input.canonicalEntry,
48
+ [input.identityField]: identity,
49
+ } as RuntimeComponentPair<KEntry, KIdentity>;
50
+ }
51
+
52
+ export const COMPILED_RUNTIME_RELEASE_COMPONENT_FIELDS = [
53
+ 'diagnosticEntrypoint', 'diagnosticArtifactIdentity',
54
+ 'browserNodeBridgeEntrypoint', 'browserNodeBridgeArtifactIdentity',
55
+ 'browserHandoffEntrypoint', 'browserHandoffArtifactIdentity',
56
+ 'processRunnerEntrypoint', 'processRunnerArtifactIdentity',
57
+ 'checkRunnerEntrypoint', 'checkRunnerArtifactIdentity',
58
+ 'pluginActionSidecarEntrypoint', 'pluginActionSidecarArtifactIdentity',
59
+ 'externalPluginProbeEntrypoint', 'externalPluginProbeArtifactIdentity',
60
+ 'codeGraphNodeEntrypoint', 'codeGraphNodeArtifactIdentity',
61
+ 'codeGraphSidecarEntrypoint', 'codeGraphSidecarArtifactIdentity',
62
+ 'codeGraphLibraryRoot', 'codeGraphLibraryArtifactIdentity',
63
+ 'packageRoot', 'packageArtifactIdentity',
64
+ 'controllerUiRoot', 'controllerUiArtifactIdentity',
65
+ ] as const satisfies readonly (keyof RuntimeReleaseManifest)[];
66
+
67
+ /**
68
+ * Compiled self-host candidates have a closed execution surface. Package
69
+ * launcher releases are a separate source-backed release form and therefore
70
+ * intentionally do not satisfy this contract.
71
+ */
72
+ export type CompleteCompiledRuntimeReleaseManifest = RuntimeReleaseManifest & Required<Pick<
73
+ RuntimeReleaseManifest,
74
+ typeof COMPILED_RUNTIME_RELEASE_COMPONENT_FIELDS[number]
75
+ >>;
76
+
77
+ export function requireCompleteCompiledRuntimeReleaseManifest(
78
+ manifest: RuntimeReleaseManifest,
79
+ ): asserts manifest is CompleteCompiledRuntimeReleaseManifest {
80
+ for (const field of COMPILED_RUNTIME_RELEASE_COMPONENT_FIELDS) {
81
+ if (manifest[field] === undefined) {
82
+ throw new Error(`RUNTIME_RELEASE_COMPILED_COMPONENT_MISSING: ${field}`);
83
+ }
84
+ }
85
+ }
86
+
16
87
  export function loadRuntimeReleaseManifest(
17
88
  path: string,
18
89
  expectedControllerHome: string,
@@ -50,18 +121,60 @@ export function loadRuntimeReleaseManifest(
50
121
  `RELEASE_DATABASE_SCHEMA_INCOMPATIBLE: runtime=${CONTROL_PLANE_SCHEMA_VERSION} supported=${minimum}-${maximum}`,
51
122
  );
52
123
  }
53
- let diagnosticEntrypoint: 'forge-cli' | undefined;
54
- let diagnosticArtifactIdentity: string | undefined;
55
- if (value.diagnosticEntrypoint !== undefined) {
56
- const diagnosticEntry = requireString(value.diagnosticEntrypoint, 'diagnosticEntrypoint');
57
- if (diagnosticEntry !== 'forge-cli') {
58
- throw new Error(`RELEASE_MANIFEST_INVALID: diagnosticEntrypoint must be forge-cli, got ${diagnosticEntry}`);
59
- }
60
- diagnosticEntrypoint = 'forge-cli';
61
- diagnosticArtifactIdentity = requireString(value.diagnosticArtifactIdentity, 'diagnosticArtifactIdentity');
62
- } else if (value.diagnosticArtifactIdentity !== undefined) {
63
- throw new Error('RELEASE_MANIFEST_INVALID: diagnosticArtifactIdentity requires diagnosticEntrypoint');
64
- }
124
+ const diagnostic = optionalRuntimeComponent({
125
+ value,
126
+ entryField: 'diagnosticEntrypoint',
127
+ identityField: 'diagnosticArtifactIdentity',
128
+ canonicalEntry: 'forge-cli',
129
+ });
130
+ const browserNodeBridge = optionalRuntimeComponent({
131
+ value,
132
+ entryField: 'browserNodeBridgeEntrypoint',
133
+ identityField: 'browserNodeBridgeArtifactIdentity',
134
+ canonicalEntry: 'browser-node-bridge-host.js',
135
+ });
136
+ const browserHandoff = optionalRuntimeComponent({
137
+ value,
138
+ entryField: 'browserHandoffEntrypoint',
139
+ identityField: 'browserHandoffArtifactIdentity',
140
+ canonicalEntry: 'browser-handoff-host.js',
141
+ });
142
+ const processRunner = optionalRuntimeComponent({
143
+ value,
144
+ entryField: 'processRunnerEntrypoint',
145
+ identityField: 'processRunnerArtifactIdentity',
146
+ canonicalEntry: 'process-runner.js',
147
+ });
148
+ const checkRunner = optionalRuntimeComponent({
149
+ value,
150
+ entryField: 'checkRunnerEntrypoint',
151
+ identityField: 'checkRunnerArtifactIdentity',
152
+ canonicalEntry: 'forge-check-runner',
153
+ });
154
+ const pluginActionSidecar = optionalRuntimeComponent({
155
+ value,
156
+ entryField: 'pluginActionSidecarEntrypoint',
157
+ identityField: 'pluginActionSidecarArtifactIdentity',
158
+ canonicalEntry: 'forge-plugin-action-sidecar',
159
+ });
160
+ const externalPluginProbe = optionalRuntimeComponent({
161
+ value,
162
+ entryField: 'externalPluginProbeEntrypoint',
163
+ identityField: 'externalPluginProbeArtifactIdentity',
164
+ canonicalEntry: 'external-unix-socket-probe.cjs',
165
+ });
166
+ const packageSnapshot = optionalRuntimeComponent({
167
+ value,
168
+ entryField: 'packageRoot',
169
+ identityField: 'packageArtifactIdentity',
170
+ canonicalEntry: 'package',
171
+ });
172
+ const controllerUi = optionalRuntimeComponent({
173
+ value,
174
+ entryField: 'controllerUiRoot',
175
+ identityField: 'controllerUiArtifactIdentity',
176
+ canonicalEntry: 'ui-dist',
177
+ });
65
178
  const browserAutomationHelperFields = [
66
179
  value.browserAutomationHelperEntrypoint,
67
180
  value.browserAutomationHelperArtifactIdentity,
@@ -112,11 +225,11 @@ export function loadRuntimeReleaseManifest(
112
225
  }
113
226
  codeGraphRuntime = {
114
227
  codeGraphNodeEntrypoint: 'codegraph-node',
115
- codeGraphNodeArtifactIdentity: requireString(value.codeGraphNodeArtifactIdentity, 'codeGraphNodeArtifactIdentity'),
228
+ codeGraphNodeArtifactIdentity: requireSha256Identity(value.codeGraphNodeArtifactIdentity, 'codeGraphNodeArtifactIdentity'),
116
229
  codeGraphSidecarEntrypoint: 'codegraph-sidecar.cjs',
117
- codeGraphSidecarArtifactIdentity: requireString(value.codeGraphSidecarArtifactIdentity, 'codeGraphSidecarArtifactIdentity'),
230
+ codeGraphSidecarArtifactIdentity: requireSha256Identity(value.codeGraphSidecarArtifactIdentity, 'codeGraphSidecarArtifactIdentity'),
118
231
  codeGraphLibraryRoot: 'codegraph-lib',
119
- codeGraphLibraryArtifactIdentity: requireString(value.codeGraphLibraryArtifactIdentity, 'codeGraphLibraryArtifactIdentity'),
232
+ codeGraphLibraryArtifactIdentity: requireSha256Identity(value.codeGraphLibraryArtifactIdentity, 'codeGraphLibraryArtifactIdentity'),
120
233
  };
121
234
  }
122
235
 
@@ -136,9 +249,17 @@ export function loadRuntimeReleaseManifest(
136
249
  releaseId: requireString(value.releaseId, 'releaseId'),
137
250
  artifactIdentity: requireString(value.artifactIdentity, 'artifactIdentity'),
138
251
  entrypoint: 'forge-runtime',
139
- ...(diagnosticEntrypoint ? { diagnosticEntrypoint, diagnosticArtifactIdentity } : {}),
252
+ ...(diagnostic ?? {}),
253
+ ...(browserNodeBridge ?? {}),
254
+ ...(browserHandoff ?? {}),
255
+ ...(processRunner ?? {}),
256
+ ...(checkRunner ?? {}),
257
+ ...(pluginActionSidecar ?? {}),
258
+ ...(externalPluginProbe ?? {}),
140
259
  ...(browserAutomationHelper ?? {}),
141
260
  ...(codeGraphRuntime ?? {}),
261
+ ...(packageSnapshot ?? {}),
262
+ ...(controllerUi ?? {}),
142
263
  arguments: argumentsValue as string[],
143
264
  configurationSchemaVersion: 1,
144
265
  controllerHome,
@@ -5,6 +5,8 @@ import { dirname, join, relative, resolve } from 'path';
5
5
  import { runProcess } from '../../effects/process-runner';
6
6
  import { resolveBunExecutable } from '../shared/process-environment';
7
7
  import { CONTROL_PLANE_SCHEMA_VERSION } from '../control-plane/persistence/sqlite-store';
8
+ import { loadRuntimeReleaseManifest, requireCompleteCompiledRuntimeReleaseManifest } from './release-manifest';
9
+ import { packageRuntimeFileIndex, stagePackageRuntimeSnapshot } from './package-runtime-release';
8
10
 
9
11
  /**
10
12
  * Stage one immutable Forge Runtime release below Controller Home. The staged
@@ -24,6 +26,7 @@ export interface MacOSRuntimeCodeSigning {
24
26
  }
25
27
 
26
28
  export interface StagedRuntimeRelease {
29
+ controllerHome: string;
27
30
  releasePath: string;
28
31
  manifestPath: string;
29
32
  releaseId: string;
@@ -38,6 +41,7 @@ export interface StagedRuntimeRelease {
38
41
  codeGraphNodeArtifactIdentity?: string;
39
42
  codeGraphSidecarArtifactIdentity?: string;
40
43
  codeGraphLibraryArtifactIdentity?: string;
44
+ packageArtifactIdentity?: string;
41
45
  controllerUiArtifactIdentity?: string;
42
46
  macosCodeSigning?: MacOSRuntimeCodeSigning;
43
47
  manifestSha256: string;
@@ -309,6 +313,13 @@ export function stageRuntimeReleaseFromCandidateSource(input: {
309
313
  let rawManifest: Record<string, unknown>;
310
314
  try { rawManifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as Record<string, unknown>; }
311
315
  catch (error) { throw new Error(`RUNTIME_RELEASE_CANDIDATE_MANIFEST_INVALID: ${error instanceof Error ? error.message : String(error)}`); }
316
+ const manifest = loadRuntimeReleaseManifest(manifestPath, controllerHome);
317
+ requireCompleteCompiledRuntimeReleaseManifest(manifest);
318
+ if (manifest.releaseId !== receipt.releaseId
319
+ || manifest.artifactIdentity !== receipt.artifactIdentity
320
+ || manifest.sourceCommit !== receipt.sourceCommit) {
321
+ throw new Error('RUNTIME_RELEASE_CANDIDATE_MANIFEST_RECEIPT_MISMATCH');
322
+ }
312
323
  const platform = dependencies.platform ?? process.platform;
313
324
  const macosCodeSigning = parseMacOSRuntimeCodeSigning(rawManifest.macosCodeSigning, 'RUNTIME_RELEASE_CANDIDATE_SIGNING_INVALID');
314
325
  if (platform === 'darwin') {
@@ -322,15 +333,30 @@ export function stageRuntimeReleaseFromCandidateSource(input: {
322
333
  }
323
334
  }
324
335
 
325
- return {
336
+ const staged: StagedRuntimeRelease = {
337
+ controllerHome,
326
338
  releasePath,
327
339
  manifestPath,
328
340
  releaseId: receipt.releaseId,
329
341
  artifactIdentity: receipt.artifactIdentity,
342
+ diagnosticArtifactIdentity: manifest.diagnosticArtifactIdentity,
343
+ browserNodeBridgeArtifactIdentity: manifest.browserNodeBridgeArtifactIdentity,
344
+ browserHandoffArtifactIdentity: manifest.browserHandoffArtifactIdentity,
345
+ processRunnerArtifactIdentity: manifest.processRunnerArtifactIdentity,
346
+ checkRunnerArtifactIdentity: manifest.checkRunnerArtifactIdentity,
347
+ pluginActionSidecarArtifactIdentity: manifest.pluginActionSidecarArtifactIdentity,
348
+ externalPluginProbeArtifactIdentity: manifest.externalPluginProbeArtifactIdentity,
349
+ codeGraphNodeArtifactIdentity: manifest.codeGraphNodeArtifactIdentity,
350
+ codeGraphSidecarArtifactIdentity: manifest.codeGraphSidecarArtifactIdentity,
351
+ codeGraphLibraryArtifactIdentity: manifest.codeGraphLibraryArtifactIdentity,
352
+ packageArtifactIdentity: manifest.packageArtifactIdentity,
353
+ controllerUiArtifactIdentity: manifest.controllerUiArtifactIdentity,
330
354
  ...(macosCodeSigning ? { macosCodeSigning } : {}),
331
355
  manifestSha256: receipt.manifestSha256,
332
356
  sourceCommit: receipt.sourceCommit,
333
357
  };
358
+ assertRuntimeReleaseFiles(staged, dependencies);
359
+ return staged;
334
360
  }
335
361
 
336
362
  function defaultCompileBinary(input: { sourceRoot: string; outputPath: string; entryPath?: string }): { ok: boolean; stderr?: string; stdout?: string; error?: string } {
@@ -547,6 +573,16 @@ export function stageRuntimeRelease(input: {
547
573
  cpSync(sourceControllerUiPath, controllerUiPath, { recursive: true, force: false });
548
574
  const controllerUiArtifactIdentity = `sha256:${sha256Directory(controllerUiPath)}`;
549
575
 
576
+ // The persistent OAuth/Connector is source-backed even when the primary
577
+ // Runtime itself is compiled. Co-locate one immutable package snapshot in
578
+ // the same release so standalone Recovery can bind the Connector to the
579
+ // exact active release instead of retaining an older package release.
580
+ const packageRoot = 'package' as const;
581
+ const packagePath = join(staging, packageRoot);
582
+ const packageRecords = packageRuntimeFileIndex(sourceRoot);
583
+ stagePackageRuntimeSnapshot(sourceRoot, packagePath, packageRecords);
584
+ const packageArtifactIdentity = `sha256:${sha256Directory(packagePath)}`;
585
+
550
586
  const manifest = {
551
587
  schemaVersion: 1,
552
588
  releaseId,
@@ -573,6 +609,8 @@ export function stageRuntimeRelease(input: {
573
609
  codeGraphSidecarArtifactIdentity,
574
610
  codeGraphLibraryRoot,
575
611
  codeGraphLibraryArtifactIdentity,
612
+ packageRoot,
613
+ packageArtifactIdentity,
576
614
  controllerUiRoot,
577
615
  controllerUiArtifactIdentity,
578
616
  arguments: [],
@@ -592,6 +630,7 @@ export function stageRuntimeRelease(input: {
592
630
  writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
593
631
  renameSync(staging, releasePath);
594
632
  return {
633
+ controllerHome: resolve(input.controllerHome),
595
634
  releasePath,
596
635
  manifestPath: join(releasePath, 'manifest.json'),
597
636
  releaseId,
@@ -602,10 +641,12 @@ export function stageRuntimeRelease(input: {
602
641
  browserHandoffArtifactIdentity,
603
642
  processRunnerArtifactIdentity,
604
643
  checkRunnerArtifactIdentity,
644
+ pluginActionSidecarArtifactIdentity,
605
645
  externalPluginProbeArtifactIdentity,
606
646
  codeGraphNodeArtifactIdentity,
607
647
  codeGraphSidecarArtifactIdentity,
608
648
  codeGraphLibraryArtifactIdentity,
649
+ packageArtifactIdentity,
609
650
  controllerUiArtifactIdentity,
610
651
  manifestSha256: createHash('sha256').update(`${JSON.stringify(manifest, null, 2)}\n`).digest('hex'),
611
652
  sourceCommit,
@@ -617,13 +658,53 @@ export function stageRuntimeRelease(input: {
617
658
  }
618
659
 
619
660
  export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease, dependencies: { platform?: NodeJS.Platform; inspectMacOSRuntime?: (executable: string) => MacOSRuntimeCodeSigning } = {}): void {
661
+ const assertRegularFile = (path: string, missingCode: string): void => {
662
+ if (!existsSync(path)) throw new Error(`${missingCode}: ${path}`);
663
+ const status = lstatSync(path);
664
+ if (status.isSymbolicLink() || !status.isFile()) throw new Error(`RUNTIME_RELEASE_PATH_NOT_REGULAR_FILE: ${path}`);
665
+ };
666
+ const assertExecutable = (path: string): void => {
667
+ if ((statSync(path).mode & 0o111) === 0) throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_NOT_EXECUTABLE: ${path}`);
668
+ };
669
+ const assertFileIdentity = (path: string, expected: string): void => {
670
+ const actual = `sha256:${sha256(path)}`;
671
+ if (actual !== expected) throw new Error(`RUNTIME_RELEASE_ARTIFACT_IDENTITY_MISMATCH: ${path}`);
672
+ };
673
+ const assertDirectoryIdentity = (path: string, expected: string): void => {
674
+ if (!existsSync(path)) throw new Error(`RUNTIME_RELEASE_DIRECTORY_MISSING: ${path}`);
675
+ const status = lstatSync(path);
676
+ if (status.isSymbolicLink() || !status.isDirectory()) throw new Error(`RUNTIME_RELEASE_PATH_NOT_DIRECTORY: ${path}`);
677
+ const actual = `sha256:${sha256Directory(path)}`;
678
+ if (actual !== expected) throw new Error(`RUNTIME_RELEASE_ARTIFACT_IDENTITY_MISMATCH: ${path}`);
679
+ };
680
+ const assertComponentFile = (input: { path: string; identity?: string; missingCode: string; executable?: boolean }): void => {
681
+ if (!input.identity) return;
682
+ assertRegularFile(input.path, input.missingCode);
683
+ assertFileIdentity(input.path, input.identity);
684
+ if (input.executable) assertExecutable(input.path);
685
+ };
686
+
620
687
  if (!existsSync(release.releasePath) || !existsSync(release.manifestPath)) {
621
688
  throw new Error(`RUNTIME_RELEASE_FILES_MISSING: ${release.releasePath}`);
622
689
  }
623
- const runtimePath = join(release.releasePath, 'forge-runtime');
624
- if (!existsSync(runtimePath)) {
625
- throw new Error(`RUNTIME_RELEASE_ENTRYPOINT_MISSING: ${runtimePath}`);
690
+ const releasesRoot = join(resolve(release.controllerHome), 'runtime', 'releases');
691
+ if (resolve(release.releasePath) !== join(releasesRoot, release.releaseId)
692
+ || resolve(release.manifestPath) !== join(release.releasePath, 'manifest.json')) {
693
+ throw new Error('RUNTIME_RELEASE_PATH_OUTSIDE_CONTROLLER_HOME');
694
+ }
695
+ const releaseStatus = lstatSync(release.releasePath);
696
+ const manifestStatus = lstatSync(release.manifestPath);
697
+ if (releaseStatus.isSymbolicLink() || !releaseStatus.isDirectory()
698
+ || manifestStatus.isSymbolicLink() || !manifestStatus.isFile()) {
699
+ throw new Error(`RUNTIME_RELEASE_PATH_NOT_PHYSICAL: ${release.releasePath}`);
626
700
  }
701
+ if (sha256(release.manifestPath) !== release.manifestSha256) {
702
+ throw new Error('RUNTIME_RELEASE_MANIFEST_IDENTITY_MISMATCH');
703
+ }
704
+ const runtimePath = join(release.releasePath, 'forge-runtime');
705
+ assertRegularFile(runtimePath, 'RUNTIME_RELEASE_ENTRYPOINT_MISSING');
706
+ assertExecutable(runtimePath);
707
+ assertFileIdentity(runtimePath, release.artifactIdentity);
627
708
  const platform = dependencies.platform ?? process.platform;
628
709
  if (platform === 'darwin' && release.macosCodeSigning) {
629
710
  const actual = (dependencies.inspectMacOSRuntime ?? inspectMacOSRuntimeCodeSigning)(runtimePath);
@@ -634,38 +715,31 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease, depende
634
715
  throw new Error('RUNTIME_RELEASE_MACOS_SIGNING_MISMATCH');
635
716
  }
636
717
  }
637
- if (release.diagnosticArtifactIdentity && !existsSync(join(release.releasePath, 'forge-cli'))) {
638
- throw new Error(`RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_MISSING: ${join(release.releasePath, 'forge-cli')}`);
639
- }
640
- if (release.browserNodeBridgeArtifactIdentity && !existsSync(join(release.releasePath, 'browser-node-bridge-host.js'))) {
641
- throw new Error(`RUNTIME_RELEASE_BROWSER_NODE_HOST_MISSING: ${join(release.releasePath, 'browser-node-bridge-host.js')}`);
642
- }
643
- if (release.browserHandoffArtifactIdentity && !existsSync(join(release.releasePath, 'browser-handoff-host.js'))) {
644
- throw new Error(`RUNTIME_RELEASE_BROWSER_HANDOFF_HOST_MISSING: ${join(release.releasePath, 'browser-handoff-host.js')}`);
645
- }
646
- if (release.processRunnerArtifactIdentity && !existsSync(join(release.releasePath, 'process-runner.js'))) {
647
- throw new Error(`RUNTIME_RELEASE_PROCESS_RUNNER_MISSING: ${join(release.releasePath, 'process-runner.js')}`);
648
- }
649
- if (release.checkRunnerArtifactIdentity && !existsSync(join(release.releasePath, 'forge-check-runner'))) {
650
- throw new Error(`RUNTIME_RELEASE_CHECK_RUNNER_MISSING: ${join(release.releasePath, 'forge-check-runner')}`);
651
- }
652
- if (release.pluginActionSidecarArtifactIdentity && !existsSync(join(release.releasePath, 'forge-plugin-action-sidecar'))) {
653
- throw new Error(`RUNTIME_RELEASE_PLUGIN_ACTION_SIDECAR_MISSING: ${join(release.releasePath, 'forge-plugin-action-sidecar')}`);
654
- }
655
- if (release.externalPluginProbeArtifactIdentity && !existsSync(join(release.releasePath, 'external-unix-socket-probe.cjs'))) {
656
- throw new Error(`RUNTIME_RELEASE_EXTERNAL_PLUGIN_PROBE_MISSING: ${join(release.releasePath, 'external-unix-socket-probe.cjs')}`);
657
- }
658
- if (release.codeGraphNodeArtifactIdentity && !existsSync(join(release.releasePath, 'codegraph-node'))) {
659
- throw new Error(`RUNTIME_RELEASE_CODEGRAPH_NODE_MISSING: ${join(release.releasePath, 'codegraph-node')}`);
660
- }
661
- if (release.codeGraphSidecarArtifactIdentity && !existsSync(join(release.releasePath, 'codegraph-sidecar.cjs'))) {
662
- throw new Error(`RUNTIME_RELEASE_CODEGRAPH_SIDECAR_MISSING: ${join(release.releasePath, 'codegraph-sidecar.cjs')}`);
718
+ assertComponentFile({ path: join(release.releasePath, 'forge-cli'), identity: release.diagnosticArtifactIdentity, missingCode: 'RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_MISSING', executable: true });
719
+ assertComponentFile({ path: join(release.releasePath, 'browser-node-bridge-host.js'), identity: release.browserNodeBridgeArtifactIdentity, missingCode: 'RUNTIME_RELEASE_BROWSER_NODE_HOST_MISSING', executable: true });
720
+ assertComponentFile({ path: join(release.releasePath, 'browser-handoff-host.js'), identity: release.browserHandoffArtifactIdentity, missingCode: 'RUNTIME_RELEASE_BROWSER_HANDOFF_HOST_MISSING', executable: true });
721
+ assertComponentFile({ path: join(release.releasePath, 'process-runner.js'), identity: release.processRunnerArtifactIdentity, missingCode: 'RUNTIME_RELEASE_PROCESS_RUNNER_MISSING', executable: true });
722
+ assertComponentFile({ path: join(release.releasePath, 'forge-check-runner'), identity: release.checkRunnerArtifactIdentity, missingCode: 'RUNTIME_RELEASE_CHECK_RUNNER_MISSING', executable: true });
723
+ assertComponentFile({ path: join(release.releasePath, 'forge-plugin-action-sidecar'), identity: release.pluginActionSidecarArtifactIdentity, missingCode: 'RUNTIME_RELEASE_PLUGIN_ACTION_SIDECAR_MISSING', executable: true });
724
+ assertComponentFile({ path: join(release.releasePath, 'external-unix-socket-probe.cjs'), identity: release.externalPluginProbeArtifactIdentity, missingCode: 'RUNTIME_RELEASE_EXTERNAL_PLUGIN_PROBE_MISSING', executable: true });
725
+ assertComponentFile({ path: join(release.releasePath, 'codegraph-node'), identity: release.codeGraphNodeArtifactIdentity, missingCode: 'RUNTIME_RELEASE_CODEGRAPH_NODE_MISSING', executable: true });
726
+ assertComponentFile({ path: join(release.releasePath, 'codegraph-sidecar.cjs'), identity: release.codeGraphSidecarArtifactIdentity, missingCode: 'RUNTIME_RELEASE_CODEGRAPH_SIDECAR_MISSING', executable: true });
727
+ if (release.codeGraphLibraryArtifactIdentity) {
728
+ const libraryRoot = join(release.releasePath, 'codegraph-lib');
729
+ if (!existsSync(join(libraryRoot, 'dist', 'index.js'))) {
730
+ throw new Error(`RUNTIME_RELEASE_CODEGRAPH_LIBRARY_MISSING: ${join(libraryRoot, 'dist', 'index.js')}`);
731
+ }
732
+ assertDirectoryIdentity(libraryRoot, release.codeGraphLibraryArtifactIdentity);
663
733
  }
664
- if (release.codeGraphLibraryArtifactIdentity && !existsSync(join(release.releasePath, 'codegraph-lib', 'dist', 'index.js'))) {
665
- throw new Error(`RUNTIME_RELEASE_CODEGRAPH_LIBRARY_MISSING: ${join(release.releasePath, 'codegraph-lib', 'dist', 'index.js')}`);
734
+ if (release.packageArtifactIdentity) {
735
+ const packageRoot = join(release.releasePath, 'package');
736
+ assertDirectoryIdentity(packageRoot, release.packageArtifactIdentity);
666
737
  }
667
- if (release.controllerUiArtifactIdentity
668
- && (!existsSync(join(release.releasePath, 'ui-dist', 'app.js')) || !existsSync(join(release.releasePath, 'ui-dist', 'app.css')))) {
669
- throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_MISSING: ${join(release.releasePath, 'ui-dist')}`);
738
+ if (release.controllerUiArtifactIdentity) {
739
+ const uiRoot = join(release.releasePath, 'ui-dist');
740
+ if (!existsSync(join(uiRoot, 'app.js')) || !existsSync(join(uiRoot, 'app.css'))) {
741
+ throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_MISSING: ${uiRoot}`);
742
+ }
743
+ assertDirectoryIdentity(uiRoot, release.controllerUiArtifactIdentity);
670
744
  }
671
745
  }
@@ -39,8 +39,14 @@ export interface RuntimeReleaseManifest {
39
39
  browserNodeBridgeArtifactIdentity?: string;
40
40
  browserHandoffEntrypoint?: 'browser-handoff-host.js';
41
41
  browserHandoffArtifactIdentity?: string;
42
+ processRunnerEntrypoint?: 'process-runner.js';
43
+ processRunnerArtifactIdentity?: string;
44
+ checkRunnerEntrypoint?: 'forge-check-runner';
45
+ checkRunnerArtifactIdentity?: string;
42
46
  pluginActionSidecarEntrypoint?: 'forge-plugin-action-sidecar';
43
47
  pluginActionSidecarArtifactIdentity?: string;
48
+ externalPluginProbeEntrypoint?: 'external-unix-socket-probe.cjs';
49
+ externalPluginProbeArtifactIdentity?: string;
44
50
  browserAutomationHelperEntrypoint?: 'browser-automation-helper';
45
51
  browserAutomationHelperArtifactIdentity?: string;
46
52
  browserAutomationHelperContractIdentity?: string;
@@ -50,6 +56,10 @@ export interface RuntimeReleaseManifest {
50
56
  codeGraphSidecarArtifactIdentity?: string;
51
57
  codeGraphLibraryRoot?: 'codegraph-lib';
52
58
  codeGraphLibraryArtifactIdentity?: string;
59
+ packageRoot?: 'package';
60
+ packageArtifactIdentity?: string;
61
+ controllerUiRoot?: 'ui-dist';
62
+ controllerUiArtifactIdentity?: string;
53
63
  arguments: string[];
54
64
  configurationSchemaVersion: 1;
55
65
  controllerHome: string;