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

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 (259) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +19 -0
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +50 -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/templates/helpers/archive-workflow.sh +0 -1
  28. package/assets/templates/helpers/capture-plan.sh +3 -3
  29. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  30. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  31. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  32. package/assets/templates/helpers/new-plan.sh +1 -1
  33. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  34. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  35. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  36. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  37. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  38. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  39. package/assets/templates/helpers/verify-sprint.sh +7 -7
  40. package/assets/templates/helpers/workstream-sync.sh +1 -1
  41. package/assets/templates/plan.template.md +1 -1
  42. package/assets/workflow-contract.v1.json +6 -3
  43. package/docs/architecture/current/README.md +10 -9
  44. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  45. package/docs/architecture/current/architecture-invariants.md +2 -2
  46. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  47. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  48. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  49. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  50. package/docs/architecture/current/entity-model.md +0 -27
  51. package/docs/architecture/current/failure-recovery.md +0 -24
  52. package/docs/architecture/current/governance.md +1 -1
  53. package/docs/architecture/current/implementation-status.md +1 -1
  54. package/docs/architecture/current/legacy-governance.md +58 -0
  55. package/docs/architecture/current/multi-repository-execution.md +1 -1
  56. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  57. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  58. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  59. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  60. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  61. package/docs/architecture/current/thin-harness-v1.md +9 -11
  62. package/docs/architecture/domains/runtime-harness.md +1 -1
  63. package/docs/forge-autonomous-goal-loop.md +3 -1
  64. package/docs/forge-chatgpt-mcp-setup.md +51 -184
  65. package/docs/images/forge-banner-cn.svg +26 -1
  66. package/docs/images/forge-banner.svg +26 -1
  67. package/docs/operations/features.md +1 -1
  68. package/docs/operations/homebrew.md +3 -3
  69. package/docs/operations/mcp-tool-exposure.md +5 -5
  70. package/docs/operations/platform-support.md +45 -37
  71. package/docs/operations/platform-support.zh-CN.md +40 -38
  72. package/docs/operations/releasing.md +3 -3
  73. package/docs/operations/releasing.zh-CN.md +3 -3
  74. package/docs/operations/troubleshooting.md +11 -6
  75. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  76. package/docs/public-usage-guide.md +28 -21
  77. package/docs/public-usage-guide.zh-CN.md +28 -21
  78. package/docs/tutorials/01-install-and-start.md +56 -35
  79. package/docs/tutorials/01-install-and-start.zh-CN.md +56 -35
  80. package/docs/tutorials/02-connect-chatgpt.md +112 -46
  81. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +113 -45
  82. package/docs/wiki/Architecture.md +1 -1
  83. package/docs/wiki/Installation.md +2 -2
  84. package/docs/wiki/Quick-Start.md +1 -1
  85. package/docs/wiki/Work-Lifecycle.md +1 -1
  86. package/install.ps1 +4 -5
  87. package/install.sh +4 -6
  88. package/package.json +15 -5
  89. package/scripts/archive-workflow.sh +0 -1
  90. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  91. package/scripts/build-controller-ui.ts +28 -0
  92. package/scripts/capture-plan.sh +4 -4
  93. package/scripts/check-agent-tooling.sh +12 -1
  94. package/scripts/check-platform-support.mjs +2 -2
  95. package/scripts/check-public-docs.sh +16 -5
  96. package/scripts/check-release-readiness.sh +5 -11
  97. package/scripts/check-runtime-architecture.mjs +14 -22
  98. package/scripts/check-tarball-install-smoke.sh +3 -2
  99. package/scripts/check-task-workflow.sh +2 -2
  100. package/scripts/codex-handoff-resume.sh +5 -5
  101. package/scripts/create-project-dirs.sh +3 -3
  102. package/scripts/ensure-task-workflow.sh +45 -35
  103. package/scripts/init-project.sh +4 -4
  104. package/scripts/install-desktop-operator-registration.ts +2 -2
  105. package/scripts/lib/project-init-lib.sh +46 -44
  106. package/scripts/migrate-project-template.sh +7 -4
  107. package/scripts/new-plan.sh +1 -1
  108. package/scripts/plan-to-todo.sh +2 -2
  109. package/scripts/prepare-codex-handoff.sh +2 -2
  110. package/scripts/prepare-handoff.sh +9 -9
  111. package/scripts/refresh-current-status.sh +2 -2
  112. package/scripts/setup-plugins.sh +3 -3
  113. package/scripts/ship-worktrees.sh +1 -1
  114. package/scripts/sprint-backlog.sh +5 -5
  115. package/scripts/sync-codex-installed-copies.sh +2 -3
  116. package/scripts/verify-sprint.sh +7 -7
  117. package/scripts/workstream-sync.sh +1 -1
  118. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  119. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  120. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  121. package/src/cli/commands/chatgpt.ts +51 -0
  122. package/src/cli/commands/doctor.ts +1 -1
  123. package/src/cli/commands/init-hook.ts +211 -70
  124. package/src/cli/commands/init.ts +5 -6
  125. package/src/cli/commands/mcp.ts +29 -5
  126. package/src/cli/commands/runtime.ts +60 -8
  127. package/src/cli/commands/setup-profile.ts +441 -0
  128. package/src/cli/controller/check-runner.ts +20 -3
  129. package/src/cli/controller/codex-continuation.ts +4 -7
  130. package/src/cli/controller/context-pack.ts +118 -14
  131. package/src/cli/controller/operational-plan.ts +4 -4
  132. package/src/cli/controller/task-ledger.ts +10 -10
  133. package/src/cli/controller/work-mode.ts +6 -13
  134. package/src/cli/index.ts +14 -35
  135. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  136. package/src/cli/local-bridge/console-automations.ts +160 -0
  137. package/src/cli/local-bridge/dashboard.ts +8 -1303
  138. package/src/cli/local-bridge/facade-api.ts +16 -7
  139. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  140. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  141. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  142. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  144. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  146. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  147. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  148. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  149. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  150. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  152. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  153. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  155. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  156. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  157. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  158. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  159. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  160. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  161. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  162. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  163. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  164. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  165. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  166. package/src/cli/local-bridge/server.ts +44 -0
  167. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  168. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  169. package/src/cli/mcp/instructions.ts +3 -3
  170. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  171. package/src/cli/mcp/multi-repository.ts +2 -0
  172. package/src/cli/mcp/policy.ts +7 -5
  173. package/src/cli/mcp/repository-tools.ts +53 -14
  174. package/src/cli/mcp/server.ts +5 -10
  175. package/src/cli/mcp/setup.ts +211 -275
  176. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  177. package/src/cli/mcp/toolset-names.ts +1 -1
  178. package/src/cli/mcp/toolset.ts +0 -1
  179. package/src/cli/mcp/transports/http.ts +24 -4
  180. package/src/cli/repositories/branch-name-policy.ts +0 -7
  181. package/src/cli/repositories/controller-home.ts +110 -3
  182. package/src/cli/repositories/safe-patch.ts +1 -1
  183. package/src/cli/repositories/selected-path-actions.ts +26 -24
  184. package/src/core/adoption/gitignore-plan.ts +2 -3
  185. package/src/core/adoption/plan.ts +4 -1
  186. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  187. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  188. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  189. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  190. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  191. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  192. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  193. package/src/runtime/control-plane/facade/types.ts +0 -1
  194. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  195. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  196. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  197. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  198. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  199. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  200. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  201. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  202. package/src/runtime/evidence/event-ledger.ts +1 -1
  203. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  204. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  205. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  206. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  207. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  208. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  209. package/src/runtime/execution/process-runtime/index.ts +3 -0
  210. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  211. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  212. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  213. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  214. package/src/runtime/execution/process-runtime/store.ts +1 -19
  215. package/src/runtime/execution/process-runtime/types.ts +4 -0
  216. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  217. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  218. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  219. package/src/runtime/execution/thin-harness/types.ts +0 -1
  220. package/src/runtime/execution/workers/executor.ts +1 -1
  221. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  222. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  223. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  224. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  225. package/src/runtime/gateway/mcp/router.ts +117 -182
  226. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  227. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  228. package/src/runtime/plugins/browser-adapter.ts +5 -0
  229. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  230. package/src/runtime/plugins/store.ts +14 -6
  231. package/src/runtime/plugins/types.ts +1 -1
  232. package/src/runtime/projections/materialized-view.ts +1 -23
  233. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  234. package/src/runtime/resources/ownership.ts +0 -1
  235. package/src/runtime/root/gateway-adapter.ts +2 -0
  236. package/src/runtime/root/mcp-transport.ts +10 -2
  237. package/src/runtime/root/package-runtime-release.ts +161 -0
  238. package/src/runtime/root/package-runtime-service.ts +172 -0
  239. package/src/runtime/root/runtime.ts +2 -1
  240. package/src/runtime/root/scheduler.ts +5 -0
  241. package/src/runtime/root/service.ts +18 -5
  242. package/src/runtime/standalone-recovery/core.ts +65 -16
  243. package/src/runtime/workflow/findings/store.ts +145 -106
  244. package/src/runtime/workflow/findings/types.ts +22 -0
  245. package/src/runtime/workflow/schedules/engine.ts +132 -2
  246. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  247. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  248. package/docs/CHANGELOG.md +0 -978
  249. package/docs/README.zh-CN.md +0 -58
  250. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  251. package/src/cli/commands/controller.ts +0 -680
  252. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  253. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  254. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  255. package/src/runtime/workflow/campaigns/review.ts +0 -315
  256. package/src/runtime/workflow/campaigns/store.ts +0 -526
  257. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  258. package/src/runtime/workflow/campaigns/types.ts +0 -290
  259. /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,72 @@ 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
+ warnings: result.warnings,
76
+ next: `forge runtime status --controller-home ${home}`,
77
+ });
78
+ });
27
79
 
28
80
  service.command('install')
29
- .description('Build an immutable Runtime release and install the single launchd Runtime service owner')
81
+ .description('Advanced/source mode: build an immutable Git Runtime release and install the macOS launchd owner')
30
82
  .requiredOption('--controller-home <path>', 'Explicit Controller Home')
31
83
  .requiredOption('--repo <path>', 'Repository root used as the immutable release source')
32
84
  .option('--host <host>', 'MCP listener host', '127.0.0.1')
@@ -115,7 +167,7 @@ export function buildRuntimeCommand(): Command {
115
167
  .requiredOption('--controller-home <path>', 'Explicit Controller Home')
116
168
  .option('--json', 'Output JSON')
117
169
  .action((opts: { controllerHome: string; json?: boolean }) => {
118
- const home = resolveControllerHome(opts.controllerHome);
170
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
119
171
  const runtime = observeRuntimeStatus(home);
120
172
  const repositories = listRepositories(home, { includeRemoved: true });
121
173
  const value = {
@@ -138,7 +190,7 @@ export function buildRuntimeCommand(): Command {
138
190
  .argument('<job-id>', 'Execution Job ID')
139
191
  .option('--controller-home <path>', 'Controller state root')
140
192
  .action((jobId: string, opts: { controllerHome?: string }) => {
141
- const home = resolveControllerHome(opts.controllerHome);
193
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
142
194
  const job = findExecutionJob(home, jobId);
143
195
  if (!job) throw new Error(`JOB_NOT_FOUND: ${jobId}`);
144
196
  output({ job, events: readJobEvents(home, job.repoId, job.jobId) });
@@ -150,7 +202,7 @@ export function buildRuntimeCommand(): Command {
150
202
  .option('--repo-id <id>', 'Repository id')
151
203
  .option('--limit <count>', 'Maximum records', '100')
152
204
  .action((opts: { controllerHome?: string; repoId?: string; limit?: string }) => {
153
- const home = resolveControllerHome(opts.controllerHome);
205
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
154
206
  if (opts.repoId) return output({ jobs: listExecutionJobs(home, opts.repoId, Number(opts.limit ?? 100)) });
155
207
  output({ jobs: listActiveExecutionJobs(home) });
156
208
  });
@@ -183,7 +235,7 @@ export function buildRuntimeCommand(): Command {
183
235
  .requiredOption('--repo-id <id>', 'Repository id')
184
236
  .option('--controller-home <path>', 'Controller state root')
185
237
  .action((opts: { controllerHome?: string; repoId: string }) => {
186
- const home = resolveControllerHome(opts.controllerHome);
238
+ const home = resolveRuntimeStateControllerHome(opts.controllerHome);
187
239
  output({ schedules: listSchedules(home, opts.repoId), occurrences: listOccurrences(home, opts.repoId, undefined, 100) });
188
240
  });
189
241
 
@@ -0,0 +1,441 @@
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
+ export interface ControllerGuidance {
210
+ controller: SetupControllerKind;
211
+ ready: boolean;
212
+ title: string;
213
+ detail: string;
214
+ command?: string;
215
+ }
216
+
217
+ function codexUserControllerConfigured(home = homedir()): boolean {
218
+ const path = join(home, '.codex', 'config.toml');
219
+ if (!existsSync(path)) return false;
220
+ try {
221
+ const text = readFileSync(path, 'utf8');
222
+ const block = text.match(/\[mcp_servers\.forge\][\s\S]*?(?=\n\[|$)/)?.[0] ?? '';
223
+ return block.includes('"mcp"') && block.includes('"serve"') && block.includes('"controller"');
224
+ } catch { return false; }
225
+ }
226
+
227
+ function claudeUserControllerConfigured(): boolean {
228
+ if (!commandExists('claude')) return false;
229
+ const probe = spawnSync('claude', ['mcp', 'get', 'forge'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 10_000 });
230
+ if (probe.status !== 0) return false;
231
+ const text = `${probe.stdout ?? ''}\n${probe.stderr ?? ''}`;
232
+ return /forge\s+mcp\s+serve|"mcp".*"serve"/is.test(text) && /controller/i.test(text);
233
+ }
234
+
235
+ export function resolveControllerGuidance(
236
+ profile: SetupProfile | undefined,
237
+ options: { controllerHome?: string; home?: string } = {},
238
+ ): ControllerGuidance | undefined {
239
+ if (!profile) return undefined;
240
+ const controllerHome = resolveControllerHome(options.controllerHome);
241
+ const localConfig = loadMcpServiceLocalConfig(controllerHome);
242
+ for (const controller of profile.controllers) {
243
+ if (controller === 'chatgpt' || controller === 'mcp') {
244
+ const localReady = localConfig?.profile === 'controller'
245
+ && localConfig.server?.host === '127.0.0.1'
246
+ && Number.isInteger(localConfig.server?.port);
247
+ const endpointReady = !profile.tunnel.endpoint || localConfig?.chatgpt?.endpoint === profile.tunnel.endpoint;
248
+ if (!localReady || !endpointReady) {
249
+ const endpoint = profile.tunnel.endpoint ? ` --endpoint ${profile.tunnel.endpoint}` : '';
250
+ return {
251
+ controller,
252
+ ready: false,
253
+ title: controller === 'chatgpt' ? 'Configure the ChatGPT controller endpoint' : 'Configure the remote MCP controller endpoint',
254
+ detail: 'Create the user-level Forge OAuth/MCP configuration. This does not install Codex or Claude and does not require a repository.',
255
+ command: `forge mcp setup chatgpt --user-level${endpoint}`,
256
+ };
257
+ }
258
+ continue;
259
+ }
260
+ if (controller === 'codex') {
261
+ if (!codexUserControllerConfigured(options.home)) {
262
+ return {
263
+ controller, ready: false, title: 'Configure Codex as a Forge controller',
264
+ detail: 'Codex is optional. Configure it only because it was explicitly selected as a controller entry.',
265
+ command: 'forge mcp setup codex --scope user --profile controller',
266
+ };
267
+ }
268
+ continue;
269
+ }
270
+ if (!commandExists('claude')) {
271
+ return {
272
+ controller, ready: false, title: 'Install Claude Code only for this selected controller',
273
+ detail: 'Claude is not a Forge dependency. Install and authenticate Claude Code, then continue setup.',
274
+ };
275
+ }
276
+ if (!claudeUserControllerConfigured()) {
277
+ return {
278
+ controller, ready: false, title: 'Configure Claude Code as a Forge controller',
279
+ detail: 'Forge delegates MCP registration to the official Claude CLI at user scope.',
280
+ command: 'forge mcp setup claude --scope user --profile controller',
281
+ };
282
+ }
283
+ }
284
+ return undefined;
285
+ }
286
+
287
+
288
+ export interface RuntimeGuidance {
289
+ ready: boolean;
290
+ title: string;
291
+ detail: string;
292
+ command?: string;
293
+ }
294
+
295
+ export function resolveRuntimeGuidance(
296
+ profile: SetupProfile | undefined,
297
+ options: { controllerHome?: string } = {},
298
+ ): RuntimeGuidance | undefined {
299
+ if (!profile || !setupNeedsRemoteAccess(profile)) return undefined;
300
+ const controllerHome = resolveControllerHome(options.controllerHome);
301
+ const status = observeRuntimeStatus(controllerHome);
302
+ if (status.ready) return { ready: true, title: 'Forge Runtime', detail: 'Canonical Runtime is running and ready.' };
303
+ if (status.running) {
304
+ return {
305
+ ready: false,
306
+ title: 'Forge Runtime is starting or unhealthy',
307
+ detail: `Runtime owner is running but not ready (${status.reasonCodes.join(', ') || 'unknown'}). Inspect status before continuing.`,
308
+ command: `forge runtime status --controller-home ${controllerHome}`,
309
+ };
310
+ }
311
+ return {
312
+ ready: false,
313
+ title: 'Install the user-level Forge Runtime',
314
+ detail: 'Use the packaged Runtime path. It does not require a Git checkout, Bun compilation, CodeGraph, or Standalone Recovery.',
315
+ command: 'forge runtime service install-package',
316
+ };
317
+ }
318
+
319
+ export function resolveTunnelGuidance(
320
+ profile: SetupProfile | undefined,
321
+ platform = detectSetupPlatform(),
322
+ options: { controllerHome?: string } = {},
323
+ ): TunnelGuidance {
324
+ if (!profile || !setupNeedsRemoteAccess(profile)) {
325
+ return { provider: 'none', ready: true, title: 'Remote controller connection', detail: 'Not required by the configured local controller set.' };
326
+ }
327
+ if (profile.tunnel.endpoint) {
328
+ 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.` };
329
+ }
330
+ let provider = profile.tunnel.provider;
331
+ if (provider === 'auto') {
332
+ if (platform.commands.tunnelClient) provider = 'openai';
333
+ else if (platform.commands.cloudflared) provider = 'cloudflare';
334
+ else if (platform.commands.tailscale) provider = 'tailscale';
335
+ else {
336
+ return {
337
+ provider: 'auto', ready: false, title: 'Choose secure remote access',
338
+ detail: 'For ChatGPT, prefer OpenAI Secure MCP Tunnel when your workspace/Platform organization supports it; otherwise choose Cloudflare Tunnel, Tailscale Funnel, an existing HTTPS endpoint, or defer remote access. Forge never requires public ingress when a private tunnel is available.',
339
+ };
340
+ }
341
+ }
342
+ if (provider === 'openai') {
343
+ if (!profile.tunnel.tunnelId) {
344
+ return {
345
+ provider, ready: false, title: 'Create an OpenAI Secure MCP Tunnel',
346
+ 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.',
347
+ command: 'forge setup configure --controller chatgpt --tunnel openai --tunnel-id tunnel_...',
348
+ };
349
+ }
350
+ if (!platform.commands.tunnelClient) {
351
+ return {
352
+ provider, ready: false, title: 'Install OpenAI tunnel-client',
353
+ 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.',
354
+ };
355
+ }
356
+ const alias = 'forge';
357
+ const status = spawnSync('tunnel-client', ['runtimes', 'status', alias, '--json'], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'], timeout: 15_000 });
358
+ if (status.status === 0) {
359
+ try {
360
+ const value = JSON.parse(status.stdout || '{}') as { process_running?: boolean; healthy?: boolean; ready?: boolean; tunnel_id?: string };
361
+ if (value.process_running === true && value.healthy === true && value.ready === true
362
+ && (!value.tunnel_id || value.tunnel_id === profile.tunnel.tunnelId)) {
363
+ return {
364
+ provider, ready: true, title: 'OpenAI Secure MCP Tunnel',
365
+ detail: `Managed runtime ${alias} is running, healthy, and ready for ${profile.tunnel.tunnelId}.`,
366
+ };
367
+ }
368
+ } catch {
369
+ // Treat malformed or older status output as not-ready instead of claiming success.
370
+ }
371
+ }
372
+ const localConfig = loadMcpServiceLocalConfig(resolveControllerHome(options.controllerHome));
373
+ const configuredPort = localConfig?.server?.port;
374
+ const mcpPort = typeof configuredPort === 'number' && Number.isInteger(configuredPort) ? configuredPort : 8765;
375
+ return {
376
+ provider, ready: false, title: 'Connect OpenAI Secure MCP Tunnel',
377
+ detail: 'Create a supervised tunnel-client runtime and verify it with runtimes status. Set CONTROL_PLANE_API_KEY in the invoking environment; Forge passes only an env reference and never reads or stores the key.',
378
+ command: `tunnel-client runtimes connect --alias ${alias} --tunnel-id ${profile.tunnel.tunnelId} --runtime-api-key env:CONTROL_PLANE_API_KEY --mcp-server-url http://127.0.0.1:${mcpPort}/mcp`,
379
+ };
380
+ }
381
+ if (provider === 'none') {
382
+ return {
383
+ provider, ready: false, title: 'Remote controller access is deferred',
384
+ detail: 'The selected controller set includes ChatGPT/remote MCP, but remote connectivity is deferred.',
385
+ command: 'forge setup configure --controller chatgpt --tunnel auto',
386
+ };
387
+ }
388
+ if (provider === 'existing') {
389
+ return {
390
+ provider, ready: false, title: 'Record existing HTTPS endpoint',
391
+ detail: 'Provide the existing public HTTPS URL ending in /mcp.',
392
+ command: 'forge setup configure --tunnel existing --endpoint https://forge.example.com/mcp',
393
+ };
394
+ }
395
+ if (provider === 'cloudflare') {
396
+ if (!platform.commands.cloudflared) {
397
+ const install = platform.environment === 'macos' && platform.commands.brew ? 'brew install cloudflared' : undefined;
398
+ return {
399
+ provider, ready: false, title: 'Install Cloudflare Tunnel',
400
+ detail: platform.environment === 'windows'
401
+ ? 'Install the official cloudflared executable or MSI, then run forge setup next.'
402
+ : platform.environment === 'linux' || platform.environment === 'wsl2'
403
+ ? 'Install cloudflared from Cloudflare’s official package repository for this Linux distribution, then run forge setup next.'
404
+ : 'Install cloudflared, then run forge setup next.',
405
+ ...(install ? { command: install } : {}),
406
+ };
407
+ }
408
+ return {
409
+ provider, ready: false, title: 'Create a stable Cloudflare Tunnel',
410
+ detail: 'Authenticate cloudflared, create/route a named tunnel to 127.0.0.1:8765, then record its HTTPS /mcp URL.',
411
+ command: 'cloudflared tunnel login',
412
+ };
413
+ }
414
+ if (!platform.commands.tailscale) {
415
+ return {
416
+ provider: 'tailscale', ready: false, title: 'Install Tailscale',
417
+ detail: platform.environment === 'macos'
418
+ ? 'Install Tailscale for macOS and enable CLI integration, then run forge setup next.'
419
+ : platform.environment === 'windows'
420
+ ? 'Install the official Tailscale Windows client, sign in, then run forge setup next.'
421
+ : 'Install Tailscale for Linux, sign in, then run forge setup next.',
422
+ };
423
+ }
424
+ return {
425
+ provider: 'tailscale', ready: false, title: 'Enable Tailscale Funnel',
426
+ detail: 'Expose only the local Forge MCP service and then record the generated HTTPS /mcp URL.',
427
+ command: 'tailscale funnel --bg 8765',
428
+ };
429
+ }
430
+
431
+ export function formatSetupProfile(profile: SetupProfile, platform = detectSetupPlatform()): string {
432
+ const tunnel = resolveTunnelGuidance(profile, platform);
433
+ return [
434
+ `Primary controller: ${profile.primaryController}`,
435
+ `Configured controllers: ${profile.controllers.join(', ')}`,
436
+ `Platform: ${platform.environment}/${platform.arch}; service=${platform.serviceManager}`,
437
+ `Remote access: ${profile.tunnel.provider}${profile.tunnel.endpoint ? ` (${profile.tunnel.endpoint})` : profile.tunnel.tunnelId ? ` (${profile.tunnel.tunnelId})` : ''}`,
438
+ `Tunnel readiness: ${tunnel.ready ? 'ready' : 'needs setup'} — ${tunnel.detail}`,
439
+ 'Forge itself remains the execution/runtime layer; it does not choose semantic actions without an external controller.',
440
+ ].join('\n');
441
+ }
@@ -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
  }
@@ -79,10 +79,10 @@ ${packet.allowedActions.map((action) => `- ${action}`).join('\n')}
79
79
  ${packet.stopConditions.map((condition) => `- ${condition}`).join('\n')}
80
80
 
81
81
  ## Instructions
82
- 1. Prefer \`forge controller completion-backlog --json\` before starting new work.
83
- 2. Run \`forge controller finish-ready-runs --apply --json\` only for low/medium auto-finishable runs.
82
+ 1. Read current Runtime state through \`rh_status\`, pending decisions through \`rh_inbox\`, and bounded Work through \`rh_work\` before starting new work.
83
+ 2. Use \`rh_work.verify/finalize/repair\` for current Work lifecycle changes; do not mutate legacy Issue/Task state.
84
84
  3. For high/destructive work, prepare a concise review summary instead of auto-approving it.
85
- 4. If a command fails because of tool/platform limitations, write a small patch or a continuation note rather than retrying the same blocked path repeatedly.
85
+ 4. If a command fails because of tool/platform limitations, create a Runtime HandoffItem or bounded continuation note rather than retrying the same blocked path repeatedly.
86
86
  5. Finish with checks, a short status summary, and explicit remaining blockers.
87
87
  `;
88
88
  }
@@ -101,7 +101,7 @@ export function prepareCodexContinuation(repoRoot: string, options: CodexContinu
101
101
  controller: 'codex-cli',
102
102
  allowedActions: [
103
103
  'inspect compact backlog and stuck-state reports',
104
- 'finish low/medium successful runs using forge controller finish-ready-runs',
104
+ 'finish validated bounded Work through rh_work.finalize when policy allows',
105
105
  'prepare patches for remaining blocker classes',
106
106
  'run declared checks and summarize failures',
107
107
  ],
@@ -123,9 +123,6 @@ export function prepareCodexContinuation(repoRoot: string, options: CodexContinu
123
123
  recommendations: stuckStates.recommendations,
124
124
  },
125
125
  nextCommands: [
126
- 'forge controller completion-backlog --json',
127
- 'forge controller stuck-states --json',
128
- ...(backlog.finishableRunIds.length ? ['forge controller finish-ready-runs --apply --json'] : []),
129
126
  'npm run check:type',
130
127
  'npm run check:mcp-compatibility',
131
128
  ],