@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
@@ -0,0 +1,277 @@
1
+ import {
2
+ getWorkContract,
3
+ transitionWorkContractPhase,
4
+ updateWorkContract,
5
+ } from '../facade/work-contract-store';
6
+ import {
7
+ listValidatingWorkHandles,
8
+ transitionWorkHandle,
9
+ type WorkHandleState,
10
+ } from './work-handle-store';
11
+ import { processCheckCompletionReceipt } from '../../execution/process-runtime/check-receipt';
12
+ import { processCheckSemanticScopeKey } from '../../execution/process-runtime/check-facade';
13
+ import { getProcessRecord } from '../../execution/process-runtime/store';
14
+ import { controllerCheckExecutionIdentity } from '../../../cli/controller/check-runner';
15
+
16
+ export type WorkValidationOutcome =
17
+ | 'not_validating'
18
+ | 'running'
19
+ | 'passed'
20
+ | 'failed'
21
+ | 'infrastructure_failure';
22
+
23
+ export interface WorkValidationReconciliation {
24
+ handle: WorkHandleState;
25
+ changed: boolean;
26
+ outcome: WorkValidationOutcome;
27
+ summary?: string;
28
+ }
29
+
30
+ export function hasCurrentWorkValidationAuthority(input: {
31
+ finalizationValidation: WorkHandleState['finalization']['validation'];
32
+ validatedInputFingerprint?: string;
33
+ evidenceState: string;
34
+ expectedFingerprint: string;
35
+ }): boolean {
36
+ return input.finalizationValidation === 'done'
37
+ && input.evidenceState === 'valid'
38
+ && input.validatedInputFingerprint === input.expectedFingerprint;
39
+ }
40
+
41
+ /**
42
+ * A new validation run invalidates delivery authority without deleting or
43
+ * rewriting the prior receipts. `stale` is the only valid transition from an
44
+ * already-valid evidence set; all other retryable states converge through
45
+ * `partial`.
46
+ */
47
+ export function markWorkValidationPending(controllerHome: string, handle: WorkHandleState): void {
48
+ const contractId = handle.workContractId;
49
+ if (!contractId) return;
50
+ const options = { controllerHome, repoId: handle.repositoryId };
51
+ const contract = getWorkContract(options, contractId);
52
+ if (!contract || contract.completionReceipt) return;
53
+ const evidenceState = contract.evidenceState === 'valid' || contract.evidenceState === 'stale'
54
+ ? 'stale'
55
+ : 'partial';
56
+ if (contract.evidenceState !== evidenceState) updateWorkContract(options, contractId, { evidenceState });
57
+ }
58
+
59
+ /**
60
+ * Project durable validation evidence into the Work-owned contract lifecycle.
61
+ * Process and Check records contribute evidence only; they never become a
62
+ * second completion or lifecycle authority.
63
+ */
64
+ export function projectWorkValidationOutcome(
65
+ controllerHome: string,
66
+ handle: WorkHandleState,
67
+ outcome: Exclude<WorkValidationOutcome, 'not_validating' | 'running'>,
68
+ summary?: string,
69
+ ): void {
70
+ const contractId = handle.workContractId;
71
+ if (!contractId) return;
72
+ const options = { controllerHome, repoId: handle.repositoryId };
73
+ const contract = getWorkContract(options, contractId);
74
+ if (!contract || contract.completionReceipt) return;
75
+
76
+ if (outcome === 'passed') {
77
+ transitionWorkContractPhase(options, contractId, {
78
+ phase: 'delivery',
79
+ status: 'running',
80
+ state: 'active',
81
+ summary: summary ?? 'All requested validation receipts passed; delivery is next.',
82
+ evidenceRefs: contract.evidenceRefs,
83
+ });
84
+ updateWorkContract(options, contractId, { evidenceState: 'valid' });
85
+ return;
86
+ }
87
+
88
+ if (outcome === 'failed') {
89
+ transitionWorkContractPhase(options, contractId, {
90
+ phase: 'cleanup',
91
+ status: 'failed',
92
+ state: 'failed',
93
+ summary: summary ?? 'A requested validation check failed; terminal cleanup is next.',
94
+ evidenceRefs: contract.evidenceRefs,
95
+ });
96
+ updateWorkContract(options, contractId, { evidenceState: 'failed' });
97
+ return;
98
+ }
99
+
100
+ transitionWorkContractPhase(options, contractId, {
101
+ phase: 'cleanup',
102
+ status: 'failed',
103
+ state: 'failed',
104
+ summary: summary ?? 'Validation infrastructure failed; terminal cleanup is required without treating this as an acceptance failure.',
105
+ evidenceRefs: contract.evidenceRefs,
106
+ });
107
+ updateWorkContract(options, contractId, {
108
+ evidenceState: contract.evidenceState === 'valid' || contract.evidenceState === 'stale' ? 'stale' : 'partial',
109
+ });
110
+ }
111
+
112
+ function settleInfrastructureFailure(
113
+ controllerHome: string,
114
+ handle: WorkHandleState,
115
+ summary: string,
116
+ ): WorkValidationReconciliation {
117
+ const next = transitionWorkHandle(controllerHome, handle, 'failed', {
118
+ finalization: { ...handle.finalization, validation: 'failed', lastError: summary },
119
+ validationRun: undefined,
120
+ failureReason: summary,
121
+ });
122
+ projectWorkValidationOutcome(controllerHome, next, 'infrastructure_failure', summary);
123
+ return { handle: next, changed: true, outcome: 'infrastructure_failure', summary };
124
+ }
125
+
126
+ /**
127
+ * Converge a persisted Work validation run from durable Process receipts.
128
+ *
129
+ * No command is launched here. Missing/running bindings stay pending; timeout,
130
+ * cancellation, missing process state, or malformed receipt makes the Work
131
+ * terminally failed for cleanup without classifying it as an acceptance failure.
132
+ */
133
+ export function reconcileWorkValidation(
134
+ controllerHome: string,
135
+ handle: WorkHandleState,
136
+ ): WorkValidationReconciliation {
137
+ const run = handle.validationRun;
138
+ if (handle.state !== 'validating' || !run) {
139
+ return { handle, changed: false, outcome: 'not_validating' };
140
+ }
141
+
142
+ if (run.requestedChecks.length === 0) {
143
+ const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
144
+ finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
145
+ validationRun: undefined,
146
+ validatedInputFingerprint: run.fingerprint,
147
+ failureReason: undefined,
148
+ });
149
+ projectWorkValidationOutcome(controllerHome, next, 'passed', 'No validation checks were required.');
150
+ return { handle: next, changed: true, outcome: 'passed' };
151
+ }
152
+
153
+ for (const checkId of run.requestedChecks) {
154
+ const binding = run.processes[checkId];
155
+ if (!binding) return { handle, changed: false, outcome: 'running' };
156
+ const record = getProcessRecord(controllerHome, handle.repositoryId, binding.processId);
157
+ if (!record) {
158
+ return settleInfrastructureFailure(
159
+ controllerHome,
160
+ handle,
161
+ `Validation process record is unavailable: ${binding.processId}`,
162
+ );
163
+ }
164
+ if (record.status === 'running' || record.status === 'starting' || record.status === 'running_recovered') {
165
+ return { handle, changed: false, outcome: 'running' };
166
+ }
167
+
168
+ try {
169
+ const currentIdentity = record.checkExecution
170
+ ? controllerCheckExecutionIdentity(handle.worktreePath, checkId, record.checkExecution.timeoutMs)
171
+ : undefined;
172
+ const receipt = processCheckCompletionReceipt(record, {
173
+ repoId: handle.repositoryId,
174
+ checkoutId: handle.checkoutId,
175
+ workId: handle.workId,
176
+ executionSessionId: handle.sessionId,
177
+ checkId,
178
+ processId: binding.processId,
179
+ ...(currentIdentity ? {
180
+ checkExecution: {
181
+ cacheKey: currentIdentity.cacheKey,
182
+ revision: currentIdentity.revision,
183
+ definitionDigest: currentIdentity.definitionDigest,
184
+ environmentFingerprint: currentIdentity.environmentFingerprint,
185
+ timeoutMs: currentIdentity.timeoutMs,
186
+ scopeKey: processCheckSemanticScopeKey({
187
+ checkoutId: handle.checkoutId,
188
+ workId: handle.workId,
189
+ verificationBinding: { executionSessionId: handle.sessionId },
190
+ }, currentIdentity.reuseScope),
191
+ },
192
+ } : {}),
193
+ });
194
+ if (!receipt.ok) {
195
+ if (receipt.status === 'timed_out' || receipt.status === 'cancelled') {
196
+ return settleInfrastructureFailure(controllerHome, handle, receipt.summary);
197
+ }
198
+ const next = transitionWorkHandle(controllerHome, handle, 'failed', {
199
+ finalization: { ...handle.finalization, validation: 'failed', lastError: receipt.summary },
200
+ validationRun: undefined,
201
+ failureReason: receipt.summary,
202
+ });
203
+ projectWorkValidationOutcome(controllerHome, next, 'failed', receipt.summary);
204
+ return { handle: next, changed: true, outcome: 'failed', summary: receipt.summary };
205
+ }
206
+ } catch (error) {
207
+ const summary = error instanceof Error ? error.message : String(error);
208
+ return settleInfrastructureFailure(controllerHome, handle, summary);
209
+ }
210
+ }
211
+
212
+ const next = transitionWorkHandle(controllerHome, handle, run.resumeState, {
213
+ finalization: { ...handle.finalization, validation: 'done', lastError: undefined },
214
+ validationRun: undefined,
215
+ validatedInputFingerprint: run.fingerprint,
216
+ failureReason: undefined,
217
+ });
218
+ projectWorkValidationOutcome(controllerHome, next, 'passed', 'All requested validation receipts passed.');
219
+ return { handle: next, changed: true, outcome: 'passed' };
220
+ }
221
+
222
+ export interface PendingWorkValidationReconciliationSummary {
223
+ repositoryId: string;
224
+ examined: number;
225
+ validating: number;
226
+ changed: number;
227
+ running: number;
228
+ passed: number;
229
+ failed: number;
230
+ infrastructureFailure: number;
231
+ errors: Array<{ workId: string; error: string }>;
232
+ truncated: boolean;
233
+ }
234
+
235
+ /**
236
+ * Scheduler-safe convergence for validation Processes that were already
237
+ * launched by a Work. This function never launches commands and never waits
238
+ * for a Process. It only projects terminal receipts into Work state, allowing
239
+ * long checks to finish without ChatGPT polling.
240
+ */
241
+ export function reconcilePendingWorkValidations(
242
+ controllerHome: string,
243
+ repositoryId: string,
244
+ limit = 500,
245
+ ): PendingWorkValidationReconciliationSummary {
246
+ const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 5_000));
247
+ const validating = listValidatingWorkHandles(controllerHome, repositoryId, boundedLimit);
248
+ const handles = validating;
249
+ const summary: PendingWorkValidationReconciliationSummary = {
250
+ repositoryId,
251
+ examined: handles.length,
252
+ validating: validating.length,
253
+ changed: 0,
254
+ running: 0,
255
+ passed: 0,
256
+ failed: 0,
257
+ infrastructureFailure: 0,
258
+ errors: [],
259
+ truncated: handles.length >= boundedLimit,
260
+ };
261
+ for (const handle of validating) {
262
+ try {
263
+ const reconciled = reconcileWorkValidation(controllerHome, handle);
264
+ if (reconciled.changed) summary.changed += 1;
265
+ if (reconciled.outcome === 'running') summary.running += 1;
266
+ else if (reconciled.outcome === 'passed') summary.passed += 1;
267
+ else if (reconciled.outcome === 'failed') summary.failed += 1;
268
+ else if (reconciled.outcome === 'infrastructure_failure') summary.infrastructureFailure += 1;
269
+ } catch (error) {
270
+ summary.errors.push({
271
+ workId: handle.workId,
272
+ error: error instanceof Error ? error.message : String(error),
273
+ });
274
+ }
275
+ }
276
+ return summary;
277
+ }
@@ -0,0 +1,169 @@
1
+ import { createHash } from 'crypto';
2
+ import {
3
+ cpSync,
4
+ existsSync,
5
+ lstatSync,
6
+ mkdirSync,
7
+ mkdtempSync,
8
+ readdirSync,
9
+ rmSync,
10
+ statSync,
11
+ symlinkSync,
12
+ writeFileSync,
13
+ } from 'fs';
14
+ import { dirname, join, resolve } from 'path';
15
+ import { spawnSync } from 'child_process';
16
+ import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
17
+ import { globMatches, normalizeMcpRelativePath } from '../../../cli/mcp/paths';
18
+ import { sanitizeFileComponent } from '../../shared/json-files';
19
+
20
+ const STALE_SNAPSHOT_MS = 6 * 60 * 60_000;
21
+ const SNAPSHOT_MARKER = '.forge-work-verification-snapshot.json';
22
+
23
+ export interface WorkVerificationSnapshotScope {
24
+ workId: string;
25
+ allowedPaths: readonly string[];
26
+ forbiddenPaths: readonly string[];
27
+ }
28
+
29
+ export interface WorkVerificationSnapshot {
30
+ root: string;
31
+ sourceHead: string;
32
+ includedPaths: string[];
33
+ excludedPaths: string[];
34
+ ownershipDigest: string;
35
+ }
36
+
37
+ function git(root: string, args: string[], maxBytes = 2 * 1024 * 1024): Buffer {
38
+ const result = spawnSync('git', ['-C', root, ...args], {
39
+ encoding: 'buffer',
40
+ stdio: ['ignore', 'pipe', 'pipe'],
41
+ timeout: 120_000,
42
+ maxBuffer: maxBytes,
43
+ });
44
+ if (result.status !== 0 || result.error) {
45
+ const stderr = Buffer.isBuffer(result.stderr) ? result.stderr.toString('utf8') : String(result.stderr ?? '');
46
+ throw new Error(`WORK_VERIFICATION_SNAPSHOT_GIT_FAILED: git ${args.join(' ')}: ${stderr || result.error?.message || `exit ${result.status}`}`);
47
+ }
48
+ return Buffer.isBuffer(result.stdout) ? result.stdout : Buffer.from(result.stdout ?? '');
49
+ }
50
+
51
+ function nulPaths(value: Buffer): string[] {
52
+ return value.toString('utf8').split('\0').filter(Boolean).map((raw) => {
53
+ const normalized = normalizeMcpRelativePath(raw);
54
+ if (!normalized.ok || !normalized.relativePath) {
55
+ throw new Error(`WORK_VERIFICATION_SNAPSHOT_PATH_INVALID: ${raw}`);
56
+ }
57
+ return normalized.relativePath;
58
+ });
59
+ }
60
+
61
+ function matchesAny(patterns: readonly string[], path: string): boolean {
62
+ return patterns.some((pattern) => globMatches(pattern, path));
63
+ }
64
+
65
+ function snapshotRoot(controllerHome: string, repoId: string, workId: string): string {
66
+ const root = join(repositoryControllerRoot(controllerHome, repoId), 'verification-snapshots');
67
+ mkdirSync(root, { recursive: true });
68
+ const now = Date.now();
69
+ for (const entry of readdirSync(root, { withFileTypes: true })) {
70
+ if (!entry.isDirectory() || !entry.name.startsWith('snapshot-')) continue;
71
+ const path = join(root, entry.name);
72
+ try {
73
+ if (now - statSync(path).mtimeMs > STALE_SNAPSHOT_MS) rmSync(path, { recursive: true, force: true });
74
+ } catch {
75
+ // Best-effort stale cleanup; a fresh snapshot must not fail because an old
76
+ // directory disappeared concurrently.
77
+ }
78
+ }
79
+ return mkdtempSync(join(root, `snapshot-${sanitizeFileComponent(workId).slice(0, 48)}-`));
80
+ }
81
+
82
+ function cloneHead(sourceRoot: string, targetRoot: string, head: string): void {
83
+ const clone = spawnSync('git', ['clone', '--shared', '--no-checkout', '--quiet', '--', sourceRoot, targetRoot], {
84
+ encoding: 'utf8',
85
+ stdio: ['ignore', 'pipe', 'pipe'],
86
+ timeout: 120_000,
87
+ });
88
+ if (clone.status !== 0 || clone.error) {
89
+ throw new Error(`WORK_VERIFICATION_SNAPSHOT_CLONE_FAILED: ${clone.stderr || clone.error?.message || `exit ${clone.status}`}`);
90
+ }
91
+ git(targetRoot, ['checkout', '--detach', '--quiet', head]);
92
+ }
93
+
94
+ function overlayPath(sourceRoot: string, targetRoot: string, relativePath: string): void {
95
+ const source = resolve(sourceRoot, relativePath);
96
+ const target = resolve(targetRoot, relativePath);
97
+ rmSync(target, { recursive: true, force: true });
98
+ if (!existsSync(source)) return;
99
+ mkdirSync(dirname(target), { recursive: true });
100
+ const stat = lstatSync(source);
101
+ if (stat.isSymbolicLink()) {
102
+ cpSync(source, target, { recursive: false, dereference: false, force: true });
103
+ return;
104
+ }
105
+ cpSync(source, target, { recursive: stat.isDirectory(), dereference: false, force: true, preserveTimestamps: true });
106
+ }
107
+
108
+ function linkIgnoredNodeModules(sourceRoot: string, targetRoot: string): void {
109
+ const source = join(sourceRoot, 'node_modules');
110
+ const target = join(targetRoot, 'node_modules');
111
+ if (!existsSync(source) || existsSync(target)) return;
112
+ const ignored = spawnSync('git', ['-C', sourceRoot, 'check-ignore', '--quiet', '--', 'node_modules'], { stdio: 'ignore', timeout: 10_000 });
113
+ if (ignored.status !== 0) return;
114
+ try {
115
+ symlinkSync(source, target, process.platform === 'win32' ? 'junction' : 'dir');
116
+ } catch {
117
+ // Dependency linking is an optimization. If it is unavailable the check may
118
+ // still succeed using its own toolchain resolution; never copy a huge cache.
119
+ }
120
+ }
121
+
122
+ export function materializeWorkVerificationSnapshot(input: {
123
+ controllerHome: string;
124
+ repoId: string;
125
+ sourceRoot: string;
126
+ scope: WorkVerificationSnapshotScope;
127
+ }): WorkVerificationSnapshot {
128
+ const sourceHead = git(input.sourceRoot, ['rev-parse', '--verify', 'HEAD']).toString('utf8').trim();
129
+ const tracked = nulPaths(git(input.sourceRoot, ['diff', '--name-only', '-z', 'HEAD', '--']));
130
+ const untracked = nulPaths(git(input.sourceRoot, ['ls-files', '--others', '--exclude-standard', '-z']));
131
+ const dirtyPaths = [...new Set([...tracked, ...untracked])].sort();
132
+ const includedPaths: string[] = [];
133
+ const excludedPaths: string[] = [];
134
+ for (const path of dirtyPaths) {
135
+ if (matchesAny(input.scope.forbiddenPaths, path)) {
136
+ excludedPaths.push(path);
137
+ continue;
138
+ }
139
+ if (input.scope.allowedPaths.length > 0 && matchesAny(input.scope.allowedPaths, path)) {
140
+ includedPaths.push(path);
141
+ continue;
142
+ }
143
+ throw new Error(`WORK_VERIFICATION_PATH_OWNERSHIP_AMBIGUOUS: ${path} is dirty but is neither allowed nor forbidden for ${input.scope.workId}`);
144
+ }
145
+
146
+ const root = snapshotRoot(input.controllerHome, input.repoId, input.scope.workId);
147
+ try {
148
+ cloneHead(input.sourceRoot, root, sourceHead);
149
+ for (const path of includedPaths) overlayPath(input.sourceRoot, root, path);
150
+ linkIgnoredNodeModules(input.sourceRoot, root);
151
+ const ownershipDigest = createHash('sha256').update(JSON.stringify({
152
+ sourceHead,
153
+ workId: input.scope.workId,
154
+ includedPaths,
155
+ excludedPaths,
156
+ allowedPaths: [...input.scope.allowedPaths].sort(),
157
+ forbiddenPaths: [...input.scope.forbiddenPaths].sort(),
158
+ })).digest('hex');
159
+ writeFileSync(join(root, SNAPSHOT_MARKER), `${JSON.stringify({ schemaVersion: 1, ...input.scope, sourceHead, includedPaths, excludedPaths, ownershipDigest }, null, 2)}\n`);
160
+ return { root, sourceHead, includedPaths, excludedPaths, ownershipDigest };
161
+ } catch (error) {
162
+ rmSync(root, { recursive: true, force: true });
163
+ throw error;
164
+ }
165
+ }
166
+
167
+ export function cleanupWorkVerificationSnapshot(snapshotRoot: string): void {
168
+ rmSync(snapshotRoot, { recursive: true, force: true });
169
+ }
@@ -142,16 +142,6 @@ const CORE_CAPABILITIES: CapabilityDescriptor[] = [
142
142
  schemaExposure: 'stable_static',
143
143
  summary: 'Plan, dispatch, review, verify, retry, and accept durable Issue and Task work through existing typed handlers.',
144
144
  },
145
- {
146
- capabilityId: 'workflow.campaign',
147
- domain: 'controller',
148
- group: 'campaign',
149
- operationClass: 'execute',
150
- risk: 'workspace_write',
151
- exposedVia: 'rh_work',
152
- schemaExposure: 'stable_static',
153
- summary: 'Coordinate multi-step campaign DAGs and review checkpoints while keeping integration and final acceptance explicit.',
154
- },
155
145
  {
156
146
  capabilityId: 'plugin.browser',
157
147
  domain: 'plugin',
@@ -1,6 +1,7 @@
1
1
  import { mkdirSync } from 'fs';
2
2
  import { join } from 'path';
3
3
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
4
+ import { withControllerLock } from '../../../cli/repositories/locks';
4
5
  import { readJsonFile, sanitizeFileComponent, writeJsonAtomic } from '../../shared/json-files';
5
6
  import {
6
7
  type HandoffCreationReason,
@@ -65,6 +66,20 @@ function nowIso(options: HandoffInboxStoreOptions): string {
65
66
  return options.now?.() ?? new Date().toISOString();
66
67
  }
67
68
 
69
+ function withHandoffInboxWriteLock<T>(
70
+ options: HandoffInboxStoreOptions,
71
+ owner: string,
72
+ operation: () => T,
73
+ ): T {
74
+ if (!options.controllerHome || !options.repoId || options.root) return operation();
75
+ return withControllerLock(
76
+ options.controllerHome,
77
+ { scope: 'task', repoId: options.repoId, taskId: 'handoff-inbox' },
78
+ owner,
79
+ operation,
80
+ );
81
+ }
82
+
68
83
  export function handoffInboxRoot(location: HandoffInboxStoreLocation): string {
69
84
  if (location.root) {
70
85
  mkdirSync(location.root, { recursive: true });
@@ -117,17 +132,19 @@ export function createHandoffItem(options: HandoffInboxStoreOptions, input: Crea
117
132
  createdAt: at,
118
133
  updatedAt: input.updatedAt ?? at,
119
134
  };
120
- const store = readHandoffInboxStore(options);
121
- if (store.items.some((existing) => existing.id === item.id)) {
122
- throw new Error(`handoff already exists: ${item.id}`);
123
- }
124
- const nextStore: HandoffInboxStore = {
125
- schemaVersion: 1,
126
- updatedAt: item.updatedAt,
127
- items: [item, ...store.items],
128
- };
129
- writeHandoffInboxStore(options, nextStore);
130
- return item;
135
+ return withHandoffInboxWriteLock(options, `create-handoff:${item.id}`, () => {
136
+ const store = readHandoffInboxStore(options);
137
+ if (store.items.some((existing) => existing.id === item.id)) {
138
+ throw new Error(`handoff already exists: ${item.id}`);
139
+ }
140
+ const nextStore: HandoffInboxStore = {
141
+ schemaVersion: 1,
142
+ updatedAt: item.updatedAt,
143
+ items: [item, ...store.items],
144
+ };
145
+ writeHandoffInboxStore(options, nextStore);
146
+ return item;
147
+ });
131
148
  }
132
149
 
133
150
  export function listHandoffItems(options: ListHandoffOptions): HandoffItem[] {
@@ -171,20 +188,22 @@ function setHandoffStatus(
171
188
  patch: Partial<Pick<HandoffItem, 'decision' | 'resolver'>> = {},
172
189
  ): HandoffItem {
173
190
  const sanitizedId = sanitizeFileComponent(id);
174
- const store = readHandoffInboxStore(options);
175
- const index = store.items.findIndex((item) => item.id === sanitizedId);
176
- if (index < 0) throw new Error(`handoff not found: ${sanitizedId}`);
177
- const at = nowIso(options);
178
- const item: HandoffItem = {
179
- ...store.items[index],
180
- ...patch,
181
- status,
182
- updatedAt: at,
183
- };
184
- const items = [...store.items];
185
- items[index] = item;
186
- writeHandoffInboxStore(options, { schemaVersion: 1, updatedAt: at, items });
187
- return item;
191
+ return withHandoffInboxWriteLock(options, `update-handoff:${sanitizedId}`, () => {
192
+ const store = readHandoffInboxStore(options);
193
+ const index = store.items.findIndex((item) => item.id === sanitizedId);
194
+ if (index < 0) throw new Error(`handoff not found: ${sanitizedId}`);
195
+ const at = nowIso(options);
196
+ const item: HandoffItem = {
197
+ ...store.items[index],
198
+ ...patch,
199
+ status,
200
+ updatedAt: at,
201
+ };
202
+ const items = [...store.items];
203
+ items[index] = item;
204
+ writeHandoffInboxStore(options, { schemaVersion: 1, updatedAt: at, items });
205
+ return item;
206
+ });
188
207
  }
189
208
 
190
209
  export function acknowledgeHandoffItem(options: HandoffInboxStoreOptions, id: string): HandoffItem {
@@ -12,8 +12,14 @@ function expectedActionRisk(action: SuggestedNextAction): SuggestedNextAction['r
12
12
  case 'controller_claim':
13
13
  case 'controller_release':
14
14
  case 'launcher_start':
15
+ case 'schedule_create':
16
+ case 'schedule_pause':
17
+ case 'schedule_resume':
18
+ case 'schedule_trigger':
15
19
  return 'workspace_write';
16
20
  case 'controller_get_owner':
21
+ case 'schedule_list':
22
+ case 'schedule_get':
17
23
  return 'readonly';
18
24
  case 'plan_create':
19
25
  case 'plan_get':
@@ -34,7 +40,7 @@ const ALLOWED_FACADE_OPERATIONS: Record<FacadeTool, readonly string[]> = {
34
40
  rh_status: ['list', 'get', 'repair'],
35
41
  rh_inbox: ['list', 'get', 'ack', 'accept', 'resolve', 'dismiss', 'create'],
36
42
  rh_context: ['list', 'get', 'search'],
37
- rh_work: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede'],
43
+ rh_work: ['start', 'continue', 'verify', 'repair', 'finalize', 'stop', 'delegate', 'controller_claim', 'controller_release', 'controller_get_owner', 'launcher_start', 'plan_create', 'plan_get', 'plan_list', 'plan_approve', 'plan_supersede', 'schedule_create', 'schedule_list', 'schedule_get', 'schedule_pause', 'schedule_resume', 'schedule_trigger'],
38
44
  };
39
45
 
40
46
  export interface SuggestedActionValidationOptions {
@@ -22,7 +22,6 @@ export const CAPABILITY_GROUPS = [
22
22
  'repository-core',
23
23
  'git',
24
24
  'issue-task',
25
- 'campaign',
26
25
  'browser',
27
26
  'ios',
28
27
  'plugin',