@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,30 +1,42 @@
1
1
  import { spawn, spawnSync } from 'child_process';
2
2
  import {
3
- claimControllerSession,
4
3
  getHandoffItem,
5
4
  getWorkContract,
6
- releaseControllerSession,
7
5
  type HandoffInboxStoreOptions,
8
6
  type WorkContractStoreOptions,
9
7
  } from '../facade';
8
+ import {
9
+ attachExternalControllerLaunchPid,
10
+ releaseExternalControllerLaunchReservation,
11
+ reserveExternalControllerLaunch,
12
+ } from './launch-reservation-store';
10
13
  import type { ControllerType } from '../facade/types';
14
+ import { codexMcpConfigArgs, resolveProviderMcpBootstrap, type ProviderMcpBootstrap } from './provider-mcp-bootstrap';
15
+ import { getChatgptWorkConversationBinding } from './chatgpt-work-binding-store';
11
16
 
12
17
  export interface ThinLauncherRequest {
13
18
  controllerType: Exclude<ControllerType, 'human'>;
14
19
  executable?: string;
15
20
  args?: string[];
16
21
  workId: string;
17
- controllerId: string;
18
- sessionId: string;
19
- leaseMs?: number;
22
+ /** Short reservation only prevents duplicate spawns; the external MCP session claims Work with its authenticated identity. */
23
+ launchReservationMs?: number;
20
24
  handoffId?: string;
25
+ /** Saved Forge ChatGPT browser session to continue. */
26
+ browserSessionId?: string;
27
+ /** Explicit ChatGPT conversation URL used when no saved Forge browser session exists. */
28
+ conversationUrl?: string;
29
+ /** Additional bounded continuation instruction, for example a Schedule occurrence. */
30
+ continuationPrompt?: string;
21
31
  cwd: string;
32
+ /** Internal launch context; populated by launchSuperController, not by callers. */
33
+ controllerHome?: string;
34
+ repoId?: string;
22
35
  }
23
36
 
24
37
  export interface ThinLauncherResult {
25
38
  controllerType: ThinLauncherRequest['controllerType'];
26
- controllerId: string;
27
- sessionId: string;
39
+ reservationId: string;
28
40
  pid: number | undefined;
29
41
  prompt: string;
30
42
  executable: string;
@@ -37,7 +49,9 @@ function resolveLauncherExecutable(request: ThinLauncherRequest): string {
37
49
  ? 'codex'
38
50
  : request.controllerType === 'claude'
39
51
  ? 'claude'
40
- : '';
52
+ : request.controllerType === 'chatgpt'
53
+ ? process.env.FORGE_CLI_EXECUTABLE?.trim() || 'forge'
54
+ : '';
41
55
  if (!executable) throw new Error(`LAUNCHER_EXECUTABLE_REQUIRED: ${request.controllerType} requires an external launcher executable`);
42
56
  const probe = spawnSync(executable, ['--version'], {
43
57
  cwd: request.cwd,
@@ -50,6 +64,71 @@ function resolveLauncherExecutable(request: ThinLauncherRequest): string {
50
64
  return executable;
51
65
  }
52
66
 
67
+ function assertChatgptConversationUrl(value: string | undefined): string | undefined {
68
+ const raw = value?.trim();
69
+ if (!raw) return undefined;
70
+ let url: URL;
71
+ try {
72
+ url = new URL(raw);
73
+ } catch {
74
+ throw new Error('LAUNCHER_CHATGPT_CONVERSATION_URL_INVALID');
75
+ }
76
+ if (url.protocol !== 'https:' || !['chatgpt.com', 'www.chatgpt.com'].includes(url.hostname)) {
77
+ throw new Error('LAUNCHER_CHATGPT_CONVERSATION_URL_INVALID');
78
+ }
79
+ return url.toString();
80
+ }
81
+
82
+ export function buildSuperControllerInvocation(
83
+ request: ThinLauncherRequest,
84
+ executable: string,
85
+ prompt: string,
86
+ mcpBootstrap?: ProviderMcpBootstrap,
87
+ ): { executable: string; args: string[] } {
88
+ if (request.controllerType === 'codex') {
89
+ if (!mcpBootstrap) throw new Error('LAUNCHER_CODEX_FORGE_MCP_REQUIRED');
90
+ return {
91
+ executable,
92
+ args: [
93
+ '--ask-for-approval', 'never',
94
+ ...codexMcpConfigArgs(mcpBootstrap),
95
+ 'exec', '--sandbox', 'workspace-write',
96
+ ...(request.args ?? []),
97
+ prompt,
98
+ ],
99
+ };
100
+ }
101
+ if (request.controllerType === 'claude') {
102
+ if (!(request.args ?? []).includes('--mcp-config')) {
103
+ throw new Error('LAUNCHER_CLAUDE_FORGE_MCP_CONFIG_REQUIRED');
104
+ }
105
+ return {
106
+ executable,
107
+ args: ['--print', '--permission-mode', 'auto', ...(request.args ?? []), prompt],
108
+ };
109
+ }
110
+ if (request.controllerType !== 'chatgpt') {
111
+ return { executable, args: [...(request.args ?? []), prompt] };
112
+ }
113
+ const browserSessionId = request.browserSessionId?.trim();
114
+ const conversationUrl = assertChatgptConversationUrl(request.conversationUrl);
115
+ if (!request.controllerHome || !request.repoId) throw new Error('LAUNCHER_CHATGPT_WORK_BINDING_CONTEXT_REQUIRED');
116
+ return {
117
+ executable,
118
+ args: [
119
+ 'chatgpt', 'work-continue',
120
+ '--repo', request.cwd,
121
+ '--controller-home', request.controllerHome,
122
+ '--repo-id', request.repoId,
123
+ '--work-id', request.workId,
124
+ '--prompt', prompt,
125
+ ...(browserSessionId ? ['--session', browserSessionId] : []),
126
+ ...(conversationUrl ? ['--conversation-url', conversationUrl] : []),
127
+ ...(request.args ?? []),
128
+ ],
129
+ };
130
+ }
131
+
53
132
  /**
54
133
  * Starts one external SuperController session. This module deliberately owns
55
134
  * provider process selection and prompt construction; the execution Kernel
@@ -62,36 +141,50 @@ export function launchSuperController(
62
141
  },
63
142
  request: ThinLauncherRequest,
64
143
  ): ThinLauncherResult {
65
- if (!request.controllerId.trim() || !request.sessionId.trim()) throw new Error('LAUNCHER_CONTROLLER_SESSION_REQUIRED');
66
144
  const executable = resolveLauncherExecutable(request);
67
145
  const work = getWorkContract(stores.work, request.workId);
68
146
  if (!work) throw new Error(`WORK_NOT_FOUND: ${request.workId}`);
69
- const session = claimControllerSession(stores.work, {
147
+ const reservation = reserveExternalControllerLaunch(stores.work, {
70
148
  workId: work.workId,
71
- controllerId: request.controllerId,
72
149
  controllerType: request.controllerType,
73
- sessionId: request.sessionId,
74
- leaseMs: request.leaseMs,
150
+ ttlMs: request.launchReservationMs,
75
151
  });
76
152
  const handoff = request.handoffId ? getHandoffItem(stores.handoff, request.handoffId) : undefined;
77
- const prompt = [
78
- `Work: ${work.workId}`,
79
- `Objective: ${work.objective}`,
80
- `Acceptance: ${work.acceptanceCriteria.join('; ') || 'none declared'}`,
81
- `Current status: ${work.status}`,
82
- handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
83
- 'Use the repository MCP facade directly. Claim the Work before mutation and create a handoff when control changes.',
84
- ].filter(Boolean).join('\n');
153
+ const chatgptBinding = request.controllerType === 'chatgpt'
154
+ ? getChatgptWorkConversationBinding(stores.work, work.workId)
155
+ : undefined;
156
+ const prompt = (request.controllerType === 'chatgpt' && chatgptBinding)
157
+ ? [
158
+ `Continue Forge Work ${work.workId} in repo ${work.repoId}.`,
159
+ handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
160
+ request.continuationPrompt?.trim() ? `Continuation: ${request.continuationPrompt.trim()}` : '',
161
+ `First call rh_work continue with repo_id=${work.repoId} and work_id=${work.workId}. Treat Forge Work/Plan/evidence as source of truth; do not invent new scope from chat history. If the claim fails, do not mutate. Continue the next safe action, finalize only when acceptance passes, and create a HandoffItem when judgement is required.`,
162
+ ].filter(Boolean).join('\n')
163
+ : [
164
+ `Work: ${work.workId}`,
165
+ `Objective: ${work.objective}`,
166
+ `Acceptance: ${work.acceptanceCriteria.join('; ') || 'none declared'}`,
167
+ `Current status: ${work.status}`,
168
+ handoff ? `Handoff: ${handoff.summary}\nNext: ${handoff.recommendedContinuationPrompt ?? handoff.recommendedPrompt}` : '',
169
+ request.continuationPrompt?.trim() ? `Continuation: ${request.continuationPrompt.trim()}` : '',
170
+ `No Controller ownership was preclaimed for you. First call rh_work continue with repo_id=${work.repoId} and work_id=${work.workId} through your authenticated MCP session; do not invent controller_id/session_id. If that claim does not succeed, do not mutate the repository: create no patch, command, commit, or test run until ownership is established. Then use the repository MCP facade, record verification evidence, finalize only when acceptance passes, and create a HandoffItem when judgement is required.`,
171
+ ].filter(Boolean).join('\n');
85
172
  try {
86
- const child = spawn(executable, [...(request.args ?? []), prompt], {
173
+ const mcpBootstrap = request.controllerType === 'codex'
174
+ ? resolveProviderMcpBootstrap(stores.work.controllerHome, 'codex', reservation.reservationId)
175
+ : undefined;
176
+ const invocation = buildSuperControllerInvocation({ ...request, controllerHome: stores.work.controllerHome, repoId: work.repoId }, executable, prompt, mcpBootstrap);
177
+ const child = spawn(invocation.executable, invocation.args, {
87
178
  cwd: request.cwd,
88
179
  detached: true,
89
180
  stdio: 'ignore',
181
+ env: mcpBootstrap?.env ?? process.env,
90
182
  });
91
183
  child.unref();
92
- return { controllerType: request.controllerType, controllerId: session.controllerId, sessionId: session.sessionId, pid: child.pid, prompt, executable };
184
+ attachExternalControllerLaunchPid(stores.work, work.workId, reservation.reservationId, child.pid);
185
+ return { controllerType: request.controllerType, reservationId: reservation.reservationId, pid: child.pid, prompt, executable };
93
186
  } catch (error) {
94
- releaseControllerSession(stores.work, work.workId, request.controllerId);
187
+ releaseExternalControllerLaunchReservation(stores.work, work.workId, reservation.reservationId, 'spawn_failed');
95
188
  throw error;
96
189
  }
97
190
  }
@@ -3,11 +3,11 @@ import { createHash } from 'crypto';
3
3
  export const ROUTE_POLICY_VERSION = 'route-policy-v3' as const;
4
4
 
5
5
  export type RouteExecutionMode = 'direct_control' | 'goal_workloop' | 'handoff_only';
6
- export type RouteWorkMode = 'direct_edit' | 'bounded_work' | 'quick_agent' | 'issue_task' | 'campaign';
7
- export type RouteExecutionPath = 'fast' | 'durable' | 'campaign';
6
+ export type RouteWorkMode = 'direct_edit' | 'bounded_work' | 'quick_agent' | 'issue_task';
7
+ export type RouteExecutionPath = 'fast' | 'durable';
8
8
  export type RouteExecutorKind = 'direct_edit' | 'local_cli' | 'remote_api' | 'cloud_agent' | 'external_controller' | 'handoff_only';
9
9
  export type RouteApprovalState = 'approval_not_required' | 'normal_authorization_required' | 'strong_confirmation_required' | 'blocked_by_policy';
10
- export type ExplicitTaskMode = 'direct' | 'plan' | 'debug' | 'review' | 'campaign' | 'release' | 'scale';
10
+ export type ExplicitTaskMode = 'direct' | 'plan' | 'debug' | 'review' | 'release' | 'scale';
11
11
 
12
12
  export interface RouteReason {
13
13
  code: string;
@@ -233,7 +233,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
233
233
  const approvalConfirmed = input.policy.approvalConfirmed === true;
234
234
  const protectedPath = paths.some((path) => PROTECTED_PATH.test(path));
235
235
  const explicitMode = input.intent.explicitMode;
236
- const explicitParallelMode = explicitMode === 'campaign' || explicitMode === 'scale';
236
+ const explicitParallelMode = explicitMode === 'scale';
237
237
  const requiresIsolation = explicitMode === 'direct'
238
238
  ? false
239
239
  : input.recovery.isolationRequired === true || explicitParallelMode;
@@ -242,7 +242,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
242
242
  || explicitMode === 'debug'
243
243
  || explicitMode === 'release'
244
244
  || explicitMode === 'scale';
245
- const campaignEligible = explicitParallelMode || input.intent.requiresIndependentDeliverables === true
245
+ const coordinationRequired = explicitParallelMode || input.intent.requiresIndependentDeliverables === true
246
246
  || independentTaskCount >= 3;
247
247
 
248
248
  if (input.policy.policyBlocked === true) {
@@ -304,7 +304,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
304
304
  if (input.intent.requiresLongRunningChecks) reasons.push({ code: 'long_checks', message: 'Long-running checks require durable continuation.' });
305
305
  if (input.intent.requiresInvestigation) reasons.push({ code: 'investigation', message: 'Investigation is required before or during implementation.' });
306
306
  if (input.intent.needsDependencies) reasons.push({ code: 'dependencies', message: 'Dependency ordering requires durable Work.' });
307
- if (campaignEligible) reasons.push({ code: 'independent_deliverables', message: 'Multiple independent deliverables require campaign-level coordination.' });
307
+ if (coordinationRequired) reasons.push({ code: 'independent_deliverables', message: 'Multiple independent deliverables require durable PlanContract/Work coordination.' });
308
308
 
309
309
  const explicitBoundedMode = explicitMode === 'plan'
310
310
  || explicitMode === 'debug'
@@ -312,7 +312,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
312
312
  || explicitMode === 'release';
313
313
  const complex = explicitMode === 'direct'
314
314
  ? false
315
- : campaignEligible
315
+ : coordinationRequired
316
316
  || explicitBoundedMode
317
317
  || requiresRecovery
318
318
  || requiresIsolation
@@ -326,19 +326,19 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
326
326
  || secretAccess
327
327
  || (mutation && (expectedFiles > 4 || expectedChangedLines > 200));
328
328
  const executionMode: RouteExecutionMode = complex ? 'goal_workloop' : 'direct_control';
329
- // Work topology is independent from executor/provider choice. Campaign is
330
- // selected only for genuinely independent/parallel deliverables; Agent
331
- // preference may choose an executor inside a tier but must not create the tier.
329
+ // Work topology is independent from executor/provider choice. Independent or
330
+ // parallel deliverables stay in the durable Goal Workloop and are decomposed by
331
+ // PlanContract/Work rather than introducing another project-level lifecycle.
332
332
  const workMode: RouteWorkMode = explicitMode === 'direct'
333
333
  ? 'direct_edit'
334
- : campaignEligible
335
- ? 'campaign'
334
+ : coordinationRequired
335
+ ? 'bounded_work'
336
336
  : input.intent.agentRequested
337
337
  ? expectedFiles > 10 || expectedChangedLines > 1_500 ? 'issue_task' : 'quick_agent'
338
338
  : complex
339
339
  ? 'bounded_work'
340
340
  : 'direct_edit';
341
- const executionPath: RouteExecutionPath = workMode === 'campaign' ? 'campaign' : complex ? 'durable' : 'fast';
341
+ const executionPath: RouteExecutionPath = complex ? 'durable' : 'fast';
342
342
  const providerSelection = selectProvider(input);
343
343
  const providersWereSupplied = input.capabilities.providers !== undefined;
344
344
  if (providersWereSupplied && !providerSelection.provider) {
@@ -364,7 +364,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
364
364
  workMode,
365
365
  executionPath,
366
366
  // Direct Control is intentionally contract-free. Persistence belongs to
367
- // Goal Workloop/Campaign/Agent tiers; bounded direct edits rely on the
367
+ // Goal Workloop/Agent tiers; bounded direct edits rely on the
368
368
  // existing permission, patch, Process, and evidence boundaries instead.
369
369
  requiresWork: executionMode !== 'direct_control',
370
370
  requiresApproval: approvalRequired,
@@ -76,7 +76,6 @@ const TEMP_SCAN_LEAF_DIR_NAMES = new Set([
76
76
  'audit',
77
77
  'local-jobs',
78
78
  'runs',
79
- 'campaigns',
80
79
  'schedules',
81
80
  'leases',
82
81
  'projections',
@@ -18,7 +18,7 @@ export interface RuntimeEntityEvent {
18
18
  eventId: string;
19
19
  eventType: string;
20
20
  repoId: string;
21
- entityType: 'job' | 'plugin' | 'schedule' | 'occurrence' | 'portfolio' | 'campaign' | 'candidate-finding' | 'release' | 'lease' | 'schedule-decision' | 'assistant-action-proposal' | 'assistant-standing-grant';
21
+ entityType: 'job' | 'plugin' | 'schedule' | 'occurrence' | 'portfolio' | 'candidate-finding' | 'release' | 'lease' | 'schedule-decision' | 'assistant-action-proposal' | 'assistant-standing-grant';
22
22
  entityId: string;
23
23
  correlationId?: string;
24
24
  causationId?: string;
@@ -1,36 +1,38 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { existsSync, mkdirSync, realpathSync } from 'fs';
3
3
  import { dirname, join, resolve } from 'path';
4
- import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
5
- import type { RepositoryRecord } from '../../../cli/repositories/types';
4
+ import { repositoryControllerRoot } from '../../cli/repositories/controller-home';
5
+ import type { RepositoryRecord } from '../../cli/repositories/types';
6
6
  import {
7
7
  addRepositoryCheckout,
8
8
  listRepositories,
9
9
  repositoryCheckoutRootMatches,
10
10
  selectRepositoryCheckout,
11
- } from '../../../cli/repositories/registry';
12
- import { managedWorktreePath } from '../../../cli/repositories/worktree-storage';
13
- import { campaignBranchName, validateBranchName } from '../../../cli/repositories/branch-name-policy';
14
- import { withControllerLock } from '../../../cli/repositories/locks';
15
- import { resolveGitExecutable } from '../../../effects/git-executable';
16
- import { runProcess } from '../../../effects/process-runner';
17
- import { readJsonFile, writeJsonAtomic } from '../../shared/json-files';
18
- import type { CampaignWorkspace } from './types';
11
+ } from '../../cli/repositories/registry';
12
+ import { managedWorktreePath } from '../../cli/repositories/worktree-storage';
13
+ import { branchSlugSegment, validateBranchName } from '../../cli/repositories/branch-name-policy';
14
+ import { withControllerLock } from '../../cli/repositories/locks';
15
+ import { resolveGitExecutable } from '../../effects/git-executable';
16
+ import { runProcess } from '../../effects/process-runner';
17
+ import { readJsonFile, writeJsonAtomic } from '../shared/json-files';
19
18
 
20
- export interface EnsureCampaignWorkspaceInput {
19
+ export interface EnsureManagedWorkspaceInput {
21
20
  requestId: string;
22
21
  title: string;
23
22
  baseRef?: string;
24
23
  branchName?: string;
25
24
  }
26
25
 
27
- /**
28
- * Neutral worktree contract used by WorkContract and legacy Campaign views.
29
- * The persistence location remains compatible with existing Campaign records.
30
- */
31
- export type EnsureManagedWorkspaceInput = EnsureCampaignWorkspaceInput;
26
+ export interface ManagedWorkspace {
27
+ mode: 'current' | 'isolated';
28
+ checkoutId?: string;
29
+ root?: string;
30
+ branch?: string | null;
31
+ baseRevision?: string | null;
32
+ managed: boolean;
33
+ }
32
34
 
33
- interface CampaignWorkspaceManifest {
35
+ interface ManagedWorkspaceManifest {
34
36
  schemaVersion: 1;
35
37
  repoId: string;
36
38
  requestId: string;
@@ -51,7 +53,7 @@ function git(root: string, args: string[], timeoutMs = 30_000): string {
51
53
  });
52
54
  if (!result.ok) {
53
55
  const detail = result.stderr || result.error || `exit ${result.status}`;
54
- throw new Error(`CAMPAIGN_WORKSPACE_GIT_FAILED: git ${args.join(' ')}: ${detail}`);
56
+ throw new Error(`MANAGED_WORKSPACE_GIT_FAILED: git ${args.join(' ')}: ${detail}`);
55
57
  }
56
58
  return result.stdout.trim();
57
59
  }
@@ -64,9 +66,9 @@ function gitSucceeds(root: string, args: string[]): boolean {
64
66
  }
65
67
 
66
68
  function assertBranch(root: string, branch: string): string {
67
- const normalized = validateBranchName(branch, { purpose: 'CAMPAIGN_WORKSPACE_BRANCH' });
69
+ const normalized = validateBranchName(branch, { purpose: 'MANAGED_WORKSPACE_BRANCH' });
68
70
  if (!gitSucceeds(root, ['check-ref-format', '--branch', normalized])) {
69
- throw new Error(`CAMPAIGN_WORKSPACE_BRANCH_INVALID: ${normalized}`);
71
+ throw new Error(`MANAGED_WORKSPACE_BRANCH_INVALID: ${normalized}`);
70
72
  }
71
73
  return normalized;
72
74
  }
@@ -77,22 +79,22 @@ function existingWorkspace(path: string, branch: string): boolean {
77
79
  timeoutMs: 15_000,
78
80
  maxOutputBytes: 8 * 1024,
79
81
  });
80
- if (!root.ok) throw new Error(`CAMPAIGN_WORKSPACE_PATH_OCCUPIED: ${path}`);
82
+ if (!root.ok) throw new Error(`MANAGED_WORKSPACE_PATH_OCCUPIED: ${path}`);
81
83
  if (realpathSync(root.stdout.trim()) !== realpathSync(path)) {
82
- throw new Error(`CAMPAIGN_WORKSPACE_PATH_MISMATCH: ${path}`);
84
+ throw new Error(`MANAGED_WORKSPACE_PATH_MISMATCH: ${path}`);
83
85
  }
84
86
  const currentBranch = git(path, ['branch', '--show-current']);
85
87
  if (currentBranch !== branch) {
86
- throw new Error(`CAMPAIGN_WORKSPACE_BRANCH_MISMATCH: expected ${branch}, found ${currentBranch || 'detached'}`);
88
+ throw new Error(`MANAGED_WORKSPACE_BRANCH_MISMATCH: expected ${branch}, found ${currentBranch || 'detached'}`);
87
89
  }
88
90
  return true;
89
91
  }
90
92
 
91
93
  function manifestPath(controllerHome: string, repoId: string, identity: string): string {
92
- return join(repositoryControllerRoot(controllerHome, repoId), 'campaigns', 'workspaces', `${identity}.json`);
94
+ return join(repositoryControllerRoot(controllerHome, repoId), 'managed-workspaces', `${identity}.json`);
93
95
  }
94
96
 
95
- export function currentCampaignWorkspace(repository: RepositoryRecord): CampaignWorkspace {
97
+ export function currentManagedWorkspace(repository: RepositoryRecord): ManagedWorkspace {
96
98
  return {
97
99
  mode: 'current',
98
100
  checkoutId: repository.activeCheckoutId,
@@ -106,41 +108,40 @@ export function currentCampaignWorkspace(repository: RepositoryRecord): Campaign
106
108
  export function ensureManagedWorkspace(
107
109
  controllerHome: string,
108
110
  repository: RepositoryRecord,
109
- input: EnsureCampaignWorkspaceInput,
110
- ): CampaignWorkspace {
111
+ input: EnsureManagedWorkspaceInput,
112
+ ): ManagedWorkspace {
111
113
  const requestId = input.requestId.trim();
112
- if (!requestId) throw new Error('CAMPAIGN_REQUEST_ID_REQUIRED');
114
+ if (!requestId) throw new Error('MANAGED_WORKSPACE_REQUEST_ID_REQUIRED');
113
115
  const identity = suffix(repository.repoId, requestId);
114
116
  const sourceRoot = repository.canonicalRoot;
115
117
  const requestedBranch = assertBranch(
116
118
  sourceRoot,
117
- input.branchName?.trim() || campaignBranchName({ title: input.title, identity }),
119
+ input.branchName?.trim() || `work/${branchSlugSegment(input.title)}-${identity}`,
118
120
  );
119
121
  const requestedBaseRef = input.baseRef?.trim() || 'HEAD';
120
122
  const requestedPath = managedWorktreePath(
121
123
  controllerHome,
122
124
  repository.repoId,
123
- `campaign-${identity}`,
125
+ `work-${identity}`,
124
126
  listRepositories(controllerHome, { includeRemoved: true }),
125
127
  );
126
128
  const statePath = manifestPath(controllerHome, repository.repoId, identity);
127
129
 
128
130
  return withControllerLock(
129
131
  controllerHome,
130
- { scope: 'worktree', repoId: repository.repoId, worktreeId: `campaign-${identity}` },
131
- `ensure-campaign-workspace:${requestId}`,
132
+ { scope: 'worktree', repoId: repository.repoId, worktreeId: `work-${identity}` },
133
+ `ensure-managed-workspace:${requestId}`,
132
134
  () => {
133
- const manifest = existsSync(statePath) ? readJsonFile<CampaignWorkspaceManifest>(statePath) : undefined;
135
+ const manifest = existsSync(statePath) ? readJsonFile<ManagedWorkspaceManifest>(statePath) : undefined;
134
136
  if (manifest && (
135
137
  manifest.repoId !== repository.repoId
136
138
  || manifest.requestId !== requestId
137
139
  || manifest.branch !== requestedBranch
138
140
  )) {
139
- throw new Error(`CAMPAIGN_WORKSPACE_REQUEST_CONFLICT: ${requestId}`);
141
+ throw new Error(`MANAGED_WORKSPACE_REQUEST_CONFLICT: ${requestId}`);
140
142
  }
141
143
  const branch = manifest?.branch ?? requestedBranch;
142
- // Existing manifests are authoritative compatibility evidence. They may
143
- // point at legacy nested worktrees; never move an active worktree in place.
144
+ // Persisted manifests are authoritative evidence; never move an active worktree in place.
144
145
  const path = manifest?.path ? resolve(manifest.path) : requestedPath;
145
146
  const baseRevision = manifest?.baseRevision ?? git(sourceRoot, ['rev-parse', '--verify', `${requestedBaseRef}^{commit}`]);
146
147
 
@@ -156,7 +157,7 @@ export function ensureManagedWorkspace(
156
157
  }
157
158
  }
158
159
  if (!repositoryCheckoutRootMatches(repository, path)) {
159
- throw new Error(`CAMPAIGN_WORKSPACE_REPOSITORY_MISMATCH: ${path}`);
160
+ throw new Error(`MANAGED_WORKSPACE_REPOSITORY_MISMATCH: ${path}`);
160
161
  }
161
162
 
162
163
  const record = addRepositoryCheckout({
@@ -170,7 +171,7 @@ export function ensureManagedWorkspace(
170
171
  existsSync(candidate.canonicalRoot)
171
172
  && realpathSync(candidate.canonicalRoot) === canonicalWorkspacePath,
172
173
  );
173
- if (!checkout) throw new Error(`CAMPAIGN_WORKSPACE_CHECKOUT_NOT_REGISTERED: ${path}`);
174
+ if (!checkout) throw new Error(`MANAGED_WORKSPACE_CHECKOUT_NOT_REGISTERED: ${path}`);
174
175
  const selected = selectRepositoryCheckout(record, checkout.checkoutId);
175
176
  if (!manifest) {
176
177
  writeJsonAtomic(statePath, {
@@ -181,7 +182,7 @@ export function ensureManagedWorkspace(
181
182
  path: selected.canonicalRoot,
182
183
  baseRevision,
183
184
  createdAt: new Date().toISOString(),
184
- } satisfies CampaignWorkspaceManifest);
185
+ } satisfies ManagedWorkspaceManifest);
185
186
  }
186
187
  return {
187
188
  mode: 'isolated',
@@ -190,11 +191,8 @@ export function ensureManagedWorkspace(
190
191
  branch,
191
192
  baseRevision,
192
193
  managed: true,
193
- } satisfies CampaignWorkspace;
194
+ } satisfies ManagedWorkspace;
194
195
  },
195
196
  120_000,
196
197
  );
197
198
  }
198
-
199
- /** @deprecated Campaign compatibility alias. New Work paths use ensureManagedWorkspace. */
200
- export const ensureCampaignWorkspace = ensureManagedWorkspace;
@@ -52,6 +52,12 @@ export interface RunCheckFacadeInput {
52
52
  issueId?: string;
53
53
  taskId?: string;
54
54
  };
55
+ /** Work-scoped verification runs against HEAD plus only scope-owned dirty paths. */
56
+ verificationSnapshot?: {
57
+ workId: string;
58
+ allowedPaths: readonly string[];
59
+ forbiddenPaths: readonly string[];
60
+ };
55
61
  }
56
62
 
57
63
  export function processCheckSemanticScopeKey(
@@ -0,0 +1,45 @@
1
+ import { createHash, randomUUID } from 'crypto';
2
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { processLogDir } from './store';
5
+
6
+ export interface PersistedCheckResultReceipt {
7
+ schemaVersion: 1;
8
+ receiptId: string;
9
+ checkId: string;
10
+ cacheKey: string;
11
+ ok: boolean;
12
+ status: number;
13
+ timedOut: boolean;
14
+ failureClass?: 'acceptance_failure' | 'infrastructure_failure';
15
+ validatedRevision?: string;
16
+ executedAt: string;
17
+ originalExecutedAt?: string;
18
+ cacheHit?: boolean;
19
+ }
20
+
21
+ export function allocatePersistedCheckResultReceiptPath(controllerHome: string, repoId: string): string {
22
+ const root = join(processLogDir(controllerHome, repoId), 'check-results');
23
+ mkdirSync(root, { recursive: true });
24
+ return join(root, `${randomUUID()}.json`);
25
+ }
26
+
27
+ export function writePersistedCheckResultReceipt(path: string, input: Omit<PersistedCheckResultReceipt, 'schemaVersion' | 'receiptId'>): PersistedCheckResultReceipt {
28
+ const digest = createHash('sha256').update(JSON.stringify(input)).digest('hex');
29
+ const receipt: PersistedCheckResultReceipt = { schemaVersion: 1, receiptId: `check_result_${digest.slice(0, 24)}`, ...input };
30
+ mkdirSync(dirname(path), { recursive: true });
31
+ const temporary = `${path}.${process.pid}.${Date.now()}.tmp`;
32
+ writeFileSync(temporary, `${JSON.stringify(receipt, null, 2)}\n`, { mode: 0o600 });
33
+ renameSync(temporary, path);
34
+ return receipt;
35
+ }
36
+
37
+ export function readPersistedCheckResultReceipt(path: string | undefined): PersistedCheckResultReceipt | undefined {
38
+ if (!path || !existsSync(path)) return undefined;
39
+ try {
40
+ const value = JSON.parse(readFileSync(path, 'utf8')) as PersistedCheckResultReceipt;
41
+ return value?.schemaVersion === 1 && typeof value.receiptId === 'string' && typeof value.cacheKey === 'string' ? value : undefined;
42
+ } catch {
43
+ return undefined;
44
+ }
45
+ }
@@ -1,16 +1,21 @@
1
1
  #!/usr/bin/env bun
2
2
  import { createHash } from 'crypto';
3
+ import { rmSync } from 'fs';
3
4
  import { resolve } from 'path';
4
5
  import {
5
- runControllerCheck,
6
+ controllerCheckExecutionIdentity,
7
+ runControllerCheckAsync,
6
8
  snapshotControllerCheck,
7
9
  } from '../../../cli/controller/check-runner';
10
+ import { writePersistedCheckResultReceipt } from './check-result';
8
11
 
9
12
  interface ParsedArgs {
10
13
  repo: string;
11
14
  checkId: string;
12
15
  timeoutMs?: number;
13
16
  expectedCheckFingerprint: string;
17
+ resultReceiptPath?: string;
18
+ cleanupRoot?: string;
14
19
  }
15
20
 
16
21
  function requiredValue(argv: string[], flag: string): string {
@@ -31,30 +36,51 @@ function parseArgs(argv: string[]): ParsedArgs {
31
36
  checkId: requiredValue(argv, '--check-id'),
32
37
  timeoutMs,
33
38
  expectedCheckFingerprint: requiredValue(argv, '--expected-check-fingerprint'),
39
+ resultReceiptPath: argv.includes('--result-receipt') ? requiredValue(argv, '--result-receipt') : undefined,
40
+ cleanupRoot: argv.includes('--cleanup-root') ? requiredValue(argv, '--cleanup-root') : undefined,
34
41
  };
35
42
  }
36
43
 
37
- export function runPersistedCheckSidecar(argv = process.argv.slice(2)): number {
44
+ export async function runPersistedCheckSidecar(argv = process.argv.slice(2)): Promise<number> {
38
45
  const args = parseArgs(argv);
39
46
  const root = resolve(args.repo);
40
- const snapshot = snapshotControllerCheck(root, args.checkId);
41
- const actualFingerprint = createHash('sha256').update(JSON.stringify(snapshot)).digest('hex');
42
- if (actualFingerprint !== args.expectedCheckFingerprint) {
43
- throw new Error('CHECK_SNAPSHOT_CHANGED: registered check changed before Process Runtime execution');
47
+ try {
48
+ const snapshot = snapshotControllerCheck(root, args.checkId);
49
+ const actualFingerprint = createHash('sha256').update(JSON.stringify(snapshot)).digest('hex');
50
+ if (actualFingerprint !== args.expectedCheckFingerprint) {
51
+ throw new Error('CHECK_SNAPSHOT_CHANGED: registered check changed before Process Runtime execution');
52
+ }
53
+ const identity = controllerCheckExecutionIdentity(root, args.checkId, args.timeoutMs, snapshot);
54
+ const result = await runControllerCheckAsync(root, args.checkId, { requestedTimeoutMs: args.timeoutMs, snapshot });
55
+ if (args.resultReceiptPath) {
56
+ writePersistedCheckResultReceipt(args.resultReceiptPath, {
57
+ checkId: args.checkId,
58
+ cacheKey: identity.cacheKey,
59
+ ok: result.ok,
60
+ status: result.status,
61
+ timedOut: result.timedOut,
62
+ failureClass: result.failureClass,
63
+ validatedRevision: result.validatedRevision,
64
+ executedAt: result.executedAt,
65
+ originalExecutedAt: result.originalExecutedAt,
66
+ cacheHit: result.cacheHit,
67
+ });
68
+ }
69
+ if (result.stdout) process.stdout.write(result.stdout);
70
+ if (result.stderr) process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
71
+ return result.ok ? 0 : Math.max(1, result.status || 1);
72
+ } finally {
73
+ if (args.cleanupRoot) rmSync(resolve(args.cleanupRoot), { recursive: true, force: true });
44
74
  }
45
- const result = runControllerCheck(root, args.checkId, args.timeoutMs, snapshot);
46
- if (result.stdout) process.stdout.write(result.stdout);
47
- if (result.stderr) process.stderr.write(result.stderr.endsWith('\n') ? result.stderr : `${result.stderr}\n`);
48
- return result.ok ? 0 : Math.max(1, result.status || 1);
49
75
  }
50
76
 
51
77
  const direct = typeof process.argv[1] === 'string'
52
78
  && (process.argv[1].includes('check-runner-sidecar') || process.argv[1].endsWith('/forge-check-runner'));
53
79
  if (direct) {
54
- try {
55
- process.exitCode = runPersistedCheckSidecar();
56
- } catch (error) {
57
- console.error(error instanceof Error ? error.message : String(error));
58
- process.exitCode = 1;
59
- }
80
+ void runPersistedCheckSidecar()
81
+ .then((code) => { process.exitCode = code; })
82
+ .catch((error) => {
83
+ console.error(error instanceof Error ? error.message : String(error));
84
+ process.exitCode = 1;
85
+ });
60
86
  }