@moretea-labs/forge 1.7.0 → 1.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (175) hide show
  1. package/AGENTS.md +2 -0
  2. package/CHANGELOG.md +13 -0
  3. package/CLAUDE.md +2 -0
  4. package/THIRD_PARTY_NOTICES.md +1 -1
  5. package/agents/openai.yaml +1 -1
  6. package/assets/partials-agents/02-operating-mode.partial.md +2 -0
  7. package/assets/plugin-registry.v1.json +3 -3
  8. package/assets/skill-version.json +2 -2
  9. package/assets/templates/helpers/check-architecture-sync.sh +17 -0
  10. package/assets/templates/helpers/inspect-project-state.ts +8 -6
  11. package/assets/templates/helpers/migrate-workflow-docs.ts +9 -7
  12. package/assets/templates/helpers/workflow-contract.ts +5 -1
  13. package/docs/architecture/CURRENT.md +12 -4
  14. package/docs/forge-plugin-management.md +1 -1
  15. package/docs/operations/controller-performance-and-502.md +19 -0
  16. package/docs/operations/homebrew.md +2 -2
  17. package/package.json +5 -2
  18. package/scripts/architecture-event.ts +2 -820
  19. package/scripts/architecture-queue.sh +2 -645
  20. package/scripts/archive-architecture-request.sh +2 -242
  21. package/scripts/archive-workflow.sh +1 -291
  22. package/scripts/benchmark-work-efficiency.ts +0 -0
  23. package/scripts/capability-config.ts +2 -392
  24. package/scripts/capability-resolver.ts +2 -503
  25. package/scripts/capture-plan.sh +1 -332
  26. package/scripts/check-agent-tooling.sh +2 -1531
  27. package/scripts/check-architecture-sync.sh +2 -302
  28. package/scripts/check-brain-manifest.sh +2 -253
  29. package/scripts/check-context-files.sh +2 -47
  30. package/scripts/check-deploy-sql-order.sh +2 -115
  31. package/scripts/check-runtime-architecture.mjs +243 -16
  32. package/scripts/check-skill-version.ts +3 -191
  33. package/scripts/check-source-duplication.mjs +61 -0
  34. package/scripts/check-stable-baseline.ts +0 -0
  35. package/scripts/check-task-sync.sh +2 -63
  36. package/scripts/check-task-workflow.sh +2 -1000
  37. package/scripts/codex-handoff-resume.sh +2 -267
  38. package/scripts/context-contract-sync.sh +2 -514
  39. package/scripts/contract-run.ts +2 -463
  40. package/scripts/contract-worktree.sh +1 -702
  41. package/scripts/ensure-task-workflow.sh +1 -1192
  42. package/scripts/inspect-project-state.ts +4 -279
  43. package/scripts/lib/project-init-lib.sh +7 -2
  44. package/scripts/lib/run-forge-helper.sh +19 -0
  45. package/scripts/lib/run-forge-helper.ts +26 -0
  46. package/scripts/migrate-project-template.sh +1 -33
  47. package/scripts/migrate-workflow-docs.ts +4 -412
  48. package/scripts/new-plan.sh +1 -180
  49. package/scripts/new-spec.sh +1 -47
  50. package/scripts/new-sprint.sh +1 -46
  51. package/scripts/plan-to-todo.sh +1 -771
  52. package/scripts/prepare-codex-handoff.sh +2 -201
  53. package/scripts/prepare-handoff.sh +1 -78
  54. package/scripts/refresh-current-status.sh +2 -467
  55. package/scripts/run-governed-gate.ts +3 -0
  56. package/scripts/select-agent-context-blocks.sh +2 -64
  57. package/scripts/ship-worktrees.sh +1 -531
  58. package/scripts/sprint-backlog.sh +1 -834
  59. package/scripts/stage-runtime-release.ts +2 -1
  60. package/scripts/summarize-failures.sh +2 -123
  61. package/scripts/switch-plan.sh +1 -150
  62. package/scripts/sync-brain-docs.sh +2 -379
  63. package/scripts/sync-generated-authority.mjs +175 -0
  64. package/scripts/verify-contract.sh +2 -593
  65. package/scripts/verify-sprint.sh +1 -500
  66. package/scripts/workflow-contract.ts +4 -201
  67. package/scripts/workstream-sync.sh +2 -261
  68. package/src/cli/agent-jobs/executor-health.ts +1 -1
  69. package/src/cli/commands/plugin.ts +12 -3
  70. package/src/cli/controller/check-runner.ts +7 -2
  71. package/src/cli/controller/context-pack.ts +46 -21
  72. package/src/cli/github/contracts.ts +16 -0
  73. package/src/cli/github/github.ts +2 -16
  74. package/src/cli/github/session.ts +2 -1
  75. package/src/cli/local-bridge/facade-api.ts +37 -47
  76. package/src/cli/local-bridge/server.ts +2 -2
  77. package/src/cli/mcp/legacy-tool-service.ts +10 -33
  78. package/src/cli/mcp/post-finalize-work-attribution.ts +150 -0
  79. package/src/cli/mcp/repository-tools.ts +85 -89
  80. package/src/cli/mcp/server.ts +57 -13
  81. package/src/cli/mcp/tool-contract.ts +14 -0
  82. package/src/cli/mcp/toolset.ts +2 -2
  83. package/src/cli/mcp/transports/http.ts +23 -0
  84. package/src/cli/repositories/command-classifier.ts +2 -2
  85. package/src/cli/repositories/command-executor.ts +24 -6
  86. package/src/cli/repositories/repository-snapshot.ts +5 -2
  87. package/src/cli/repositories/structured-git.ts +57 -3
  88. package/src/cli/repository/inspector.ts +11 -3
  89. package/src/runtime/context/generic-lsp-provider.ts +1 -1
  90. package/src/runtime/context/semantic-navigation-contract.ts +61 -0
  91. package/src/runtime/context/semantic-navigation.ts +9 -61
  92. package/src/runtime/control-plane/execution/direct-edit-work-completion.ts +18 -25
  93. package/src/runtime/control-plane/execution/repository-work-attribution.ts +106 -0
  94. package/src/runtime/control-plane/execution/retained-work-resume.ts +203 -0
  95. package/src/runtime/control-plane/execution/work-completion-authority.ts +58 -0
  96. package/src/runtime/control-plane/execution/work-evidence-policy.ts +200 -0
  97. package/src/runtime/control-plane/execution/work-execution-support.ts +386 -0
  98. package/src/runtime/control-plane/execution/work-finalization-service.ts +1913 -0
  99. package/src/runtime/control-plane/execution/work-handle-authority.ts +96 -0
  100. package/src/runtime/control-plane/execution/work-handle-store.ts +1 -1
  101. package/src/runtime/control-plane/execution/work-operation-service.ts +468 -0
  102. package/src/runtime/control-plane/execution/work-path-scope.ts +37 -0
  103. package/src/runtime/control-plane/execution/work-preparation-service.ts +479 -0
  104. package/src/runtime/control-plane/execution/work-process-evidence.ts +129 -0
  105. package/src/runtime/control-plane/execution/work-task-receipt.ts +4 -43
  106. package/src/runtime/control-plane/execution/work-terminal-cleanup.ts +13 -3
  107. package/src/runtime/control-plane/execution/work-verification-context.ts +56 -0
  108. package/src/runtime/control-plane/execution/work-verification-service.ts +388 -0
  109. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +5 -1
  110. package/src/runtime/control-plane/facade/controller-round-relay.ts +164 -37
  111. package/src/runtime/control-plane/facade/controller-session-store.ts +183 -24
  112. package/src/runtime/control-plane/facade/goal-workloop-access.ts +15 -1
  113. package/src/runtime/control-plane/facade/goal-workloop.ts +329 -148
  114. package/src/runtime/control-plane/facade/index.ts +2 -0
  115. package/src/runtime/control-plane/facade/plan-contract-store.ts +90 -22
  116. package/src/runtime/control-plane/facade/repository-work-admission.ts +147 -0
  117. package/src/runtime/control-plane/facade/requirement-authority.ts +83 -0
  118. package/src/runtime/control-plane/facade/types.ts +68 -5
  119. package/src/runtime/control-plane/facade/work-contract-store.ts +3 -209
  120. package/src/runtime/control-plane/facade/work-state-machine.ts +250 -0
  121. package/src/runtime/control-plane/global-scheduler/maintenance.ts +14 -7
  122. package/src/runtime/control-plane/global-scheduler/scheduler.ts +5 -1
  123. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +13 -0
  124. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +15 -6
  125. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +54 -5
  126. package/src/runtime/control-plane/launcher/thin-launcher.ts +56 -7
  127. package/src/runtime/control-plane/persistence/requirement-store.ts +0 -12
  128. package/src/runtime/control-plane/repo-actor/registry.ts +4 -2
  129. package/src/runtime/control-plane/routing/route-policy.ts +18 -6
  130. package/src/runtime/control-plane/routing/workspace-admission.ts +70 -0
  131. package/src/runtime/evidence/process-check-execution.ts +18 -0
  132. package/src/runtime/execution/jobs/child-reference.ts +6 -2
  133. package/src/runtime/execution/process-runtime/check-result.ts +53 -18
  134. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +7 -1
  135. package/src/runtime/execution/process-runtime/command-facade.ts +39 -17
  136. package/src/runtime/execution/process-runtime/lightweight-managed.ts +20 -3
  137. package/src/runtime/execution/process-runtime/persisted-check.ts +4 -1
  138. package/src/runtime/execution/process-runtime/runtime.ts +10 -0
  139. package/src/runtime/execution/process-runtime/types.ts +2 -12
  140. package/src/runtime/execution/work-activity.ts +21 -0
  141. package/src/runtime/execution/workers/executor.ts +18 -13
  142. package/src/runtime/gateway/mcp/execution-tools.ts +35 -2930
  143. package/src/runtime/gateway/mcp/legacy-ios-tool-adapter.ts +61 -0
  144. package/src/runtime/gateway/mcp/router.ts +2 -2
  145. package/src/runtime/gateway/mcp/runtime-tool-definitions.ts +21 -2
  146. package/src/runtime/gateway/mcp/runtime-tools.ts +805 -1009
  147. package/src/runtime/plugins/app-store-connect-adapter.ts +93 -28
  148. package/src/runtime/plugins/browser-adapter.ts +256 -1083
  149. package/src/runtime/plugins/browser-handoff-host.ts +2 -25
  150. package/src/runtime/plugins/browser-handoff.ts +18 -3
  151. package/src/runtime/plugins/browser-macos-bridge.ts +121 -16
  152. package/src/runtime/plugins/browser-manifest-surface.ts +603 -0
  153. package/src/runtime/plugins/browser-native-session-service.ts +222 -0
  154. package/src/runtime/plugins/browser-runtime-contract.ts +2 -0
  155. package/src/runtime/plugins/browser-runtime.ts +10 -0
  156. package/src/runtime/plugins/browser-session-store.ts +121 -0
  157. package/src/runtime/plugins/browser-session-types.ts +98 -0
  158. package/src/runtime/plugins/desktop-operator-registration.ts +4 -4
  159. package/src/runtime/plugins/external-adapter.ts +92 -58
  160. package/src/runtime/plugins/macos-capability-broker.ts +0 -8
  161. package/src/runtime/plugins/store.ts +66 -9
  162. package/src/runtime/plugins/types.ts +3 -1
  163. package/src/runtime/recovery/maintenance-executor.ts +61 -25
  164. package/src/runtime/root/package-connector-service.ts +46 -4
  165. package/src/runtime/root/package-runtime-release.ts +1 -1
  166. package/src/runtime/root/package-runtime-service.ts +33 -2
  167. package/src/runtime/root/release-manifest.ts +137 -16
  168. package/src/runtime/root/release-materialize.ts +109 -35
  169. package/src/runtime/root/types.ts +10 -0
  170. package/src/runtime/standalone-recovery/core.ts +425 -194
  171. package/src/runtime/standalone-recovery/entry.ts +1 -1
  172. package/src/runtime/standalone-recovery/installer.ts +1 -1
  173. package/src/runtime/workflow/schedules/engine.ts +113 -87
  174. package/src/runtime/workflow/schedules/settlement.ts +26 -23
  175. package/src/runtime/workflow/schedules/store.ts +79 -10
@@ -5,6 +5,7 @@ import {
5
5
  writeControlPlaneRecord,
6
6
  } from '../persistence/sqlite-store';
7
7
  import type { ControllerType } from '../facade/types';
8
+ import { redactProcessOutput } from '../../../effects/process-runner';
8
9
 
9
10
  const NAMESPACE = 'external_controller_launch_reservation';
10
11
  const DEFAULT_TTL_MS = 2 * 60_000;
@@ -24,6 +25,9 @@ export interface ExternalControllerLaunchReservation {
24
25
  exitSignal?: string;
25
26
  releasedAt?: string;
26
27
  releaseReason?: string;
28
+ /** Bounded, redacted provider output captured only during launcher startup. */
29
+ stdoutTail?: string;
30
+ stderrTail?: string;
27
31
  }
28
32
 
29
33
  export interface LaunchReservationStoreOptions {
@@ -57,6 +61,14 @@ export function getExternalControllerLaunchReservation(
57
61
  return active(record(options, workId)?.value);
58
62
  }
59
63
 
64
+ /** Read the latest reservation record including released/failed startup evidence. */
65
+ export function readExternalControllerLaunchReservationRecord(
66
+ options: LaunchReservationStoreOptions,
67
+ workId: string,
68
+ ): ExternalControllerLaunchReservation | undefined {
69
+ return record(options, workId)?.value;
70
+ }
71
+
60
72
  export function reserveExternalControllerLaunch(
61
73
  options: LaunchReservationStoreOptions,
62
74
  input: {
@@ -131,11 +143,43 @@ export function attachExternalControllerLaunchPid(
131
143
  );
132
144
  }
133
145
 
146
+ export function recordExternalControllerLaunchDiagnostics(
147
+ options: LaunchReservationStoreOptions,
148
+ workId: string,
149
+ reservationId: string,
150
+ diagnostics: { stdoutTail?: string; stderrTail?: string },
151
+ ): ExternalControllerLaunchReservation | undefined {
152
+ return withControllerLock(
153
+ options.controllerHome,
154
+ { scope: 'task', repoId: options.repoId, taskId: `controller-launch-${workId}` },
155
+ `controller-launch-diagnostics:${reservationId}`,
156
+ () => {
157
+ const existing = record(options, workId);
158
+ if (!existing || existing.value.reservationId !== reservationId) return existing?.value;
159
+ const next: ExternalControllerLaunchReservation = {
160
+ ...existing.value,
161
+ ...(diagnostics.stdoutTail ? { stdoutTail: redactProcessOutput(diagnostics.stdoutTail).slice(-8 * 1024) } : {}),
162
+ ...(diagnostics.stderrTail ? { stderrTail: redactProcessOutput(diagnostics.stderrTail).slice(-8 * 1024) } : {}),
163
+ };
164
+ writeControlPlaneRecord(options.controllerHome, {
165
+ namespace: NAMESPACE,
166
+ scope: options.repoId,
167
+ key: workId,
168
+ schemaVersion: 1,
169
+ value: next,
170
+ action: 'external_controller_launch_diagnostics',
171
+ expectedRevision: existing.revision,
172
+ });
173
+ return next;
174
+ },
175
+ );
176
+ }
177
+
134
178
  export function recordExternalControllerLaunchExit(
135
179
  options: LaunchReservationStoreOptions,
136
180
  workId: string,
137
181
  reservationId: string,
138
- exit: { exitCode: number | null; signal: string | null },
182
+ exit: { exitCode: number | null; signal: string | null; stdoutTail?: string; stderrTail?: string },
139
183
  ): ExternalControllerLaunchReservation | undefined {
140
184
  return withControllerLock(
141
185
  options.controllerHome,
@@ -143,16 +187,21 @@ export function recordExternalControllerLaunchExit(
143
187
  `controller-launch-exit:${reservationId}`,
144
188
  () => {
145
189
  const existing = record(options, workId);
146
- if (!existing || existing.value.reservationId !== reservationId || existing.value.releasedAt) return existing?.value;
190
+ if (!existing || existing.value.reservationId !== reservationId) return existing?.value;
147
191
  const exitedAt = nowIso(options);
192
+ const alreadyReleased = Boolean(existing.value.releasedAt);
148
193
  const next: ExternalControllerLaunchReservation = {
149
194
  ...existing.value,
150
- expiresAt: exitedAt,
195
+ expiresAt: alreadyReleased ? existing.value.expiresAt : exitedAt,
151
196
  exitedAt,
152
197
  ...(exit.exitCode === null ? {} : { exitCode: exit.exitCode }),
153
198
  ...(exit.signal ? { exitSignal: exit.signal } : {}),
154
- releasedAt: exitedAt,
155
- releaseReason: `process_exit:code=${String(exit.exitCode ?? 'null')}:signal=${exit.signal ?? 'none'}`.slice(0, 240),
199
+ ...(exit.stdoutTail ? { stdoutTail: redactProcessOutput(exit.stdoutTail).slice(-8 * 1024) } : {}),
200
+ ...(exit.stderrTail ? { stderrTail: redactProcessOutput(exit.stderrTail).slice(-8 * 1024) } : {}),
201
+ ...(alreadyReleased ? {} : {
202
+ releasedAt: exitedAt,
203
+ releaseReason: `process_exit:code=${String(exit.exitCode ?? 'null')}:signal=${exit.signal ?? 'none'}`.slice(0, 240),
204
+ }),
156
205
  };
157
206
  writeControlPlaneRecord(options.controllerHome, {
158
207
  namespace: NAMESPACE,
@@ -7,6 +7,7 @@ import {
7
7
  } from '../facade';
8
8
  import {
9
9
  attachExternalControllerLaunchPid,
10
+ recordExternalControllerLaunchDiagnostics,
10
11
  recordExternalControllerLaunchExit,
11
12
  releaseExternalControllerLaunchReservation,
12
13
  reserveExternalControllerLaunch,
@@ -16,6 +17,7 @@ import { getControllerSession } from '../facade/controller-session-store';
16
17
  import { codexMcpConfigArgs, resolveProviderMcpBootstrap, type ProviderMcpBootstrap } from './provider-mcp-bootstrap';
17
18
  import { getChatgptWorkConversationBinding } from './chatgpt-work-binding-store';
18
19
  import { repositoryChildProcessEnvironment } from '../../shared/process-environment';
20
+ import { redactProcessOutput } from '../../../effects/process-runner';
19
21
 
20
22
  export interface ThinLauncherRequest {
21
23
  controllerType: Exclude<ControllerType, 'human'>;
@@ -47,7 +49,22 @@ export interface ThinLauncherResult {
47
49
 
48
50
  const LAUNCHER_STARTUP_GRACE_MS = 250;
49
51
  const CODEX_WORK_CLAIM_TIMEOUT_MS = 30_000;
52
+ const CODEX_WORK_CLAIM_SETTLEMENT_GRACE_MS = 1_500;
50
53
  const CODEX_WORK_CLAIM_POLL_INTERVAL_MS = 100;
54
+ const STARTUP_DIAGNOSTIC_BYTES = 8 * 1024;
55
+
56
+ function appendStartupDiagnosticTail(current: string, chunk: unknown): string {
57
+ const text = redactProcessOutput(Buffer.isBuffer(chunk) ? chunk.toString('utf8') : String(chunk ?? ''));
58
+ const buffer = Buffer.from(`${current}${text}`, 'utf8');
59
+ return buffer.length <= STARTUP_DIAGNOSTIC_BYTES
60
+ ? buffer.toString('utf8')
61
+ : buffer.subarray(buffer.length - STARTUP_DIAGNOSTIC_BYTES).toString('utf8');
62
+ }
63
+
64
+ function startupDiagnosticSummary(stdoutTail: string, stderrTail: string): string {
65
+ const value = redactProcessOutput(stderrTail.trim() || stdoutTail.trim()).replace(/\s+/g, ' ').slice(0, 1200);
66
+ return value ? `; startup_output=${value}` : '';
67
+ }
51
68
 
52
69
  interface ExternalControllerClaimExpectation {
53
70
  controllerType: 'codex';
@@ -59,6 +76,7 @@ interface ExternalControllerClaimExpectation {
59
76
  export interface ThinLauncherDependencies {
60
77
  resolveProviderMcpBootstrap?: typeof resolveProviderMcpBootstrap;
61
78
  claimTimeoutMs?: number;
79
+ claimSettlementGraceMs?: number;
62
80
  claimPollIntervalMs?: number;
63
81
  }
64
82
 
@@ -102,11 +120,31 @@ async function awaitExternalControllerStartup(
102
120
  workId: string,
103
121
  reservationId: string,
104
122
  claimExpectation?: ExternalControllerClaimExpectation,
105
- options: { claimTimeoutMs?: number; claimPollIntervalMs?: number } = {},
123
+ options: { claimTimeoutMs?: number; claimSettlementGraceMs?: number; claimPollIntervalMs?: number } = {},
106
124
  ): Promise<void> {
107
125
  await new Promise<void>((resolve, reject) => {
108
126
  let startupSettled = false;
109
127
  let timer: ReturnType<typeof setTimeout> | undefined;
128
+ let stdoutTail = '';
129
+ let stderrTail = '';
130
+ const captureStdout = (chunk: unknown) => { stdoutTail = appendStartupDiagnosticTail(stdoutTail, chunk); };
131
+ const captureStderr = (chunk: unknown) => { stderrTail = appendStartupDiagnosticTail(stderrTail, chunk); };
132
+ child.stdout?.on('data', captureStdout);
133
+ child.stderr?.on('data', captureStderr);
134
+ const closeStartupPipes = () => {
135
+ child.stdout?.off('data', captureStdout);
136
+ child.stderr?.off('data', captureStderr);
137
+ child.stdout?.destroy();
138
+ child.stderr?.destroy();
139
+ };
140
+ const persistDiagnostics = () => {
141
+ if (!stdoutTail && !stderrTail) return;
142
+ try {
143
+ recordExternalControllerLaunchDiagnostics(stores.work, workId, reservationId, { stdoutTail, stderrTail });
144
+ } catch {
145
+ // Startup diagnostics are evidence only; preserve the primary ownership/claim outcome.
146
+ }
147
+ };
110
148
  const clearTimer = () => {
111
149
  if (timer) clearTimeout(timer);
112
150
  timer = undefined;
@@ -126,9 +164,12 @@ async function awaitExternalControllerStartup(
126
164
  if (startupSettled) return;
127
165
  startupSettled = true;
128
166
  clearTimer();
167
+ persistDiagnostics();
168
+ closeStartupPipes();
129
169
  if (terminate) terminateUnclaimedChild();
130
170
  releaseFailure(reason);
131
- reject(error);
171
+ const diagnostics = startupDiagnosticSummary(stdoutTail, stderrTail);
172
+ reject(diagnostics ? new Error(`${error.message}${diagnostics}`) : error);
132
173
  };
133
174
  const expectedClaimMatches = (): { matches: boolean; mismatch?: string } => {
134
175
  if (!claimExpectation) return { matches: true };
@@ -156,6 +197,8 @@ async function awaitExternalControllerStartup(
156
197
  recordExternalControllerLaunchExit(stores.work, workId, reservationId, {
157
198
  exitCode,
158
199
  signal: signal ? String(signal) : null,
200
+ stdoutTail,
201
+ stderrTail,
159
202
  });
160
203
  } catch {
161
204
  // Exit evidence is best-effort after another authority has already released the reservation.
@@ -163,22 +206,26 @@ async function awaitExternalControllerStartup(
163
206
  if (startupSettled) return;
164
207
  startupSettled = true;
165
208
  clearTimer();
209
+ closeStartupPipes();
166
210
  const phase = claimExpectation ? 'before exact Work claim became live' : 'during startup grace';
167
- reject(new Error(`LAUNCHER_STARTUP_FAILED: external Controller exited ${phase} (code=${String(exitCode ?? 'null')}, signal=${signal ?? 'none'})`));
211
+ reject(new Error(`LAUNCHER_STARTUP_FAILED: external Controller exited ${phase} (code=${String(exitCode ?? 'null')}, signal=${signal ?? 'none'})${startupDiagnosticSummary(stdoutTail, stderrTail)}`));
168
212
  });
169
213
 
170
214
  if (!claimExpectation) {
171
215
  timer = setTimeout(() => {
172
216
  if (startupSettled) return;
173
217
  startupSettled = true;
218
+ closeStartupPipes();
174
219
  resolve();
175
220
  }, LAUNCHER_STARTUP_GRACE_MS);
176
221
  return;
177
222
  }
178
223
 
179
224
  const claimTimeoutMs = Math.max(100, Math.min(options.claimTimeoutMs ?? CODEX_WORK_CLAIM_TIMEOUT_MS, 60_000));
225
+ const claimSettlementGraceMs = Math.max(0, Math.min(options.claimSettlementGraceMs ?? CODEX_WORK_CLAIM_SETTLEMENT_GRACE_MS, 5_000));
180
226
  const claimPollIntervalMs = Math.max(10, Math.min(options.claimPollIntervalMs ?? CODEX_WORK_CLAIM_POLL_INTERVAL_MS, 1_000));
181
227
  const claimDeadline = Date.now() + claimTimeoutMs;
228
+ const claimSettlementDeadline = claimDeadline + claimSettlementGraceMs;
182
229
  const pollClaim = () => {
183
230
  if (startupSettled) return;
184
231
  let observation: ReturnType<typeof expectedClaimMatches>;
@@ -213,14 +260,15 @@ async function awaitExternalControllerStartup(
213
260
  }
214
261
  startupSettled = true;
215
262
  timer = undefined;
263
+ closeStartupPipes();
216
264
  resolve();
217
265
  }, LAUNCHER_STARTUP_GRACE_MS);
218
266
  return;
219
267
  }
220
- if (Date.now() >= claimDeadline) {
268
+ if (Date.now() >= claimSettlementDeadline) {
221
269
  fail(
222
- new Error(`LAUNCHER_CLAIM_TIMEOUT: Codex pid=${String(child.pid ?? 'unknown')} did not claim exact Work ${workId} through Forge MCP within ${claimTimeoutMs}ms`),
223
- `claim_timeout:${claimTimeoutMs}ms`,
270
+ new Error(`LAUNCHER_CLAIM_TIMEOUT: Codex pid=${String(child.pid ?? 'unknown')} did not claim exact Work ${workId} through Forge MCP within ${claimTimeoutMs}ms plus ${claimSettlementGraceMs}ms settlement grace`),
271
+ `claim_timeout:${claimTimeoutMs}ms+${claimSettlementGraceMs}ms_settlement`,
224
272
  true,
225
273
  );
226
274
  return;
@@ -345,7 +393,7 @@ export async function launchSuperController(
345
393
  const child = spawn(invocation.executable, invocation.args, {
346
394
  cwd: request.cwd,
347
395
  detached: true,
348
- stdio: 'ignore',
396
+ stdio: ['ignore', 'pipe', 'pipe'],
349
397
  env: launcherProcessEnvironment(mcpBootstrap?.env ?? process.env),
350
398
  });
351
399
  attachExternalControllerLaunchPid(stores.work, work.workId, reservation.reservationId, child.pid);
@@ -362,6 +410,7 @@ export async function launchSuperController(
362
410
  } : undefined,
363
411
  {
364
412
  claimTimeoutMs: dependencies.claimTimeoutMs,
413
+ claimSettlementGraceMs: dependencies.claimSettlementGraceMs,
365
414
  claimPollIntervalMs: dependencies.claimPollIntervalMs,
366
415
  },
367
416
  );
@@ -165,18 +165,6 @@ export function updateRequirement(
165
165
  });
166
166
  }
167
167
 
168
- /** @deprecated Migration/legacy compatibility only. New Plan creation must bind Plan.requirementId and derive active Plan slices by query. */
169
- export function setRequirementPlan(
170
- options: RequirementStoreOptions,
171
- input: { requirementId: string; planId: string; action?: string },
172
- ): Requirement {
173
- return updateRequirement(options, {
174
- requirementId: input.requirementId,
175
- action: input.action ?? 'requirement_active_plan_bound',
176
- mutate: (current) => ({ ...current, activePlanId: id(input.planId) }),
177
- });
178
- }
179
-
180
168
  export interface RequirementCompletionInput {
181
169
  requirementId: string;
182
170
  work: Pick<WorkContract,
@@ -2,16 +2,18 @@ import { RepoActor } from './actor';
2
2
 
3
3
  export class RepoActorRegistry {
4
4
  private readonly controllerHome: string;
5
+ private readonly defaultMaxConcurrentWorkers: number;
5
6
  private readonly actors = new Map<string, RepoActor>();
6
- constructor(controllerHome: string) {
7
+ constructor(controllerHome: string, options: { maxConcurrentWorkers?: number } = {}) {
7
8
  this.controllerHome = controllerHome;
9
+ this.defaultMaxConcurrentWorkers = Math.max(1, options.maxConcurrentWorkers ?? 4);
8
10
  }
9
11
 
10
12
  get(repoId: string): RepoActor {
11
13
  let actor = this.actors.get(repoId);
12
14
  if (!actor) {
13
15
  actor = new RepoActor(this.controllerHome, repoId, {
14
- maxConcurrentWorkers: Number(process.env.FORGE_PER_REPO_WORKERS ?? 2),
16
+ maxConcurrentWorkers: Number(process.env.FORGE_PER_REPO_WORKERS ?? this.defaultMaxConcurrentWorkers),
15
17
  });
16
18
  this.actors.set(repoId, actor);
17
19
  }
@@ -1,6 +1,6 @@
1
1
  import { createHash } from 'crypto';
2
2
 
3
- export const ROUTE_POLICY_VERSION = 'route-policy-v4' as const;
3
+ export const ROUTE_POLICY_VERSION = 'route-policy-v5' as const;
4
4
 
5
5
  export type RouteExecutionMode = 'direct_control' | 'goal_workloop' | 'handoff_only';
6
6
  export type RouteWorkMode = 'direct_edit' | 'bounded_work' | 'quick_agent' | 'issue_task';
@@ -50,6 +50,10 @@ export interface RoutePolicyInput {
50
50
  dirty?: boolean;
51
51
  checkoutId?: string;
52
52
  fingerprint?: string;
53
+ /** Canonical typed placement constraint resolved before routing. */
54
+ placement?: 'current' | 'isolated' | 'auto';
55
+ /** Admission fence: Direct Control/current-main mutation is not permitted. */
56
+ directMainProhibited?: boolean;
53
57
  };
54
58
  policy: {
55
59
  risk?: string;
@@ -234,9 +238,11 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
234
238
  const protectedPath = paths.some((path) => PROTECTED_PATH.test(path));
235
239
  const explicitMode = input.intent.explicitMode;
236
240
  const explicitParallelMode = explicitMode === 'scale';
237
- const requiresIsolation = explicitMode === 'direct'
238
- ? false
239
- : input.recovery.isolationRequired === true || explicitParallelMode;
241
+ const placementIsolationRequired = input.workspace.placement === 'isolated'
242
+ || input.workspace.directMainProhibited === true;
243
+ const requiresIsolation = placementIsolationRequired
244
+ || input.recovery.isolationRequired === true
245
+ || explicitParallelMode;
240
246
  const requiresRecovery = input.recovery.required === true
241
247
  || explicitMode === 'release'
242
248
  || explicitMode === 'scale';
@@ -295,6 +301,8 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
295
301
  if (explicitMode) reasons.push({ code: `explicit_${explicitMode}`, message: `Explicit -${explicitMode} mode overrides automatic work topology while policy gates remain authoritative.` });
296
302
  if (protectedPath) reasons.push({ code: 'protected_path', message: 'The predicted scope includes a protected or release-sensitive path.' });
297
303
  if (requiresRecovery) reasons.push({ code: 'recovery_required', message: 'The operation needs resumable Work and bounded recovery.' });
304
+ if (input.workspace.placement === 'isolated') reasons.push({ code: 'placement_isolated', message: 'Typed Work admission requires an isolated workspace.' });
305
+ if (input.workspace.directMainProhibited === true) reasons.push({ code: 'direct_main_prohibited', message: 'Typed Work admission forbids the Direct Control/current-main mutation lane.' });
298
306
  if (requiresIsolation) reasons.push({ code: 'isolation_required', message: 'The operation requires an isolated checkout or serialized lane.' });
299
307
  if (input.intent.requiresLongRunningChecks) reasons.push({ code: 'long_checks', message: 'Long-running checks may use a lightweight handle; duration alone does not require durable Work.' });
300
308
  if (input.intent.requiresInvestigation) reasons.push({ code: 'investigation', message: 'Investigation is required before or during implementation.' });
@@ -303,7 +311,11 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
303
311
 
304
312
  const explicitBoundedMode = explicitMode === 'plan'
305
313
  || explicitMode === 'release';
306
- const complex = explicitMode === 'direct'
314
+ const directModeCanRemainFast = explicitMode === 'direct'
315
+ && !requiresIsolation
316
+ && !requiresRecovery
317
+ && !coordinationRequired;
318
+ const complex = directModeCanRemainFast
307
319
  ? false
308
320
  : coordinationRequired
309
321
  || explicitBoundedMode
@@ -320,7 +332,7 @@ export function decideRoute(input: RoutePolicyInput): RouteDecision {
320
332
  // Work topology is independent from executor/provider choice. Independent or
321
333
  // parallel deliverables stay in the durable Goal Workloop and are decomposed by
322
334
  // PlanContract/Work rather than introducing another project-level lifecycle.
323
- const workMode: RouteWorkMode = explicitMode === 'direct'
335
+ const workMode: RouteWorkMode = directModeCanRemainFast
324
336
  ? 'direct_edit'
325
337
  : coordinationRequired
326
338
  ? 'bounded_work'
@@ -0,0 +1,70 @@
1
+ export type WorkspacePlacementMode = 'current' | 'isolated' | 'auto';
2
+
3
+ export interface WorkspaceAdmissionConstraintInput {
4
+ workspaceMode?: WorkspacePlacementMode;
5
+ requireWorktree?: boolean;
6
+ /**
7
+ * Forbid repository mutation through the current/direct-main lane. This is
8
+ * an admission constraint, not a routing hint: callers must never silently
9
+ * downgrade it to Direct Control.
10
+ */
11
+ directMainProhibited?: boolean;
12
+ }
13
+
14
+ export interface WorkspaceAdmissionConstraint {
15
+ workspaceMode: WorkspacePlacementMode;
16
+ requireWorktree: boolean;
17
+ directMainProhibited: boolean;
18
+ requiresIsolation: boolean;
19
+ }
20
+
21
+ export type WorkspaceAdmissionResolution =
22
+ | { ok: true; constraint: WorkspaceAdmissionConstraint }
23
+ | { ok: false; code: 'WORKSPACE_PLACEMENT_CONSTRAINT_CONFLICT'; message: string };
24
+
25
+ /**
26
+ * Canonicalize Work placement before Route Policy runs. Placement is a policy
27
+ * fence: explicit isolation / direct-main prohibition has precedence over an
28
+ * operator request for the fast Direct Control topology.
29
+ */
30
+ export function resolveWorkspaceAdmissionConstraint(
31
+ input: WorkspaceAdmissionConstraintInput = {},
32
+ ): WorkspaceAdmissionResolution {
33
+ const workspaceMode = input.workspaceMode ?? 'auto';
34
+ const directMainProhibited = input.directMainProhibited === true;
35
+
36
+ if (workspaceMode === 'current' && input.requireWorktree === true) {
37
+ return {
38
+ ok: false,
39
+ code: 'WORKSPACE_PLACEMENT_CONSTRAINT_CONFLICT',
40
+ message: 'workspaceMode=current conflicts with requireWorktree=true.',
41
+ };
42
+ }
43
+ if (workspaceMode === 'isolated' && input.requireWorktree === false) {
44
+ return {
45
+ ok: false,
46
+ code: 'WORKSPACE_PLACEMENT_CONSTRAINT_CONFLICT',
47
+ message: 'workspaceMode=isolated conflicts with requireWorktree=false.',
48
+ };
49
+ }
50
+ if (workspaceMode === 'current' && directMainProhibited) {
51
+ return {
52
+ ok: false,
53
+ code: 'WORKSPACE_PLACEMENT_CONSTRAINT_CONFLICT',
54
+ message: 'workspaceMode=current conflicts with directMainProhibited=true because the current lane cannot prove a non-main placement at admission time.',
55
+ };
56
+ }
57
+
58
+ const requiresIsolation = workspaceMode === 'isolated'
59
+ || input.requireWorktree === true
60
+ || directMainProhibited;
61
+ return {
62
+ ok: true,
63
+ constraint: {
64
+ workspaceMode: requiresIsolation ? 'isolated' : workspaceMode,
65
+ requireWorktree: requiresIsolation,
66
+ directMainProhibited,
67
+ requiresIsolation,
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Content-bound Check identity shared by Process Runtime and Work validation.
3
+ *
4
+ * This is durable evidence metadata, not Process lifecycle state. Keeping the
5
+ * contract outside Process Runtime prevents WorkHandle persistence from
6
+ * depending on the Process implementation layer.
7
+ */
8
+ export interface ProcessCheckExecutionIdentity {
9
+ schemaVersion: 1;
10
+ checkId: string;
11
+ cacheKey: string;
12
+ revision: string;
13
+ definitionDigest: string;
14
+ environmentFingerprint: string;
15
+ timeoutMs: number;
16
+ reuseScope: 'repository' | 'checkout';
17
+ scopeKey: string;
18
+ }
@@ -1,5 +1,9 @@
1
1
  import type { ExecutionJob } from './types';
2
- import type { OperationReceipt } from './receipt-store';
2
+
3
+ export interface ChildReferenceReceiptLike {
4
+ childReference?: unknown;
5
+ result?: Record<string, unknown>;
6
+ }
3
7
 
4
8
  /** Durable pointer from a short-lived parent Execution Job to its child Agent Run. */
5
9
  export interface ExecutionChildReference {
@@ -72,7 +76,7 @@ export function childReferenceFromUnknown(value: unknown): ExecutionChildReferen
72
76
  return pickChildReference(value as Record<string, unknown>);
73
77
  }
74
78
 
75
- export function childReferenceFromReceipt(receipt: OperationReceipt | undefined): ExecutionChildReference | undefined {
79
+ export function childReferenceFromReceipt(receipt: ChildReferenceReceiptLike | undefined): ExecutionChildReference | undefined {
76
80
  if (!receipt) return undefined;
77
81
  return childReferenceFromUnknown(receipt.childReference)
78
82
  ?? childReferenceFromUnknown(receipt.result);
@@ -66,6 +66,37 @@ export interface PersistedCheckTerminalEvidence {
66
66
  infrastructureReason?: string;
67
67
  }
68
68
 
69
+ export interface TerminalCheckEvidenceClassificationInput {
70
+ processError?: { code: string; message: string };
71
+ structuredPresent: boolean;
72
+ structuredMatches: boolean;
73
+ legacyPresent: boolean;
74
+ legacyMatches: boolean;
75
+ }
76
+
77
+ /**
78
+ * Compatibility-level classifier for terminal Check evidence. Keep this pure so
79
+ * legacy/public callers and the Process Runtime record adapter share one semantic
80
+ * authority instead of carrying duplicate decision trees.
81
+ */
82
+ export function classifyTerminalCheckEvidence(
83
+ input: TerminalCheckEvidenceClassificationInput,
84
+ ): PersistedCheckTerminalEvidence {
85
+ if (input.structuredMatches || input.legacyMatches) return { state: 'matched' };
86
+ if (!input.structuredPresent && !input.legacyPresent && input.processError?.message?.trim()) {
87
+ const reason = input.processError.message.trim().slice(0, 512);
88
+ return {
89
+ state: 'process_runtime_failed_before_result',
90
+ warning: `check process failed before structured result receipt: ${reason}`,
91
+ infrastructureReason: reason,
92
+ };
93
+ }
94
+ if (input.structuredPresent || input.legacyPresent) {
95
+ return { state: 'mismatch', warning: 'check result receipt did not match the terminal Process semantic identity' };
96
+ }
97
+ return { state: 'missing', warning: 'check result receipt is missing for the terminal Check Process' };
98
+ }
99
+
69
100
  /**
70
101
  * Classify one terminal Check Process using the structured result emitted by
71
102
  * the check-runner sidecar. A Process Runtime/admission failure can terminate
@@ -74,9 +105,15 @@ export interface PersistedCheckTerminalEvidence {
74
105
  * rejected the candidate. Missing/mismatched result identity also fails closed
75
106
  * as infrastructure rather than manufacturing acceptance evidence.
76
107
  */
108
+ export interface LegacyCheckEvidenceLike {
109
+ cacheKey?: string;
110
+ failureClass?: PersistedCheckResultReceipt['failureClass'];
111
+ }
112
+
77
113
  export function classifyPersistedCheckTerminalEvidence(
78
114
  record: ManagedProcessRecord,
79
115
  expectedCheckId: string,
116
+ options: { legacyEvidence?: LegacyCheckEvidenceLike } = {},
80
117
  ): PersistedCheckTerminalEvidence {
81
118
  const structured = readPersistedCheckResultReceipt(record.origin?.checkResultReceiptPath);
82
119
  const structuredMatches = Boolean(
@@ -85,22 +122,20 @@ export function classifyPersistedCheckTerminalEvidence(
85
122
  && structured.checkId === expectedCheckId
86
123
  && structured.cacheKey === record.checkExecution.cacheKey,
87
124
  );
88
- if (structuredMatches) {
89
- return { state: 'matched', failureClass: structured?.failureClass };
90
- }
91
- if (!structured && record.error?.message?.trim()) {
92
- const reason = record.error.message.trim().slice(0, 512);
93
- return {
94
- state: 'process_runtime_failed_before_result',
95
- warning: `check process failed before structured result receipt: ${reason}`,
96
- infrastructureReason: reason,
97
- };
98
- }
99
- if (structured) {
100
- return {
101
- state: 'mismatch',
102
- warning: 'check result receipt did not match the terminal Process semantic identity',
103
- };
104
- }
105
- return { state: 'missing', warning: 'check result receipt is missing for the terminal Check Process' };
125
+ const legacy = options.legacyEvidence;
126
+ const legacyMatches = Boolean(
127
+ legacy?.cacheKey
128
+ && record.checkExecution?.cacheKey
129
+ && legacy.cacheKey === record.checkExecution.cacheKey,
130
+ );
131
+ const classified = classifyTerminalCheckEvidence({
132
+ processError: record.error,
133
+ structuredPresent: Boolean(structured),
134
+ structuredMatches,
135
+ legacyPresent: Boolean(legacy),
136
+ legacyMatches,
137
+ });
138
+ return classified.state === 'matched'
139
+ ? { ...classified, failureClass: structuredMatches ? structured?.failureClass : legacy?.failureClass }
140
+ : classified;
106
141
  }
@@ -16,6 +16,7 @@ interface ParsedArgs {
16
16
  expectedCheckFingerprint: string;
17
17
  resultReceiptPath?: string;
18
18
  cleanupRoot?: string;
19
+ isolatedControllerHome?: string;
19
20
  }
20
21
 
21
22
  function requiredValue(argv: string[], flag: string): string {
@@ -38,6 +39,7 @@ function parseArgs(argv: string[]): ParsedArgs {
38
39
  expectedCheckFingerprint: requiredValue(argv, '--expected-check-fingerprint'),
39
40
  resultReceiptPath: argv.includes('--result-receipt') ? requiredValue(argv, '--result-receipt') : undefined,
40
41
  cleanupRoot: argv.includes('--cleanup-root') ? requiredValue(argv, '--cleanup-root') : undefined,
42
+ isolatedControllerHome: argv.includes('--isolated-controller-home') ? requiredValue(argv, '--isolated-controller-home') : undefined,
41
43
  };
42
44
  }
43
45
 
@@ -51,7 +53,11 @@ export async function runPersistedCheckSidecar(argv = process.argv.slice(2)): Pr
51
53
  throw new Error('CHECK_SNAPSHOT_CHANGED: registered check changed before Process Runtime execution');
52
54
  }
53
55
  const identity = controllerCheckExecutionIdentity(root, args.checkId, args.timeoutMs, snapshot);
54
- const result = await runControllerCheckAsync(root, args.checkId, { requestedTimeoutMs: args.timeoutMs, snapshot });
56
+ const result = await runControllerCheckAsync(root, args.checkId, {
57
+ requestedTimeoutMs: args.timeoutMs,
58
+ snapshot,
59
+ isolatedControllerHome: args.isolatedControllerHome,
60
+ });
55
61
  if (args.resultReceiptPath) {
56
62
  writePersistedCheckResultReceipt(args.resultReceiptPath, {
57
63
  checkId: args.checkId,