@moretea-labs/forge 1.5.0-rc.1 → 1.5.1

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 (264) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +27 -1
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +52 -55
  7. package/SKILL.md +8 -11
  8. package/THIRD_PARTY_NOTICES.md +9 -3
  9. package/assets/branding/README.md +4 -2
  10. package/assets/hooks/lib/workflow-state.sh +19 -18
  11. package/assets/hooks/post-edit-guard.sh +6 -75
  12. package/assets/hooks/prompt-guard.sh +24 -12
  13. package/assets/hooks/session-start-context.sh +17 -17
  14. package/assets/hooks/stop-orchestrator.sh +2 -2
  15. package/assets/partials/04-project-structure.partial.md +6 -6
  16. package/assets/partials/05-workflow.partial.md +6 -6
  17. package/assets/partials/07-footer.partial.md +1 -1
  18. package/assets/partials/08-orchestration.partial.md +2 -2
  19. package/assets/partials-agents/02-operating-mode.partial.md +5 -5
  20. package/assets/partials-agents/03-orchestration.partial.md +3 -3
  21. package/assets/partials-agents/04-task-protocol.partial.md +3 -3
  22. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  23. package/assets/reference-configs/agentic-development-flow.md +49 -86
  24. package/assets/reference-configs/external-tooling.md +21 -61
  25. package/assets/reference-configs/handoff-protocol.md +25 -20
  26. package/assets/reference-configs/harness-overview.md +8 -8
  27. package/assets/skill-version.json +2 -2
  28. package/assets/templates/helpers/archive-workflow.sh +0 -1
  29. package/assets/templates/helpers/capture-plan.sh +3 -3
  30. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  31. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  32. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  33. package/assets/templates/helpers/new-plan.sh +1 -1
  34. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  35. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  36. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  37. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  38. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  39. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  40. package/assets/templates/helpers/verify-sprint.sh +7 -7
  41. package/assets/templates/helpers/workstream-sync.sh +1 -1
  42. package/assets/templates/plan.template.md +1 -1
  43. package/assets/workflow-contract.v1.json +6 -3
  44. package/docs/architecture/current/README.md +10 -9
  45. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  46. package/docs/architecture/current/architecture-invariants.md +2 -2
  47. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  48. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  49. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  50. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  51. package/docs/architecture/current/entity-model.md +0 -27
  52. package/docs/architecture/current/failure-recovery.md +0 -24
  53. package/docs/architecture/current/governance.md +1 -1
  54. package/docs/architecture/current/implementation-status.md +1 -1
  55. package/docs/architecture/current/legacy-governance.md +58 -0
  56. package/docs/architecture/current/multi-repository-execution.md +1 -1
  57. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  58. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  59. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  60. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  61. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  62. package/docs/architecture/current/thin-harness-v1.md +9 -11
  63. package/docs/architecture/domains/runtime-harness.md +1 -1
  64. package/docs/forge-autonomous-goal-loop.md +3 -1
  65. package/docs/forge-chatgpt-mcp-setup.md +67 -176
  66. package/docs/images/forge-banner-cn.svg +26 -1
  67. package/docs/images/forge-banner.svg +26 -1
  68. package/docs/operations/features.md +1 -1
  69. package/docs/operations/homebrew.md +3 -3
  70. package/docs/operations/mcp-tool-exposure.md +5 -5
  71. package/docs/operations/platform-support.md +45 -37
  72. package/docs/operations/platform-support.zh-CN.md +40 -38
  73. package/docs/operations/releasing.md +3 -3
  74. package/docs/operations/releasing.zh-CN.md +3 -3
  75. package/docs/operations/troubleshooting.md +11 -6
  76. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  77. package/docs/public-usage-guide.md +28 -21
  78. package/docs/public-usage-guide.zh-CN.md +28 -21
  79. package/docs/tutorials/01-install-and-start.md +58 -35
  80. package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
  81. package/docs/tutorials/02-connect-chatgpt.md +118 -46
  82. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -45
  83. package/docs/wiki/Architecture.md +1 -1
  84. package/docs/wiki/Installation.md +2 -2
  85. package/docs/wiki/Quick-Start.md +1 -1
  86. package/docs/wiki/Work-Lifecycle.md +1 -1
  87. package/install.ps1 +4 -5
  88. package/install.sh +4 -6
  89. package/package.json +15 -5
  90. package/scripts/archive-workflow.sh +0 -1
  91. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  92. package/scripts/build-controller-ui.ts +28 -0
  93. package/scripts/capture-plan.sh +4 -4
  94. package/scripts/check-agent-tooling.sh +12 -1
  95. package/scripts/check-platform-support.mjs +2 -2
  96. package/scripts/check-public-docs.sh +16 -5
  97. package/scripts/check-release-readiness.sh +5 -11
  98. package/scripts/check-runtime-architecture.mjs +14 -22
  99. package/scripts/check-tarball-install-smoke.sh +3 -2
  100. package/scripts/check-task-workflow.sh +2 -2
  101. package/scripts/codex-handoff-resume.sh +5 -5
  102. package/scripts/create-project-dirs.sh +3 -3
  103. package/scripts/ensure-task-workflow.sh +45 -35
  104. package/scripts/init-project.sh +4 -4
  105. package/scripts/install-desktop-operator-registration.ts +2 -2
  106. package/scripts/lib/project-init-lib.sh +46 -44
  107. package/scripts/migrate-project-template.sh +7 -4
  108. package/scripts/new-plan.sh +1 -1
  109. package/scripts/plan-to-todo.sh +2 -2
  110. package/scripts/prepare-codex-handoff.sh +2 -2
  111. package/scripts/prepare-handoff.sh +9 -9
  112. package/scripts/refresh-current-status.sh +2 -2
  113. package/scripts/setup-plugins.sh +3 -3
  114. package/scripts/ship-worktrees.sh +1 -1
  115. package/scripts/sprint-backlog.sh +5 -5
  116. package/scripts/sync-codex-installed-copies.sh +2 -3
  117. package/scripts/verify-sprint.sh +7 -7
  118. package/scripts/workstream-sync.sh +1 -1
  119. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  120. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  121. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  122. package/src/cli/commands/chatgpt.ts +51 -0
  123. package/src/cli/commands/doctor.ts +1 -1
  124. package/src/cli/commands/init-hook.ts +213 -70
  125. package/src/cli/commands/init.ts +5 -6
  126. package/src/cli/commands/mcp.ts +31 -5
  127. package/src/cli/commands/runtime.ts +61 -8
  128. package/src/cli/commands/setup-profile.ts +472 -0
  129. package/src/cli/controller/check-runner.ts +20 -3
  130. package/src/cli/controller/codex-continuation.ts +4 -7
  131. package/src/cli/controller/context-pack.ts +118 -14
  132. package/src/cli/controller/operational-plan.ts +4 -4
  133. package/src/cli/controller/task-ledger.ts +10 -10
  134. package/src/cli/controller/work-mode.ts +6 -13
  135. package/src/cli/index.ts +14 -35
  136. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  137. package/src/cli/local-bridge/console-assets.ts +17 -0
  138. package/src/cli/local-bridge/console-automations.ts +160 -0
  139. package/src/cli/local-bridge/dashboard.ts +8 -1303
  140. package/src/cli/local-bridge/facade-api.ts +16 -7
  141. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  142. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  144. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  146. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  147. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  148. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  149. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  150. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  152. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  153. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  155. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  156. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  157. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  158. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  159. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  160. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  161. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  162. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  163. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  164. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  165. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  166. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  167. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  168. package/src/cli/local-bridge/server.ts +45 -0
  169. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  170. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  171. package/src/cli/mcp/auth.ts +2 -0
  172. package/src/cli/mcp/instructions.ts +3 -3
  173. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  174. package/src/cli/mcp/multi-repository.ts +2 -0
  175. package/src/cli/mcp/policy.ts +7 -5
  176. package/src/cli/mcp/repository-tools.ts +53 -14
  177. package/src/cli/mcp/server.ts +5 -10
  178. package/src/cli/mcp/setup.ts +220 -276
  179. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  180. package/src/cli/mcp/toolset-names.ts +1 -1
  181. package/src/cli/mcp/toolset.ts +0 -1
  182. package/src/cli/mcp/transports/http.ts +51 -4
  183. package/src/cli/repositories/branch-name-policy.ts +0 -7
  184. package/src/cli/repositories/controller-home.ts +110 -3
  185. package/src/cli/repositories/safe-patch.ts +1 -1
  186. package/src/cli/repositories/selected-path-actions.ts +26 -24
  187. package/src/core/adoption/gitignore-plan.ts +2 -3
  188. package/src/core/adoption/plan.ts +4 -1
  189. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  190. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  191. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  192. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  193. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  194. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  195. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  196. package/src/runtime/control-plane/facade/types.ts +0 -1
  197. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  198. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  199. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  200. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  201. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  202. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  203. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  204. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  205. package/src/runtime/evidence/event-ledger.ts +1 -1
  206. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  207. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  208. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  209. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  210. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  211. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  212. package/src/runtime/execution/process-runtime/index.ts +3 -0
  213. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  214. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  215. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  216. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  217. package/src/runtime/execution/process-runtime/store.ts +1 -19
  218. package/src/runtime/execution/process-runtime/types.ts +4 -0
  219. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  220. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  221. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  222. package/src/runtime/execution/thin-harness/types.ts +0 -1
  223. package/src/runtime/execution/workers/executor.ts +1 -1
  224. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  225. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  226. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  227. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  228. package/src/runtime/gateway/mcp/router.ts +117 -182
  229. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  230. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  231. package/src/runtime/plugins/browser-adapter.ts +5 -0
  232. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  233. package/src/runtime/plugins/store.ts +14 -6
  234. package/src/runtime/plugins/types.ts +1 -1
  235. package/src/runtime/projections/materialized-view.ts +1 -23
  236. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  237. package/src/runtime/resources/ownership.ts +0 -1
  238. package/src/runtime/root/gateway-adapter.ts +2 -0
  239. package/src/runtime/root/mcp-transport.ts +10 -2
  240. package/src/runtime/root/package-connector-service.ts +136 -0
  241. package/src/runtime/root/package-runtime-release.ts +161 -0
  242. package/src/runtime/root/package-runtime-service.ts +185 -0
  243. package/src/runtime/root/release-materialize.ts +19 -0
  244. package/src/runtime/root/runtime.ts +2 -1
  245. package/src/runtime/root/scheduler.ts +5 -0
  246. package/src/runtime/root/service.ts +18 -5
  247. package/src/runtime/standalone-recovery/core.ts +65 -16
  248. package/src/runtime/workflow/findings/store.ts +145 -106
  249. package/src/runtime/workflow/findings/types.ts +22 -0
  250. package/src/runtime/workflow/schedules/engine.ts +132 -2
  251. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  252. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  253. package/docs/CHANGELOG.md +0 -978
  254. package/docs/README.zh-CN.md +0 -58
  255. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  256. package/src/cli/commands/controller.ts +0 -680
  257. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  258. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  259. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  260. package/src/runtime/workflow/campaigns/review.ts +0 -315
  261. package/src/runtime/workflow/campaigns/store.ts +0 -526
  262. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  263. package/src/runtime/workflow/campaigns/types.ts +0 -290
  264. /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
@@ -0,0 +1,161 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFileSync } from 'fs';
3
+ import { dirname, join, relative, resolve } from 'path';
4
+ import { ensureControllerHome } from '../../cli/repositories/controller-home';
5
+ import { CONTROL_PLANE_SCHEMA_VERSION } from '../control-plane/persistence/sqlite-store';
6
+ import { packageRuntimeSourceRoot } from '../control-plane/runtime-generation';
7
+ import { FORGE_VERSION } from '../../version';
8
+ import { publishRuntimeRelease, type RuntimeReleaseAuthority } from './release-store';
9
+ import type { RuntimeReleaseManifest } from './types';
10
+
11
+ export interface PackageRuntimeFileRecord {
12
+ path: string;
13
+ sha256: string;
14
+ bytes: number;
15
+ }
16
+
17
+ export interface PackageRuntimeRelease {
18
+ releaseId: string;
19
+ releaseRoot: string;
20
+ packageRoot: string;
21
+ packageVersion: string;
22
+ packageFingerprint: string;
23
+ artifactIdentity: string;
24
+ entrypointPath: string;
25
+ manifestPath: string;
26
+ indexPath: string;
27
+ fileCount: number;
28
+ authority: RuntimeReleaseAuthority;
29
+ }
30
+
31
+ const PACKAGE_RUNTIME_ROOTS = ['src', 'bin', 'assets', 'scripts'] as const;
32
+ const PACKAGE_RUNTIME_FILES = ['package.json'] as const;
33
+
34
+ function sha256(value: Buffer | string): string {
35
+ return createHash('sha256').update(value).digest('hex');
36
+ }
37
+
38
+ function atomicWrite(path: string, content: string, mode = 0o600): void {
39
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
40
+ const temporary = `${path}.${process.pid}.${randomUUID().slice(0, 8)}.tmp`;
41
+ writeFileSync(temporary, content, { encoding: 'utf8', mode });
42
+ renameSync(temporary, path);
43
+ }
44
+
45
+ function walkRegularFiles(root: string, current: string, output: string[]): void {
46
+ if (!existsSync(current)) return;
47
+ const stat = lstatSync(current);
48
+ if (stat.isSymbolicLink()) return;
49
+ if (stat.isFile()) {
50
+ output.push(relative(root, current).split('\\').join('/'));
51
+ return;
52
+ }
53
+ if (!stat.isDirectory()) return;
54
+ for (const entry of readdirSync(current).sort()) walkRegularFiles(root, join(current, entry), output);
55
+ }
56
+
57
+ export function packageRuntimeFileIndex(packageRoot = packageRuntimeSourceRoot()): PackageRuntimeFileRecord[] {
58
+ const root = resolve(packageRoot);
59
+ const paths: string[] = [];
60
+ for (const directory of PACKAGE_RUNTIME_ROOTS) walkRegularFiles(root, join(root, directory), paths);
61
+ for (const file of PACKAGE_RUNTIME_FILES) walkRegularFiles(root, join(root, file), paths);
62
+ const unique = [...new Set(paths)].sort();
63
+ if (!unique.includes('package.json') || !unique.some((path) => path === 'bin/forge-runtime.mjs')) {
64
+ throw new Error(`PACKAGE_RUNTIME_SURFACE_INCOMPLETE: ${root}`);
65
+ }
66
+ return unique.map((path) => {
67
+ const bytes = readFileSync(join(root, path));
68
+ return { path, sha256: sha256(bytes), bytes: bytes.length };
69
+ });
70
+ }
71
+
72
+ export function packageRuntimeFingerprint(records: PackageRuntimeFileRecord[]): string {
73
+ const hash = createHash('sha256');
74
+ for (const record of records) hash.update(record.path).update('\0').update(record.sha256).update('\0').update(String(record.bytes)).update('\n');
75
+ return hash.digest('hex');
76
+ }
77
+
78
+ function packageVersion(root: string): string {
79
+ try {
80
+ const value = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as { name?: unknown; version?: unknown };
81
+ if (value.name !== '@moretea-labs/forge') throw new Error('package name mismatch');
82
+ if (typeof value.version !== 'string' || !value.version.trim()) throw new Error('package version missing');
83
+ return value.version.trim();
84
+ } catch (error) {
85
+ throw new Error(`PACKAGE_RUNTIME_IDENTITY_INVALID: ${error instanceof Error ? error.message : String(error)}`);
86
+ }
87
+ }
88
+
89
+ function launcherSource(input: { packageRoot: string; indexPath: string; indexSha256: string }): string {
90
+ return `#!/usr/bin/env node\n`
91
+ + `import { createHash } from 'node:crypto';\n`
92
+ + `import { existsSync, readFileSync } from 'node:fs';\n`
93
+ + `import { join } from 'node:path';\n`
94
+ + `import { spawn } from 'node:child_process';\n`
95
+ + `const packageRoot=${JSON.stringify(input.packageRoot)};\n`
96
+ + `const indexPath=${JSON.stringify(input.indexPath)};\n`
97
+ + `const expectedIndex=${JSON.stringify(input.indexSha256)};\n`
98
+ + `const digest=(v)=>createHash('sha256').update(v).digest('hex');\n`
99
+ + `let raw; try { raw=readFileSync(indexPath); } catch (error) { console.error('FORGE_PACKAGE_RUNTIME_INDEX_MISSING: '+error.message); process.exit(78); }\n`
100
+ + `if(digest(raw)!==expectedIndex){console.error('FORGE_PACKAGE_RUNTIME_INDEX_CHANGED');process.exit(78);}\n`
101
+ + `let records; try { records=JSON.parse(raw.toString('utf8')).files; } catch { console.error('FORGE_PACKAGE_RUNTIME_INDEX_INVALID'); process.exit(78); }\n`
102
+ + `for(const record of records){const path=join(packageRoot,record.path);if(!existsSync(path)||digest(readFileSync(path))!==record.sha256){console.error('FORGE_PACKAGE_RUNTIME_SOURCE_CHANGED: '+record.path);process.exit(78);}}\n`
103
+ + `const runtime=join(packageRoot,'bin','forge-runtime.mjs');\n`
104
+ + `const child=spawn(process.execPath,[runtime,...process.argv.slice(2)],{stdio:'inherit',env:{...process.env,FORGE_FORCE_NODE:'1'}});\n`
105
+ + `for(const signal of ['SIGINT','SIGTERM','SIGHUP']){try{process.on(signal,()=>child.kill(signal));}catch{}}\n`
106
+ + `child.on('error',(error)=>{console.error('FORGE_PACKAGE_RUNTIME_LAUNCH_FAILED: '+error.message);process.exit(1);});\n`
107
+ + `child.on('exit',(code)=>process.exit(code??1));\n`;
108
+ }
109
+
110
+ export function materializePackageRuntimeRelease(input: {
111
+ controllerHome?: string;
112
+ packageRoot?: string;
113
+ operationId?: string;
114
+ } = {}): PackageRuntimeRelease {
115
+ const controllerHome = ensureControllerHome(input.controllerHome);
116
+ const packageRoot = resolve(input.packageRoot ?? packageRuntimeSourceRoot());
117
+ const version = packageVersion(packageRoot);
118
+ const records = packageRuntimeFileIndex(packageRoot);
119
+ const fingerprint = packageRuntimeFingerprint(records);
120
+ const safeVersion = version.replace(/[^A-Za-z0-9._-]+/g, '-');
121
+ const releaseId = `package-${safeVersion}-${fingerprint.slice(0, 16)}`;
122
+ const releaseRoot = join(controllerHome, 'runtime', 'releases', releaseId);
123
+ const indexPath = join(releaseRoot, 'package-files.json');
124
+ const indexJson = `${JSON.stringify({ schemaVersion: 1, package: '@moretea-labs/forge', version, fingerprint, files: records }, null, 2)}\n`;
125
+ atomicWrite(indexPath, indexJson);
126
+ const entrypointPath = join(releaseRoot, 'forge-runtime');
127
+ const launcher = launcherSource({ packageRoot, indexPath, indexSha256: sha256(Buffer.from(indexJson)) });
128
+ atomicWrite(entrypointPath, launcher, 0o700);
129
+ chmodSync(entrypointPath, 0o700);
130
+ const artifactIdentity = `sha256:${sha256(Buffer.from(launcher))}`;
131
+ const manifest: RuntimeReleaseManifest = {
132
+ schemaVersion: 1,
133
+ releaseId,
134
+ artifactIdentity,
135
+ entrypoint: 'forge-runtime',
136
+ arguments: [],
137
+ configurationSchemaVersion: 1,
138
+ controllerHome,
139
+ databaseSchemaCompatibility: { minimum: CONTROL_PLANE_SCHEMA_VERSION, maximum: CONTROL_PLANE_SCHEMA_VERSION },
140
+ workerProtocolVersion: 1,
141
+ releaseRevision: `package:${version}:${fingerprint}`,
142
+ cleanWorkspace: true,
143
+ createdAt: new Date().toISOString(),
144
+ };
145
+ const manifestPath = join(releaseRoot, 'manifest.json');
146
+ atomicWrite(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
147
+ const authority = publishRuntimeRelease(controllerHome, manifestPath, input.operationId ?? `package-runtime-${FORGE_VERSION}-${Date.now()}`);
148
+ return {
149
+ releaseId,
150
+ releaseRoot,
151
+ packageRoot,
152
+ packageVersion: version,
153
+ packageFingerprint: fingerprint,
154
+ artifactIdentity,
155
+ entrypointPath,
156
+ manifestPath,
157
+ indexPath,
158
+ fileCount: records.length,
159
+ authority,
160
+ };
161
+ }
@@ -0,0 +1,185 @@
1
+ import { spawn, spawnSync, type SpawnSyncReturns } from 'child_process';
2
+ import { closeSync, mkdirSync, openSync, renameSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { dirname, join, resolve } from 'path';
5
+ import { resolveControllerHome } from '../../cli/repositories/controller-home';
6
+ import { RUNTIME_WRITE_CLAIM_ENV } from './write-fence';
7
+ import {
8
+ activeRuntimeEntrypoint,
9
+ activeRuntimeLaunchSpec,
10
+ forgeRuntimeServicePaths,
11
+ installForgeRuntimeService,
12
+ syncForgeRuntimeActiveEntrypoint,
13
+ writeForgeRuntimeServiceConfig,
14
+ type ForgeRuntimeServiceConfig,
15
+ } from './service';
16
+ import { materializePackageRuntimeRelease, type PackageRuntimeRelease } from './package-runtime-release';
17
+ import { loadMcpServiceLocalConfig } from '../../cli/mcp/auth';
18
+ import { installPackageConnectorService, type PackageConnectorServiceResult } from './package-connector-service';
19
+
20
+ export type PackageRuntimeServiceMode = 'launchd' | 'systemd-user' | 'portable';
21
+
22
+ export interface PackageRuntimeServiceInstallResult {
23
+ status: 'installed';
24
+ mode: PackageRuntimeServiceMode;
25
+ persistent: boolean;
26
+ controllerHome: string;
27
+ release: PackageRuntimeRelease;
28
+ servicePath?: string;
29
+ pid?: number;
30
+ warnings: string[];
31
+ connector?: PackageConnectorServiceResult;
32
+ }
33
+
34
+ export interface PackageRuntimeServiceOptions {
35
+ controllerHome?: string;
36
+ packageRoot?: string;
37
+ host?: string;
38
+ port?: number;
39
+ authTokenFile: string;
40
+ exclusiveWorkId?: string;
41
+ platform?: NodeJS.Platform;
42
+ env?: NodeJS.ProcessEnv;
43
+ forcePortable?: boolean;
44
+ }
45
+
46
+ function atomicWrite(path: string, content: string, mode = 0o600): void {
47
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
48
+ const temporary = `${path}.${process.pid}.tmp`;
49
+ writeFileSync(temporary, content, { encoding: 'utf8', mode });
50
+ renameSync(temporary, path);
51
+ }
52
+
53
+ function systemdEscape(value: string): string {
54
+ return `"${value.replaceAll('%', '%%').replaceAll('\\', '\\\\').replaceAll('"', '\\"')}"`;
55
+ }
56
+
57
+ export function renderForgeRuntimeSystemdUserUnit(input: {
58
+ description?: string;
59
+ executable: string;
60
+ args: string[];
61
+ environment: Record<string, string>;
62
+ }): string {
63
+ const environment = Object.entries(input.environment)
64
+ .sort(([left], [right]) => left.localeCompare(right))
65
+ .map(([key, value]) => `Environment=${systemdEscape(`${key}=${value}`)}`)
66
+ .join('\n');
67
+ return [
68
+ '[Unit]',
69
+ `Description=${input.description ?? 'Forge Runtime'}`,
70
+ 'After=network-online.target',
71
+ '',
72
+ '[Service]',
73
+ 'Type=simple',
74
+ `ExecStart=${[input.executable, ...input.args].map(systemdEscape).join(' ')}`,
75
+ ...(environment ? [environment] : []),
76
+ 'Restart=on-failure',
77
+ 'RestartSec=5',
78
+ '',
79
+ '[Install]',
80
+ 'WantedBy=default.target',
81
+ '',
82
+ ].join('\n');
83
+ }
84
+
85
+ function commandSucceeded(command: string, args: string[], env: NodeJS.ProcessEnv): SpawnSyncReturns<string> {
86
+ return spawnSync(command, args, { encoding: 'utf8', env, timeout: 30_000 });
87
+ }
88
+
89
+ export function systemdUserAvailable(env: NodeJS.ProcessEnv = process.env): boolean {
90
+ const result = commandSucceeded('systemctl', ['--user', 'show-environment'], env);
91
+ return result.status === 0;
92
+ }
93
+
94
+ function cleanRuntimeInstallerEnvironment(env: NodeJS.ProcessEnv, releaseEnvironment: Record<string, string>): NodeJS.ProcessEnv {
95
+ const next = { ...env };
96
+ for (const key of Object.values(RUNTIME_WRITE_CLAIM_ENV)) delete next[key];
97
+ delete next.FORGE_CONTROLLER_LIFECYCLE_OWNER;
98
+ delete next.FORGE_RELEASE_PATH;
99
+ delete next.FORGE_RELEASE_REVISION;
100
+ delete next.FORGE_RELEASE_SOURCE_COMMIT;
101
+ delete next.FORGE_RELEASE_CLEAN_WORKSPACE;
102
+ return { ...next, ...releaseEnvironment };
103
+ }
104
+
105
+ function installSystemdUserService(controllerHome: string, env: NodeJS.ProcessEnv): string {
106
+ const entrypoint = activeRuntimeEntrypoint(controllerHome);
107
+ const launch = activeRuntimeLaunchSpec(controllerHome);
108
+ if (!entrypoint || !launch) throw new Error('FORGE_PACKAGE_RUNTIME_LAUNCH_SPEC_MISSING');
109
+ const label = forgeRuntimeServicePaths(controllerHome).label;
110
+ const unitName = `${label}.service`;
111
+ const unitPath = join(env.HOME ?? homedir(), '.config', 'systemd', 'user', unitName);
112
+ atomicWrite(unitPath, renderForgeRuntimeSystemdUserUnit({ executable: entrypoint, args: launch.args, environment: launch.environment }), 0o644);
113
+ for (const args of [['--user', 'daemon-reload'], ['--user', 'enable', '--now', unitName]]) {
114
+ const result = commandSucceeded('systemctl', args, env);
115
+ if (result.status !== 0) throw new Error(`FORGE_RUNTIME_SYSTEMD_INSTALL_FAILED: systemctl ${args.join(' ')}: ${(result.stderr || result.stdout || '').trim()}`);
116
+ }
117
+ return unitPath;
118
+ }
119
+
120
+ function startPortableRuntime(controllerHome: string, env: NodeJS.ProcessEnv): number {
121
+ const entrypoint = activeRuntimeEntrypoint(controllerHome);
122
+ const launch = activeRuntimeLaunchSpec(controllerHome);
123
+ if (!entrypoint || !launch) throw new Error('FORGE_PACKAGE_RUNTIME_LAUNCH_SPEC_MISSING');
124
+ const paths = forgeRuntimeServicePaths(controllerHome);
125
+ mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
126
+ const stdout = openSync(paths.stdoutPath, 'a', 0o600);
127
+ const stderr = openSync(paths.stderrPath, 'a', 0o600);
128
+ try {
129
+ const child = spawn(entrypoint, launch.args, {
130
+ detached: true,
131
+ stdio: ['ignore', stdout, stderr],
132
+ env: cleanRuntimeInstallerEnvironment(env, launch.environment),
133
+ });
134
+ if (!child.pid) throw new Error('FORGE_PORTABLE_RUNTIME_START_FAILED: child pid unavailable');
135
+ child.unref();
136
+ atomicWrite(join(paths.serviceRoot, 'portable.json'), `${JSON.stringify({ schemaVersion: 1, pid: child.pid, startedAt: new Date().toISOString() }, null, 2)}\n`);
137
+ return child.pid;
138
+ } finally {
139
+ closeSync(stdout);
140
+ closeSync(stderr);
141
+ }
142
+ }
143
+
144
+ export async function installPackageRuntimeService(options: PackageRuntimeServiceOptions): Promise<PackageRuntimeServiceInstallResult> {
145
+ const controllerHome = resolveControllerHome(options.controllerHome);
146
+ const release = materializePackageRuntimeRelease({ controllerHome, packageRoot: options.packageRoot });
147
+ const config: ForgeRuntimeServiceConfig = {
148
+ schemaVersion: 1,
149
+ controllerHome,
150
+ repositoryRoot: release.packageRoot,
151
+ host: options.host ?? '127.0.0.1',
152
+ port: options.port ?? 8765,
153
+ authTokenFile: options.authTokenFile,
154
+ ...(options.exclusiveWorkId?.trim() ? { exclusiveWorkId: options.exclusiveWorkId.trim() } : {}),
155
+ };
156
+ writeForgeRuntimeServiceConfig(config);
157
+ syncForgeRuntimeActiveEntrypoint(controllerHome);
158
+ const platform = options.platform ?? process.platform;
159
+ const env = options.env ?? process.env;
160
+
161
+ let base: PackageRuntimeServiceInstallResult;
162
+ if (!options.forcePortable && platform === 'darwin') {
163
+ const paths = await installForgeRuntimeService({ config, runnerPath: join(release.packageRoot, 'bin', 'forge-runtime-service.mjs'), nodeExecutable: process.execPath });
164
+ base = { status: 'installed', mode: 'launchd', persistent: true, controllerHome, release, servicePath: paths.installedPlistPath, warnings: [] };
165
+ } else if (!options.forcePortable && platform === 'linux' && systemdUserAvailable(env)) {
166
+ const unitPath = installSystemdUserService(controllerHome, env);
167
+ base = { status: 'installed', mode: 'systemd-user', persistent: true, controllerHome, release, servicePath: unitPath, warnings: [] };
168
+ } else {
169
+ const pid = startPortableRuntime(controllerHome, env);
170
+ base = {
171
+ status: 'installed', mode: 'portable', persistent: false, controllerHome, release, pid,
172
+ warnings: [platform === 'win32'
173
+ ? 'Native Windows is preview: Forge started detached Runtime and OAuth Gateway processes for this session; automatic login/reboot persistence is not yet claimed.'
174
+ : 'systemd --user is unavailable: Forge started detached Runtime and OAuth Gateway processes; enable a user service manager for reboot persistence.'],
175
+ };
176
+ }
177
+
178
+ const localConfig = loadMcpServiceLocalConfig(controllerHome);
179
+ const connectorEndpoint = localConfig?.chatgpt?.localEndpoint;
180
+ if (!connectorEndpoint) return base;
181
+ const connector = await installPackageConnectorService({
182
+ release, controllerHome, endpoint: connectorEndpoint, platform, env, forcePortable: options.forcePortable === true,
183
+ });
184
+ return { ...base, connector };
185
+ }
@@ -29,6 +29,7 @@ export interface StagedRuntimeRelease {
29
29
  codeGraphNodeArtifactIdentity?: string;
30
30
  codeGraphSidecarArtifactIdentity?: string;
31
31
  codeGraphLibraryArtifactIdentity?: string;
32
+ controllerUiArtifactIdentity?: string;
32
33
  manifestSha256: string;
33
34
  sourceCommit: string;
34
35
  }
@@ -395,6 +396,17 @@ export function stageRuntimeRelease(input: {
395
396
  const codeGraphSidecarArtifactIdentity = `sha256:${sha256(codeGraphSidecarPath)}`;
396
397
  const codeGraphLibraryArtifactIdentity = `sha256:${sha256Directory(codeGraphLibraryPath)}`;
397
398
 
399
+ const controllerUiRoot = 'ui-dist' as const;
400
+ const sourceControllerUiPath = join(sourceRoot, 'src', 'cli', 'local-bridge', controllerUiRoot);
401
+ const sourceControllerUiJs = join(sourceControllerUiPath, 'app.js');
402
+ const sourceControllerUiCss = join(sourceControllerUiPath, 'app.css');
403
+ if (!existsSync(sourceControllerUiJs) || !existsSync(sourceControllerUiCss)) {
404
+ throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_SOURCE_MISSING: ${sourceControllerUiPath}`);
405
+ }
406
+ const controllerUiPath = join(staging, controllerUiRoot);
407
+ cpSync(sourceControllerUiPath, controllerUiPath, { recursive: true, force: false });
408
+ const controllerUiArtifactIdentity = `sha256:${sha256Directory(controllerUiPath)}`;
409
+
398
410
  const desktopHelperEntrypoint = 'forge-desktop-helper.mjs' as const;
399
411
  const sourceDesktopHelperPath = join(sourceRoot, 'bin', desktopHelperEntrypoint);
400
412
  if (!existsSync(sourceDesktopHelperPath)) {
@@ -429,6 +441,8 @@ export function stageRuntimeRelease(input: {
429
441
  codeGraphSidecarArtifactIdentity,
430
442
  codeGraphLibraryRoot,
431
443
  codeGraphLibraryArtifactIdentity,
444
+ controllerUiRoot,
445
+ controllerUiArtifactIdentity,
432
446
  arguments: [],
433
447
  configurationSchemaVersion: 1,
434
448
  controllerHome: resolve(input.controllerHome),
@@ -460,6 +474,7 @@ export function stageRuntimeRelease(input: {
460
474
  codeGraphNodeArtifactIdentity,
461
475
  codeGraphSidecarArtifactIdentity,
462
476
  codeGraphLibraryArtifactIdentity,
477
+ controllerUiArtifactIdentity,
463
478
  manifestSha256: createHash('sha256').update(`${JSON.stringify(manifest, null, 2)}\n`).digest('hex'),
464
479
  sourceCommit,
465
480
  };
@@ -506,4 +521,8 @@ export function assertRuntimeReleaseFiles(release: StagedRuntimeRelease): void {
506
521
  if (release.codeGraphLibraryArtifactIdentity && !existsSync(join(release.releasePath, 'codegraph-lib', 'dist', 'index.js'))) {
507
522
  throw new Error(`RUNTIME_RELEASE_CODEGRAPH_LIBRARY_MISSING: ${join(release.releasePath, 'codegraph-lib', 'dist', 'index.js')}`);
508
523
  }
524
+ if (release.controllerUiArtifactIdentity
525
+ && (!existsSync(join(release.releasePath, 'ui-dist', 'app.js')) || !existsSync(join(release.releasePath, 'ui-dist', 'app.css')))) {
526
+ throw new Error(`RUNTIME_RELEASE_CONTROLLER_UI_MISSING: ${join(release.releasePath, 'ui-dist')}`);
527
+ }
509
528
  }
@@ -228,11 +228,12 @@ export class CanonicalForgeRuntime {
228
228
  port: this.config.port,
229
229
  authToken: this.config.authToken,
230
230
  readiness: () => this.readiness(),
231
- createServer: (principalId, sessionId) => createRuntimeGatewayServer(this.controller!, principalId, {
231
+ createServer: (principalId, sessionId, controllerType) => createRuntimeGatewayServer(this.controller!, principalId, {
232
232
  controllerHome: this.config.controllerHome,
233
233
  runtimeInstanceId: this.runtimeInstanceId,
234
234
  runtimeSourceRoot,
235
235
  sessionId,
236
+ controllerType,
236
237
  }),
237
238
  onFatal: (error) => this.failCore('MCP_TRANSPORT_FAILED', error.message),
238
239
  });
@@ -1,4 +1,5 @@
1
1
  import { GlobalScheduler, readSchedulerHealthSnapshot } from '../control-plane/global-scheduler/scheduler';
2
+ import { applyPendingContinuationActivations } from '../workflow/schedules/pending-activation';
2
3
 
3
4
  export interface RuntimeSchedulerHandle {
4
5
  ready: Promise<void>;
@@ -34,6 +35,10 @@ export function startInProcessScheduler(
34
35
  readyTimeoutMs = 5_000,
35
36
  ): RuntimeSchedulerHandle {
36
37
  const abort = new AbortController();
38
+ // Old Runtime releases ignore pending activation markers entirely. A naturally
39
+ // restarted Runtime that contains this code consumes them once, before its
40
+ // sole Scheduler starts, so no second scheduler or rollout hook is required.
41
+ applyPendingContinuationActivations(controllerHome);
37
42
  const startedAfterMs = Date.now();
38
43
  const scheduler = new GlobalScheduler(controllerHome, {}, {
39
44
  controllerPid: process.pid,
@@ -97,6 +97,7 @@ interface RuntimeReleaseManifestRecord {
97
97
  controllerHome: string;
98
98
  artifactIdentity: string;
99
99
  arguments?: string[];
100
+ diagnosticEntrypoint?: string;
100
101
  browserAutomationHelperEntrypoint?: string;
101
102
  browserAutomationHelperArtifactIdentity?: string;
102
103
  browserAutomationHelperContractIdentity?: string;
@@ -113,7 +114,7 @@ interface ActiveRuntimeReleaseRecord {
113
114
  manifest: RuntimeReleaseManifestRecord;
114
115
  }
115
116
 
116
- interface ActiveRuntimeLaunchSpec {
117
+ export interface ActiveRuntimeLaunchSpec {
117
118
  args: string[];
118
119
  environment: Record<string, string>;
119
120
  }
@@ -168,7 +169,7 @@ async function retireLegacyBrowserAutomationLaunchAgent(controllerHome: string):
168
169
  rmSync(legacy.installedPlistPath, { force: true });
169
170
  }
170
171
 
171
- function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpec | undefined {
172
+ export function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpec | undefined {
172
173
  const home = resolveControllerHome(controllerHome);
173
174
  const active = readActiveRuntimeRelease(home);
174
175
  if (!active) return undefined;
@@ -178,6 +179,12 @@ function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpe
178
179
  if (!Array.isArray(manifestArguments) || !manifestArguments.every((argument) => typeof argument === 'string')) {
179
180
  throw new Error('FORGE_RUNTIME_RELEASE_ARGUMENTS_INVALID');
180
181
  }
182
+ const diagnosticEntrypoint = typeof active.manifest.diagnosticEntrypoint === 'string' && active.manifest.diagnosticEntrypoint.trim()
183
+ ? resolve(active.releaseRoot, active.manifest.diagnosticEntrypoint)
184
+ : undefined;
185
+ if (diagnosticEntrypoint && (!isInside(active.releaseRoot, diagnosticEntrypoint) || !existsSync(diagnosticEntrypoint))) {
186
+ throw new Error('FORGE_RUNTIME_RELEASE_DIAGNOSTIC_ENTRYPOINT_INVALID');
187
+ }
181
188
  return {
182
189
  args: [
183
190
  '--controller-home', home,
@@ -192,6 +199,7 @@ function activeRuntimeLaunchSpec(controllerHome: string): ActiveRuntimeLaunchSpe
192
199
  environment: {
193
200
  FORGE_CONTROLLER_HOME: home,
194
201
  FORGE_CONTROLLER_RUNTIME_SOURCE_ROOT: active.releaseRoot,
202
+ ...(diagnosticEntrypoint ? { FORGE_CLI_EXECUTABLE: diagnosticEntrypoint } : {}),
195
203
  FORGE_RELEASE_PATH: active.releaseRoot,
196
204
  FORGE_RELEASE_ID: active.releaseId,
197
205
  FORGE_RELEASE_REVISION: active.manifest.releaseRevision ?? active.releaseId,
@@ -261,12 +269,17 @@ export function renderForgeRuntimeLaunchAgent(input: { paths: ForgeRuntimeServic
261
269
  return `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0">\n<dict>\n <key>Label</key>\n <string>${xml(input.paths.label)}</string>\n <key>ProgramArguments</key>\n <array>\n${args.map((arg) => ` <string>${xml(arg)}</string>`).join('\n')}\n </array>\n${environmentXml} <key>RunAtLoad</key>\n <true/>\n <key>KeepAlive</key>\n <dict>\n <key>SuccessfulExit</key>\n <false/>\n </dict>\n <key>ThrottleInterval</key>\n <integer>5</integer>\n <key>ProcessType</key>\n <string>Interactive</string>\n <key>StandardOutPath</key>\n <string>${xml(input.paths.stdoutPath)}</string>\n <key>StandardErrorPath</key>\n <string>${xml(input.paths.stderrPath)}</string>\n</dict>\n</plist>\n`;
262
270
  }
263
271
 
264
- export async function installForgeRuntimeService(input: { config: ForgeRuntimeServiceConfig; runnerPath: string; nodeExecutable?: string }): Promise<ForgeRuntimeServicePaths> {
265
- if (process.platform !== 'darwin') throw new Error('FORGE_RUNTIME_SERVICE_PLATFORM_UNSUPPORTED: launchd requires macOS');
266
- const config = validateForgeRuntimeServiceConfig(input.config);
272
+ export function writeForgeRuntimeServiceConfig(input: ForgeRuntimeServiceConfig): { config: ForgeRuntimeServiceConfig; paths: ForgeRuntimeServicePaths } {
273
+ const config = validateForgeRuntimeServiceConfig(input);
267
274
  const paths = forgeRuntimeServicePaths(config.controllerHome);
268
275
  mkdirSync(join(paths.serviceRoot, 'logs'), { recursive: true, mode: 0o700 });
269
276
  atomicWrite(paths.configPath, `${JSON.stringify(config, null, 2)}\n`);
277
+ return { config, paths };
278
+ }
279
+
280
+ export async function installForgeRuntimeService(input: { config: ForgeRuntimeServiceConfig; runnerPath: string; nodeExecutable?: string }): Promise<ForgeRuntimeServicePaths> {
281
+ if (process.platform !== 'darwin') throw new Error('FORGE_RUNTIME_SERVICE_PLATFORM_UNSUPPORTED: launchd requires macOS');
282
+ const { config, paths } = writeForgeRuntimeServiceConfig(input.config);
270
283
  ensureForgeRuntimeLaunchAgentContract({
271
284
  controllerHome: config.controllerHome,
272
285
  bootstrapNodeExecutable: input.nodeExecutable ?? process.execPath,
@@ -16,6 +16,12 @@ import {
16
16
  } from '../root/release-store';
17
17
  import type { RuntimeReleaseManifest } from '../root/types';
18
18
  import { createRecoveryHttpTransport, type RecoveryHttpTransport } from './http-transport';
19
+ import {
20
+ migrateStoppedRepoLocalControllerHomeStorage,
21
+ repoLocalControllerHomeStorageNeedsMigration,
22
+ rollbackStoppedRepoLocalControllerHomeStorage,
23
+ type ControllerHomeStorageMigration,
24
+ } from '../../cli/repositories/controller-home';
19
25
 
20
26
  /** Standalone recovery reads only canonical Runtime observation and whole-release authority. */
21
27
  export interface PublicTunnelServiceConfig {
@@ -1590,7 +1596,8 @@ export async function activateRuntimeRelease(
1590
1596
  audit(config, 'runtime_release_activation_candidate_invalid', { detail, ...(guard.requestId ? { requestId: guard.requestId } : {}) });
1591
1597
  return { ok: false, attempted: false, noOp: true, detail };
1592
1598
  }
1593
- if ((dependencies.platform ?? process.platform) !== 'darwin') {
1599
+ const platform = dependencies.platform ?? process.platform;
1600
+ if (platform !== 'darwin') {
1594
1601
  return { ok: false, attempted: false, noOp: true, detail: 'Runtime release activation currently requires the configured macOS launchd service' };
1595
1602
  }
1596
1603
  const uid = await (dependencies.currentUid ?? currentUid)();
@@ -1638,7 +1645,19 @@ export async function activateRuntimeRelease(
1638
1645
  });
1639
1646
  return { ok: false, attempted: false, noOp: true, detail, operationId } satisfies RuntimeReleaseActivationResult;
1640
1647
  }
1648
+ let storageMigrationRequired = false;
1649
+ try {
1650
+ storageMigrationRequired = repoLocalControllerHomeStorageNeedsMigration(config.controllerHome, platform);
1651
+ } catch (error) {
1652
+ const detail = error instanceof Error ? error.message : 'Controller Home storage migration preflight failed';
1653
+ audit(config, 'runtime_release_activation_storage_preflight_failed', { operationId, requestId: lockRequestId, detail });
1654
+ return { ok: false, attempted: false, noOp: true, detail, operationId } satisfies RuntimeReleaseActivationResult;
1655
+ }
1641
1656
  if (current && current.active.releaseId === candidate.manifest.releaseId) {
1657
+ if (storageMigrationRequired) {
1658
+ const detail = 'CONTROLLER_HOME_STORAGE_MIGRATION_REQUIRES_STAGED_RELEASE: stage a fresh immutable Runtime release so Recovery can migrate storage inside the activation transaction';
1659
+ return { ok: false, attempted: false, noOp: true, detail, operationId } satisfies RuntimeReleaseActivationResult;
1660
+ }
1642
1661
  return {
1643
1662
  ok: true,
1644
1663
  attempted: false,
@@ -1649,7 +1668,7 @@ export async function activateRuntimeRelease(
1649
1668
  } satisfies RuntimeReleaseActivationResult;
1650
1669
  }
1651
1670
  if (current) {
1652
- if (runtimeBehaviorEquivalent(current.active.manifestPath, candidate.manifestPath)) {
1671
+ if (runtimeBehaviorEquivalent(current.active.manifestPath, candidate.manifestPath) && !storageMigrationRequired) {
1653
1672
  const verify = await verifyLocal(config);
1654
1673
  audit(config, 'runtime_release_activation_behavior_identical', {
1655
1674
  operationId,
@@ -1727,24 +1746,44 @@ export async function activateRuntimeRelease(
1727
1746
  audit(config, 'runtime_release_activation_launchd_contract_failed', { serviceTarget: service.target, operationId, detail });
1728
1747
  return { ok: false, attempted: true, detail, serviceTarget: service.target, verify: await verifyLocal(config) } satisfies RuntimeReleaseActivationResult;
1729
1748
  }
1730
- const started = await ensureLaunchdServiceStarted(service, runCommand);
1749
+ let storageMigration: ControllerHomeStorageMigration | undefined;
1731
1750
  let after: VerifyResult;
1732
1751
  let activationFailureDetail: string | undefined;
1733
- if (!started.ok) {
1734
- activationFailureDetail = started.detail;
1735
- audit(config, 'runtime_release_activation_start_failed', { serviceTarget: service.target, operationId, detail: started.detail });
1752
+ try {
1753
+ storageMigration = migrateStoppedRepoLocalControllerHomeStorage(config.controllerHome, platform);
1754
+ if (storageMigration.migrated) {
1755
+ audit(config, 'runtime_controller_home_noindex_migrated', {
1756
+ serviceTarget: service.target,
1757
+ operationId,
1758
+ logicalHome: storageMigration.logicalHome,
1759
+ physicalHome: storageMigration.physicalHome,
1760
+ });
1761
+ }
1762
+ } catch (error) {
1763
+ activationFailureDetail = `Controller Home .noindex migration failed: ${error instanceof Error ? error.message : String(error)}`;
1764
+ audit(config, 'runtime_controller_home_noindex_migration_failed', { serviceTarget: service.target, operationId, detail: activationFailureDetail });
1765
+ }
1766
+
1767
+ if (activationFailureDetail) {
1736
1768
  after = await verifyLocal(config);
1737
1769
  } else {
1738
- after = await verifyPrimaryRuntimeAfterStart({
1739
- config,
1740
- timeoutMs: configuredPrimaryRuntimeService(config).postRestartVerifyTimeoutMs ?? 60_000,
1741
- now,
1742
- wait,
1743
- verifyLocal,
1744
- });
1745
- if (after.ok && after.releases.active?.revision === candidate.manifest.releaseId) {
1746
- audit(config, 'runtime_release_activation_succeeded', { serviceTarget: service.target, operationId, requestId: lockRequestId, activeRevision: after.releases.active?.revision, expectedAuthorityRevision: guard.expectedAuthorityRevision, expectedActiveReleaseId: guard.expectedActiveReleaseId });
1747
- return { ok: true, attempted: true, detail: 'requested Runtime release activated and passed whole-Runtime verification', serviceTarget: service.target, operationId, verify: after } satisfies RuntimeReleaseActivationResult;
1770
+ const started = await ensureLaunchdServiceStarted(service, runCommand);
1771
+ if (!started.ok) {
1772
+ activationFailureDetail = started.detail;
1773
+ audit(config, 'runtime_release_activation_start_failed', { serviceTarget: service.target, operationId, detail: started.detail });
1774
+ after = await verifyLocal(config);
1775
+ } else {
1776
+ after = await verifyPrimaryRuntimeAfterStart({
1777
+ config,
1778
+ timeoutMs: configuredPrimaryRuntimeService(config).postRestartVerifyTimeoutMs ?? 60_000,
1779
+ now,
1780
+ wait,
1781
+ verifyLocal,
1782
+ });
1783
+ if (after.ok && after.releases.active?.revision === candidate.manifest.releaseId) {
1784
+ audit(config, 'runtime_release_activation_succeeded', { serviceTarget: service.target, operationId, requestId: lockRequestId, activeRevision: after.releases.active?.revision, expectedAuthorityRevision: guard.expectedAuthorityRevision, expectedActiveReleaseId: guard.expectedActiveReleaseId, controllerHomeStorageMigrated: storageMigration?.migrated === true });
1785
+ return { ok: true, attempted: true, detail: storageMigration?.migrated ? 'requested Runtime release activated, Controller Home migrated to .noindex storage, and whole-Runtime verification passed' : 'requested Runtime release activated and passed whole-Runtime verification', serviceTarget: service.target, operationId, verify: after } satisfies RuntimeReleaseActivationResult;
1786
+ }
1748
1787
  }
1749
1788
  }
1750
1789
 
@@ -1772,6 +1811,16 @@ export async function activateRuntimeRelease(
1772
1811
  try {
1773
1812
  const rollbackOperationId = `recovery-activate-runtime-rollback-${Date.now()}-${randomUUID().slice(0, 8)}`;
1774
1813
  const restored = rollbackRuntimeRelease(config.controllerHome, rollbackOperationId);
1814
+ if (storageMigration?.migrated) {
1815
+ rollbackStoppedRepoLocalControllerHomeStorage(storageMigration);
1816
+ audit(config, 'runtime_controller_home_noindex_migration_rolled_back', {
1817
+ serviceTarget: service.target,
1818
+ operationId,
1819
+ rollbackOperationId,
1820
+ logicalHome: storageMigration.logicalHome,
1821
+ physicalHome: storageMigration.physicalHome,
1822
+ });
1823
+ }
1775
1824
  ensureForgeRuntimeLaunchAgentContract({ controllerHome: config.controllerHome });
1776
1825
  const previousRevision = previousActive?.releaseId ?? '';
1777
1826
  const previousIdentity = previousActive?.artifactIdentity;