@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
@@ -1,8 +1,8 @@
1
1
  import { Command } from 'commander';
2
- import { existsSync, writeFileSync } from 'fs';
2
+ import { existsSync, mkdirSync, writeFileSync } from 'fs';
3
3
  import { dirname, join, resolve } from 'path';
4
4
  import { fileURLToPath } from 'url';
5
- import { resolveControllerHome } from '../repositories/controller-home';
5
+ import { resolveControllerHome, resolveRepoPreferredControllerHome } from '../repositories/controller-home';
6
6
  import { findExecutionJob, listActiveExecutionJobs, listExecutionJobs } from '../../runtime/execution/jobs/store';
7
7
  import { readJobEvents } from '../../runtime/evidence/event-ledger';
8
8
  import { getRepository, listRepositories } from '../repositories/registry';
@@ -13,20 +13,73 @@ import { observeRuntimeStatus } from '../../runtime/root/status';
13
13
  import { readMcpServiceBearerToken } from '../mcp/auth';
14
14
  import { publishRuntimeRelease } from '../../runtime/root/release-store';
15
15
  import { installForgeRuntimeService } from '../../runtime/root/service';
16
+ import { installPackageRuntimeService } from '../../runtime/root/package-runtime-service';
16
17
  import { assertRuntimeReleaseFiles, stageRuntimeReleaseFromCandidateSource } from '../../runtime/root/release-materialize';
17
18
 
18
19
  function output(value: unknown, json = true): void {
19
20
  console.log(json ? JSON.stringify(value, null, 2) : String(value));
20
21
  }
21
22
 
23
+ export function resolveRuntimeStateControllerHome(explicit?: string, cwd = process.cwd()): string {
24
+ return resolveRepoPreferredControllerHome(cwd, explicit);
25
+ }
26
+
22
27
  export function buildRuntimeCommand(): Command {
23
28
  const command = new Command('runtime').description('Inspect the canonical Runtime and durable execution state');
24
29
 
25
30
  const service = command.command('service')
26
- .description('Install the macOS launchd owner for the single Forge Runtime root process');
31
+ .description('Install or inspect the user-level Forge Runtime owner');
32
+
33
+ service.command('install-package')
34
+ .description('Install the current packaged Forge Runtime without requiring a Git checkout, Bun compilation, CodeGraph, or Standalone Recovery')
35
+ .option('--controller-home <path>', 'Controller Home; defaults to the user-level Forge Controller Home')
36
+ .option('--host <host>', 'MCP listener host', '127.0.0.1')
37
+ .option('--port <port>', 'MCP listener port', '8765')
38
+ .option('--auth-token-file <path>', 'Raw bearer token file (created from user-level MCP config when missing)')
39
+ .option('--portable', 'Force a detached session process instead of launchd/systemd user persistence')
40
+ .action(async (opts: { controllerHome?: string; host: string; port: string; authTokenFile?: string; portable?: boolean }) => {
41
+ const home = resolveControllerHome(opts.controllerHome);
42
+ const port = Number(opts.port);
43
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) throw new Error('RUNTIME_SERVICE_PORT_INVALID');
44
+ const tokenPath = resolve(opts.authTokenFile ?? join(home, 'mcp', 'runtime-token'));
45
+ if (!existsSync(tokenPath)) {
46
+ const token = readMcpServiceBearerToken(home);
47
+ if (!token) throw new Error('RUNTIME_SERVICE_AUTH_TOKEN_UNAVAILABLE: run forge mcp setup chatgpt --user-level first');
48
+ mkdirSync(dirname(tokenPath), { recursive: true, mode: 0o700 });
49
+ writeFileSync(tokenPath, `${token}\n`, { mode: 0o600 });
50
+ }
51
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..', '..', '..');
52
+ const result = await installPackageRuntimeService({
53
+ controllerHome: home,
54
+ packageRoot,
55
+ host: opts.host,
56
+ port,
57
+ authTokenFile: tokenPath,
58
+ forcePortable: opts.portable === true,
59
+ });
60
+ output({
61
+ status: result.status,
62
+ mode: result.mode,
63
+ persistent: result.persistent,
64
+ controllerHome: result.controllerHome,
65
+ release: {
66
+ releaseId: result.release.releaseId,
67
+ packageVersion: result.release.packageVersion,
68
+ packageFingerprint: result.release.packageFingerprint,
69
+ artifactIdentity: result.release.artifactIdentity,
70
+ manifestPath: result.release.manifestPath,
71
+ fileCount: result.release.fileCount,
72
+ },
73
+ servicePath: result.servicePath,
74
+ pid: result.pid,
75
+ connector: result.connector,
76
+ warnings: result.warnings,
77
+ next: `forge runtime status --controller-home ${home}`,
78
+ });
79
+ });
27
80
 
28
81
  service.command('install')
29
- .description('Build an immutable Runtime release and install the single launchd Runtime service owner')
82
+ .description('Advanced/source mode: build an immutable Git Runtime release and install the macOS launchd owner')
30
83
  .requiredOption('--controller-home <path>', 'Explicit Controller Home')
31
84
  .requiredOption('--repo <path>', 'Repository root used as the immutable release source')
32
85
  .option('--host <host>', 'MCP listener host', '127.0.0.1')
@@ -115,7 +168,7 @@ export function buildRuntimeCommand(): Command {
115
168
  .requiredOption('--controller-home <path>', 'Explicit Controller Home')
116
169
  .option('--json', 'Output JSON')
117
170
  .action((opts: { controllerHome: string; json?: boolean }) => {
118
- const home = resolveControllerHome(opts.controllerHome);
171
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
119
172
  const runtime = observeRuntimeStatus(home);
120
173
  const repositories = listRepositories(home, { includeRemoved: true });
121
174
  const value = {
@@ -138,7 +191,7 @@ export function buildRuntimeCommand(): Command {
138
191
  .argument('<job-id>', 'Execution Job ID')
139
192
  .option('--controller-home <path>', 'Controller state root')
140
193
  .action((jobId: string, opts: { controllerHome?: string }) => {
141
- const home = resolveControllerHome(opts.controllerHome);
194
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
142
195
  const job = findExecutionJob(home, jobId);
143
196
  if (!job) throw new Error(`JOB_NOT_FOUND: ${jobId}`);
144
197
  output({ job, events: readJobEvents(home, job.repoId, job.jobId) });
@@ -150,7 +203,7 @@ export function buildRuntimeCommand(): Command {
150
203
  .option('--repo-id <id>', 'Repository id')
151
204
  .option('--limit <count>', 'Maximum records', '100')
152
205
  .action((opts: { controllerHome?: string; repoId?: string; limit?: string }) => {
153
- const home = resolveControllerHome(opts.controllerHome);
206
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
154
207
  if (opts.repoId) return output({ jobs: listExecutionJobs(home, opts.repoId, Number(opts.limit ?? 100)) });
155
208
  output({ jobs: listActiveExecutionJobs(home) });
156
209
  });
@@ -183,7 +236,7 @@ export function buildRuntimeCommand(): Command {
183
236
  .requiredOption('--repo-id <id>', 'Repository id')
184
237
  .option('--controller-home <path>', 'Controller state root')
185
238
  .action((opts: { controllerHome?: string; repoId: string }) => {
186
- const home = resolveControllerHome(opts.controllerHome);
239
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
187
240
  output({ schedules: listSchedules(home, opts.repoId), occurrences: listOccurrences(home, opts.repoId, undefined, 100) });
188
241
  });
189
242
 
@@ -0,0 +1,472 @@
1
+ import { spawnSync } from 'child_process';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
3
+ import { homedir } from 'os';
4
+ import { loadMcpServiceLocalConfig } from '../mcp/auth';
5
+ import { resolveControllerHome } from '../repositories/controller-home';
6
+ import { observeRuntimeStatus } from '../../runtime/root/status';
7
+ import { dirname, join, resolve } from 'path';
8
+
9
+ export type SetupControllerKind = 'chatgpt' | 'codex' | 'claude' | 'mcp';
10
+ export type SetupTunnelProvider = 'auto' | 'openai' | 'cloudflare' | 'tailscale' | 'existing' | 'none';
11
+ export type SetupHostTarget = 'none' | 'codex' | 'claude' | 'both';
12
+
13
+ export interface SetupProfile {
14
+ schemaVersion: 1;
15
+ primaryController: SetupControllerKind;
16
+ controllers: SetupControllerKind[];
17
+ tunnel: {
18
+ provider: SetupTunnelProvider;
19
+ endpoint?: string;
20
+ tunnelId?: string;
21
+ };
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+
26
+ export interface SetupPlatformSnapshot {
27
+ platform: NodeJS.Platform;
28
+ arch: string;
29
+ environment: 'macos' | 'linux' | 'wsl2' | 'windows' | 'other';
30
+ serviceManager: 'launchd' | 'systemd-user' | 'windows-preview' | 'foreground';
31
+ commands: {
32
+ brew: boolean;
33
+ cloudflared: boolean;
34
+ tailscale: boolean;
35
+ tunnelClient: boolean;
36
+ systemctl: boolean;
37
+ winget: boolean;
38
+ };
39
+ }
40
+
41
+ export interface SetupProfileOptions {
42
+ setupRoot?: string;
43
+ env?: NodeJS.ProcessEnv;
44
+ now?: () => Date;
45
+ }
46
+
47
+ export interface ConfigureSetupProfileOptions extends SetupProfileOptions {
48
+ controller?: string;
49
+ addControllers?: string[];
50
+ tunnel?: string;
51
+ endpoint?: string;
52
+ tunnelId?: string;
53
+ }
54
+
55
+ export interface TunnelGuidance {
56
+ provider: SetupTunnelProvider;
57
+ ready: boolean;
58
+ title: string;
59
+ detail: string;
60
+ command?: string;
61
+ }
62
+
63
+ const CONTROLLERS: readonly SetupControllerKind[] = ['chatgpt', 'codex', 'claude', 'mcp'];
64
+ const TUNNELS: readonly SetupTunnelProvider[] = ['auto', 'openai', 'cloudflare', 'tailscale', 'existing', 'none'];
65
+
66
+ function setupHome(options: SetupProfileOptions = {}): string {
67
+ const env = options.env ?? process.env;
68
+ return resolve(options.setupRoot ?? env.FORGE_SETUP_HOME ?? join(env.HOME ?? homedir(), '.forge'));
69
+ }
70
+
71
+ export function setupProfilePath(options: SetupProfileOptions = {}): string {
72
+ return join(setupHome(options), 'setup', 'profile.json');
73
+ }
74
+
75
+ function normalizeController(value: string | undefined, label: string): SetupControllerKind | undefined {
76
+ if (value === undefined) return undefined;
77
+ const normalized = value.trim().toLowerCase();
78
+ if (!CONTROLLERS.includes(normalized as SetupControllerKind)) {
79
+ throw new Error(`${label}: invalid controller "${value}" (expected: ${CONTROLLERS.join(', ')})`);
80
+ }
81
+ return normalized as SetupControllerKind;
82
+ }
83
+
84
+ function normalizeTunnel(value: string | undefined): SetupTunnelProvider | undefined {
85
+ if (value === undefined) return undefined;
86
+ const normalized = value.trim().toLowerCase();
87
+ if (!TUNNELS.includes(normalized as SetupTunnelProvider)) {
88
+ throw new Error(`invalid tunnel provider "${value}" (expected: ${TUNNELS.join(', ')})`);
89
+ }
90
+ return normalized as SetupTunnelProvider;
91
+ }
92
+
93
+ function normalizeTunnelId(value: string | undefined): string | undefined {
94
+ const trimmed = value?.trim();
95
+ if (!trimmed) return undefined;
96
+ if (!/^tunnel_[0-9a-f]{32}$/.test(trimmed)) throw new Error('invalid OpenAI tunnel id: expected tunnel_ followed by 32 lowercase hexadecimal characters');
97
+ return trimmed;
98
+ }
99
+
100
+ function normalizeEndpoint(value: string | undefined): string | undefined {
101
+ const trimmed = value?.trim();
102
+ if (!trimmed) return undefined;
103
+ let parsed: URL;
104
+ try { parsed = new URL(trimmed); } catch { throw new Error(`invalid setup endpoint "${value}": expected HTTPS URL ending in /mcp`); }
105
+ if (parsed.protocol !== 'https:' || parsed.pathname !== '/mcp' || parsed.search || parsed.hash || parsed.username || parsed.password) {
106
+ throw new Error(`invalid setup endpoint "${value}": expected HTTPS URL ending in /mcp`);
107
+ }
108
+ return parsed.toString();
109
+ }
110
+
111
+ export function readSetupProfile(options: SetupProfileOptions = {}): SetupProfile | undefined {
112
+ try {
113
+ const value = JSON.parse(readFileSync(setupProfilePath(options), 'utf8')) as SetupProfile;
114
+ if (value.schemaVersion !== 1 || !CONTROLLERS.includes(value.primaryController)) return undefined;
115
+ const controllers = Array.isArray(value.controllers)
116
+ ? value.controllers.filter((entry): entry is SetupControllerKind => CONTROLLERS.includes(entry))
117
+ : [];
118
+ return { ...value, controllers: Array.from(new Set([value.primaryController, ...controllers])) };
119
+ } catch {
120
+ return undefined;
121
+ }
122
+ }
123
+
124
+ export function configureSetupProfile(options: ConfigureSetupProfileOptions): SetupProfile {
125
+ const previous = readSetupProfile(options);
126
+ const primaryController = normalizeController(options.controller, 'forge setup configure --controller')
127
+ ?? previous?.primaryController
128
+ ?? 'chatgpt';
129
+ const additions = (options.addControllers ?? []).map((entry) => normalizeController(entry, 'forge setup configure --add-controller')!);
130
+ const controllers = Array.from(new Set([primaryController, ...(previous?.controllers ?? []), ...additions]));
131
+ const provider = normalizeTunnel(options.tunnel)
132
+ ?? previous?.tunnel.provider
133
+ ?? (controllers.some((entry) => entry === 'chatgpt' || entry === 'mcp') ? 'auto' : 'none');
134
+ const explicitTunnel = normalizeTunnel(options.tunnel);
135
+ const endpoint = provider === 'openai' || provider === 'none'
136
+ ? undefined
137
+ : normalizeEndpoint(options.endpoint) ?? (previous?.tunnel.provider === provider ? previous.tunnel.endpoint : undefined);
138
+ const tunnelId = provider === 'openai'
139
+ ? normalizeTunnelId(options.tunnelId) ?? (previous?.tunnel.provider === 'openai' ? previous.tunnel.tunnelId : undefined)
140
+ : undefined;
141
+ if (provider === 'existing' && !endpoint) {
142
+ throw new Error('forge setup configure --tunnel existing requires --endpoint https://.../mcp');
143
+ }
144
+ const now = (options.now ?? (() => new Date()))().toISOString();
145
+ const profile: SetupProfile = {
146
+ schemaVersion: 1,
147
+ primaryController,
148
+ controllers,
149
+ tunnel: { provider, ...(endpoint ? { endpoint } : {}), ...(tunnelId ? { tunnelId } : {}) },
150
+ createdAt: previous?.createdAt ?? now,
151
+ updatedAt: now,
152
+ };
153
+ const path = setupProfilePath(options);
154
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
155
+ const temporary = `${path}.${process.pid}.tmp`;
156
+ writeFileSync(temporary, `${JSON.stringify(profile, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
157
+ renameSync(temporary, path);
158
+ return profile;
159
+ }
160
+
161
+ export function setupHostTarget(profile: SetupProfile | undefined): SetupHostTarget {
162
+ if (!profile) return 'none';
163
+ const codex = profile.controllers.includes('codex');
164
+ const claude = profile.controllers.includes('claude');
165
+ if (codex && claude) return 'both';
166
+ if (codex) return 'codex';
167
+ if (claude) return 'claude';
168
+ return 'none';
169
+ }
170
+
171
+ export function setupNeedsRemoteAccess(profile: SetupProfile | undefined): boolean {
172
+ return Boolean(profile?.controllers.some((entry) => entry === 'chatgpt' || entry === 'mcp'));
173
+ }
174
+
175
+ function commandExists(command: string, platform = process.platform): boolean {
176
+ const probe = platform === 'win32'
177
+ ? spawnSync('where.exe', [command], { stdio: 'ignore' })
178
+ : spawnSync('sh', ['-lc', `command -v ${command}`], { stdio: 'ignore' });
179
+ return probe.status === 0;
180
+ }
181
+
182
+ function isWsl(env: NodeJS.ProcessEnv): boolean {
183
+ if (env.WSL_DISTRO_NAME || env.WSL_INTEROP) return true;
184
+ try { return /microsoft/i.test(readFileSync('/proc/version', 'utf8')); } catch { return false; }
185
+ }
186
+
187
+ export function detectSetupPlatform(options: { platform?: NodeJS.Platform; arch?: string; env?: NodeJS.ProcessEnv } = {}): SetupPlatformSnapshot {
188
+ const platform = options.platform ?? process.platform;
189
+ const env = options.env ?? process.env;
190
+ const wsl = platform === 'linux' && isWsl(env);
191
+ const systemctl = platform === 'linux' && commandExists('systemctl', platform);
192
+ return {
193
+ platform,
194
+ arch: options.arch ?? process.arch,
195
+ environment: platform === 'darwin' ? 'macos' : platform === 'linux' ? (wsl ? 'wsl2' : 'linux') : platform === 'win32' ? 'windows' : 'other',
196
+ serviceManager: platform === 'darwin' ? 'launchd' : platform === 'linux' && systemctl ? 'systemd-user' : platform === 'win32' ? 'windows-preview' : 'foreground',
197
+ commands: {
198
+ brew: platform === 'darwin' && commandExists('brew', platform),
199
+ cloudflared: commandExists('cloudflared', platform),
200
+ tailscale: commandExists('tailscale', platform),
201
+ tunnelClient: commandExists('tunnel-client', platform),
202
+ systemctl,
203
+ winget: platform === 'win32' && commandExists('winget', platform),
204
+ },
205
+ };
206
+ }
207
+
208
+
209
+ function isLoopbackMcpEndpoint(value: string | undefined): boolean {
210
+ if (!value) return false;
211
+ try {
212
+ const parsed = new URL(value);
213
+ return parsed.protocol === 'http:'
214
+ && (parsed.hostname === '127.0.0.1' || parsed.hostname === 'localhost' || parsed.hostname === '::1')
215
+ && parsed.pathname === '/mcp'
216
+ && !parsed.search
217
+ && !parsed.hash;
218
+ } catch {
219
+ return false;
220
+ }
221
+ }
222
+
223
+ export interface ControllerGuidance {
224
+ controller: SetupControllerKind;
225
+ ready: boolean;
226
+ title: string;
227
+ detail: string;
228
+ command?: string;
229
+ }
230
+
231
+ function codexUserControllerConfigured(home = homedir()): boolean {
232
+ const path = join(home, '.codex', 'config.toml');
233
+ if (!existsSync(path)) return false;
234
+ try {
235
+ const text = readFileSync(path, 'utf8');
236
+ const block = text.match(/\[mcp_servers\.forge\][\s\S]*?(?=\n\[|$)/)?.[0] ?? '';
237
+ return block.includes('"mcp"') && block.includes('"serve"') && block.includes('"controller"');
238
+ } catch { return false; }
239
+ }
240
+
241
+ function claudeUserControllerConfigured(): boolean {
242
+ if (!commandExists('claude')) return false;
243
+ const probe = spawnSync('claude', ['mcp', 'get', 'forge'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 });
244
+ if (probe.status !== 0) return false;
245
+ const text = `${probe.stdout ?? ''}\n${probe.stderr ?? ''}`;
246
+ return /forge\s+mcp\s+serve|"mcp".*"serve"/is.test(text) && /controller/i.test(text);
247
+ }
248
+
249
+ export function resolveControllerGuidance(
250
+ profile: SetupProfile | undefined,
251
+ options: { controllerHome?: string; home?: string } = {},
252
+ ): ControllerGuidance | undefined {
253
+ if (!profile) return undefined;
254
+ const controllerHome = resolveControllerHome(options.controllerHome);
255
+ const localConfig = loadMcpServiceLocalConfig(controllerHome);
256
+ for (const controller of profile.controllers) {
257
+ if (controller === 'chatgpt' || controller === 'mcp') {
258
+ const localReady = localConfig?.profile === 'controller'
259
+ && localConfig.server?.host === '127.0.0.1'
260
+ && Number.isInteger(localConfig.server?.port);
261
+ const connectorReady = controller !== 'chatgpt' || isLoopbackMcpEndpoint(localConfig?.chatgpt?.localEndpoint);
262
+ const endpointReady = !profile.tunnel.endpoint || localConfig?.chatgpt?.endpoint === profile.tunnel.endpoint;
263
+ if (!localReady || !connectorReady || !endpointReady) {
264
+ const endpoint = profile.tunnel.endpoint ? ` --endpoint ${profile.tunnel.endpoint}` : '';
265
+ return {
266
+ controller,
267
+ ready: false,
268
+ title: controller === 'chatgpt' ? 'Configure the ChatGPT controller endpoint' : 'Configure the remote MCP controller endpoint',
269
+ detail: 'Create the user-level Forge OAuth/MCP configuration. This does not install Codex or Claude and does not require a repository.',
270
+ command: `forge mcp setup chatgpt --user-level${endpoint}`,
271
+ };
272
+ }
273
+ continue;
274
+ }
275
+ if (controller === 'codex') {
276
+ if (!codexUserControllerConfigured(options.home)) {
277
+ return {
278
+ controller, ready: false, title: 'Configure Codex as a Forge controller',
279
+ detail: 'Codex is optional. Configure it only because it was explicitly selected as a controller entry.',
280
+ command: 'forge mcp setup codex --scope user --profile controller',
281
+ };
282
+ }
283
+ continue;
284
+ }
285
+ if (!commandExists('claude')) {
286
+ return {
287
+ controller, ready: false, title: 'Install Claude Code only for this selected controller',
288
+ detail: 'Claude is not a Forge dependency. Install and authenticate Claude Code, then continue setup.',
289
+ };
290
+ }
291
+ if (!claudeUserControllerConfigured()) {
292
+ return {
293
+ controller, ready: false, title: 'Configure Claude Code as a Forge controller',
294
+ detail: 'Forge delegates MCP registration to the official Claude CLI at user scope.',
295
+ command: 'forge mcp setup claude --scope user --profile controller',
296
+ };
297
+ }
298
+ }
299
+ return undefined;
300
+ }
301
+
302
+
303
+ export interface RuntimeGuidance {
304
+ ready: boolean;
305
+ title: string;
306
+ detail: string;
307
+ command?: string;
308
+ }
309
+
310
+ export function resolveRuntimeGuidance(
311
+ profile: SetupProfile | undefined,
312
+ options: { controllerHome?: string } = {},
313
+ ): RuntimeGuidance | undefined {
314
+ if (!profile || !setupNeedsRemoteAccess(profile)) return undefined;
315
+ const controllerHome = resolveControllerHome(options.controllerHome);
316
+ const status = observeRuntimeStatus(controllerHome);
317
+ if (status.ready) return { ready: true, title: 'Forge Runtime', detail: 'Canonical Runtime is running and ready.' };
318
+ if (status.running) {
319
+ return {
320
+ ready: false,
321
+ title: 'Forge Runtime is starting or unhealthy',
322
+ detail: `Runtime owner is running but not ready (${status.reasonCodes.join(', ') || 'unknown'}). Inspect status before continuing.`,
323
+ command: `forge runtime status --controller-home ${controllerHome}`,
324
+ };
325
+ }
326
+ return {
327
+ ready: false,
328
+ title: 'Install the user-level Forge Runtime',
329
+ detail: 'Use the packaged Runtime path. It does not require a Git checkout, Bun compilation, CodeGraph, or Standalone Recovery.',
330
+ command: 'forge runtime service install-package',
331
+ };
332
+ }
333
+
334
+ export function resolveTunnelGuidance(
335
+ profile: SetupProfile | undefined,
336
+ platform = detectSetupPlatform(),
337
+ options: { controllerHome?: string; env?: NodeJS.ProcessEnv } = {},
338
+ ): TunnelGuidance {
339
+ if (!profile || !setupNeedsRemoteAccess(profile)) {
340
+ return { provider: 'none', ready: true, title: 'Remote controller connection', detail: 'Not required by the configured local controller set.' };
341
+ }
342
+ if (profile.tunnel.endpoint) {
343
+ return { provider: profile.tunnel.provider, ready: true, title: 'HTTPS endpoint configured', detail: `Configured: ${profile.tunnel.endpoint}. Live connector reachability is verified in the ChatGPT connection step.` };
344
+ }
345
+ let provider = profile.tunnel.provider;
346
+ if (provider === 'auto') {
347
+ // ChatGPT's private outbound path is the default. Do not silently choose a
348
+ // public ingress provider merely because cloudflared/tailscale happens to
349
+ // be installed; users without Secure Tunnel access can explicitly select a
350
+ // fallback provider.
351
+ provider = 'openai';
352
+ }
353
+ if (provider === 'openai') {
354
+ if (!profile.tunnel.tunnelId) {
355
+ return {
356
+ provider, ready: false, title: 'Create an OpenAI Secure MCP Tunnel',
357
+ detail: 'Create a tunnel in OpenAI Platform Tunnels, associate it with the target ChatGPT workspace, then record the non-secret tunnel_id in Forge. Do not paste a runtime API key into Forge setup.',
358
+ command: 'forge setup configure --controller chatgpt --tunnel openai --tunnel-id tunnel_...',
359
+ };
360
+ }
361
+ if (!platform.commands.tunnelClient) {
362
+ return {
363
+ provider, ready: false, title: 'Install OpenAI tunnel-client',
364
+ detail: 'Install the supported tunnel-client binary from OpenAI Platform Tunnels or the official openai/tunnel-client release. The runtime API key remains an environment/file reference owned by tunnel-client; Forge does not store it.',
365
+ };
366
+ }
367
+ const preferredAlias = 'forge';
368
+ const aliases = [preferredAlias];
369
+ const listed = spawnSync('tunnel-client', ['runtimes', 'list', '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000, env: options.env ?? process.env });
370
+ if (listed.status === 0) {
371
+ try {
372
+ const value = JSON.parse(listed.stdout || '{}') as { aliases?: Array<{ alias?: string; tunnel_id?: string }> };
373
+ for (const entry of value.aliases ?? []) {
374
+ if (entry.tunnel_id === profile.tunnel.tunnelId && entry.alias) aliases.push(entry.alias);
375
+ }
376
+ } catch {
377
+ // Older clients may not expose local aliases as JSON. Keep the preferred alias fallback.
378
+ }
379
+ }
380
+ for (const alias of Array.from(new Set(aliases))) {
381
+ const status = spawnSync('tunnel-client', ['runtimes', 'status', alias, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000, env: options.env ?? process.env });
382
+ if (status.status !== 0) continue;
383
+ try {
384
+ const value = JSON.parse(status.stdout || '{}') as { process_running?: boolean; healthy?: boolean; ready?: boolean; tunnel_id?: string };
385
+ if (value.process_running === true && value.healthy === true && value.ready === true
386
+ && (!value.tunnel_id || value.tunnel_id === profile.tunnel.tunnelId)) {
387
+ return {
388
+ provider, ready: true, title: 'OpenAI Secure MCP Tunnel',
389
+ detail: `Managed runtime ${alias} is running, healthy, and ready for ${profile.tunnel.tunnelId}.`,
390
+ };
391
+ }
392
+ } catch {
393
+ // Treat malformed or older status output as not-ready instead of claiming success.
394
+ }
395
+ }
396
+ const alias = preferredAlias;
397
+ const localConfig = loadMcpServiceLocalConfig(resolveControllerHome(options.controllerHome));
398
+ const localEndpoint = localConfig?.chatgpt?.localEndpoint;
399
+ if (!isLoopbackMcpEndpoint(localEndpoint)) {
400
+ return {
401
+ provider, ready: false, title: 'Prepare the local ChatGPT OAuth endpoint',
402
+ detail: 'Secure Tunnel must terminate at Forge’s loopback OAuth Gateway, not the bearer-only Canonical Runtime. Refresh the user-level ChatGPT MCP configuration first.',
403
+ command: 'forge mcp setup chatgpt --user-level',
404
+ };
405
+ }
406
+ return {
407
+ provider, ready: false, title: 'Connect OpenAI Secure MCP Tunnel',
408
+ detail: 'Create a supervised tunnel-client runtime and verify it with runtimes status. Keep the runtime API key in an environment or file reference owned by tunnel-client; Forge never reads or stores the key.',
409
+ command: `tunnel-client runtimes connect --alias ${alias} --tunnel-id ${profile.tunnel.tunnelId} --runtime-api-key env:CONTROL_PLANE_API_KEY --mcp-server-url ${localEndpoint}`,
410
+ };
411
+ }
412
+ if (provider === 'none') {
413
+ return {
414
+ provider, ready: false, title: 'Remote controller access is deferred',
415
+ detail: 'The selected controller set includes ChatGPT/remote MCP, but remote connectivity is deferred.',
416
+ command: 'forge setup configure --controller chatgpt --tunnel auto',
417
+ };
418
+ }
419
+ if (provider === 'existing') {
420
+ return {
421
+ provider, ready: false, title: 'Record existing HTTPS endpoint',
422
+ detail: 'Provide the existing public HTTPS URL ending in /mcp.',
423
+ command: 'forge setup configure --tunnel existing --endpoint https://forge.example.com/mcp',
424
+ };
425
+ }
426
+ if (provider === 'cloudflare') {
427
+ if (!platform.commands.cloudflared) {
428
+ const install = platform.environment === 'macos' && platform.commands.brew ? 'brew install cloudflared' : undefined;
429
+ return {
430
+ provider, ready: false, title: 'Install Cloudflare Tunnel',
431
+ detail: platform.environment === 'windows'
432
+ ? 'Install the official cloudflared executable or MSI, then run forge setup next.'
433
+ : platform.environment === 'linux' || platform.environment === 'wsl2'
434
+ ? 'Install cloudflared from Cloudflare’s official package repository for this Linux distribution, then run forge setup next.'
435
+ : 'Install cloudflared, then run forge setup next.',
436
+ ...(install ? { command: install } : {}),
437
+ };
438
+ }
439
+ return {
440
+ provider, ready: false, title: 'Create a stable Cloudflare Tunnel',
441
+ detail: 'Authenticate cloudflared, create/route a named tunnel to 127.0.0.1:8765, then record its HTTPS /mcp URL.',
442
+ command: 'cloudflared tunnel login',
443
+ };
444
+ }
445
+ if (!platform.commands.tailscale) {
446
+ return {
447
+ provider: 'tailscale', ready: false, title: 'Install Tailscale',
448
+ detail: platform.environment === 'macos'
449
+ ? 'Install Tailscale for macOS and enable CLI integration, then run forge setup next.'
450
+ : platform.environment === 'windows'
451
+ ? 'Install the official Tailscale Windows client, sign in, then run forge setup next.'
452
+ : 'Install Tailscale for Linux, sign in, then run forge setup next.',
453
+ };
454
+ }
455
+ return {
456
+ provider: 'tailscale', ready: false, title: 'Enable Tailscale Funnel',
457
+ detail: 'Expose only the local Forge MCP service and then record the generated HTTPS /mcp URL.',
458
+ command: 'tailscale funnel --bg 8765',
459
+ };
460
+ }
461
+
462
+ export function formatSetupProfile(profile: SetupProfile, platform = detectSetupPlatform()): string {
463
+ const tunnel = resolveTunnelGuidance(profile, platform);
464
+ return [
465
+ `Primary controller: ${profile.primaryController}`,
466
+ `Configured controllers: ${profile.controllers.join(', ')}`,
467
+ `Platform: ${platform.environment}/${platform.arch}; service=${platform.serviceManager}`,
468
+ `Remote access: ${profile.tunnel.provider}${profile.tunnel.endpoint ? ` (${profile.tunnel.endpoint})` : profile.tunnel.tunnelId ? ` (${profile.tunnel.tunnelId})` : ''}`,
469
+ `Tunnel readiness: ${tunnel.ready ? 'ready' : 'needs setup'} — ${tunnel.detail}`,
470
+ 'Forge itself remains the execution/runtime layer; it does not choose semantic actions without an external controller.',
471
+ ].join('\n');
472
+ }
@@ -21,6 +21,8 @@ export interface ControllerCheckEffects {
21
21
  temp?: 'isolated' | 'shared';
22
22
  git?: 'read' | 'index' | 'refs' | 'write';
23
23
  network?: 'read' | 'write';
24
+ /** Host-wide coordination resources used by checks that exercise shared local services/devices. */
25
+ hostServices?: string[];
24
26
  }
25
27
 
26
28
  export interface ControllerCheck {
@@ -131,7 +133,7 @@ function normalizeCwd(repoRoot: string, value: string | undefined): string {
131
133
  return back || '.';
132
134
  }
133
135
 
134
- const CHECK_EFFECT_KEYS = new Set(['reads', 'writes', 'cache', 'temp', 'git', 'network']);
136
+ const CHECK_EFFECT_KEYS = new Set(['reads', 'writes', 'cache', 'temp', 'git', 'network', 'hostServices']);
135
137
 
136
138
  function normalizeEffectPaths(repoRoot: string, field: 'reads' | 'writes', value: unknown): string[] | undefined {
137
139
  if (value === undefined) return undefined;
@@ -141,6 +143,16 @@ function normalizeEffectPaths(repoRoot: string, field: 'reads' | 'writes', value
141
143
  return [...new Set(value.map((entry) => normalizeCwd(repoRoot, String(entry))))].sort();
142
144
  }
143
145
 
146
+ function normalizeHostServices(value: unknown): string[] | undefined {
147
+ if (value === undefined) return undefined;
148
+ if (!Array.isArray(value) || value.some((entry) => typeof entry !== 'string' || entry.trim().length === 0)) {
149
+ throw new Error('check effects.hostServices must be an array of non-empty strings');
150
+ }
151
+ const normalized = value.map((entry) => String(entry).trim().toLowerCase().replace(/[^a-z0-9._:-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 120));
152
+ if (normalized.some((entry) => !entry)) throw new Error('check effects.hostServices contains an invalid service key');
153
+ return [...new Set(normalized)].sort();
154
+ }
155
+
144
156
  function normalizeEffectMode<T extends string>(field: string, value: unknown, allowed: readonly T[]): T | undefined {
145
157
  if (value === undefined) return undefined;
146
158
  if (typeof value !== 'string' || !allowed.includes(value as T)) {
@@ -164,6 +176,7 @@ function normalizeCheckEffects(repoRoot: string, value: unknown): ControllerChec
164
176
  ...(raw.temp !== undefined ? { temp: normalizeEffectMode('temp', raw.temp, ['isolated', 'shared'] as const)! } : {}),
165
177
  ...(raw.git !== undefined ? { git: normalizeEffectMode('git', raw.git, ['read', 'index', 'refs', 'write'] as const)! } : {}),
166
178
  ...(raw.network !== undefined ? { network: normalizeEffectMode('network', raw.network, ['read', 'write'] as const)! } : {}),
179
+ ...(raw.hostServices !== undefined ? { hostServices: normalizeHostServices(raw.hostServices)! } : {}),
167
180
  };
168
181
  }
169
182
 
@@ -350,7 +363,7 @@ function validateControllerCheckSnapshot(repoRoot: string, snapshot: ControllerC
350
363
  };
351
364
  }
352
365
 
353
- const CONTROLLER_CHECK_EXECUTION_IDENTITY_VERSION = 'controller-check-execution-v1';
366
+ const CONTROLLER_CHECK_EXECUTION_IDENTITY_VERSION = 'controller-check-execution-v2';
354
367
 
355
368
  function checkEnvironmentFingerprint(check: ControllerCheck): string {
356
369
  const env = repositoryChildProcessEnvironment(process.env);
@@ -360,7 +373,10 @@ function checkEnvironmentFingerprint(check: ControllerCheck): string {
360
373
  arch: process.arch,
361
374
  node: process.version,
362
375
  bun: typeof Bun !== 'undefined' ? Bun.version : undefined,
363
- execPath: process.execPath,
376
+ // Fingerprint the executable that the check actually runs, not the
377
+ // Controller launcher binary. Compiled Runtime uses active-forge-runtime
378
+ // for the parent and forge-check-runner for the sidecar; process.execPath
379
+ // therefore differs even though both execute the exact same check/toolchain.
364
380
  commandExecutable: check.command[0],
365
381
  path: env.PATH ?? '',
366
382
  home: env.HOME ?? env.USERPROFILE ?? '',
@@ -396,6 +412,7 @@ function checkEffectsAllowCrossCheckoutReuse(check: ControllerCheck): boolean {
396
412
  if ((effects.writes?.length ?? 0) > 0) return false;
397
413
  if (effects.git !== undefined && effects.git !== 'read') return false;
398
414
  if (effects.network !== undefined) return false;
415
+ if ((effects.hostServices?.length ?? 0) > 0) return false;
399
416
  if (effects.temp === 'shared') return false;
400
417
  return true;
401
418
  }