@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.
- package/AGENTS.md +9 -11
- package/CHANGELOG.md +19 -0
- package/CLAUDE.md +8 -10
- package/DESIGN.md +278 -0
- package/README.md +70 -90
- package/README.zh-CN.md +50 -55
- package/SKILL.md +8 -11
- package/THIRD_PARTY_NOTICES.md +9 -3
- package/assets/branding/README.md +4 -2
- package/assets/hooks/lib/workflow-state.sh +19 -18
- package/assets/hooks/post-edit-guard.sh +6 -75
- package/assets/hooks/prompt-guard.sh +24 -12
- package/assets/hooks/session-start-context.sh +17 -17
- package/assets/hooks/stop-orchestrator.sh +2 -2
- package/assets/partials/04-project-structure.partial.md +6 -6
- package/assets/partials/05-workflow.partial.md +6 -6
- package/assets/partials/07-footer.partial.md +1 -1
- package/assets/partials/08-orchestration.partial.md +2 -2
- package/assets/partials-agents/02-operating-mode.partial.md +5 -5
- package/assets/partials-agents/03-orchestration.partial.md +3 -3
- package/assets/partials-agents/04-task-protocol.partial.md +3 -3
- package/assets/partials-agents/08-deep-docs.partial.md +1 -1
- package/assets/reference-configs/agentic-development-flow.md +49 -86
- package/assets/reference-configs/external-tooling.md +21 -61
- package/assets/reference-configs/handoff-protocol.md +25 -20
- package/assets/reference-configs/harness-overview.md +8 -8
- package/assets/templates/helpers/archive-workflow.sh +0 -1
- package/assets/templates/helpers/capture-plan.sh +3 -3
- package/assets/templates/helpers/check-task-workflow.sh +2 -2
- package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
- package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
- package/assets/templates/helpers/new-plan.sh +1 -1
- package/assets/templates/helpers/plan-to-todo.sh +2 -2
- package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
- package/assets/templates/helpers/prepare-handoff.sh +9 -9
- package/assets/templates/helpers/refresh-current-status.sh +2 -2
- package/assets/templates/helpers/ship-worktrees.sh +1 -1
- package/assets/templates/helpers/sprint-backlog.sh +1 -1
- package/assets/templates/helpers/verify-sprint.sh +7 -7
- package/assets/templates/helpers/workstream-sync.sh +1 -1
- package/assets/templates/plan.template.md +1 -1
- package/assets/workflow-contract.v1.json +6 -3
- package/docs/architecture/current/README.md +10 -9
- package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
- package/docs/architecture/current/architecture-invariants.md +2 -2
- package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
- package/docs/architecture/current/codegraph-context-provider.md +2 -2
- package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
- package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
- package/docs/architecture/current/entity-model.md +0 -27
- package/docs/architecture/current/failure-recovery.md +0 -24
- package/docs/architecture/current/governance.md +1 -1
- package/docs/architecture/current/implementation-status.md +1 -1
- package/docs/architecture/current/legacy-governance.md +58 -0
- package/docs/architecture/current/multi-repository-execution.md +1 -1
- package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
- package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
- package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
- package/docs/architecture/current/target-requirements-traceability.md +2 -2
- package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
- package/docs/architecture/current/thin-harness-v1.md +9 -11
- package/docs/architecture/domains/runtime-harness.md +1 -1
- package/docs/forge-autonomous-goal-loop.md +3 -1
- package/docs/forge-chatgpt-mcp-setup.md +51 -184
- package/docs/images/forge-banner-cn.svg +26 -1
- package/docs/images/forge-banner.svg +26 -1
- package/docs/operations/features.md +1 -1
- package/docs/operations/homebrew.md +3 -3
- package/docs/operations/mcp-tool-exposure.md +5 -5
- package/docs/operations/platform-support.md +45 -37
- package/docs/operations/platform-support.zh-CN.md +40 -38
- package/docs/operations/releasing.md +3 -3
- package/docs/operations/releasing.zh-CN.md +3 -3
- package/docs/operations/troubleshooting.md +11 -6
- package/docs/operations/troubleshooting.zh-CN.md +11 -6
- package/docs/public-usage-guide.md +28 -21
- package/docs/public-usage-guide.zh-CN.md +28 -21
- package/docs/tutorials/01-install-and-start.md +56 -35
- package/docs/tutorials/01-install-and-start.zh-CN.md +56 -35
- package/docs/tutorials/02-connect-chatgpt.md +112 -46
- package/docs/tutorials/02-connect-chatgpt.zh-CN.md +113 -45
- package/docs/wiki/Architecture.md +1 -1
- package/docs/wiki/Installation.md +2 -2
- package/docs/wiki/Quick-Start.md +1 -1
- package/docs/wiki/Work-Lifecycle.md +1 -1
- package/install.ps1 +4 -5
- package/install.sh +4 -6
- package/package.json +15 -5
- package/scripts/archive-workflow.sh +0 -1
- package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
- package/scripts/build-controller-ui.ts +28 -0
- package/scripts/capture-plan.sh +4 -4
- package/scripts/check-agent-tooling.sh +12 -1
- package/scripts/check-platform-support.mjs +2 -2
- package/scripts/check-public-docs.sh +16 -5
- package/scripts/check-release-readiness.sh +5 -11
- package/scripts/check-runtime-architecture.mjs +14 -22
- package/scripts/check-tarball-install-smoke.sh +3 -2
- package/scripts/check-task-workflow.sh +2 -2
- package/scripts/codex-handoff-resume.sh +5 -5
- package/scripts/create-project-dirs.sh +3 -3
- package/scripts/ensure-task-workflow.sh +45 -35
- package/scripts/init-project.sh +4 -4
- package/scripts/install-desktop-operator-registration.ts +2 -2
- package/scripts/lib/project-init-lib.sh +46 -44
- package/scripts/migrate-project-template.sh +7 -4
- package/scripts/new-plan.sh +1 -1
- package/scripts/plan-to-todo.sh +2 -2
- package/scripts/prepare-codex-handoff.sh +2 -2
- package/scripts/prepare-handoff.sh +9 -9
- package/scripts/refresh-current-status.sh +2 -2
- package/scripts/setup-plugins.sh +3 -3
- package/scripts/ship-worktrees.sh +1 -1
- package/scripts/sprint-backlog.sh +5 -5
- package/scripts/sync-codex-installed-copies.sh +2 -3
- package/scripts/verify-sprint.sh +7 -7
- package/scripts/workstream-sync.sh +1 -1
- package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
- package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
- package/src/cli/chatgpt-browser/file-policy.ts +3 -1
- package/src/cli/commands/chatgpt.ts +51 -0
- package/src/cli/commands/doctor.ts +1 -1
- package/src/cli/commands/init-hook.ts +211 -70
- package/src/cli/commands/init.ts +5 -6
- package/src/cli/commands/mcp.ts +29 -5
- package/src/cli/commands/runtime.ts +60 -8
- package/src/cli/commands/setup-profile.ts +441 -0
- package/src/cli/controller/check-runner.ts +20 -3
- package/src/cli/controller/codex-continuation.ts +4 -7
- package/src/cli/controller/context-pack.ts +118 -14
- package/src/cli/controller/operational-plan.ts +4 -4
- package/src/cli/controller/task-ledger.ts +10 -10
- package/src/cli/controller/work-mode.ts +6 -13
- package/src/cli/index.ts +14 -35
- package/src/cli/local-bridge/connector-freshness.ts +1 -1
- package/src/cli/local-bridge/console-automations.ts +160 -0
- package/src/cli/local-bridge/dashboard.ts +8 -1303
- package/src/cli/local-bridge/facade-api.ts +16 -7
- package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
- package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
- package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
- package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
- package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
- package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
- package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
- package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
- package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
- package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
- package/src/cli/local-bridge/frontend/src/styles.css +12 -0
- package/src/cli/local-bridge/frontend/src/types.ts +14 -0
- package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
- package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
- package/src/cli/local-bridge/server.ts +44 -0
- package/src/cli/local-bridge/ui-dist/app.css +1 -0
- package/src/cli/local-bridge/ui-dist/app.js +11094 -0
- package/src/cli/mcp/instructions.ts +3 -3
- package/src/cli/mcp/legacy-tool-service.ts +27 -23
- package/src/cli/mcp/multi-repository.ts +2 -0
- package/src/cli/mcp/policy.ts +7 -5
- package/src/cli/mcp/repository-tools.ts +53 -14
- package/src/cli/mcp/server.ts +5 -10
- package/src/cli/mcp/setup.ts +211 -275
- package/src/cli/mcp/tool-capability-cache.ts +1 -1
- package/src/cli/mcp/toolset-names.ts +1 -1
- package/src/cli/mcp/toolset.ts +0 -1
- package/src/cli/mcp/transports/http.ts +24 -4
- package/src/cli/repositories/branch-name-policy.ts +0 -7
- package/src/cli/repositories/controller-home.ts +110 -3
- package/src/cli/repositories/safe-patch.ts +1 -1
- package/src/cli/repositories/selected-path-actions.ts +26 -24
- package/src/core/adoption/gitignore-plan.ts +2 -3
- package/src/core/adoption/plan.ts +4 -1
- package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
- package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
- package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
- package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
- package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
- package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
- package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
- package/src/runtime/control-plane/facade/types.ts +0 -1
- package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
- package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
- package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
- package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
- package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
- package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
- package/src/runtime/control-plane/routing/route-policy.ts +14 -14
- package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
- package/src/runtime/evidence/event-ledger.ts +1 -1
- package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
- package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
- package/src/runtime/execution/process-runtime/check-result.ts +45 -0
- package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
- package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
- package/src/runtime/execution/process-runtime/gc.ts +1 -1
- package/src/runtime/execution/process-runtime/index.ts +3 -0
- package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
- package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
- package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
- package/src/runtime/execution/process-runtime/runtime.ts +18 -1
- package/src/runtime/execution/process-runtime/store.ts +1 -19
- package/src/runtime/execution/process-runtime/types.ts +4 -0
- package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
- package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
- package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
- package/src/runtime/execution/thin-harness/types.ts +0 -1
- package/src/runtime/execution/workers/executor.ts +1 -1
- package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
- package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
- package/src/runtime/gateway/mcp/process-tools.ts +16 -3
- package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
- package/src/runtime/gateway/mcp/router.ts +117 -182
- package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
- package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
- package/src/runtime/plugins/browser-adapter.ts +5 -0
- package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
- package/src/runtime/plugins/store.ts +14 -6
- package/src/runtime/plugins/types.ts +1 -1
- package/src/runtime/projections/materialized-view.ts +1 -23
- package/src/runtime/recovery/maintenance-executor.ts +9 -0
- package/src/runtime/resources/ownership.ts +0 -1
- package/src/runtime/root/gateway-adapter.ts +2 -0
- package/src/runtime/root/mcp-transport.ts +10 -2
- package/src/runtime/root/package-runtime-release.ts +161 -0
- package/src/runtime/root/package-runtime-service.ts +172 -0
- package/src/runtime/root/runtime.ts +2 -1
- package/src/runtime/root/scheduler.ts +5 -0
- package/src/runtime/root/service.ts +18 -5
- package/src/runtime/standalone-recovery/core.ts +65 -16
- package/src/runtime/workflow/findings/store.ts +145 -106
- package/src/runtime/workflow/findings/types.ts +22 -0
- package/src/runtime/workflow/schedules/engine.ts +132 -2
- package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
- package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
- package/docs/CHANGELOG.md +0 -978
- package/docs/README.zh-CN.md +0 -58
- package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
- package/src/cli/commands/controller.ts +0 -680
- package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
- package/src/runtime/workflow/campaigns/engine.ts +0 -112
- package/src/runtime/workflow/campaigns/normalize.ts +0 -100
- package/src/runtime/workflow/campaigns/review.ts +0 -315
- package/src/runtime/workflow/campaigns/store.ts +0 -526
- package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
- package/src/runtime/workflow/campaigns/types.ts +0 -290
- /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
|
|
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('
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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-
|
|
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
|
-
|
|
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.
|
|
83
|
-
2.
|
|
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,
|
|
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
|
|
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
|
],
|