@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
@@ -0,0 +1,556 @@
1
+ import { createHash } from 'crypto';
2
+ import {
3
+ getEditSession,
4
+ recordEditSessionProcessCheckReceipts,
5
+ type EditSession,
6
+ } from '../../../cli/editing/edit-session';
7
+ import {
8
+ controllerCheckExecutionIdentity,
9
+ listControllerChecks,
10
+ } from '../../../cli/controller/check-runner';
11
+ import type { RepositoryRecord } from '../../../cli/repositories/types';
12
+ import { executionIdentityForRepository } from './execution-identity';
13
+ import {
14
+ claimsForCheck,
15
+ getProcessRecord,
16
+ processCheckCompletionReceipt,
17
+ processCheckSemanticScopeKey,
18
+ runPersistedCheckViaProcessRuntime,
19
+ type ProcessCheckCompletionReceipt,
20
+ } from '../../execution/process-runtime';
21
+ import { resourceClaimsConflict } from '../../resources/claims/conflicts';
22
+ import {
23
+ mutateControlPlaneRecord,
24
+ readControlPlaneRecord,
25
+ } from '../persistence/sqlite-store';
26
+
27
+ const EDIT_VALIDATION_RUN_NAMESPACE = 'execution_edit_validation_run';
28
+ const EDIT_VALIDATION_INDEX_NAMESPACE = 'execution_edit_validation_index';
29
+ const EDIT_VALIDATION_INDEX_KEY = 'v1';
30
+
31
+ export interface EditValidationRunState {
32
+ schemaVersion: 1;
33
+ validationId: string;
34
+ repositoryId: string;
35
+ checkoutId: string;
36
+ editSessionId: string;
37
+ editRevision: number;
38
+ requestId: string;
39
+ checkIds: string[];
40
+ lanes: string[][];
41
+ processes: Record<string, { processId: string; requestId: string }>;
42
+ reviewer?: string;
43
+ note?: string;
44
+ timeoutMs?: number;
45
+ leaseWaitMs: number;
46
+ status: 'running' | 'completed' | 'failed';
47
+ ok?: boolean;
48
+ error?: { code: string; message: string };
49
+ receipts?: ProcessCheckCompletionReceipt[];
50
+ createdAt: string;
51
+ updatedAt: string;
52
+ }
53
+
54
+ interface EditValidationIndex {
55
+ schemaVersion: 1;
56
+ updatedAt: string;
57
+ validationIds: string[];
58
+ }
59
+
60
+ export interface EditValidationCoordinatorResult {
61
+ accepted: boolean;
62
+ mode: 'direct' | 'managed' | 'durable';
63
+ path: 'process_direct' | 'process_managed' | 'durable';
64
+ completed: boolean;
65
+ ok?: boolean;
66
+ sessionId: string;
67
+ editRevision: number;
68
+ validationRequestId: string;
69
+ validationId: string;
70
+ processes: Array<{ checkId: string; processId: string; requestId: string; completed?: boolean; ok?: boolean; status?: string }>;
71
+ receipts?: ProcessCheckCompletionReceipt[];
72
+ session?: EditSession;
73
+ error?: { code: string; message: string };
74
+ message?: string;
75
+ suggestedOperation?: string;
76
+ next: string;
77
+ durableSideEffects: {
78
+ executionJobCount: 0;
79
+ localJobCount: 0;
80
+ workerSpawnCount: 0;
81
+ projectionUpdateCount: 0;
82
+ };
83
+ }
84
+
85
+ export interface EditValidationStartInput {
86
+ editSessionId: string;
87
+ checkIds?: string[];
88
+ requestId?: string;
89
+ validationRequestId?: string;
90
+ reviewer?: string;
91
+ note?: string;
92
+ timeoutMs?: number;
93
+ leaseWaitMs?: number;
94
+ }
95
+
96
+ const ZERO_DURABLE_SIDE_EFFECTS = {
97
+ executionJobCount: 0 as const,
98
+ localJobCount: 0 as const,
99
+ workerSpawnCount: 0 as const,
100
+ projectionUpdateCount: 0 as const,
101
+ };
102
+
103
+ function now(): string {
104
+ return new Date().toISOString();
105
+ }
106
+
107
+ function normalized(values: readonly string[]): string[] {
108
+ return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
109
+ }
110
+
111
+ function validationId(repositoryId: string, editSessionId: string, editRevision: number, requestId: string): string {
112
+ return `editval_${createHash('sha256')
113
+ .update(`${repositoryId}\n${editSessionId}\n${editRevision}\n${requestId}`)
114
+ .digest('hex')
115
+ .slice(0, 24)}`;
116
+ }
117
+
118
+ function readIndex(controllerHome: string, repositoryId: string): EditValidationIndex {
119
+ return readControlPlaneRecord<EditValidationIndex>(
120
+ controllerHome,
121
+ EDIT_VALIDATION_INDEX_NAMESPACE,
122
+ repositoryId,
123
+ EDIT_VALIDATION_INDEX_KEY,
124
+ )?.value ?? { schemaVersion: 1, updatedAt: now(), validationIds: [] };
125
+ }
126
+
127
+ function setIndexMembership(controllerHome: string, repositoryId: string, id: string, active: boolean): void {
128
+ mutateControlPlaneRecord<EditValidationIndex>(controllerHome, {
129
+ namespace: EDIT_VALIDATION_INDEX_NAMESPACE,
130
+ scope: repositoryId,
131
+ key: EDIT_VALIDATION_INDEX_KEY,
132
+ schemaVersion: 1,
133
+ action: active ? 'edit_validation_index_add' : 'edit_validation_index_remove',
134
+ mutate: (current) => {
135
+ const ids = new Set(current?.value.validationIds ?? []);
136
+ if (active) ids.add(id); else ids.delete(id);
137
+ return { schemaVersion: 1, updatedAt: now(), validationIds: [...ids].sort() };
138
+ },
139
+ });
140
+ }
141
+
142
+ function readRun(controllerHome: string, repositoryId: string, id: string): EditValidationRunState | undefined {
143
+ return readControlPlaneRecord<EditValidationRunState>(
144
+ controllerHome,
145
+ EDIT_VALIDATION_RUN_NAMESPACE,
146
+ repositoryId,
147
+ id,
148
+ )?.value;
149
+ }
150
+
151
+ function saveRun(controllerHome: string, run: EditValidationRunState): EditValidationRunState {
152
+ const saved = mutateControlPlaneRecord<EditValidationRunState>(controllerHome, {
153
+ namespace: EDIT_VALIDATION_RUN_NAMESPACE,
154
+ scope: run.repositoryId,
155
+ key: run.validationId,
156
+ schemaVersion: 1,
157
+ action: `edit_validation_${run.status}`,
158
+ mutate: (current) => ({
159
+ ...run,
160
+ createdAt: current?.value.createdAt ?? run.createdAt,
161
+ processes: { ...(current?.value.processes ?? {}), ...run.processes },
162
+ updatedAt: now(),
163
+ }),
164
+ }).value;
165
+ setIndexMembership(controllerHome, run.repositoryId, run.validationId, saved.status === 'running');
166
+ return saved;
167
+ }
168
+
169
+ function checkLanes(repository: RepositoryRecord, checkIds: string[]): string[][] {
170
+ const checks = new Map(listControllerChecks(repository.canonicalRoot).map((check) => [check.id, check] as const));
171
+ const claimSets = checkIds.map((checkId) => {
172
+ const check = checks.get(checkId);
173
+ return claimsForCheck(
174
+ checkId,
175
+ check?.command,
176
+ repository.repoId,
177
+ repository.activeCheckoutId,
178
+ check?.effects,
179
+ );
180
+ });
181
+ const lanes: string[][] = [];
182
+ const assigned = new Set<number>();
183
+ for (let start = 0; start < checkIds.length; start += 1) {
184
+ if (assigned.has(start)) continue;
185
+ const lane: number[] = [];
186
+ const queue = [start];
187
+ assigned.add(start);
188
+ while (queue.length > 0) {
189
+ const current = queue.shift()!;
190
+ lane.push(current);
191
+ for (let candidate = 0; candidate < checkIds.length; candidate += 1) {
192
+ if (assigned.has(candidate)) continue;
193
+ const conflicts = claimSets[current]!.some((left) =>
194
+ claimSets[candidate]!.some((right) => resourceClaimsConflict(left, right)));
195
+ if (!conflicts) continue;
196
+ assigned.add(candidate);
197
+ queue.push(candidate);
198
+ }
199
+ }
200
+ lanes.push(lane.sort((left, right) => left - right).map((index) => checkIds[index]!));
201
+ }
202
+ return lanes;
203
+ }
204
+
205
+ function processSummaries(controllerHome: string, run: EditValidationRunState): EditValidationCoordinatorResult['processes'] {
206
+ return run.checkIds.flatMap((checkId) => {
207
+ const binding = run.processes[checkId];
208
+ if (!binding) return [];
209
+ const record = getProcessRecord(controllerHome, run.repositoryId, binding.processId);
210
+ return [{
211
+ checkId,
212
+ processId: binding.processId,
213
+ requestId: binding.requestId,
214
+ ...(record ? {
215
+ completed: !['starting', 'running', 'running_recovered'].includes(record.status),
216
+ ok: record.status === 'succeeded',
217
+ status: record.status,
218
+ } : {}),
219
+ }];
220
+ });
221
+ }
222
+
223
+ function coordinatorResult(
224
+ controllerHome: string,
225
+ run: EditValidationRunState,
226
+ session?: EditSession,
227
+ ): EditValidationCoordinatorResult {
228
+ const failed = run.status === 'failed';
229
+ const completed = run.status === 'completed' || failed;
230
+ return {
231
+ accepted: !failed,
232
+ mode: failed || run.status === 'completed' ? 'direct' : 'managed',
233
+ path: failed || run.status === 'completed' ? 'process_direct' : 'process_managed',
234
+ completed,
235
+ ...(run.ok !== undefined ? { ok: run.ok } : {}),
236
+ sessionId: run.editSessionId,
237
+ editRevision: run.editRevision,
238
+ validationRequestId: run.requestId,
239
+ validationId: run.validationId,
240
+ processes: processSummaries(controllerHome, run),
241
+ ...(run.receipts ? { receipts: run.receipts } : {}),
242
+ ...(session ? { session } : {}),
243
+ ...(run.error ? { error: run.error } : {}),
244
+ ...(run.error ? { message: run.error.message } : {}),
245
+ next: run.status === 'running'
246
+ ? 'Validation is running in Process Runtime. Forge Scheduler will advance conflict lanes automatically; continue independent work and join once when validation becomes a real dependency.'
247
+ : run.status === 'completed'
248
+ ? 'Validation is complete for the exact edit revision.'
249
+ : 'Validation could not continue. Inspect the structured error before retrying or changing the edit revision.',
250
+ durableSideEffects: ZERO_DURABLE_SIDE_EFFECTS,
251
+ };
252
+ }
253
+
254
+ function failedRun(
255
+ controllerHome: string,
256
+ run: EditValidationRunState,
257
+ code: string,
258
+ message: string,
259
+ ): EditValidationCoordinatorResult {
260
+ const failed = saveRun(controllerHome, {
261
+ ...run,
262
+ status: 'failed',
263
+ ok: false,
264
+ error: { code, message },
265
+ });
266
+ return coordinatorResult(controllerHome, failed);
267
+ }
268
+
269
+ function receiptFor(
270
+ controllerHome: string,
271
+ repository: RepositoryRecord,
272
+ run: EditValidationRunState,
273
+ checkId: string,
274
+ ): ProcessCheckCompletionReceipt {
275
+ const binding = run.processes[checkId];
276
+ if (!binding) throw new Error(`EDIT_VALIDATION_PROCESS_MISSING: ${checkId}`);
277
+ const record = getProcessRecord(controllerHome, repository.repoId, binding.processId);
278
+ if (!record) throw new Error(`EDIT_VALIDATION_PROCESS_RECORD_MISSING: ${binding.processId}`);
279
+ const currentIdentity = record.checkExecution
280
+ ? controllerCheckExecutionIdentity(repository.canonicalRoot, checkId, record.checkExecution.timeoutMs)
281
+ : undefined;
282
+ return processCheckCompletionReceipt(record, {
283
+ repoId: repository.repoId,
284
+ checkoutId: repository.activeCheckoutId,
285
+ editSessionId: run.editSessionId,
286
+ editRevision: run.editRevision,
287
+ checkId,
288
+ processId: binding.processId,
289
+ ...(currentIdentity ? {
290
+ checkExecution: {
291
+ cacheKey: currentIdentity.cacheKey,
292
+ revision: currentIdentity.revision,
293
+ definitionDigest: currentIdentity.definitionDigest,
294
+ environmentFingerprint: currentIdentity.environmentFingerprint,
295
+ timeoutMs: currentIdentity.timeoutMs,
296
+ scopeKey: processCheckSemanticScopeKey({
297
+ checkoutId: repository.activeCheckoutId,
298
+ verificationBinding: {
299
+ editSessionId: run.editSessionId,
300
+ editRevision: run.editRevision,
301
+ },
302
+ }, currentIdentity.reuseScope),
303
+ },
304
+ } : {}),
305
+ });
306
+ }
307
+
308
+ export async function reconcileEditValidationRun(
309
+ controllerHome: string,
310
+ repository: RepositoryRecord,
311
+ runInput: EditValidationRunState,
312
+ ): Promise<EditValidationCoordinatorResult> {
313
+ let run = readRun(controllerHome, repository.repoId, runInput.validationId) ?? runInput;
314
+ if (run.status !== 'running') return coordinatorResult(controllerHome, run);
315
+ if (run.checkoutId !== repository.activeCheckoutId) {
316
+ return failedRun(controllerHome, run, 'EDIT_VALIDATION_CHECKOUT_CHANGED', `Validation checkout ${run.checkoutId} no longer matches ${repository.activeCheckoutId}.`);
317
+ }
318
+ const session = getEditSession(repository.canonicalRoot, run.editSessionId);
319
+ if (session.currentRevision !== run.editRevision) {
320
+ return failedRun(controllerHome, run, 'EDIT_VALIDATION_STALE_REVISION', `Validation targets revision ${run.editRevision}, current edit revision is ${session.currentRevision}.`);
321
+ }
322
+
323
+ for (const lane of run.lanes) {
324
+ for (const checkId of lane) {
325
+ const binding = run.processes[checkId];
326
+ if (binding) {
327
+ const record = getProcessRecord(controllerHome, repository.repoId, binding.processId);
328
+ if (!record) return failedRun(controllerHome, run, 'EDIT_VALIDATION_PROCESS_RECORD_MISSING', `Process record is unavailable: ${binding.processId}`);
329
+ if (['starting', 'running', 'running_recovered'].includes(record.status)) break;
330
+ continue;
331
+ }
332
+
333
+ const index = run.checkIds.indexOf(checkId);
334
+ const checkRequestId = `${run.requestId}:check:${index + 1}:${createHash('sha256').update(checkId).digest('hex').slice(0, 12)}`;
335
+ const facade = await runPersistedCheckViaProcessRuntime({
336
+ controllerHome,
337
+ repoId: repository.repoId,
338
+ checkoutId: repository.activeCheckoutId,
339
+ repoRoot: repository.canonicalRoot,
340
+ executionIdentity: executionIdentityForRepository(repository),
341
+ checkId,
342
+ timeoutMs: run.timeoutMs,
343
+ interactiveWaitMs: 0,
344
+ requestId: checkRequestId,
345
+ commandId: checkRequestId,
346
+ verificationBinding: {
347
+ editSessionId: run.editSessionId,
348
+ editRevision: run.editRevision,
349
+ issueId: session.issueId,
350
+ taskId: session.taskId,
351
+ },
352
+ leaseWaitMs: run.leaseWaitMs,
353
+ });
354
+ if (facade.mode === 'durable' || !facade.process) {
355
+ return failedRun(
356
+ controllerHome,
357
+ run,
358
+ 'EDIT_VALIDATION_DURABLE_CHECK_REQUIRED',
359
+ facade.durable?.reason ?? `Check ${checkId} cannot run through Process Runtime.`,
360
+ );
361
+ }
362
+ run = saveRun(controllerHome, {
363
+ ...run,
364
+ processes: {
365
+ ...run.processes,
366
+ [checkId]: { processId: facade.process.processId, requestId: checkRequestId },
367
+ },
368
+ });
369
+ if (!facade.process.completed) break;
370
+ }
371
+ }
372
+
373
+ const allBound = run.checkIds.every((checkId) => Boolean(run.processes[checkId]));
374
+ if (!allBound) return coordinatorResult(controllerHome, run, session);
375
+ const records = run.checkIds.map((checkId) => {
376
+ const binding = run.processes[checkId]!;
377
+ return getProcessRecord(controllerHome, repository.repoId, binding.processId);
378
+ });
379
+ if (records.some((record) => !record)) {
380
+ return failedRun(controllerHome, run, 'EDIT_VALIDATION_PROCESS_RECORD_MISSING', 'One or more validation process records are unavailable.');
381
+ }
382
+ if (records.some((record) => ['starting', 'running', 'running_recovered'].includes(record!.status))) {
383
+ return coordinatorResult(controllerHome, run, session);
384
+ }
385
+
386
+ try {
387
+ const receipts = run.checkIds.map((checkId) => receiptFor(controllerHome, repository, run, checkId));
388
+ const checked = recordEditSessionProcessCheckReceipts(repository.canonicalRoot, run.editSessionId, {
389
+ repoId: repository.repoId,
390
+ checkoutId: repository.activeCheckoutId,
391
+ receipts,
392
+ reviewer: run.reviewer,
393
+ note: run.note,
394
+ });
395
+ run = saveRun(controllerHome, {
396
+ ...run,
397
+ status: 'completed',
398
+ ok: receipts.every((receipt) => receipt.ok),
399
+ receipts,
400
+ error: undefined,
401
+ });
402
+ return coordinatorResult(controllerHome, run, checked);
403
+ } catch (error) {
404
+ return failedRun(
405
+ controllerHome,
406
+ run,
407
+ 'EDIT_VALIDATION_RECEIPT_REJECTED',
408
+ error instanceof Error ? error.message : String(error),
409
+ );
410
+ }
411
+ }
412
+
413
+ export async function startOrJoinEditValidation(
414
+ controllerHome: string,
415
+ repository: RepositoryRecord,
416
+ input: EditValidationStartInput,
417
+ ): Promise<EditValidationCoordinatorResult> {
418
+ const session = getEditSession(repository.canonicalRoot, input.editSessionId);
419
+ const checkIds = normalized(input.checkIds ?? session.requestedChecks);
420
+ const requestId = input.validationRequestId?.trim()
421
+ || input.requestId?.trim()
422
+ || `verify-edit:${session.sessionId}:r${session.currentRevision}:${createHash('sha256').update(JSON.stringify(checkIds)).digest('hex').slice(0, 16)}`;
423
+ const id = validationId(repository.repoId, session.sessionId, session.currentRevision, requestId);
424
+ const existing = readRun(controllerHome, repository.repoId, id);
425
+ if (existing) {
426
+ if (existing.checkIds.join('\n') !== checkIds.join('\n')) {
427
+ return failedRun(controllerHome, existing, 'EDIT_VALIDATION_REQUEST_CONFLICT', 'The validation request id is already bound to a different check set.');
428
+ }
429
+ return reconcileEditValidationRun(controllerHome, repository, existing);
430
+ }
431
+
432
+ if (checkIds.length === 0) {
433
+ try {
434
+ const checked = recordEditSessionProcessCheckReceipts(repository.canonicalRoot, session.sessionId, {
435
+ repoId: repository.repoId,
436
+ checkoutId: repository.activeCheckoutId,
437
+ receipts: [],
438
+ reviewer: input.reviewer,
439
+ note: input.note,
440
+ });
441
+ const completed = saveRun(controllerHome, {
442
+ schemaVersion: 1,
443
+ validationId: id,
444
+ repositoryId: repository.repoId,
445
+ checkoutId: repository.activeCheckoutId,
446
+ editSessionId: session.sessionId,
447
+ editRevision: session.currentRevision,
448
+ requestId,
449
+ checkIds,
450
+ lanes: [],
451
+ processes: {},
452
+ reviewer: input.reviewer,
453
+ note: input.note,
454
+ timeoutMs: input.timeoutMs,
455
+ leaseWaitMs: 0,
456
+ status: 'completed',
457
+ ok: true,
458
+ receipts: [],
459
+ createdAt: now(),
460
+ updatedAt: now(),
461
+ });
462
+ return coordinatorResult(controllerHome, completed, checked);
463
+ } catch (error) {
464
+ const run: EditValidationRunState = {
465
+ schemaVersion: 1,
466
+ validationId: id,
467
+ repositoryId: repository.repoId,
468
+ checkoutId: repository.activeCheckoutId,
469
+ editSessionId: session.sessionId,
470
+ editRevision: session.currentRevision,
471
+ requestId,
472
+ checkIds,
473
+ lanes: [],
474
+ processes: {},
475
+ reviewer: input.reviewer,
476
+ note: input.note,
477
+ timeoutMs: input.timeoutMs,
478
+ leaseWaitMs: 0,
479
+ status: 'running',
480
+ createdAt: now(),
481
+ updatedAt: now(),
482
+ };
483
+ return failedRun(controllerHome, run, 'EDIT_VALIDATION_EMPTY_RECEIPT_REJECTED', error instanceof Error ? error.message : String(error));
484
+ }
485
+ }
486
+
487
+ const run = saveRun(controllerHome, {
488
+ schemaVersion: 1,
489
+ validationId: id,
490
+ repositoryId: repository.repoId,
491
+ checkoutId: repository.activeCheckoutId,
492
+ editSessionId: session.sessionId,
493
+ editRevision: session.currentRevision,
494
+ requestId,
495
+ checkIds,
496
+ lanes: checkLanes(repository, checkIds),
497
+ processes: {},
498
+ reviewer: input.reviewer,
499
+ note: input.note,
500
+ timeoutMs: input.timeoutMs,
501
+ leaseWaitMs: typeof input.leaseWaitMs === 'number' && Number.isFinite(input.leaseWaitMs)
502
+ ? Math.max(0, Math.min(Math.trunc(input.leaseWaitMs), 15_000))
503
+ : 0,
504
+ status: 'running',
505
+ createdAt: now(),
506
+ updatedAt: now(),
507
+ });
508
+ return reconcileEditValidationRun(controllerHome, repository, run);
509
+ }
510
+
511
+ export interface PendingEditValidationSummary {
512
+ repositoryId: string;
513
+ examined: number;
514
+ running: number;
515
+ completed: number;
516
+ failed: number;
517
+ errors: Array<{ validationId: string; error: string }>;
518
+ truncated: boolean;
519
+ }
520
+
521
+ export async function reconcilePendingEditValidations(
522
+ controllerHome: string,
523
+ repository: RepositoryRecord,
524
+ limit = 200,
525
+ ): Promise<PendingEditValidationSummary> {
526
+ const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 2_000));
527
+ const index = readIndex(controllerHome, repository.repoId);
528
+ const ids = index.validationIds.slice(0, boundedLimit);
529
+ const summary: PendingEditValidationSummary = {
530
+ repositoryId: repository.repoId,
531
+ examined: ids.length,
532
+ running: 0,
533
+ completed: 0,
534
+ failed: 0,
535
+ errors: [],
536
+ truncated: index.validationIds.length > boundedLimit,
537
+ };
538
+ const stale: string[] = [];
539
+ for (const id of ids) {
540
+ const run = readRun(controllerHome, repository.repoId, id);
541
+ if (!run || run.status !== 'running') {
542
+ stale.push(id);
543
+ continue;
544
+ }
545
+ try {
546
+ const result = await reconcileEditValidationRun(controllerHome, repository, run);
547
+ if (!result.completed) summary.running += 1;
548
+ else if (result.ok === true) summary.completed += 1;
549
+ else summary.failed += 1;
550
+ } catch (error) {
551
+ summary.errors.push({ validationId: id, error: error instanceof Error ? error.message : String(error) });
552
+ }
553
+ }
554
+ for (const id of stale) setIndexMembership(controllerHome, repository.repoId, id, false);
555
+ return summary;
556
+ }
@@ -3,7 +3,7 @@ import { existsSync } from 'fs';
3
3
  import { join } from 'path';
4
4
  import { repositoryControllerRoot } from '../../../cli/repositories/controller-home';
5
5
  import { readJsonFile, sanitizeFileComponent } from '../../shared/json-files';
6
- import { listControlPlaneRecords, readOrImportControlPlaneRecord, writeControlPlaneRecord } from '../persistence/sqlite-store';
6
+ import { listControlPlaneRecords, mutateControlPlaneRecord, readOrImportControlPlaneRecord, writeControlPlaneRecord } from '../persistence/sqlite-store';
7
7
 
8
8
  export const WORK_HANDLE_STATES = [
9
9
  'prepared',
@@ -102,6 +102,33 @@ export interface WorkValidationRunState {
102
102
  processes: Record<string, { processId: string; requestId: string }>;
103
103
  }
104
104
 
105
+ interface WorkValidationIndex {
106
+ schemaVersion: 1;
107
+ updatedAt: string;
108
+ workIds: string[];
109
+ }
110
+
111
+ const WORK_VALIDATION_INDEX_NAMESPACE = 'execution_work_validation_index';
112
+ const WORK_VALIDATION_INDEX_KEY = 'v1';
113
+
114
+ function normalizedWorkIds(values: readonly string[]): string[] {
115
+ return [...new Set(values.filter((value) => typeof value === 'string' && value.length > 0))].sort();
116
+ }
117
+
118
+ function isValidationPending(handle: Pick<WorkHandleState, 'state' | 'validationRun'>): boolean {
119
+ return handle.state === 'validating' && Boolean(handle.validationRun);
120
+ }
121
+
122
+ function readValidationIndex(controllerHome: string, repositoryId: string): WorkValidationIndex | undefined {
123
+ return readOrImportControlPlaneRecord<WorkValidationIndex>(controllerHome, {
124
+ namespace: WORK_VALIDATION_INDEX_NAMESPACE,
125
+ scope: repositoryId,
126
+ key: WORK_VALIDATION_INDEX_KEY,
127
+ schemaVersion: 1,
128
+ readLegacy: () => undefined,
129
+ })?.value;
130
+ }
131
+
105
132
  export interface WorkHandleState {
106
133
  schemaVersion: 1;
107
134
  /** SQLite envelope revision used for compare-and-swap lifecycle writes. */
@@ -173,6 +200,100 @@ export function listWorkHandles(controllerHome: string, repositoryId: string, li
173
200
  }).map((record) => ({ ...record.value, recordRevision: record.revision }));
174
201
  }
175
202
 
203
+ function replaceValidationIndex(
204
+ controllerHome: string,
205
+ repositoryId: string,
206
+ workIds: readonly string[],
207
+ action: string,
208
+ ): WorkValidationIndex {
209
+ return mutateControlPlaneRecord<WorkValidationIndex>(controllerHome, {
210
+ namespace: WORK_VALIDATION_INDEX_NAMESPACE,
211
+ scope: repositoryId,
212
+ key: WORK_VALIDATION_INDEX_KEY,
213
+ schemaVersion: 1,
214
+ action,
215
+ mutate: () => ({
216
+ schemaVersion: 1,
217
+ updatedAt: now(),
218
+ workIds: normalizedWorkIds(workIds),
219
+ }),
220
+ }).value;
221
+ }
222
+
223
+ function rebuildValidationIndex(controllerHome: string, repositoryId: string): WorkValidationIndex {
224
+ const workIds = listWorkHandles(controllerHome, repositoryId, 5_000)
225
+ .filter(isValidationPending)
226
+ .map((handle) => handle.workId);
227
+ return replaceValidationIndex(
228
+ controllerHome,
229
+ repositoryId,
230
+ workIds,
231
+ 'work_validation_index_rebuild',
232
+ );
233
+ }
234
+
235
+ function ensureValidationIndex(controllerHome: string, repositoryId: string): WorkValidationIndex {
236
+ return readValidationIndex(controllerHome, repositoryId) ?? rebuildValidationIndex(controllerHome, repositoryId);
237
+ }
238
+
239
+ function updateValidationIndexMembership(
240
+ controllerHome: string,
241
+ repositoryId: string,
242
+ workId: string,
243
+ validating: boolean,
244
+ ): void {
245
+ const existing = readValidationIndex(controllerHome, repositoryId);
246
+ if (!existing) {
247
+ // The WorkHandle write is already durable. Rebuild once from authoritative
248
+ // handles so legacy repositories establish an exact index without a scan
249
+ // on every scheduler tick.
250
+ rebuildValidationIndex(controllerHome, repositoryId);
251
+ return;
252
+ }
253
+ mutateControlPlaneRecord<WorkValidationIndex>(controllerHome, {
254
+ namespace: WORK_VALIDATION_INDEX_NAMESPACE,
255
+ scope: repositoryId,
256
+ key: WORK_VALIDATION_INDEX_KEY,
257
+ schemaVersion: 1,
258
+ action: 'work_validation_index_update',
259
+ mutate: (current) => {
260
+ const base = current?.value ?? existing;
261
+ const workIds = new Set(base.workIds);
262
+ if (validating) workIds.add(workId); else workIds.delete(workId);
263
+ return { schemaVersion: 1, updatedAt: now(), workIds: [...workIds].sort() };
264
+ },
265
+ });
266
+ }
267
+
268
+ export function listValidatingWorkHandles(
269
+ controllerHome: string,
270
+ repositoryId: string,
271
+ limit = 500,
272
+ ): WorkHandleState[] {
273
+ const boundedLimit = Math.max(1, Math.min(Math.trunc(limit), 5_000));
274
+ const index = ensureValidationIndex(controllerHome, repositoryId);
275
+ const handles: WorkHandleState[] = [];
276
+ const staleIds: string[] = [];
277
+ for (const workId of index.workIds.slice(0, boundedLimit)) {
278
+ const handle = readWorkHandle(controllerHome, repositoryId, workId);
279
+ if (!handle || !isValidationPending(handle)) {
280
+ staleIds.push(workId);
281
+ continue;
282
+ }
283
+ handles.push(handle);
284
+ }
285
+ if (staleIds.length > 0) {
286
+ const stale = new Set(staleIds);
287
+ replaceValidationIndex(
288
+ controllerHome,
289
+ repositoryId,
290
+ index.workIds.filter((workId) => !stale.has(workId)),
291
+ 'work_validation_index_reconcile',
292
+ );
293
+ }
294
+ return handles;
295
+ }
296
+
176
297
  export function writeWorkHandle(controllerHome: string, handle: WorkHandleState): WorkHandleState {
177
298
  const { recordRevision, ...persistedHandle } = handle;
178
299
  const updated: Omit<WorkHandleState, 'recordRevision'> = { ...persistedHandle, updatedAt: now() };
@@ -185,6 +306,12 @@ export function writeWorkHandle(controllerHome: string, handle: WorkHandleState)
185
306
  action: 'work_handle_write',
186
307
  ...(recordRevision !== undefined ? { expectedRevision: recordRevision } : {}),
187
308
  });
309
+ updateValidationIndexMembership(
310
+ controllerHome,
311
+ record.value.repositoryId,
312
+ record.value.workId,
313
+ isValidationPending(record.value),
314
+ );
188
315
  return { ...record.value, recordRevision: record.revision };
189
316
  }
190
317