@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,526 +0,0 @@
1
- import { createHash, randomUUID } from 'crypto';
2
- import { existsSync, readdirSync } from 'fs';
3
- import { join } from 'path';
4
- import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
5
- import { withControllerLock } from '../../../cli/repositories/locks';
6
- import { appendRuntimeEvent } from '../../evidence/event-ledger';
7
- import { markRepositoryProjectionDirty } from '../../projections/invalidation';
8
- import { touchSchedulerWakeSignal } from '../../control-plane/global-scheduler/wake-signal';
9
- import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
10
- import type {
11
- Campaign,
12
- CampaignBudget,
13
- CampaignStatus,
14
- CampaignSupervisorConfig,
15
- CampaignTask,
16
- CreateCampaignInput,
17
- CreateCampaignTaskInput,
18
- } from './types';
19
- import {
20
- assertCampaignOperationSupported,
21
- normalizeCampaignDependencyReferences,
22
- normalizeCampaignOperationName,
23
- normalizeCampaignTaskReferences,
24
- } from './normalize';
25
-
26
- interface CampaignIndexEntry {
27
- campaignId: string;
28
- title: string;
29
- status: CampaignStatus;
30
- updatedAt: string;
31
- }
32
-
33
- interface CampaignIndex {
34
- schemaVersion: 1;
35
- updatedAt: string;
36
- active: CampaignIndexEntry[];
37
- recent: CampaignIndexEntry[];
38
- }
39
-
40
- interface CampaignRequestRecord {
41
- schemaVersion: 1;
42
- requestId: string;
43
- semanticKey: string;
44
- campaignId: string;
45
- repoId: string;
46
- createdAt: string;
47
- }
48
-
49
- interface CampaignMutationRecord {
50
- schemaVersion: 1;
51
- requestId: string;
52
- campaignId: string;
53
- repoId: string;
54
- fingerprint: string;
55
- revision: number;
56
- createdAt: string;
57
- }
58
-
59
- const ACTIVE_CAMPAIGN_STATUSES = new Set<CampaignStatus>(['active', 'waiting_for_supervisor']);
60
-
61
- const DEFAULT_BUDGET: CampaignBudget = {
62
- maxParallelTasks: 2,
63
- maxExecutionJobs: 100,
64
- maxSupervisorReviews: 50,
65
- defaultTaskMaxAttempts: 3,
66
- taskTimeoutMs: 60 * 60_000,
67
- retryBaseDelayMs: 5_000,
68
- retryMaxDelayMs: 5 * 60_000,
69
- reviewPacketMaxBytes: 256 * 1024,
70
- };
71
-
72
- const DEFAULT_SUPERVISOR: CampaignSupervisorConfig = {
73
- mode: 'pull',
74
- triggerCooldownMs: 60_000,
75
- maxTriggerAttempts: 3,
76
- decisionTimeoutMs: 5 * 60_000,
77
- };
78
-
79
- function now(): string { return new Date().toISOString(); }
80
-
81
- function root(controllerHome: string, repoId: string): string {
82
- return join(repositoryControllerRoot(controllerHome, repoId), 'campaigns');
83
- }
84
-
85
- function recordPath(controllerHome: string, repoId: string, campaignId: string): string {
86
- return join(root(controllerHome, repoId), 'records', `${sanitizeFileComponent(campaignId)}.json`);
87
- }
88
-
89
- function indexPath(controllerHome: string, repoId: string): string {
90
- return join(root(controllerHome, repoId), 'indexes', 'campaigns.json');
91
- }
92
-
93
- function requestPath(controllerHome: string, repoId: string, requestId: string): string {
94
- const hash = createHash('sha256').update(requestId).digest('hex');
95
- return join(root(controllerHome, repoId), 'indexes', 'requests', `${hash}.json`);
96
- }
97
-
98
- function mutationPath(controllerHome: string, repoId: string, requestId: string): string {
99
- const hash = createHash('sha256').update(requestId).digest('hex');
100
- return join(root(controllerHome, repoId), 'indexes', 'mutations', `${hash}.json`);
101
- }
102
-
103
- export function computeCampaignGoalHash(statement: string, acceptanceCriteria: string[], nonGoals: string[]): string {
104
- return createHash('sha256').update(JSON.stringify({ statement, acceptanceCriteria, nonGoals })).digest('hex');
105
- }
106
-
107
- function emptyIndex(): CampaignIndex {
108
- return { schemaVersion: 1, updatedAt: now(), active: [], recent: [] };
109
- }
110
-
111
- function readIndex(controllerHome: string, repoId: string): CampaignIndex {
112
- return readJsonFile<CampaignIndex>(indexPath(controllerHome, repoId), emptyIndex());
113
- }
114
-
115
- function upsertIndexUnlocked(controllerHome: string, campaign: Campaign): void {
116
- const index = readIndex(controllerHome, campaign.repoId);
117
- index.active = index.active.filter((entry) => entry.campaignId !== campaign.campaignId);
118
- index.recent = index.recent.filter((entry) => entry.campaignId !== campaign.campaignId);
119
- const entry: CampaignIndexEntry = {
120
- campaignId: campaign.campaignId,
121
- title: campaign.title,
122
- status: campaign.status,
123
- updatedAt: campaign.updatedAt,
124
- };
125
- if (ACTIVE_CAMPAIGN_STATUSES.has(campaign.status)) index.active.push(entry);
126
- index.recent.push(entry);
127
- index.active.sort((left, right) => left.updatedAt.localeCompare(right.updatedAt));
128
- index.recent.sort((left, right) => right.updatedAt.localeCompare(left.updatedAt));
129
- index.active = index.active.slice(-5_000);
130
- index.recent = index.recent.slice(0, 5_000);
131
- index.updatedAt = now();
132
- writeJsonAtomic(indexPath(controllerHome, campaign.repoId), index);
133
- }
134
-
135
- function boundedInteger(value: number | undefined, fallback: number, minimum: number, maximum: number): number {
136
- if (!Number.isFinite(value)) return fallback;
137
- return Math.max(minimum, Math.min(maximum, Math.trunc(value!)));
138
- }
139
-
140
- export function normalizeCampaignBudget(input: Partial<CampaignBudget> = {}): CampaignBudget {
141
- return {
142
- maxParallelTasks: boundedInteger(input.maxParallelTasks, DEFAULT_BUDGET.maxParallelTasks, 1, 16),
143
- maxExecutionJobs: boundedInteger(input.maxExecutionJobs, DEFAULT_BUDGET.maxExecutionJobs, 1, 10_000),
144
- maxSupervisorReviews: boundedInteger(input.maxSupervisorReviews, DEFAULT_BUDGET.maxSupervisorReviews, 1, 1_000),
145
- defaultTaskMaxAttempts: boundedInteger(input.defaultTaskMaxAttempts, DEFAULT_BUDGET.defaultTaskMaxAttempts, 1, 10),
146
- taskTimeoutMs: boundedInteger(input.taskTimeoutMs, DEFAULT_BUDGET.taskTimeoutMs, 1_000, 24 * 60 * 60_000),
147
- retryBaseDelayMs: boundedInteger(input.retryBaseDelayMs, DEFAULT_BUDGET.retryBaseDelayMs, 250, 60 * 60_000),
148
- retryMaxDelayMs: boundedInteger(input.retryMaxDelayMs, DEFAULT_BUDGET.retryMaxDelayMs, 250, 24 * 60 * 60_000),
149
- reviewPacketMaxBytes: boundedInteger(input.reviewPacketMaxBytes, DEFAULT_BUDGET.reviewPacketMaxBytes, 16 * 1024, 2 * 1024 * 1024),
150
- };
151
- }
152
-
153
- export function normalizeSupervisorConfig(input: Partial<CampaignSupervisorConfig> = {}): CampaignSupervisorConfig {
154
- const mode = input.mode === 'operation'
155
- ? 'operation'
156
- : input.mode === 'workspace_agent' ? 'workspace_agent' : 'pull';
157
- if (mode === 'operation' && !input.operation?.trim()) throw new Error('CAMPAIGN_SUPERVISOR_OPERATION_REQUIRED');
158
- const workspaceAgentId = input.workspaceAgentId?.trim();
159
- if (mode === 'workspace_agent' && !workspaceAgentId) throw new Error('CAMPAIGN_WORKSPACE_AGENT_ID_REQUIRED');
160
- if (workspaceAgentId && !/^agtch_[A-Za-z0-9_-]+$/.test(workspaceAgentId)) {
161
- throw new Error('CAMPAIGN_WORKSPACE_AGENT_ID_INVALID');
162
- }
163
- const conversationKey = input.conversationKey?.trim();
164
- const operation = mode === 'operation' ? assertCampaignOperationSupported(input.operation!.trim()) : undefined;
165
- return {
166
- mode,
167
- operation,
168
- workspaceAgentId: mode === 'workspace_agent' ? workspaceAgentId : undefined,
169
- conversationKey: mode === 'workspace_agent' && conversationKey ? conversationKey : undefined,
170
- arguments: mode === 'operation' && input.arguments && typeof input.arguments === 'object' ? input.arguments : undefined,
171
- priority: input.priority ?? 'P1',
172
- resourceClaims: mode === 'operation' ? input.resourceClaims ?? [] : [],
173
- triggerCooldownMs: boundedInteger(input.triggerCooldownMs, DEFAULT_SUPERVISOR.triggerCooldownMs, 1_000, 24 * 60 * 60_000),
174
- maxTriggerAttempts: boundedInteger(input.maxTriggerAttempts, DEFAULT_SUPERVISOR.maxTriggerAttempts, 1, 10),
175
- decisionTimeoutMs: boundedInteger(input.decisionTimeoutMs, DEFAULT_SUPERVISOR.decisionTimeoutMs, 1_000, 24 * 60 * 60_000),
176
- };
177
- }
178
-
179
- function normalizeTask(input: CreateCampaignTaskInput, budget: CampaignBudget): CampaignTask {
180
- const taskId = input.taskId.trim();
181
- const title = input.title.trim();
182
- const operation = assertCampaignOperationSupported(input.operation.trim());
183
- if (!taskId) throw new Error('CAMPAIGN_TASK_ID_REQUIRED');
184
- if (!title) throw new Error(`CAMPAIGN_TASK_TITLE_REQUIRED: ${taskId}`);
185
- if (!operation) throw new Error(`CAMPAIGN_TASK_OPERATION_REQUIRED: ${taskId}`);
186
- return {
187
- taskId,
188
- title,
189
- objective: input.objective?.trim() || title,
190
- operation,
191
- arguments: input.arguments ? structuredClone(input.arguments) : undefined,
192
- dependsOn: normalizeCampaignDependencyReferences(input.dependsOn ?? []),
193
- priority: input.priority ?? 'P1',
194
- resourceClaims: structuredClone(input.resourceClaims ?? []),
195
- reviewRequired: input.reviewRequired ?? true,
196
- requiresChanges: input.requiresChanges ?? false,
197
- maxAttempts: boundedInteger(input.maxAttempts, budget.defaultTaskMaxAttempts, 1, 10),
198
- status: 'pending',
199
- attempt: 0,
200
- evidenceIds: [],
201
- executor: input.executor ? structuredClone(input.executor) : undefined,
202
- };
203
- }
204
-
205
- export function validateCampaignTasks(tasks: CampaignTask[]): void {
206
- if (tasks.length === 0) throw new Error('CAMPAIGN_TASKS_REQUIRED');
207
- const byId = new Map<string, CampaignTask>();
208
- for (const task of tasks) {
209
- if (byId.has(task.taskId)) throw new Error(`CAMPAIGN_TASK_DUPLICATE: ${task.taskId}`);
210
- byId.set(task.taskId, task);
211
- }
212
- for (const task of tasks) {
213
- for (const dependency of task.dependsOn) {
214
- if (dependency === task.taskId) throw new Error(`CAMPAIGN_TASK_SELF_DEPENDENCY: ${task.taskId}`);
215
- if (!byId.has(dependency)) throw new Error(`CAMPAIGN_TASK_DEPENDENCY_MISSING: ${task.taskId} -> ${dependency}`);
216
- }
217
- }
218
- const visiting = new Set<string>();
219
- const visited = new Set<string>();
220
- const visit = (taskId: string): void => {
221
- if (visited.has(taskId)) return;
222
- if (visiting.has(taskId)) throw new Error(`CAMPAIGN_TASK_CYCLE: ${taskId}`);
223
- visiting.add(taskId);
224
- for (const dependency of byId.get(taskId)!.dependsOn) visit(dependency);
225
- visiting.delete(taskId);
226
- visited.add(taskId);
227
- };
228
- for (const taskId of byId.keys()) visit(taskId);
229
- }
230
-
231
- export function validateCreateCampaignTasks(
232
- inputs: CreateCampaignTaskInput[],
233
- budgetInput: Partial<CampaignBudget> = {},
234
- ): void {
235
- const budget = normalizeCampaignBudget(budgetInput);
236
- validateCampaignTasks(normalizeCampaignTaskReferences(inputs.map((input) => normalizeTask(input, budget))));
237
- }
238
-
239
- export function getCampaign(controllerHome: string, repoId: string, campaignId: string): Campaign {
240
- const campaign = readJsonFile<Campaign>(recordPath(controllerHome, repoId, campaignId));
241
- if (campaign.repoId !== repoId || campaign.campaignId !== campaignId) throw new Error('CAMPAIGN_IDENTITY_MISMATCH');
242
- campaign.tasks = normalizeCampaignTaskReferences(campaign.tasks.map((task) => ({
243
- ...task,
244
- operation: normalizeCampaignOperationName(task.operation),
245
- dependsOn: normalizeCampaignDependencyReferences(task.dependsOn),
246
- })));
247
- if (campaign.supervisor.mode === 'operation' && campaign.supervisor.operation) {
248
- campaign.supervisor = {
249
- ...campaign.supervisor,
250
- operation: normalizeCampaignOperationName(campaign.supervisor.operation),
251
- };
252
- }
253
- if (!campaign.workspace) {
254
- campaign.workspace = {
255
- mode: 'current',
256
- checkoutId: campaign.checkoutId,
257
- managed: false,
258
- };
259
- }
260
- return campaign;
261
- }
262
-
263
- function findCampaignByRequestId(controllerHome: string, repoId: string, requestId: string): Campaign | undefined {
264
- try {
265
- for (const name of readdirSync(join(root(controllerHome, repoId), 'records'))) {
266
- if (!name.endsWith('.json')) continue;
267
- const campaign = readJsonFile<Campaign>(join(root(controllerHome, repoId), 'records', name));
268
- if (campaign.requestId === requestId) return campaign;
269
- }
270
- } catch { /* no campaign directory yet */ }
271
- return undefined;
272
- }
273
-
274
- export function createCampaign(controllerHome: string, input: CreateCampaignInput): { campaign: Campaign; deduplicated: boolean } {
275
- const repoId = input.repoId.trim();
276
- const requestId = input.requestId.trim();
277
- const semanticKey = input.semanticKey.trim();
278
- const title = input.title.trim();
279
- const goal = input.goal.trim();
280
- if (!repoId) throw new Error('CAMPAIGN_REPO_ID_REQUIRED');
281
- if (!requestId) throw new Error('CAMPAIGN_REQUEST_ID_REQUIRED');
282
- if (!semanticKey) throw new Error('CAMPAIGN_SEMANTIC_KEY_REQUIRED');
283
- if (!title) throw new Error('CAMPAIGN_TITLE_REQUIRED');
284
- if (!goal) throw new Error('CAMPAIGN_GOAL_REQUIRED');
285
- const budget = normalizeCampaignBudget(input.budget);
286
- const tasks = normalizeCampaignTaskReferences(input.tasks.map((task) => normalizeTask(task, budget)));
287
- validateCampaignTasks(tasks);
288
- const requestLock = createHash('sha256').update(`${repoId}:${requestId}`).digest('hex').slice(0, 24);
289
- return withControllerLock(controllerHome, { scope: 'global', resource: `campaign-request-${requestLock}` }, `create-campaign:${requestId}`, () => {
290
- const existingPath = requestPath(controllerHome, repoId, requestId);
291
- if (existsSync(existingPath)) {
292
- const record = readJsonFile<CampaignRequestRecord>(existingPath);
293
- if (record.semanticKey !== semanticKey) throw new Error(`CAMPAIGN_REQUEST_ID_CONFLICT: ${requestId}`);
294
- return { campaign: getCampaign(controllerHome, repoId, record.campaignId), deduplicated: true };
295
- }
296
- const recovered = findCampaignByRequestId(controllerHome, repoId, requestId);
297
- if (recovered) {
298
- if (recovered.semanticKey !== semanticKey) throw new Error(`CAMPAIGN_REQUEST_ID_CONFLICT: ${requestId}`);
299
- writeJsonAtomic(existingPath, {
300
- schemaVersion: 1, requestId, semanticKey, campaignId: recovered.campaignId, repoId, createdAt: recovered.createdAt,
301
- } satisfies CampaignRequestRecord);
302
- return { campaign: recovered, deduplicated: true };
303
- }
304
- const timestamp = now();
305
- const workspace = input.workspace ? structuredClone(input.workspace) : {
306
- mode: 'current' as const,
307
- checkoutId: input.checkoutId,
308
- managed: false,
309
- };
310
- const campaign: Campaign = {
311
- schemaVersion: 1,
312
- revision: 1,
313
- campaignId: `CMP-${Date.now()}-${randomUUID().slice(0, 8)}`,
314
- repoId,
315
- checkoutId: workspace.checkoutId ?? input.checkoutId,
316
- workspace,
317
- requestId,
318
- semanticKey,
319
- title,
320
- status: 'active',
321
- goals: (() => {
322
- const acceptanceCriteria = [...new Set((input.acceptanceCriteria ?? []).map((value) => value.trim()).filter(Boolean))];
323
- const nonGoals = [...new Set((input.nonGoals ?? []).map((value) => value.trim()).filter(Boolean))];
324
- return [{
325
- revision: 1,
326
- goalHash: computeCampaignGoalHash(goal, acceptanceCriteria, nonGoals),
327
- statement: goal,
328
- acceptanceCriteria,
329
- nonGoals,
330
- changedBy: input.createdBy?.trim() || 'user',
331
- changedAt: timestamp,
332
- }];
333
- })(),
334
- tasks,
335
- checkpoints: [],
336
- budget,
337
- supervisor: normalizeSupervisorConfig(input.supervisor),
338
- reviewPolicy: input.reviewPolicy ?? 'every_task',
339
- counters: { executionJobsCreated: 0, supervisorReviewsOpened: 0, supervisorDecisionsAccepted: 0 },
340
- createdAt: timestamp,
341
- updatedAt: timestamp,
342
- };
343
- writeJsonAtomic(recordPath(controllerHome, repoId, campaign.campaignId), campaign);
344
- writeJsonAtomic(existingPath, {
345
- schemaVersion: 1,
346
- requestId,
347
- semanticKey,
348
- campaignId: campaign.campaignId,
349
- repoId,
350
- createdAt: timestamp,
351
- } satisfies CampaignRequestRecord);
352
- upsertIndexUnlocked(controllerHome, campaign);
353
- appendRuntimeEvent(controllerHome, {
354
- repoId,
355
- entityType: 'campaign',
356
- entityId: campaign.campaignId,
357
- eventType: 'campaign_created',
358
- requestId,
359
- revision: campaign.revision,
360
- data: { title, taskCount: tasks.length, reviewPolicy: campaign.reviewPolicy, workspace: campaign.workspace },
361
- });
362
- markRepositoryProjectionDirty(controllerHome, repoId, `campaign:${campaign.campaignId}:created`);
363
- touchSchedulerWakeSignal(controllerHome, `campaign-created:${campaign.campaignId}`);
364
- return { campaign, deduplicated: false };
365
- }, 10_000);
366
- }
367
-
368
- export interface UpdateCampaignOptions {
369
- expectedRevision?: number;
370
- eventType?: string;
371
- eventData?: Record<string, unknown>;
372
- wakeScheduler?: boolean;
373
- requestFingerprint?: string;
374
- }
375
-
376
- export function updateCampaign(
377
- controllerHome: string,
378
- repoId: string,
379
- campaignId: string,
380
- requestId: string,
381
- updater: (current: Campaign) => Campaign,
382
- options: UpdateCampaignOptions = {},
383
- ): Campaign {
384
- return withControllerLock(controllerHome, { scope: 'task', repoId, taskId: `campaign-${campaignId}` }, `update-campaign:${campaignId}`, () => {
385
- const mutationFingerprint = createHash('sha256').update(options.requestFingerprint ?? JSON.stringify({
386
- campaignId,
387
- eventType: options.eventType ?? 'campaign_updated',
388
- eventData: options.eventData ?? null,
389
- })).digest('hex');
390
- const mutationRecordPath = mutationPath(controllerHome, repoId, requestId);
391
- const current = getCampaign(controllerHome, repoId, campaignId);
392
- const embeddedReceipt = current.mutationReceipts?.find((receipt) => receipt.requestId === requestId);
393
- if (embeddedReceipt) {
394
- if (embeddedReceipt.fingerprint !== mutationFingerprint) throw new Error(`CAMPAIGN_REQUEST_ID_CONFLICT: ${requestId}`);
395
- return current;
396
- }
397
- if (existsSync(mutationRecordPath)) {
398
- const record = readJsonFile<CampaignMutationRecord>(mutationRecordPath);
399
- if (record.repoId !== repoId || record.campaignId !== campaignId || record.fingerprint !== mutationFingerprint) {
400
- throw new Error(`CAMPAIGN_REQUEST_ID_CONFLICT: ${requestId}`);
401
- }
402
- return current;
403
- }
404
- if (options.expectedRevision !== undefined && current.revision !== options.expectedRevision) {
405
- throw new Error(`CAMPAIGN_REVISION_CONFLICT: expected ${options.expectedRevision}, current ${current.revision}`);
406
- }
407
- const next = updater(structuredClone(current));
408
- if (
409
- next.campaignId !== current.campaignId
410
- || next.repoId !== current.repoId
411
- || next.requestId !== current.requestId
412
- || next.semanticKey !== current.semanticKey
413
- || next.checkoutId !== current.checkoutId
414
- || JSON.stringify(next.workspace) !== JSON.stringify(current.workspace)
415
- ) {
416
- throw new Error('CAMPAIGN_IDENTITY_IMMUTABLE');
417
- }
418
- normalizeCampaignTaskReferences(next.tasks);
419
- validateCampaignTasks(next.tasks);
420
- next.revision = current.revision + 1;
421
- next.updatedAt = now();
422
- next.mutationReceipts = [
423
- ...(current.mutationReceipts ?? []).filter((receipt) => receipt.requestId !== requestId),
424
- { requestId, fingerprint: mutationFingerprint, revision: next.revision, appliedAt: next.updatedAt },
425
- ].slice(-1_000);
426
- writeJsonAtomic(recordPath(controllerHome, repoId, campaignId), next);
427
- writeJsonAtomic(mutationRecordPath, {
428
- schemaVersion: 1,
429
- requestId,
430
- campaignId,
431
- repoId,
432
- fingerprint: mutationFingerprint,
433
- revision: next.revision,
434
- createdAt: next.updatedAt,
435
- } satisfies CampaignMutationRecord);
436
- upsertIndexUnlocked(controllerHome, next);
437
- appendRuntimeEvent(controllerHome, {
438
- repoId,
439
- entityType: 'campaign',
440
- entityId: campaignId,
441
- eventType: options.eventType ?? 'campaign_updated',
442
- requestId,
443
- revision: next.revision,
444
- data: options.eventData ?? { status: next.status },
445
- });
446
- markRepositoryProjectionDirty(controllerHome, repoId, `campaign:${campaignId}:${next.status}`);
447
- if (options.wakeScheduler !== false) touchSchedulerWakeSignal(controllerHome, `campaign-updated:${campaignId}`);
448
- return next;
449
- }, 10_000);
450
- }
451
-
452
- function readIndexedCampaigns(controllerHome: string, repoId: string, entries: CampaignIndexEntry[], limit: number): Campaign[] {
453
- return entries.slice(0, limit).flatMap((entry) => {
454
- try { return [getCampaign(controllerHome, repoId, entry.campaignId)]; } catch { return []; }
455
- });
456
- }
457
-
458
- export function listActiveCampaigns(controllerHome: string, repoId: string, limit = 500): Campaign[] {
459
- const bounded = Math.max(1, Math.min(limit, 5_000));
460
- const indexed = readIndexedCampaigns(controllerHome, repoId, readIndex(controllerHome, repoId).active, bounded)
461
- .filter((campaign) => ACTIVE_CAMPAIGN_STATUSES.has(campaign.status));
462
- if (indexed.length > 0) return indexed;
463
- try {
464
- return readdirSync(join(root(controllerHome, repoId), 'records'))
465
- .filter((name) => name.endsWith('.json'))
466
- .map((name) => name.replace(/\.json$/, ''))
467
- .map((campaignId) => getCampaign(controllerHome, repoId, campaignId))
468
- .filter((campaign) => ACTIVE_CAMPAIGN_STATUSES.has(campaign.status))
469
- .sort((left, right) => left.updatedAt.localeCompare(right.updatedAt))
470
- .slice(0, bounded);
471
- } catch { return []; }
472
- }
473
-
474
- export function listCampaigns(controllerHome: string, repoId: string, limit = 100): Campaign[] {
475
- const bounded = Math.max(1, Math.min(limit, 1_000));
476
- const indexed = readIndexedCampaigns(controllerHome, repoId, readIndex(controllerHome, repoId).recent, bounded);
477
- if (indexed.length > 0) return indexed;
478
- try {
479
- return readdirSync(join(root(controllerHome, repoId), 'records'))
480
- .filter((name) => name.endsWith('.json'))
481
- .map((name) => name.replace(/\.json$/, ''))
482
- .map((campaignId) => getCampaign(controllerHome, repoId, campaignId))
483
- .sort((left, right) => right.updatedAt.localeCompare(left.updatedAt))
484
- .slice(0, bounded);
485
- } catch { return []; }
486
- }
487
-
488
- export function addCampaignTask(
489
- controllerHome: string,
490
- repoId: string,
491
- campaignId: string,
492
- requestId: string,
493
- input: CreateCampaignTaskInput,
494
- expectedRevision?: number,
495
- ): Campaign {
496
- return updateCampaign(controllerHome, repoId, campaignId, requestId, (campaign) => {
497
- if (['completed', 'cancelled', 'cancelled_with_leaks'].includes(campaign.status)) throw new Error(`CAMPAIGN_TERMINAL: ${campaign.status}`);
498
- const task = normalizeTask(input, campaign.budget);
499
- if (campaign.tasks.some((entry) => entry.taskId === task.taskId)) throw new Error(`CAMPAIGN_TASK_DUPLICATE: ${task.taskId}`);
500
- campaign.tasks.push(task);
501
- normalizeCampaignTaskReferences(campaign.tasks);
502
- if (campaign.status === 'ready_for_human_acceptance' || campaign.status === 'failed') campaign.status = 'active';
503
- campaign.nextReconcileAt = undefined;
504
- return campaign;
505
- }, { expectedRevision, eventType: 'campaign_task_added', eventData: { taskId: input.taskId }, requestFingerprint: JSON.stringify(input) });
506
- }
507
-
508
- export function setCampaignStatus(
509
- controllerHome: string,
510
- repoId: string,
511
- campaignId: string,
512
- requestId: string,
513
- status: CampaignStatus,
514
- reason?: string,
515
- expectedRevision?: number,
516
- ): Campaign {
517
- return updateCampaign(controllerHome, repoId, campaignId, requestId, (campaign) => {
518
- if (campaign.status === 'completed' && status !== 'completed') throw new Error('CAMPAIGN_ALREADY_COMPLETED');
519
- campaign.status = status;
520
- campaign.pauseReason = status === 'paused' ? reason || 'Paused by supervisor.' : undefined;
521
- campaign.failureReason = status === 'failed' ? reason || 'Campaign failed.' : campaign.failureReason;
522
- if (['completed', 'cancelled', 'cancelled_with_leaks'].includes(status)) campaign.completedAt = now();
523
- if (status === 'active') campaign.nextReconcileAt = undefined;
524
- return campaign;
525
- }, { expectedRevision, eventType: `campaign_${status}`, eventData: { reason } });
526
- }
@@ -1,112 +0,0 @@
1
- import type { ExecutionJobPayload, ExecutionJobPriority, ResourceClaimSpec } from '../../execution/jobs/types';
2
- import type { Campaign, CampaignCheckpoint, CampaignReviewPacket, CampaignSupervisorDecision } from './types';
3
- import { assertCampaignOperationSupported } from './normalize';
4
-
5
- export interface CampaignSupervisorTriggerSpec {
6
- operation: string;
7
- arguments: Record<string, unknown>;
8
- target?: ExecutionJobPayload['target'];
9
- priority: ExecutionJobPriority;
10
- resourceClaims: ResourceClaimSpec[];
11
- timeoutMs?: number;
12
- }
13
-
14
- export interface CampaignSupervisorAdapter {
15
- readonly mode: Campaign['supervisor']['mode'];
16
- reviewPacket(campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignReviewPacket;
17
- triggerSpec(campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignSupervisorTriggerSpec | undefined;
18
- validateDecision(campaign: Campaign, checkpoint: CampaignCheckpoint, decision: CampaignSupervisorDecision): void;
19
- }
20
-
21
- function validateOpenDecision(checkpoint: CampaignCheckpoint, decision: CampaignSupervisorDecision): void {
22
- if (checkpoint.status !== 'open') throw new Error(`CAMPAIGN_CHECKPOINT_ALREADY_SUBMITTED: ${checkpoint.checkpointId}`);
23
- if (!decision.summary.trim()) throw new Error('CAMPAIGN_REVIEW_SUMMARY_REQUIRED');
24
- }
25
-
26
- export class PullCampaignSupervisorAdapter implements CampaignSupervisorAdapter {
27
- readonly mode = 'pull' as const;
28
- reviewPacket(_campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignReviewPacket { return checkpoint.packet; }
29
- triggerSpec(): undefined { return undefined; }
30
- validateDecision(_campaign: Campaign, checkpoint: CampaignCheckpoint, decision: CampaignSupervisorDecision): void {
31
- validateOpenDecision(checkpoint, decision);
32
- }
33
- }
34
-
35
- export class OperationCampaignSupervisorAdapter implements CampaignSupervisorAdapter {
36
- readonly mode = 'operation' as const;
37
- reviewPacket(_campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignReviewPacket { return checkpoint.packet; }
38
- triggerSpec(campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignSupervisorTriggerSpec | undefined {
39
- const operation = campaign.supervisor.operation?.trim()
40
- ? assertCampaignOperationSupported(campaign.supervisor.operation)
41
- : undefined;
42
- if (!operation) return undefined;
43
- return {
44
- operation,
45
- arguments: {
46
- ...(campaign.supervisor.arguments ?? {}),
47
- repo_id: campaign.repoId,
48
- campaign_id: campaign.campaignId,
49
- checkpoint_id: checkpoint.checkpointId,
50
- checkpoint_nonce: checkpoint.nonce,
51
- goal_revision: checkpoint.goalRevision,
52
- },
53
- priority: campaign.supervisor.priority ?? 'P1',
54
- resourceClaims: campaign.supervisor.resourceClaims ?? [],
55
- };
56
- }
57
- validateDecision(_campaign: Campaign, checkpoint: CampaignCheckpoint, decision: CampaignSupervisorDecision): void {
58
- validateOpenDecision(checkpoint, decision);
59
- }
60
- }
61
-
62
- function workspaceAgentPrompt(campaign: Campaign, checkpoint: CampaignCheckpoint): string {
63
- return [
64
- 'Review one forge Campaign checkpoint and write the decision back through the connected forge MCP tools.',
65
- '',
66
- `Repository id: ${campaign.repoId}`,
67
- `Campaign id: ${campaign.campaignId}`,
68
- `Checkpoint id: ${checkpoint.checkpointId}`,
69
- `Checkpoint nonce: ${checkpoint.nonce}`,
70
- `Goal revision: ${checkpoint.goalRevision}`,
71
- `Campaign revision at trigger: ${campaign.revision}`,
72
- '',
73
- 'Required workflow:',
74
- '1. Call get_campaign_review_packet with repo_id, campaign_id, and checkpoint_id. Read any referenced Evidence Plane artifacts needed for a grounded review.',
75
- '2. Review the goal, acceptance criteria, task result, incremental evidence, risks, and any failed checks.',
76
- `3. Call submit_campaign_review exactly once with request_id workspace-agent-review:${checkpoint.checkpointId}:${checkpoint.nonce}, the same checkpoint nonce, and the same goal revision.`,
77
- '4. Choose the most appropriate action. Use request_changes or retry only with concrete instructions; use pause or escalate when human input is required; use approve_final only when the final packet is ready for human acceptance.',
78
- '5. Do not merely answer in chat. The MCP write-back is the durable result of this review.',
79
- ].join('\n');
80
- }
81
-
82
- export class WorkspaceAgentCampaignSupervisorAdapter implements CampaignSupervisorAdapter {
83
- readonly mode = 'workspace_agent' as const;
84
- reviewPacket(_campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignReviewPacket { return checkpoint.packet; }
85
- triggerSpec(campaign: Campaign, checkpoint: CampaignCheckpoint): CampaignSupervisorTriggerSpec | undefined {
86
- const agentId = campaign.supervisor.workspaceAgentId?.trim();
87
- if (!agentId) return undefined;
88
- return {
89
- operation: 'trigger-workspace-agent',
90
- target: 'workspace-agent',
91
- arguments: {
92
- agent_id: agentId,
93
- input: workspaceAgentPrompt(campaign, checkpoint),
94
- conversation_key: campaign.supervisor.conversationKey?.trim()
95
- || `forge:${campaign.repoId}:${campaign.campaignId}`,
96
- idempotency_key: `${checkpoint.checkpointId}:${checkpoint.triggerAttempts + 1}`,
97
- },
98
- priority: campaign.supervisor.priority ?? 'P1',
99
- resourceClaims: [],
100
- timeoutMs: 30_000,
101
- };
102
- }
103
- validateDecision(_campaign: Campaign, checkpoint: CampaignCheckpoint, decision: CampaignSupervisorDecision): void {
104
- validateOpenDecision(checkpoint, decision);
105
- }
106
- }
107
-
108
- export function campaignSupervisorAdapter(campaign: Campaign): CampaignSupervisorAdapter {
109
- if (campaign.supervisor.mode === 'operation') return new OperationCampaignSupervisorAdapter();
110
- if (campaign.supervisor.mode === 'workspace_agent') return new WorkspaceAgentCampaignSupervisorAdapter();
111
- return new PullCampaignSupervisorAdapter();
112
- }