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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/AGENTS.md +9 -11
  2. package/CHANGELOG.md +27 -1
  3. package/CLAUDE.md +8 -10
  4. package/DESIGN.md +278 -0
  5. package/README.md +70 -90
  6. package/README.zh-CN.md +52 -55
  7. package/SKILL.md +8 -11
  8. package/THIRD_PARTY_NOTICES.md +9 -3
  9. package/assets/branding/README.md +4 -2
  10. package/assets/hooks/lib/workflow-state.sh +19 -18
  11. package/assets/hooks/post-edit-guard.sh +6 -75
  12. package/assets/hooks/prompt-guard.sh +24 -12
  13. package/assets/hooks/session-start-context.sh +17 -17
  14. package/assets/hooks/stop-orchestrator.sh +2 -2
  15. package/assets/partials/04-project-structure.partial.md +6 -6
  16. package/assets/partials/05-workflow.partial.md +6 -6
  17. package/assets/partials/07-footer.partial.md +1 -1
  18. package/assets/partials/08-orchestration.partial.md +2 -2
  19. package/assets/partials-agents/02-operating-mode.partial.md +5 -5
  20. package/assets/partials-agents/03-orchestration.partial.md +3 -3
  21. package/assets/partials-agents/04-task-protocol.partial.md +3 -3
  22. package/assets/partials-agents/08-deep-docs.partial.md +1 -1
  23. package/assets/reference-configs/agentic-development-flow.md +49 -86
  24. package/assets/reference-configs/external-tooling.md +21 -61
  25. package/assets/reference-configs/handoff-protocol.md +25 -20
  26. package/assets/reference-configs/harness-overview.md +8 -8
  27. package/assets/skill-version.json +2 -2
  28. package/assets/templates/helpers/archive-workflow.sh +0 -1
  29. package/assets/templates/helpers/capture-plan.sh +3 -3
  30. package/assets/templates/helpers/check-task-workflow.sh +2 -2
  31. package/assets/templates/helpers/codex-handoff-resume.sh +5 -5
  32. package/assets/templates/helpers/ensure-task-workflow.sh +37 -35
  33. package/assets/templates/helpers/new-plan.sh +1 -1
  34. package/assets/templates/helpers/plan-to-todo.sh +2 -2
  35. package/assets/templates/helpers/prepare-codex-handoff.sh +2 -2
  36. package/assets/templates/helpers/prepare-handoff.sh +9 -9
  37. package/assets/templates/helpers/refresh-current-status.sh +2 -2
  38. package/assets/templates/helpers/ship-worktrees.sh +1 -1
  39. package/assets/templates/helpers/sprint-backlog.sh +1 -1
  40. package/assets/templates/helpers/verify-sprint.sh +7 -7
  41. package/assets/templates/helpers/workstream-sync.sh +1 -1
  42. package/assets/templates/plan.template.md +1 -1
  43. package/assets/workflow-contract.v1.json +6 -3
  44. package/docs/architecture/current/README.md +10 -9
  45. package/docs/architecture/current/approved-target-architecture.zh-CN.md +2 -2
  46. package/docs/architecture/current/architecture-invariants.md +2 -2
  47. package/docs/architecture/current/automation-and-schedule-engine.md +15 -15
  48. package/docs/architecture/current/codegraph-context-provider.md +2 -2
  49. package/docs/architecture/current/control-plane-authority-inventory.md +12 -0
  50. package/docs/architecture/current/dispatch-and-agent-strategy.md +11 -11
  51. package/docs/architecture/current/entity-model.md +0 -27
  52. package/docs/architecture/current/failure-recovery.md +0 -24
  53. package/docs/architecture/current/governance.md +1 -1
  54. package/docs/architecture/current/implementation-status.md +1 -1
  55. package/docs/architecture/current/legacy-governance.md +58 -0
  56. package/docs/architecture/current/multi-repository-execution.md +1 -1
  57. package/docs/architecture/current/peer-supercontroller-runtime.md +1 -1
  58. package/docs/architecture/current/personal-assistant-plugin-baseline.md +4 -5
  59. package/docs/architecture/current/scheduler-and-resource-claims.md +0 -10
  60. package/docs/architecture/current/target-requirements-traceability.md +2 -2
  61. package/docs/architecture/current/thin-harness-governance-policy.md +1 -1
  62. package/docs/architecture/current/thin-harness-v1.md +9 -11
  63. package/docs/architecture/domains/runtime-harness.md +1 -1
  64. package/docs/forge-autonomous-goal-loop.md +3 -1
  65. package/docs/forge-chatgpt-mcp-setup.md +67 -176
  66. package/docs/images/forge-banner-cn.svg +26 -1
  67. package/docs/images/forge-banner.svg +26 -1
  68. package/docs/operations/features.md +1 -1
  69. package/docs/operations/homebrew.md +3 -3
  70. package/docs/operations/mcp-tool-exposure.md +5 -5
  71. package/docs/operations/platform-support.md +45 -37
  72. package/docs/operations/platform-support.zh-CN.md +40 -38
  73. package/docs/operations/releasing.md +3 -3
  74. package/docs/operations/releasing.zh-CN.md +3 -3
  75. package/docs/operations/troubleshooting.md +11 -6
  76. package/docs/operations/troubleshooting.zh-CN.md +11 -6
  77. package/docs/public-usage-guide.md +28 -21
  78. package/docs/public-usage-guide.zh-CN.md +28 -21
  79. package/docs/tutorials/01-install-and-start.md +58 -35
  80. package/docs/tutorials/01-install-and-start.zh-CN.md +58 -35
  81. package/docs/tutorials/02-connect-chatgpt.md +118 -46
  82. package/docs/tutorials/02-connect-chatgpt.zh-CN.md +119 -45
  83. package/docs/wiki/Architecture.md +1 -1
  84. package/docs/wiki/Installation.md +2 -2
  85. package/docs/wiki/Quick-Start.md +1 -1
  86. package/docs/wiki/Work-Lifecycle.md +1 -1
  87. package/install.ps1 +4 -5
  88. package/install.sh +4 -6
  89. package/package.json +15 -5
  90. package/scripts/archive-workflow.sh +0 -1
  91. package/scripts/benchmark-thin-harness-mcp-e2e.ts +27 -14
  92. package/scripts/build-controller-ui.ts +28 -0
  93. package/scripts/capture-plan.sh +4 -4
  94. package/scripts/check-agent-tooling.sh +12 -1
  95. package/scripts/check-platform-support.mjs +2 -2
  96. package/scripts/check-public-docs.sh +16 -5
  97. package/scripts/check-release-readiness.sh +5 -11
  98. package/scripts/check-runtime-architecture.mjs +14 -22
  99. package/scripts/check-tarball-install-smoke.sh +3 -2
  100. package/scripts/check-task-workflow.sh +2 -2
  101. package/scripts/codex-handoff-resume.sh +5 -5
  102. package/scripts/create-project-dirs.sh +3 -3
  103. package/scripts/ensure-task-workflow.sh +45 -35
  104. package/scripts/init-project.sh +4 -4
  105. package/scripts/install-desktop-operator-registration.ts +2 -2
  106. package/scripts/lib/project-init-lib.sh +46 -44
  107. package/scripts/migrate-project-template.sh +7 -4
  108. package/scripts/new-plan.sh +1 -1
  109. package/scripts/plan-to-todo.sh +2 -2
  110. package/scripts/prepare-codex-handoff.sh +2 -2
  111. package/scripts/prepare-handoff.sh +9 -9
  112. package/scripts/refresh-current-status.sh +2 -2
  113. package/scripts/setup-plugins.sh +3 -3
  114. package/scripts/ship-worktrees.sh +1 -1
  115. package/scripts/sprint-backlog.sh +5 -5
  116. package/scripts/sync-codex-installed-copies.sh +2 -3
  117. package/scripts/verify-sprint.sh +7 -7
  118. package/scripts/workstream-sync.sh +1 -1
  119. package/src/cli/chatgpt-browser/bridge-extension.ts +1 -1
  120. package/src/cli/chatgpt-browser/bridge-provider.ts +29 -0
  121. package/src/cli/chatgpt-browser/file-policy.ts +3 -1
  122. package/src/cli/commands/chatgpt.ts +51 -0
  123. package/src/cli/commands/doctor.ts +1 -1
  124. package/src/cli/commands/init-hook.ts +213 -70
  125. package/src/cli/commands/init.ts +5 -6
  126. package/src/cli/commands/mcp.ts +31 -5
  127. package/src/cli/commands/runtime.ts +61 -8
  128. package/src/cli/commands/setup-profile.ts +472 -0
  129. package/src/cli/controller/check-runner.ts +20 -3
  130. package/src/cli/controller/codex-continuation.ts +4 -7
  131. package/src/cli/controller/context-pack.ts +118 -14
  132. package/src/cli/controller/operational-plan.ts +4 -4
  133. package/src/cli/controller/task-ledger.ts +10 -10
  134. package/src/cli/controller/work-mode.ts +6 -13
  135. package/src/cli/index.ts +14 -35
  136. package/src/cli/local-bridge/connector-freshness.ts +1 -1
  137. package/src/cli/local-bridge/console-assets.ts +17 -0
  138. package/src/cli/local-bridge/console-automations.ts +160 -0
  139. package/src/cli/local-bridge/dashboard.ts +8 -1303
  140. package/src/cli/local-bridge/facade-api.ts +16 -7
  141. package/src/cli/local-bridge/frontend/src/App.tsx +10 -0
  142. package/src/cli/local-bridge/frontend/src/components/Button.tsx +1 -0
  143. package/src/cli/local-bridge/frontend/src/components/DetailPane.tsx +3 -0
  144. package/src/cli/local-bridge/frontend/src/components/SearchField.tsx +1 -0
  145. package/src/cli/local-bridge/frontend/src/components/SectionHeader.tsx +1 -0
  146. package/src/cli/local-bridge/frontend/src/components/Segmented.tsx +1 -0
  147. package/src/cli/local-bridge/frontend/src/components/StatusText.tsx +3 -0
  148. package/src/cli/local-bridge/frontend/src/design/icons.tsx +11 -0
  149. package/src/cli/local-bridge/frontend/src/design/tokens.css +8 -0
  150. package/src/cli/local-bridge/frontend/src/features/automations/AutomationsView.tsx +3 -0
  151. package/src/cli/local-bridge/frontend/src/features/capabilities/CapabilitiesView.tsx +4 -0
  152. package/src/cli/local-bridge/frontend/src/features/overview/OverviewView.tsx +3 -0
  153. package/src/cli/local-bridge/frontend/src/features/repositories/RepositoriesView.tsx +2 -0
  154. package/src/cli/local-bridge/frontend/src/features/settings/SettingsView.tsx +2 -0
  155. package/src/cli/local-bridge/frontend/src/features/system/SystemView.tsx +2 -0
  156. package/src/cli/local-bridge/frontend/src/features/work/WorkView.tsx +3 -0
  157. package/src/cli/local-bridge/frontend/src/lib/api.ts +23 -0
  158. package/src/cli/local-bridge/frontend/src/lib/format.ts +4 -0
  159. package/src/cli/local-bridge/frontend/src/lib/routes.ts +7 -0
  160. package/src/cli/local-bridge/frontend/src/main.tsx +1 -0
  161. package/src/cli/local-bridge/frontend/src/shell/AppShell.tsx +1 -0
  162. package/src/cli/local-bridge/frontend/src/shell/CommandBar.tsx +2 -0
  163. package/src/cli/local-bridge/frontend/src/shell/Sidebar.tsx +3 -0
  164. package/src/cli/local-bridge/frontend/src/styles.css +12 -0
  165. package/src/cli/local-bridge/frontend/src/types.ts +14 -0
  166. package/src/cli/local-bridge/frontend/src/vite-env.d.ts +1 -0
  167. package/src/cli/local-bridge/frontend/vite.config.ts +27 -0
  168. package/src/cli/local-bridge/server.ts +45 -0
  169. package/src/cli/local-bridge/ui-dist/app.css +1 -0
  170. package/src/cli/local-bridge/ui-dist/app.js +11094 -0
  171. package/src/cli/mcp/auth.ts +2 -0
  172. package/src/cli/mcp/instructions.ts +3 -3
  173. package/src/cli/mcp/legacy-tool-service.ts +27 -23
  174. package/src/cli/mcp/multi-repository.ts +2 -0
  175. package/src/cli/mcp/policy.ts +7 -5
  176. package/src/cli/mcp/repository-tools.ts +53 -14
  177. package/src/cli/mcp/server.ts +5 -10
  178. package/src/cli/mcp/setup.ts +220 -276
  179. package/src/cli/mcp/tool-capability-cache.ts +1 -1
  180. package/src/cli/mcp/toolset-names.ts +1 -1
  181. package/src/cli/mcp/toolset.ts +0 -1
  182. package/src/cli/mcp/transports/http.ts +51 -4
  183. package/src/cli/repositories/branch-name-policy.ts +0 -7
  184. package/src/cli/repositories/controller-home.ts +110 -3
  185. package/src/cli/repositories/safe-patch.ts +1 -1
  186. package/src/cli/repositories/selected-path-actions.ts +26 -24
  187. package/src/core/adoption/gitignore-plan.ts +2 -3
  188. package/src/core/adoption/plan.ts +4 -1
  189. package/src/runtime/control-plane/execution/edit-validation-coordinator.ts +556 -0
  190. package/src/runtime/control-plane/execution/work-handle-store.ts +128 -1
  191. package/src/runtime/control-plane/execution/work-validation-reconciler.ts +277 -0
  192. package/src/runtime/control-plane/execution/work-verification-snapshot.ts +169 -0
  193. package/src/runtime/control-plane/facade/capability-registry.ts +0 -10
  194. package/src/runtime/control-plane/facade/handoff-inbox-store.ts +44 -25
  195. package/src/runtime/control-plane/facade/suggested-actions.ts +7 -1
  196. package/src/runtime/control-plane/facade/types.ts +0 -1
  197. package/src/runtime/control-plane/global-scheduler/scheduler.ts +30 -8
  198. package/src/runtime/control-plane/launcher/chatgpt-work-binding-store.ts +114 -0
  199. package/src/runtime/control-plane/launcher/chatgpt-work-continuation.ts +175 -0
  200. package/src/runtime/control-plane/launcher/launch-reservation-store.ts +161 -0
  201. package/src/runtime/control-plane/launcher/provider-mcp-bootstrap.ts +64 -0
  202. package/src/runtime/control-plane/launcher/thin-launcher.ts +117 -24
  203. package/src/runtime/control-plane/routing/route-policy.ts +14 -14
  204. package/src/runtime/control-plane/runtime-cleanup.ts +0 -1
  205. package/src/runtime/evidence/event-ledger.ts +1 -1
  206. package/src/runtime/{workflow/campaigns/workspace.ts → execution/managed-workspace.ts} +41 -43
  207. package/src/runtime/execution/process-runtime/check-facade.ts +6 -0
  208. package/src/runtime/execution/process-runtime/check-result.ts +45 -0
  209. package/src/runtime/execution/process-runtime/check-runner-sidecar.ts +42 -16
  210. package/src/runtime/execution/process-runtime/command-facade.ts +4 -2
  211. package/src/runtime/execution/process-runtime/gc.ts +1 -1
  212. package/src/runtime/execution/process-runtime/index.ts +3 -0
  213. package/src/runtime/execution/process-runtime/interactive-admission.ts +14 -5
  214. package/src/runtime/execution/process-runtime/persisted-check.ts +285 -0
  215. package/src/runtime/execution/process-runtime/resource-claims.ts +30 -0
  216. package/src/runtime/execution/process-runtime/runtime.ts +18 -1
  217. package/src/runtime/execution/process-runtime/store.ts +1 -19
  218. package/src/runtime/execution/process-runtime/types.ts +4 -0
  219. package/src/runtime/execution/thin-harness/execution-router.ts +0 -1
  220. package/src/runtime/execution/thin-harness/fast-executor.ts +1 -1
  221. package/src/runtime/execution/thin-harness/lightweight-lanes.ts +1 -2
  222. package/src/runtime/execution/thin-harness/types.ts +0 -1
  223. package/src/runtime/execution/workers/executor.ts +1 -1
  224. package/src/runtime/gateway/mcp/execution-tools.ts +1 -1
  225. package/src/runtime/gateway/mcp/persisted-check-process.ts +3 -255
  226. package/src/runtime/gateway/mcp/process-tools.ts +16 -3
  227. package/src/runtime/gateway/mcp/resource-policy.ts +0 -9
  228. package/src/runtime/gateway/mcp/router.ts +117 -182
  229. package/src/runtime/gateway/mcp/runtime-tools.ts +331 -409
  230. package/src/runtime/gateway/mcp/work-validation-reconciler.ts +1 -219
  231. package/src/runtime/plugins/browser-adapter.ts +5 -0
  232. package/src/runtime/plugins/desktop-operator-registration.ts +1 -1
  233. package/src/runtime/plugins/store.ts +14 -6
  234. package/src/runtime/plugins/types.ts +1 -1
  235. package/src/runtime/projections/materialized-view.ts +1 -23
  236. package/src/runtime/recovery/maintenance-executor.ts +9 -0
  237. package/src/runtime/resources/ownership.ts +0 -1
  238. package/src/runtime/root/gateway-adapter.ts +2 -0
  239. package/src/runtime/root/mcp-transport.ts +10 -2
  240. package/src/runtime/root/package-connector-service.ts +136 -0
  241. package/src/runtime/root/package-runtime-release.ts +161 -0
  242. package/src/runtime/root/package-runtime-service.ts +185 -0
  243. package/src/runtime/root/release-materialize.ts +19 -0
  244. package/src/runtime/root/runtime.ts +2 -1
  245. package/src/runtime/root/scheduler.ts +5 -0
  246. package/src/runtime/root/service.ts +18 -5
  247. package/src/runtime/standalone-recovery/core.ts +65 -16
  248. package/src/runtime/workflow/findings/store.ts +145 -106
  249. package/src/runtime/workflow/findings/types.ts +22 -0
  250. package/src/runtime/workflow/schedules/engine.ts +132 -2
  251. package/src/runtime/workflow/schedules/pending-activation.ts +139 -0
  252. package/src/runtime/workflow/schedules/work-continuation.ts +218 -0
  253. package/docs/CHANGELOG.md +0 -978
  254. package/docs/README.zh-CN.md +0 -58
  255. package/scripts/smoke-chatgpt-supervised-automation.ts +0 -208
  256. package/src/cli/commands/controller.ts +0 -680
  257. package/src/runtime/workflow/campaigns/cleanup.ts +0 -263
  258. package/src/runtime/workflow/campaigns/engine.ts +0 -112
  259. package/src/runtime/workflow/campaigns/normalize.ts +0 -100
  260. package/src/runtime/workflow/campaigns/review.ts +0 -315
  261. package/src/runtime/workflow/campaigns/store.ts +0 -526
  262. package/src/runtime/workflow/campaigns/supervisor.ts +0 -112
  263. package/src/runtime/workflow/campaigns/types.ts +0 -290
  264. /package/src/runtime/{workflow/campaigns → execution}/workspace-agent.ts +0 -0
@@ -28,7 +28,6 @@ import { RepoActorRegistry } from '../repo-actor/registry';
28
28
  import { reconcileExecutionJobsAsync } from './reconciliation';
29
29
  import { tickSchedules } from '../../workflow/schedules/engine';
30
30
  import { tickPortfolioWorkflows } from '../../workflow/portfolio/engine';
31
- import { tickCampaigns } from '../../workflow/campaigns/engine';
32
31
  import { tickGoalLoopsForController } from '../goal-loop';
33
32
  import { readJsonFile, writeJsonAtomic } from '../../shared/json-files';
34
33
  import { resolveBunExecutable } from '../../shared/process-environment';
@@ -36,6 +35,9 @@ import { isProcessAlive, terminateProcessTree } from '../../shared/process-tree'
36
35
  import { readSchedulerWakeSignal, waitForSchedulerWakeSignal } from './wake-signal';
37
36
  import { cleanupControllerRuntimeState } from '../runtime-cleanup';
38
37
  import { reconcileTerminalWorkCleanups } from '../execution/work-terminal-cleanup';
38
+ import { gcTerminalProcesses } from '../../execution/process-runtime/gc';
39
+ import { reconcilePendingWorkValidations } from '../execution/work-validation-reconciler';
40
+ import { reconcilePendingEditValidations } from '../execution/edit-validation-coordinator';
39
41
  import { schedulerDispatchAllowed } from '../facade/work-admission-policy';
40
42
  import { rebuildRepositoryProjection, refreshRepositoryProjectionForRepository } from '../../projections/materialized-view';
41
43
  import { readRepositoryGitStatusSample, sampleRepositoryGitStatusForRepositories } from '../../projections/git-status-sampler';
@@ -215,7 +217,6 @@ export class GlobalScheduler {
215
217
  private readonly fatalOnTickError: boolean;
216
218
  private lastScheduleTick = 0;
217
219
  private lastPortfolioTick = 0;
218
- private lastCampaignTick = 0;
219
220
  private lastGoalLoopTick = 0;
220
221
  private lastReconcile = 0;
221
222
  private lastPersistedAt = 0;
@@ -232,6 +233,10 @@ export class GlobalScheduler {
232
233
  private sourceScansAvoided = 0;
233
234
  private runtimeCleanup = cleanupControllerRuntimeState;
234
235
  private terminalWorkCleanup = reconcileTerminalWorkCleanups;
236
+ private processGc = gcTerminalProcesses;
237
+ private workValidationReconcile = reconcilePendingWorkValidations;
238
+ private editValidationReconcile = reconcilePendingEditValidations;
239
+ private repositoryList = listRepositories;
235
240
  private lastDarwinMemorySampleAt = 0;
236
241
  private cachedDarwinAvailableMemoryMb: number | undefined;
237
242
  private darwinMemorySampleInFlight: Promise<void> | undefined;
@@ -612,6 +617,7 @@ export class GlobalScheduler {
612
617
  this.lastHeartbeatAt = new Date(now).toISOString();
613
618
  this.lastTickAt = this.lastHeartbeatAt;
614
619
  this.persistState();
620
+ const repositories = this.repositoryList(this.controllerHome).filter((repo) => repo.enabled && !repo.removedAt);
615
621
  if (now - this.lastCleanupAt >= RUNTIME_CLEANUP_INTERVAL_MS) {
616
622
  // Advance the interval before cleanup so a failing pass cannot create a
617
623
  // tight retry loop on every scheduler tick.
@@ -630,13 +636,32 @@ export class GlobalScheduler {
630
636
  } catch (error) {
631
637
  console.error('[forge cleanup] terminal Work cleanup failed:', error);
632
638
  }
639
+ if (repositories.length > 0) {
640
+ const slot = Math.floor(now / RUNTIME_CLEANUP_INTERVAL_MS) % repositories.length;
641
+ const repo = repositories[slot]!;
642
+ const result = this.processGc({ controllerHome: this.controllerHome, repoId: repo.repoId });
643
+ if (!result.ok) console.error('[forge cleanup] Process GC failed:', result.error ?? 'unknown error');
644
+ }
633
645
  }
634
646
  if (now - this.lastReconcile >= 5_000) {
635
647
  await reconcileExecutionJobsAsync(this.controllerHome);
648
+ for (const repository of repositories) {
649
+ const validation = this.workValidationReconcile(this.controllerHome, repository.repoId, 500);
650
+ if (validation.errors.length > 0) {
651
+ console.error(
652
+ `[forge validation] background Work reconciliation reported ${validation.errors.length} error(s) for ${repository.repoId}`,
653
+ );
654
+ }
655
+ const editValidation = await this.editValidationReconcile(this.controllerHome, repository, 200);
656
+ if (editValidation.errors.length > 0) {
657
+ console.error(
658
+ `[forge validation] background EditSession reconciliation reported ${editValidation.errors.length} error(s) for ${repository.repoId}`,
659
+ );
660
+ }
661
+ }
636
662
  this.lastReconcile = now;
637
663
  this.lastReconcileAt = new Date(now).toISOString();
638
664
  }
639
- const repositories = listRepositories(this.controllerHome).filter((repo) => repo.enabled && !repo.removedAt);
640
665
  const activeJobSnapshot = listActiveExecutionJobs(this.controllerHome);
641
666
  const hasActiveWork = activeJobSnapshot.length > 0;
642
667
  const sourceScanDue = hasActiveWork || now - this.lastSourceScanAt >= IDLE_REPOSITORY_SCAN_INTERVAL_MS;
@@ -843,12 +868,9 @@ export class GlobalScheduler {
843
868
  this.spawnWorker(pending.repoId, pending.jobId);
844
869
  }
845
870
  // Dispatch is latency-sensitive. Run repository workflow maintenance only
846
- // after queued Jobs have had a chance to claim capacity. Campaign and Goal
871
+ // after queued Jobs have had a chance to claim capacity. Goal
847
872
  // state transitions still wake the scheduler immediately when they enqueue work.
848
- if (now - this.lastCampaignTick >= 1_000) {
849
- tickCampaigns(this.controllerHome, repositories.map((repo) => repo.repoId));
850
- this.lastCampaignTick = now;
851
- }
873
+
852
874
  if (now - this.lastGoalLoopTick >= 5_000) {
853
875
  try {
854
876
  tickGoalLoopsForController(
@@ -0,0 +1,114 @@
1
+ import { withControllerLock } from '../../../cli/repositories/locks';
2
+ import { readControlPlaneRecord, writeControlPlaneRecord } from '../persistence/sqlite-store';
3
+
4
+ const NAMESPACE = 'chatgpt_work_conversation_binding';
5
+
6
+ export interface ChatgptWorkConversationBinding {
7
+ schemaVersion: 1;
8
+ repoId: string;
9
+ workId: string;
10
+ conversationUrl: string;
11
+ conversationId: string;
12
+ localAlias: string;
13
+ latestBrowserSessionId?: string;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ lastContinuedAt?: string;
17
+ }
18
+
19
+ export interface ChatgptWorkBindingStoreOptions {
20
+ controllerHome: string;
21
+ repoId: string;
22
+ now?: () => string;
23
+ }
24
+
25
+ function nowIso(options: ChatgptWorkBindingStoreOptions): string {
26
+ return options.now?.() ?? new Date().toISOString();
27
+ }
28
+
29
+ export function parseChatgptConversationIdentity(value: string): { conversationUrl: string; conversationId: string } {
30
+ let url: URL;
31
+ try {
32
+ url = new URL(value);
33
+ } catch {
34
+ throw new Error('CHATGPT_WORK_CONVERSATION_URL_INVALID');
35
+ }
36
+ if (url.protocol !== 'https:' || !['chatgpt.com', 'www.chatgpt.com', 'chat.openai.com'].includes(url.hostname)) {
37
+ throw new Error('CHATGPT_WORK_CONVERSATION_URL_INVALID');
38
+ }
39
+ const parts = url.pathname.split('/').filter(Boolean);
40
+ const c = parts.lastIndexOf('c');
41
+ const conversationId = c >= 0 ? parts[c + 1]?.trim() : undefined;
42
+ if (!conversationId) throw new Error('CHATGPT_WORK_CONVERSATION_ID_MISSING');
43
+ url.protocol = 'https:';
44
+ url.hostname = 'chatgpt.com';
45
+ url.search = '';
46
+ url.hash = '';
47
+ return { conversationUrl: url.toString(), conversationId };
48
+ }
49
+
50
+ function record(options: ChatgptWorkBindingStoreOptions, workId: string) {
51
+ return readControlPlaneRecord<ChatgptWorkConversationBinding>(
52
+ options.controllerHome,
53
+ NAMESPACE,
54
+ options.repoId,
55
+ workId,
56
+ );
57
+ }
58
+
59
+ export function getChatgptWorkConversationBinding(
60
+ options: ChatgptWorkBindingStoreOptions,
61
+ workId: string,
62
+ ): ChatgptWorkConversationBinding | undefined {
63
+ return record(options, workId)?.value;
64
+ }
65
+
66
+ export function bindChatgptWorkConversation(
67
+ options: ChatgptWorkBindingStoreOptions,
68
+ input: {
69
+ workId: string;
70
+ conversationUrl: string;
71
+ latestBrowserSessionId?: string;
72
+ localAlias?: string;
73
+ },
74
+ ): ChatgptWorkConversationBinding {
75
+ if (!input.workId.trim()) throw new Error('CHATGPT_WORK_BINDING_WORK_REQUIRED');
76
+ const identity = parseChatgptConversationIdentity(input.conversationUrl);
77
+ return withControllerLock(
78
+ options.controllerHome,
79
+ { scope: 'task', repoId: options.repoId, taskId: `chatgpt-work-binding-${input.workId}` },
80
+ `chatgpt-work-binding:${input.workId}`,
81
+ () => {
82
+ const existing = record(options, input.workId);
83
+ if (existing && existing.value.conversationId !== identity.conversationId) {
84
+ throw new Error(`CHATGPT_WORK_CONVERSATION_REBIND_REQUIRED: ${input.workId}:${existing.value.conversationId}`);
85
+ }
86
+ const now = nowIso(options);
87
+ const localAlias = input.localAlias?.trim()
88
+ || existing?.value.localAlias
89
+ || `Forge · ${options.repoId} · ${input.workId} · ${identity.conversationId.slice(0, 8)}`;
90
+ const binding: ChatgptWorkConversationBinding = {
91
+ schemaVersion: 1,
92
+ repoId: options.repoId,
93
+ workId: input.workId,
94
+ conversationUrl: identity.conversationUrl,
95
+ conversationId: identity.conversationId,
96
+ localAlias: localAlias.slice(0, 180),
97
+ latestBrowserSessionId: input.latestBrowserSessionId ?? existing?.value.latestBrowserSessionId,
98
+ createdAt: existing?.value.createdAt ?? now,
99
+ updatedAt: now,
100
+ lastContinuedAt: now,
101
+ };
102
+ writeControlPlaneRecord(options.controllerHome, {
103
+ namespace: NAMESPACE,
104
+ scope: options.repoId,
105
+ key: input.workId,
106
+ schemaVersion: 1,
107
+ value: binding,
108
+ action: existing ? 'chatgpt_work_conversation_continue' : 'chatgpt_work_conversation_bind',
109
+ expectedRevision: existing?.revision ?? null,
110
+ });
111
+ return binding;
112
+ },
113
+ );
114
+ }
@@ -0,0 +1,175 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { executeBrowserPluginAction, readBrowserPluginConfiguration } from '../../plugins/browser-adapter';
3
+ import { controllerPluginRepository } from '../../plugins/store';
4
+ import { getWorkContract } from '../facade/work-contract-store';
5
+ import {
6
+ bindChatgptWorkConversation,
7
+ getChatgptWorkConversationBinding,
8
+ type ChatgptWorkConversationBinding,
9
+ } from './chatgpt-work-binding-store';
10
+
11
+ const CONTROLLER_CHATGPT_SESSION_ID = 'forge-chatgpt-supercontroller';
12
+ const CHATGPT_PROMPT_SELECTOR = '[name="prompt-textarea"]';
13
+ const CHATGPT_SEND_KEY = 'Enter';
14
+ const CHATGPT_ALLOWED_DOMAINS = ['chatgpt.com', 'www.chatgpt.com'];
15
+
16
+ export interface WorkChatgptContinuationInput {
17
+ controllerHome: string;
18
+ repoId: string;
19
+ repoRoot: string;
20
+ workId: string;
21
+ prompt: string;
22
+ title?: string;
23
+ browserSessionId?: string;
24
+ conversationUrl?: string;
25
+ timeoutMs?: number;
26
+ }
27
+
28
+ export interface WorkChatgptContinuationResult {
29
+ status: 'dispatched' | 'failed';
30
+ provider: 'controller-browser';
31
+ browserSessionId: string;
32
+ conversationUrl?: string;
33
+ conversationId?: string;
34
+ localAlias?: string;
35
+ resumedFromBinding: boolean;
36
+ error?: { code: string; message: string };
37
+ }
38
+
39
+ function requestId(workId: string, actionId: string): string {
40
+ return `chatgpt-work:${workId}:${actionId}:${randomUUID()}`;
41
+ }
42
+
43
+ async function controllerBrowserAction(
44
+ controllerHome: string,
45
+ workId: string,
46
+ actionId: string,
47
+ args: Record<string, unknown>,
48
+ timeoutMs?: number,
49
+ ): Promise<Record<string, unknown>> {
50
+ const repository = controllerPluginRepository(controllerHome);
51
+ const repoRoot = repository.canonicalRoot ?? repository.localRoot;
52
+ if (!repoRoot) throw new Error('CHATGPT_CONTROLLER_BROWSER_ROOT_UNAVAILABLE');
53
+ return executeBrowserPluginAction({
54
+ controllerHome,
55
+ repoId: repository.repoId,
56
+ repoRoot,
57
+ pluginId: 'browser',
58
+ actionId,
59
+ requestId: requestId(workId, actionId),
60
+ args,
61
+ timeoutMs,
62
+ origin: { surface: 'schedule', actor: 'chatgpt-work-continuation' },
63
+ });
64
+ }
65
+
66
+ async function ensureControllerChatgptBrowser(controllerHome: string, workId: string): Promise<void> {
67
+ const repository = controllerPluginRepository(controllerHome);
68
+ const repoRoot = repository.canonicalRoot ?? repository.localRoot;
69
+ if (!repoRoot) throw new Error('CHATGPT_CONTROLLER_BROWSER_ROOT_UNAVAILABLE');
70
+ const existing = readBrowserPluginConfiguration(repoRoot);
71
+ const allowedDomains = [...new Set([...existing.allowedDomains, ...CHATGPT_ALLOWED_DOMAINS])];
72
+ await controllerBrowserAction(controllerHome, workId, 'configure', {
73
+ enabled: true,
74
+ ...(!existing.enabled ? {
75
+ browser_mode: 'attach_preferred',
76
+ profile_mode: 'repo_local',
77
+ browser_channel: 'chromium',
78
+ cdp_attach_fallback: 'fail_closed',
79
+ native_attach_mode: 'auto',
80
+ native_browser_candidates: ['chrome'],
81
+ } : {}),
82
+ allowed_domains: allowedDomains,
83
+ });
84
+ }
85
+
86
+ function stringField(value: unknown): string | undefined {
87
+ return typeof value === 'string' && value.trim() ? value.trim() : undefined;
88
+ }
89
+
90
+ function resultUrl(result: Record<string, unknown>): string | undefined {
91
+ return stringField(result.url)
92
+ ?? stringField((result.session as Record<string, unknown> | undefined)?.url);
93
+ }
94
+
95
+ /**
96
+ * Dispatches a bounded resume prompt to one controller-owned ChatGPT Web tab.
97
+ * Chat history is transport context only. Forge Work/Plan/evidence remain authoritative.
98
+ */
99
+ export async function runWorkChatgptContinuation(input: WorkChatgptContinuationInput): Promise<WorkChatgptContinuationResult> {
100
+ const store = { controllerHome: input.controllerHome, repoId: input.repoId };
101
+ const existing = getChatgptWorkConversationBinding(store, input.workId);
102
+ const seedUrl = input.conversationUrl?.trim() || existing?.conversationUrl;
103
+ const sessionId = input.browserSessionId?.trim() || CONTROLLER_CHATGPT_SESSION_ID;
104
+ let binding: ChatgptWorkConversationBinding | undefined = existing;
105
+
106
+ try {
107
+ const work = getWorkContract(store, input.workId);
108
+ if (!work || work.repoId !== input.repoId) {
109
+ throw new Error(`CHATGPT_WORK_CONTRACT_NOT_FOUND: ${input.repoId}:${input.workId}`);
110
+ }
111
+ await ensureControllerChatgptBrowser(input.controllerHome, input.workId);
112
+ if (seedUrl && !binding) {
113
+ binding = bindChatgptWorkConversation(store, {
114
+ workId: input.workId,
115
+ conversationUrl: seedUrl,
116
+ latestBrowserSessionId: sessionId,
117
+ localAlias: input.title,
118
+ });
119
+ }
120
+ const targetUrl = binding?.conversationUrl ?? seedUrl ?? 'https://chatgpt.com/';
121
+ await controllerBrowserAction(input.controllerHome, input.workId, 'navigate', {
122
+ session_id: sessionId,
123
+ url: targetUrl,
124
+ wait_until: 'domcontentloaded',
125
+ timeout_ms: input.timeoutMs ?? 60_000,
126
+ retries: 1,
127
+ }, input.timeoutMs);
128
+ await controllerBrowserAction(input.controllerHome, input.workId, 'fill', {
129
+ session_id: sessionId,
130
+ selector: CHATGPT_PROMPT_SELECTOR,
131
+ text: input.prompt,
132
+ timeout_ms: input.timeoutMs ?? 60_000,
133
+ post_action_wait_ms: 100,
134
+ }, input.timeoutMs);
135
+ const sent = await controllerBrowserAction(input.controllerHome, input.workId, 'press', {
136
+ session_id: sessionId,
137
+ selector: CHATGPT_PROMPT_SELECTOR,
138
+ key: CHATGPT_SEND_KEY,
139
+ timeout_ms: input.timeoutMs ?? 60_000,
140
+ post_action_wait_ms: 1_500,
141
+ }, input.timeoutMs);
142
+ const observedUrl = resultUrl(sent) ?? targetUrl;
143
+ if (/\/c\/[^/?#]+/.test(observedUrl)) {
144
+ binding = bindChatgptWorkConversation(store, {
145
+ workId: input.workId,
146
+ conversationUrl: observedUrl,
147
+ latestBrowserSessionId: sessionId,
148
+ localAlias: binding?.localAlias ?? input.title,
149
+ });
150
+ }
151
+ return {
152
+ status: 'dispatched',
153
+ provider: 'controller-browser',
154
+ browserSessionId: sessionId,
155
+ conversationUrl: binding?.conversationUrl ?? observedUrl,
156
+ conversationId: binding?.conversationId,
157
+ localAlias: binding?.localAlias,
158
+ resumedFromBinding: Boolean(existing),
159
+ };
160
+ } catch (error) {
161
+ return {
162
+ status: 'failed',
163
+ provider: 'controller-browser',
164
+ browserSessionId: sessionId,
165
+ conversationUrl: binding?.conversationUrl ?? seedUrl,
166
+ conversationId: binding?.conversationId,
167
+ localAlias: binding?.localAlias,
168
+ resumedFromBinding: Boolean(existing),
169
+ error: {
170
+ code: error instanceof Error && error.message.includes(':') ? error.message.split(':', 1)[0] : 'CHATGPT_CONTROLLER_BROWSER_FAILED',
171
+ message: error instanceof Error ? error.message : String(error),
172
+ },
173
+ };
174
+ }
175
+ }
@@ -0,0 +1,161 @@
1
+ import { randomUUID } from 'crypto';
2
+ import { withControllerLock } from '../../../cli/repositories/locks';
3
+ import {
4
+ readControlPlaneRecord,
5
+ writeControlPlaneRecord,
6
+ } from '../persistence/sqlite-store';
7
+ import type { ControllerType } from '../facade/types';
8
+
9
+ const NAMESPACE = 'external_controller_launch_reservation';
10
+ const DEFAULT_TTL_MS = 2 * 60_000;
11
+ const MAX_TTL_MS = 10 * 60_000;
12
+
13
+ export interface ExternalControllerLaunchReservation {
14
+ schemaVersion: 1;
15
+ repoId: string;
16
+ workId: string;
17
+ reservationId: string;
18
+ controllerType: Exclude<ControllerType, 'human'>;
19
+ createdAt: string;
20
+ expiresAt: string;
21
+ pid?: number;
22
+ releasedAt?: string;
23
+ releaseReason?: string;
24
+ }
25
+
26
+ export interface LaunchReservationStoreOptions {
27
+ controllerHome: string;
28
+ repoId: string;
29
+ now?: () => string;
30
+ }
31
+
32
+ function nowIso(options: LaunchReservationStoreOptions): string {
33
+ return options.now?.() ?? new Date().toISOString();
34
+ }
35
+
36
+ function record(options: LaunchReservationStoreOptions, workId: string) {
37
+ return readControlPlaneRecord<ExternalControllerLaunchReservation>(
38
+ options.controllerHome,
39
+ NAMESPACE,
40
+ options.repoId,
41
+ workId,
42
+ );
43
+ }
44
+
45
+ function active(value: ExternalControllerLaunchReservation | undefined, at = Date.now()): ExternalControllerLaunchReservation | undefined {
46
+ if (!value || value.releasedAt) return undefined;
47
+ return Date.parse(value.expiresAt) > at ? value : undefined;
48
+ }
49
+
50
+ export function getExternalControllerLaunchReservation(
51
+ options: LaunchReservationStoreOptions,
52
+ workId: string,
53
+ ): ExternalControllerLaunchReservation | undefined {
54
+ return active(record(options, workId)?.value);
55
+ }
56
+
57
+ export function reserveExternalControllerLaunch(
58
+ options: LaunchReservationStoreOptions,
59
+ input: {
60
+ workId: string;
61
+ controllerType: Exclude<ControllerType, 'human'>;
62
+ ttlMs?: number;
63
+ },
64
+ ): ExternalControllerLaunchReservation {
65
+ if (!input.workId.trim()) throw new Error('CONTROLLER_LAUNCH_WORK_REQUIRED');
66
+ return withControllerLock(
67
+ options.controllerHome,
68
+ { scope: 'task', repoId: options.repoId, taskId: `controller-launch-${input.workId}` },
69
+ `controller-launch-reserve:${input.workId}`,
70
+ () => {
71
+ const existing = record(options, input.workId);
72
+ const current = active(existing?.value);
73
+ if (current) {
74
+ throw new Error(`CONTROLLER_LAUNCH_ALREADY_RESERVED: ${input.workId}:${current.reservationId}`);
75
+ }
76
+ const createdAt = nowIso(options);
77
+ const ttlMs = Math.max(5_000, Math.min(input.ttlMs ?? DEFAULT_TTL_MS, MAX_TTL_MS));
78
+ const reservation: ExternalControllerLaunchReservation = {
79
+ schemaVersion: 1,
80
+ repoId: options.repoId,
81
+ workId: input.workId,
82
+ reservationId: randomUUID(),
83
+ controllerType: input.controllerType,
84
+ createdAt,
85
+ expiresAt: new Date(Date.parse(createdAt) + ttlMs).toISOString(),
86
+ };
87
+ writeControlPlaneRecord(options.controllerHome, {
88
+ namespace: NAMESPACE,
89
+ scope: options.repoId,
90
+ key: input.workId,
91
+ schemaVersion: 1,
92
+ value: reservation,
93
+ action: 'external_controller_launch_reserve',
94
+ expectedRevision: existing?.revision ?? null,
95
+ });
96
+ return reservation;
97
+ },
98
+ );
99
+ }
100
+
101
+ export function attachExternalControllerLaunchPid(
102
+ options: LaunchReservationStoreOptions,
103
+ workId: string,
104
+ reservationId: string,
105
+ pid: number | undefined,
106
+ ): ExternalControllerLaunchReservation {
107
+ return withControllerLock(
108
+ options.controllerHome,
109
+ { scope: 'task', repoId: options.repoId, taskId: `controller-launch-${workId}` },
110
+ `controller-launch-bind:${reservationId}`,
111
+ () => {
112
+ const existing = record(options, workId);
113
+ if (!existing || existing.value.reservationId !== reservationId) {
114
+ throw new Error(`CONTROLLER_LAUNCH_RESERVATION_STALE: ${workId}:${reservationId}`);
115
+ }
116
+ const next = { ...existing.value, ...(pid ? { pid } : {}) };
117
+ writeControlPlaneRecord(options.controllerHome, {
118
+ namespace: NAMESPACE,
119
+ scope: options.repoId,
120
+ key: workId,
121
+ schemaVersion: 1,
122
+ value: next,
123
+ action: 'external_controller_launch_bind_pid',
124
+ expectedRevision: existing.revision,
125
+ });
126
+ return next;
127
+ },
128
+ );
129
+ }
130
+
131
+ export function releaseExternalControllerLaunchReservation(
132
+ options: LaunchReservationStoreOptions,
133
+ workId: string,
134
+ reservationId: string,
135
+ reason: string,
136
+ ): void {
137
+ withControllerLock(
138
+ options.controllerHome,
139
+ { scope: 'task', repoId: options.repoId, taskId: `controller-launch-${workId}` },
140
+ `controller-launch-release:${reservationId}`,
141
+ () => {
142
+ const existing = record(options, workId);
143
+ if (!existing || existing.value.reservationId !== reservationId || existing.value.releasedAt) return;
144
+ const releasedAt = nowIso(options);
145
+ writeControlPlaneRecord(options.controllerHome, {
146
+ namespace: NAMESPACE,
147
+ scope: options.repoId,
148
+ key: workId,
149
+ schemaVersion: 1,
150
+ value: {
151
+ ...existing.value,
152
+ expiresAt: releasedAt,
153
+ releasedAt,
154
+ releaseReason: reason.slice(0, 240),
155
+ },
156
+ action: 'external_controller_launch_release',
157
+ expectedRevision: existing.revision,
158
+ });
159
+ },
160
+ );
161
+ }
@@ -0,0 +1,64 @@
1
+ import { readFileSync } from 'fs';
2
+ import { forgeRuntimeServicePaths, readForgeRuntimeServiceConfig } from '../../root/service';
3
+ import { observeRuntimeStatus, readRuntimeStatusSnapshot } from '../../root/status';
4
+
5
+ export const FORGE_RUNTIME_MCP_TOKEN_ENV = 'FORGE_RUNTIME_MCP_TOKEN';
6
+
7
+ export interface ProviderMcpBootstrap {
8
+ url: string;
9
+ bearerTokenEnvVar: typeof FORGE_RUNTIME_MCP_TOKEN_ENV;
10
+ principalId: string;
11
+ sessionId: string;
12
+ env: NodeJS.ProcessEnv;
13
+ }
14
+
15
+ function runtimeMcpUrl(controllerHome: string): string {
16
+ const observed = observeRuntimeStatus(controllerHome);
17
+ if (!observed.running || !observed.ready) {
18
+ throw new Error(`LAUNCHER_RUNTIME_MCP_NOT_READY: ${observed.reasonCodes.join(',') || 'runtime_not_ready'}`);
19
+ }
20
+ const snapshot = readRuntimeStatusSnapshot(controllerHome);
21
+ if (snapshot?.endpoint?.trim()) return snapshot.endpoint.trim();
22
+ const config = readForgeRuntimeServiceConfig(forgeRuntimeServicePaths(controllerHome).configPath);
23
+ const host = config.host === '0.0.0.0' || config.host === '::' ? '127.0.0.1' : config.host;
24
+ const renderedHost = host.includes(':') && !host.startsWith('[') ? `[${host}]` : host;
25
+ return `http://${renderedHost}:${config.port}/mcp`;
26
+ }
27
+
28
+ export function resolveProviderMcpBootstrap(
29
+ controllerHome: string,
30
+ provider: 'codex',
31
+ reservationId: string,
32
+ ): ProviderMcpBootstrap {
33
+ const config = readForgeRuntimeServiceConfig(forgeRuntimeServicePaths(controllerHome).configPath);
34
+ const token = readFileSync(config.authTokenFile, 'utf8').trim();
35
+ if (!token) throw new Error('LAUNCHER_RUNTIME_MCP_TOKEN_EMPTY');
36
+ const suffix = reservationId.trim();
37
+ if (!suffix) throw new Error('LAUNCHER_RESERVATION_ID_REQUIRED');
38
+ return {
39
+ url: runtimeMcpUrl(controllerHome),
40
+ bearerTokenEnvVar: FORGE_RUNTIME_MCP_TOKEN_ENV,
41
+ principalId: `external:${provider}:${suffix}`,
42
+ sessionId: `external-session:${provider}:${suffix}`,
43
+ env: {
44
+ ...process.env,
45
+ [FORGE_RUNTIME_MCP_TOKEN_ENV]: token,
46
+ },
47
+ };
48
+ }
49
+
50
+ export function codexMcpConfigArgs(bootstrap: ProviderMcpBootstrap): string[] {
51
+ const headers = {
52
+ 'X-Forge-Forwarded-Principal-Id': bootstrap.principalId,
53
+ 'X-Forge-Forwarded-Session-Id': bootstrap.sessionId,
54
+ 'X-Forge-Forwarded-Controller-Type': 'codex',
55
+ };
56
+ const headerTable = `{${Object.entries(headers)
57
+ .map(([key, value]) => `${JSON.stringify(key)}=${JSON.stringify(value)}`)
58
+ .join(',')}}`;
59
+ return [
60
+ '-c', `mcp_servers.forge.url=${JSON.stringify(bootstrap.url)}`,
61
+ '-c', `mcp_servers.forge.bearer_token_env_var=${JSON.stringify(bootstrap.bearerTokenEnvVar)}`,
62
+ '-c', `mcp_servers.forge.http_headers=${headerTable}`,
63
+ ];
64
+ }