@ccdd/project 2.0.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 (163) hide show
  1. package/README.md +25 -0
  2. package/dist/monitor-ui/assets/GraphView-CYMJ1Nb7.css +1 -0
  3. package/dist/monitor-ui/assets/GraphView-UjtW4QRC.js +27 -0
  4. package/dist/monitor-ui/assets/ValidationView-CC1FvqUl.js +1 -0
  5. package/dist/monitor-ui/assets/ValidationView-DWl4b8Ug.css +1 -0
  6. package/dist/monitor-ui/assets/_plugin-vue_export-helper-BDNMzG2s.js +1 -0
  7. package/dist/monitor-ui/assets/elk-api-yRi2AOS6.js +1 -0
  8. package/dist/monitor-ui/assets/elk.bundled-CtLobQTy.js +24 -0
  9. package/dist/monitor-ui/assets/graph-layout-worker-CgfWoa9t.js +22 -0
  10. package/dist/monitor-ui/assets/index-CUzg-dht.css +1 -0
  11. package/dist/monitor-ui/assets/index-Cpm0fC0p.js +8 -0
  12. package/dist/monitor-ui/index.html +15 -0
  13. package/dist/scripts/prepare-demo.d.ts +26 -0
  14. package/dist/scripts/prepare-demo.js +121 -0
  15. package/dist/scripts/prepare-demo.js.map +1 -0
  16. package/dist/src/artifacts/groups.d.ts +9 -0
  17. package/dist/src/artifacts/groups.js +77 -0
  18. package/dist/src/artifacts/groups.js.map +1 -0
  19. package/dist/src/artifacts/human.d.ts +38 -0
  20. package/dist/src/artifacts/human.js +156 -0
  21. package/dist/src/artifacts/human.js.map +1 -0
  22. package/dist/src/artifacts/index.d.ts +149 -0
  23. package/dist/src/artifacts/index.js +354 -0
  24. package/dist/src/artifacts/index.js.map +1 -0
  25. package/dist/src/artifacts/instruction.d.ts +23 -0
  26. package/dist/src/artifacts/instruction.js +94 -0
  27. package/dist/src/artifacts/instruction.js.map +1 -0
  28. package/dist/src/artifacts/mcp-server.d.ts +6 -0
  29. package/dist/src/artifacts/mcp-server.js +96 -0
  30. package/dist/src/artifacts/mcp-server.js.map +1 -0
  31. package/dist/src/artifacts/tool-check.d.ts +40 -0
  32. package/dist/src/artifacts/tool-check.js +113 -0
  33. package/dist/src/artifacts/tool-check.js.map +1 -0
  34. package/dist/src/artifacts/types.d.ts +43 -0
  35. package/dist/src/artifacts/types.js +97 -0
  36. package/dist/src/artifacts/types.js.map +1 -0
  37. package/dist/src/broker/config.d.ts +11 -0
  38. package/dist/src/broker/config.js +141 -0
  39. package/dist/src/broker/config.js.map +1 -0
  40. package/dist/src/broker/graph.d.ts +69 -0
  41. package/dist/src/broker/graph.js +94 -0
  42. package/dist/src/broker/graph.js.map +1 -0
  43. package/dist/src/broker/index.d.ts +170 -0
  44. package/dist/src/broker/index.js +931 -0
  45. package/dist/src/broker/index.js.map +1 -0
  46. package/dist/src/cli.d.ts +24 -0
  47. package/dist/src/cli.js +340 -0
  48. package/dist/src/cli.js.map +1 -0
  49. package/dist/src/contracts.d.ts +116 -0
  50. package/dist/src/contracts.js +2 -0
  51. package/dist/src/contracts.js.map +1 -0
  52. package/dist/src/definitions.d.ts +53 -0
  53. package/dist/src/definitions.js +2 -0
  54. package/dist/src/definitions.js.map +1 -0
  55. package/dist/src/doctor/index.d.ts +41 -0
  56. package/dist/src/doctor/index.js +189 -0
  57. package/dist/src/doctor/index.js.map +1 -0
  58. package/dist/src/executors/auth.d.ts +16 -0
  59. package/dist/src/executors/auth.js +150 -0
  60. package/dist/src/executors/auth.js.map +1 -0
  61. package/dist/src/executors/errors.d.ts +6 -0
  62. package/dist/src/executors/errors.js +10 -0
  63. package/dist/src/executors/errors.js.map +1 -0
  64. package/dist/src/executors/index.d.ts +20 -0
  65. package/dist/src/executors/index.js +262 -0
  66. package/dist/src/executors/index.js.map +1 -0
  67. package/dist/src/executors/pi.d.ts +32 -0
  68. package/dist/src/executors/pi.js +190 -0
  69. package/dist/src/executors/pi.js.map +1 -0
  70. package/dist/src/executors/process.d.ts +22 -0
  71. package/dist/src/executors/process.js +70 -0
  72. package/dist/src/executors/process.js.map +1 -0
  73. package/dist/src/local.d.ts +13 -0
  74. package/dist/src/local.js +20 -0
  75. package/dist/src/local.js.map +1 -0
  76. package/dist/src/monitor/actions.d.ts +12 -0
  77. package/dist/src/monitor/actions.js +74 -0
  78. package/dist/src/monitor/actions.js.map +1 -0
  79. package/dist/src/monitor/server.d.ts +8 -0
  80. package/dist/src/monitor/server.js +493 -0
  81. package/dist/src/monitor/server.js.map +1 -0
  82. package/dist/src/monitor/store.d.ts +22 -0
  83. package/dist/src/monitor/store.js +517 -0
  84. package/dist/src/monitor/store.js.map +1 -0
  85. package/dist/src/monitor/types.d.ts +143 -0
  86. package/dist/src/monitor/types.js +2 -0
  87. package/dist/src/monitor/types.js.map +1 -0
  88. package/dist/src/monitor/ui/api.d.ts +11 -0
  89. package/dist/src/monitor/ui/api.js +29 -0
  90. package/dist/src/monitor/ui/api.js.map +1 -0
  91. package/dist/src/monitor/ui/critic-presentation.d.ts +11 -0
  92. package/dist/src/monitor/ui/critic-presentation.js +60 -0
  93. package/dist/src/monitor/ui/critic-presentation.js.map +1 -0
  94. package/dist/src/monitor/ui/format.d.ts +9 -0
  95. package/dist/src/monitor/ui/format.js +22 -0
  96. package/dist/src/monitor/ui/format.js.map +1 -0
  97. package/dist/src/monitor/ui/graph-layout.d.ts +43 -0
  98. package/dist/src/monitor/ui/graph-layout.js +191 -0
  99. package/dist/src/monitor/ui/graph-layout.js.map +1 -0
  100. package/dist/src/monitor/ui/tool-content.d.ts +13 -0
  101. package/dist/src/monitor/ui/tool-content.js +22 -0
  102. package/dist/src/monitor/ui/tool-content.js.map +1 -0
  103. package/dist/src/monitor/ui/tool-input.d.ts +23 -0
  104. package/dist/src/monitor/ui/tool-input.js +112 -0
  105. package/dist/src/monitor/ui/tool-input.js.map +1 -0
  106. package/dist/src/project/cli.d.ts +9 -0
  107. package/dist/src/project/cli.js +306 -0
  108. package/dist/src/project/cli.js.map +1 -0
  109. package/dist/src/project/graph.d.ts +4 -0
  110. package/dist/src/project/graph.js +26 -0
  111. package/dist/src/project/graph.js.map +1 -0
  112. package/dist/src/project/identity.d.ts +5 -0
  113. package/dist/src/project/identity.js +87 -0
  114. package/dist/src/project/identity.js.map +1 -0
  115. package/dist/src/project/index.d.ts +19 -0
  116. package/dist/src/project/index.js +26 -0
  117. package/dist/src/project/index.js.map +1 -0
  118. package/dist/src/project/query.d.ts +9 -0
  119. package/dist/src/project/query.js +136 -0
  120. package/dist/src/project/query.js.map +1 -0
  121. package/dist/src/project/store.d.ts +15 -0
  122. package/dist/src/project/store.js +47 -0
  123. package/dist/src/project/store.js.map +1 -0
  124. package/dist/src/project/types.d.ts +106 -0
  125. package/dist/src/project/types.js +2 -0
  126. package/dist/src/project/types.js.map +1 -0
  127. package/dist/src/requester/index.d.ts +16 -0
  128. package/dist/src/requester/index.js +96 -0
  129. package/dist/src/requester/index.js.map +1 -0
  130. package/dist/src/runtime-paths.d.ts +1 -0
  131. package/dist/src/runtime-paths.js +4 -0
  132. package/dist/src/runtime-paths.js.map +1 -0
  133. package/dist/src/sdk.d.ts +9 -0
  134. package/dist/src/sdk.js +3 -0
  135. package/dist/src/sdk.js.map +1 -0
  136. package/dist/src/tools/contracts.d.ts +108 -0
  137. package/dist/src/tools/contracts.js +2 -0
  138. package/dist/src/tools/contracts.js.map +1 -0
  139. package/dist/src/tools/host.d.ts +7 -0
  140. package/dist/src/tools/host.js +98 -0
  141. package/dist/src/tools/host.js.map +1 -0
  142. package/dist/src/tools/paths.d.ts +2 -0
  143. package/dist/src/tools/paths.js +18 -0
  144. package/dist/src/tools/paths.js.map +1 -0
  145. package/dist/src/tools/runner.d.ts +54 -0
  146. package/dist/src/tools/runner.js +194 -0
  147. package/dist/src/tools/runner.js.map +1 -0
  148. package/dist/src/tools/runtime-host.d.ts +1 -0
  149. package/dist/src/tools/runtime-host.js +116 -0
  150. package/dist/src/tools/runtime-host.js.map +1 -0
  151. package/dist/src/tools/schema.d.ts +6 -0
  152. package/dist/src/tools/schema.js +106 -0
  153. package/dist/src/tools/schema.js.map +1 -0
  154. package/dist/src/worker-client.d.ts +27 -0
  155. package/dist/src/worker-client.js +96 -0
  156. package/dist/src/worker-client.js.map +1 -0
  157. package/dist/src/worker.d.ts +1 -0
  158. package/dist/src/worker.js +50 -0
  159. package/dist/src/worker.js.map +1 -0
  160. package/dist/src/workspaces/index.d.ts +33 -0
  161. package/dist/src/workspaces/index.js +394 -0
  162. package/dist/src/workspaces/index.js.map +1 -0
  163. package/package.json +26 -0
@@ -0,0 +1,931 @@
1
+ import { DatabaseSync } from 'node:sqlite';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { execFileSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ import { isDeepStrictEqual } from 'node:util';
7
+ import { setTimeout as delay } from 'node:timers/promises';
8
+ import { prepareReviewRequests, readStoredArtifactScope } from '../requester/index.js';
9
+ import { readWorkspaceConfig } from './config.js';
10
+ import { createGraphDefinition, prerequisiteCriticIds } from './graph.js';
11
+ import { createReviewTools } from '../tools/runner.js';
12
+ import { prepareWorkspace, reopenWorkspace } from '../workspaces/index.js';
13
+ import { createProjectSnapshot } from '../project/identity.js';
14
+ import { includedCritics, planProject } from '../project/query.js';
15
+ import { readEvidence } from '../project/store.js';
16
+ const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ const errorCode = (error) => object(error) && typeof error.code === 'string' ? error.code : undefined;
18
+ const parseStored = (value) => { if (typeof value !== 'string')
19
+ throw new Error('Broker store contains a non-text JSON record.'); return JSON.parse(value); };
20
+ const required = (value, label) => { if (value == null)
21
+ throw new Error(`Unknown ${label}.`); return value; };
22
+ const terminal = new Set(['GREEN', 'RED', 'ERROR', 'INCOMPLETE']);
23
+ const now = () => new Date().toISOString();
24
+ const errorText = (error) => String(object(error) && typeof error.message === 'string' ? error.message : error).slice(0, 2000);
25
+ const copy = (value) => JSON.parse(JSON.stringify(value));
26
+ const codedError = (message, code) => Object.assign(new Error(message), { code });
27
+ const MAX_CONCURRENT_EXECUTORS = 4;
28
+ const fatalExecutionError = (error) => {
29
+ const code = errorCode(error);
30
+ return Boolean(code?.startsWith('WORKSPACE_') || ['RUN_OWNERSHIP_LOST', 'REVIEW_CANCELED', 'WORKER_STOPPED', 'WORKER_EXITED', 'REVIEW_GRAPH_INVALID'].includes(code ?? ''));
31
+ };
32
+ function storedObservation(value) {
33
+ if (!object(value) || typeof value.artifactId !== 'string' || typeof value.operation !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,63}$/.test(value.operation))
34
+ return undefined;
35
+ const observation = { artifactId: value.artifactId.slice(0, 64), operation: value.operation };
36
+ if (value.kind === 'content' || value.kind === 'empty')
37
+ observation.kind = value.kind;
38
+ if (typeof value.detail === 'string')
39
+ observation.detail = value.detail.slice(0, 2000);
40
+ for (const key of ['startLine', 'endLine', 'lineCount', 'totalLines']) {
41
+ const number = value[key];
42
+ if (number === null || (typeof number === 'number' && Number.isSafeInteger(number) && number >= 0))
43
+ observation[key] = number;
44
+ }
45
+ return observation;
46
+ }
47
+ function validateResult(value) {
48
+ if (!object(value) || (value.verdict !== 'GREEN' && value.verdict !== 'RED') || typeof value.summary !== 'string' || !value.summary.trim() ||
49
+ !Array.isArray(value.evidence) || value.evidence.some(item => typeof item !== 'string')) {
50
+ throw new Error('Review result requires GREEN/RED verdict, a summary, and string evidence[].');
51
+ }
52
+ const result = { verdict: value.verdict, summary: value.summary.slice(0, 12_000), evidence: value.evidence.slice(0, 100).map(item => item.slice(0, 4000)) };
53
+ for (const key of ['provider', 'model', 'stdout', 'stderr']) {
54
+ const field = value[key];
55
+ if (typeof field === 'string')
56
+ result[key] = field.slice(0, 24_000);
57
+ }
58
+ for (const key of ['durationMs', 'exitCode']) {
59
+ const field = value[key];
60
+ if (typeof field === 'number' && Number.isFinite(field))
61
+ result[key] = field;
62
+ }
63
+ if (Array.isArray(value.toolCalls))
64
+ result.toolCalls = value.toolCalls.slice(0, 100).filter((item) => object(item) && typeof item.name === 'string').map(item => {
65
+ const call = { name: item.name, ...(item.arguments === undefined ? {} : { arguments: copy(item.arguments) }) };
66
+ const observation = storedObservation(item.observation);
67
+ if (observation)
68
+ call.observation = observation;
69
+ return call;
70
+ });
71
+ if (JSON.stringify(result).length > 256_000)
72
+ throw new Error('Review result exceeds the supported size.');
73
+ return result;
74
+ }
75
+ function processIdentity(pid) {
76
+ try {
77
+ if (process.platform === 'linux') {
78
+ const stat = fs.readFileSync(`/proc/${pid}/stat`, 'utf8');
79
+ return `linux:${stat.slice(stat.lastIndexOf(')') + 2).split(' ')[19]}`;
80
+ }
81
+ return `ps:${execFileSync('ps', ['-p', String(pid), '-o', 'lstart='], { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'ignore'] }).trim()}`;
82
+ }
83
+ catch {
84
+ return null;
85
+ }
86
+ }
87
+ const ownProcessIdentity = processIdentity(process.pid);
88
+ function ownerAlive(owner) {
89
+ if (!owner || !Number.isSafeInteger(owner.pid) || owner.pid <= 0)
90
+ return false;
91
+ try {
92
+ process.kill(owner.pid, 0);
93
+ }
94
+ catch (error) {
95
+ if (errorCode(error) === 'ESRCH')
96
+ return false;
97
+ }
98
+ const identity = owner.pid === process.pid ? ownProcessIdentity : processIdentity(owner.pid);
99
+ return !owner.process_identity || !identity || owner.process_identity === identity;
100
+ }
101
+ function canonicalFuturePath(value) {
102
+ let existing = path.resolve(value);
103
+ const missing = [];
104
+ while (!fs.existsSync(existing)) {
105
+ const parent = path.dirname(existing);
106
+ if (parent === existing)
107
+ break;
108
+ missing.unshift(path.basename(existing));
109
+ existing = parent;
110
+ }
111
+ return path.join(fs.realpathSync(existing), ...missing);
112
+ }
113
+ /** Read an existing store's repository identity without creating state or needing its source. */
114
+ export function readStateContext(stateDir) {
115
+ if (typeof stateDir !== 'string' || !stateDir)
116
+ throw new Error('An existing stateDir is required.');
117
+ const canonical = fs.realpathSync(stateDir);
118
+ const filename = path.join(canonical, 'broker.sqlite');
119
+ if (!fs.statSync(filename).isFile())
120
+ throw new Error('State directory does not contain a broker store.');
121
+ const database = new DatabaseSync(filename, { readOnly: true });
122
+ try {
123
+ const row = database.prepare('SELECT value FROM metadata WHERE key = ?').get('registered-repo');
124
+ const identity = row ? parseStored(row.value) : null;
125
+ if (!object(identity) || typeof identity.repoPath !== 'string' || !path.isAbsolute(identity.repoPath) || typeof identity.repoId !== 'string')
126
+ throw new Error('State directory has no valid repository identity.');
127
+ return { repoPath: identity.repoPath, repoId: identity.repoId, stateDir: canonical };
128
+ }
129
+ finally {
130
+ database.close();
131
+ }
132
+ }
133
+ function prepareStateDirectory(repoPath, stateDir) {
134
+ if (typeof stateDir !== 'string' || !stateDir)
135
+ throw new Error('An external stateDir is required.');
136
+ const canonical = canonicalFuturePath(stateDir);
137
+ const relative = path.relative(repoPath, canonical);
138
+ if (relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))) {
139
+ throw codedError('CCDD state, logs and copied workspaces must be outside the reviewed repository.', 'WORKSPACE_UNSAFE');
140
+ }
141
+ fs.mkdirSync(canonical, { recursive: true, mode: 0o700 });
142
+ return fs.realpathSync(canonical);
143
+ }
144
+ /** Durable broker operations. Merely opening the store never starts a worker. */
145
+ export function createBroker({ repoPath, stateDir, repoId = 'demo', executors, workspaceAdapter = { prepareWorkspace, reopenWorkspace } }) {
146
+ try {
147
+ repoPath = fs.realpathSync(repoPath);
148
+ if (!fs.statSync(repoPath).isDirectory())
149
+ throw new Error('Workspace must be a directory.');
150
+ }
151
+ catch (error) {
152
+ if (errorCode(error) !== 'ENOENT')
153
+ throw error;
154
+ const identity = readStateContext(stateDir);
155
+ if (canonicalFuturePath(repoPath) !== identity.repoPath)
156
+ throw new Error('This state directory belongs to a different registered repository.');
157
+ repoPath = identity.repoPath;
158
+ }
159
+ stateDir = prepareStateDirectory(repoPath, stateDir);
160
+ if (typeof repoId !== 'string' || !repoId.trim() || repoId.length > 200)
161
+ throw new Error('A registered repoId is required (maximum 200 characters).');
162
+ let db;
163
+ try {
164
+ db = new DatabaseSync(path.join(stateDir, 'broker.sqlite'));
165
+ db.exec(`PRAGMA busy_timeout=5000; PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON;
166
+ CREATE TABLE IF NOT EXISTS runs (id TEXT PRIMARY KEY, created_at TEXT NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
167
+ CREATE TABLE IF NOT EXISTS requests (id TEXT PRIMARY KEY, run_id TEXT NOT NULL REFERENCES runs(id), ordinal INTEGER NOT NULL, status TEXT NOT NULL, data TEXT NOT NULL);
168
+ CREATE INDEX IF NOT EXISTS requests_run ON requests(run_id, ordinal);
169
+ CREATE INDEX IF NOT EXISTS requests_validation_input ON requests(json_extract(data, '$.validationInput.key'));
170
+ CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, run_id TEXT NOT NULL REFERENCES runs(id), request_id TEXT, created_at TEXT NOT NULL, type TEXT NOT NULL, message TEXT NOT NULL, data TEXT);
171
+ CREATE TABLE IF NOT EXISTS metadata (key TEXT PRIMARY KEY, value TEXT NOT NULL);
172
+ CREATE TABLE IF NOT EXISTS run_owners (run_id TEXT PRIMARY KEY REFERENCES runs(id), pid INTEGER NOT NULL, process_identity TEXT, token TEXT NOT NULL, claimed_at TEXT NOT NULL);`);
173
+ const previousRepo = db.prepare('SELECT value FROM metadata WHERE key = ?').get('registered-repo');
174
+ const identity = JSON.stringify({ repoPath, repoId });
175
+ if (previousRepo && previousRepo.value !== identity)
176
+ throw new Error('This state directory belongs to a different registered repository.');
177
+ db.prepare('INSERT OR IGNORE INTO metadata(key,value) VALUES (?,?)').run('registered-repo', identity);
178
+ // Recheck after INSERT OR IGNORE, since another process may initialize concurrently.
179
+ if (db.prepare('SELECT value FROM metadata WHERE key = ?').get('registered-repo')?.value !== identity)
180
+ throw new Error('This state directory belongs to a different registered repository.');
181
+ }
182
+ catch (error) {
183
+ db?.close();
184
+ throw error;
185
+ }
186
+ let closed = false;
187
+ let closing = false;
188
+ const active = new Map();
189
+ const listeners = new Set();
190
+ const ensureOpen = () => { if (closed || closing)
191
+ throw new Error('Broker is closed.'); };
192
+ const requireExecutors = () => {
193
+ if (!executors || typeof executors.canExecute !== 'function' || typeof executors.execute !== 'function')
194
+ throw new Error('Review submission and execution require an executor registry.');
195
+ return executors;
196
+ };
197
+ const transaction = (callback) => {
198
+ db.exec('BEGIN IMMEDIATE');
199
+ try {
200
+ const result = callback();
201
+ db.exec('COMMIT');
202
+ return result;
203
+ }
204
+ catch (error) {
205
+ db.exec('ROLLBACK');
206
+ throw error;
207
+ }
208
+ };
209
+ const requestData = (id) => {
210
+ const row = db.prepare('SELECT data FROM requests WHERE id = ?').get(id);
211
+ return row ? parseStored(row.data) : null;
212
+ };
213
+ const runData = (id) => {
214
+ const row = db.prepare('SELECT data FROM runs WHERE id = ?').get(id);
215
+ return row ? parseStored(row.data) : null;
216
+ };
217
+ const ownerData = (id) => db.prepare('SELECT * FROM run_owners WHERE run_id = ?').get(id);
218
+ const runRequests = (id) => db.prepare('SELECT data FROM requests WHERE run_id = ? ORDER BY ordinal').all(id).map(row => parseStored(row.data));
219
+ const saveRequest = (request) => db.prepare('UPDATE requests SET status = ?, data = ? WHERE id = ?').run(request.status, JSON.stringify(request), request.id);
220
+ const saveRun = (run) => db.prepare('UPDATE runs SET status = ?, data = ? WHERE id = ?').run(run.status, JSON.stringify(run), run.id);
221
+ const appendEvent = (runId, requestId, type, message, data = null) => {
222
+ db.prepare('INSERT INTO events(run_id,request_id,created_at,type,message,data) VALUES (?,?,?,?,?,?)').run(runId, requestId, now(), type, message.slice(0, 4000), data === null ? null : JSON.stringify(data));
223
+ };
224
+ const changed = () => { for (const callback of listeners) {
225
+ try {
226
+ callback();
227
+ }
228
+ catch { }
229
+ } };
230
+ function prerequisites(request, run, requests) {
231
+ if (run.project)
232
+ return []; // Project requests are issued only after a pull readiness check.
233
+ if (run.scope?.kind === 'critic')
234
+ return [];
235
+ if (run.graph) {
236
+ const byCritic = new Map(requests.map(item => [item.criticId, item]));
237
+ return prerequisiteCriticIds(request, run.graph).map(id => {
238
+ const dependency = byCritic.get(id);
239
+ if (!dependency || dependency.runId !== run.id)
240
+ throw codedError(`Required Critic ${id} is missing from this Run.`, 'REVIEW_GRAPH_INVALID');
241
+ return dependency;
242
+ });
243
+ }
244
+ // Historical chains retain their explicit links; no target Artifact is guessed.
245
+ if (request.predecessorId === null || (request.predecessorId === undefined && requests.length === 1))
246
+ return [];
247
+ const previous = requests.find(item => item.id === request.predecessorId);
248
+ if (!previous || previous.runId !== run.id)
249
+ throw codedError('This historical Run has no usable predecessor link. Submit a new review.', 'REVIEW_GRAPH_INVALID');
250
+ return [previous];
251
+ }
252
+ function dependencyFailure(request, run, requests, seen = new Set()) {
253
+ if (seen.has(request.id))
254
+ return null;
255
+ seen.add(request.id);
256
+ for (const dependency of prerequisites(request, run, requests)) {
257
+ if (dependency.status === 'RED' || dependency.status === 'ERROR')
258
+ return dependency;
259
+ const failure = dependencyFailure(dependency, run, requests, seen);
260
+ if (failure)
261
+ return failure;
262
+ }
263
+ return null;
264
+ }
265
+ function refreshReadinessWithin(runId) {
266
+ const run = required(runData(runId), 'Run'), requests = runRequests(runId);
267
+ if (run.project) {
268
+ if (terminal.has(run.status))
269
+ return;
270
+ const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: requests });
271
+ for (const item of plan.items.filter(item => item.action === 'EXECUTE')) {
272
+ const envelope = run.project.templates.find(template => template.criticId === item.id);
273
+ if (!envelope)
274
+ throw codedError(`Missing prepared Critic ${item.id}.`, 'REVIEW_GRAPH_INVALID');
275
+ const request = { ...copy(envelope), validationInput: copy(item.input), id: randomUUID(), runId,
276
+ workspace: run.workspace, worktreePath: run.workspace.path, status: 'QUEUED', createdAt: now(),
277
+ startedAt: null, completedAt: null, claimedBy: null, claimedAt: null, notifiedAt: null, result: null, error: null, blockedReason: null };
278
+ db.prepare('INSERT INTO requests(id,run_id,ordinal,status,data) VALUES (?,?,?,?,?)').run(request.id, runId, requests.length, request.status, JSON.stringify(request));
279
+ requests.push(request);
280
+ appendEvent(runId, request.id, 'request.queued', 'Pull validation found an executable Critic requiring an actual review.');
281
+ }
282
+ return;
283
+ }
284
+ for (const request of requests) {
285
+ if (request.status !== 'BLOCKED')
286
+ continue;
287
+ const dependencies = prerequisites(request, run, requests);
288
+ if (dependencies.every(item => item.status === 'GREEN')) {
289
+ request.status = 'QUEUED';
290
+ request.blockedReason = null;
291
+ saveRequest(request);
292
+ appendEvent(runId, request.id, 'request.queued', 'All required Artifact reviews are GREEN.');
293
+ }
294
+ else {
295
+ const failed = dependencyFailure(request, run, requests);
296
+ const reason = failed ? `${failed.criticId} returned ${failed.status}; dependent reviews cannot start.` : `Waiting for ${dependencies.filter(item => item.status !== 'GREEN').map(item => item.criticId).join(', ')} to become GREEN.`;
297
+ if (request.blockedReason !== reason) {
298
+ request.blockedReason = reason;
299
+ saveRequest(request);
300
+ }
301
+ }
302
+ }
303
+ }
304
+ const updateRunStatus = (runId) => {
305
+ const states = runRequests(runId).map(request => request.status);
306
+ const run = required(runData(runId), 'Run');
307
+ if (run.project) {
308
+ if (terminal.has(run.status))
309
+ return;
310
+ const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
311
+ const status = states.includes('RUNNING') ? 'RUNNING' : states.includes('QUEUED') ? 'QUEUED' : states.includes('WAITING_HUMAN') ? 'WAITING_HUMAN' :
312
+ states.includes('ERROR') ? 'ERROR' : states.includes('RED') ? 'RED' : plan.satisfied ? 'GREEN' : 'INCOMPLETE';
313
+ if (run.status !== status) {
314
+ run.status = status;
315
+ if (terminal.has(status)) {
316
+ run.completedAt = now();
317
+ run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
318
+ }
319
+ saveRun(run);
320
+ appendEvent(runId, null, 'run.status', `Run ${status}`, { status });
321
+ }
322
+ return;
323
+ }
324
+ // A branch failure is a verdict for that branch, not a stop signal for its siblings.
325
+ const status = states.includes('RUNNING') ? 'RUNNING' : states.includes('QUEUED') ? 'QUEUED' :
326
+ states.includes('WAITING_HUMAN') ? 'WAITING_HUMAN' : states.includes('ERROR') ? 'ERROR' : states.includes('RED') ? 'RED' :
327
+ states.length > 0 && states.every(state => state === 'GREEN') ? 'GREEN' : 'ERROR';
328
+ if (run.status !== status) {
329
+ run.status = status;
330
+ if (terminal.has(status))
331
+ run.completedAt = now();
332
+ else
333
+ delete run.completedAt;
334
+ saveRun(run);
335
+ appendEvent(runId, null, 'run.status', `Run ${status}`, { status });
336
+ }
337
+ };
338
+ function finishWithin(requestId, outcome, expectedStates = ['RUNNING', 'WAITING_HUMAN']) {
339
+ const { result, error } = outcome, hasError = Object.hasOwn(outcome, 'error');
340
+ const request = requestData(requestId);
341
+ if (!request || !expectedStates.includes(request.status) || terminal.has(required(runData(request.runId), 'Run').status))
342
+ return false;
343
+ request.status = hasError ? 'ERROR' : required(result, 'review result').verdict;
344
+ request.result = result ?? null;
345
+ request.error = hasError ? errorText(error) : null;
346
+ request.errorCode = errorCode(error) ?? null;
347
+ request.completedAt = now();
348
+ saveRequest(request);
349
+ appendEvent(request.runId, request.id, hasError ? 'request.error' : 'request.completed', hasError ? required(request.error, 'error message') : required(result, 'review result').summary, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
350
+ refreshReadinessWithin(request.runId);
351
+ updateRunStatus(request.runId);
352
+ return true;
353
+ }
354
+ function failWithin(runId, error) {
355
+ const run = runData(runId);
356
+ if (!run || terminal.has(run.status))
357
+ return;
358
+ for (const request of runRequests(runId)) {
359
+ if (terminal.has(request.status))
360
+ continue;
361
+ request.status = 'ERROR';
362
+ request.result = null;
363
+ request.error = errorText(error);
364
+ request.errorCode = errorCode(error) ?? null;
365
+ request.completedAt = now();
366
+ request.blockedReason = null;
367
+ saveRequest(request);
368
+ appendEvent(runId, request.id, 'request.error', request.error, { status: request.status, ...(request.errorCode ? { code: request.errorCode } : {}) });
369
+ }
370
+ if (run.project) {
371
+ const plan = planProject(run.project.snapshot, readEvidence(db), { selection: run.project.selection, recursive: run.project.recursive, force: run.project.force, runId, attempts: runRequests(runId) });
372
+ run.project.evidenceRequestIds = [...new Set(plan.critics.flatMap(c => c.result ? [c.result.requestId] : []))];
373
+ }
374
+ run.status = 'ERROR';
375
+ run.error = errorText(error);
376
+ run.completedAt = now();
377
+ saveRun(run);
378
+ appendEvent(runId, null, 'run.error', run.error);
379
+ }
380
+ function reconcileWithin(runId) {
381
+ const owner = ownerData(runId);
382
+ if (!owner || ownerAlive(owner))
383
+ return;
384
+ const run = runData(runId);
385
+ const unfinished = runRequests(runId).filter(request => !terminal.has(request.status));
386
+ const safeHumanWait = run?.workspace?.mode === 'copy' && unfinished.some(request => request.status === 'WAITING_HUMAN') &&
387
+ unfinished.every(request => request.status === 'BLOCKED' || (request.status === 'WAITING_HUMAN' && request.notifiedAt));
388
+ if (!safeHumanWait) {
389
+ failWithin(runId, codedError('The review worker exited before completing its work. Submit a new run to retry.', 'WORKER_EXITED'));
390
+ }
391
+ db.prepare('DELETE FROM run_owners WHERE run_id = ? AND token = ?').run(runId, owner.token);
392
+ appendEvent(runId, null, 'worker.exited', 'The previous review worker is no longer running.');
393
+ }
394
+ function reconcile(runId) {
395
+ ensureOpen();
396
+ transaction(() => {
397
+ if (runId === undefined)
398
+ for (const row of db.prepare('SELECT run_id FROM run_owners').all())
399
+ reconcileWithin(String(row.run_id));
400
+ else
401
+ reconcileWithin(runId);
402
+ });
403
+ }
404
+ function getRun(id) {
405
+ ensureOpen();
406
+ if (!runData(id))
407
+ return null;
408
+ if (ownerData(id))
409
+ reconcile(id);
410
+ const run = required(runData(id), 'Run');
411
+ const owner = ownerData(id);
412
+ const events = db.prepare('SELECT * FROM (SELECT * FROM events WHERE run_id = ? ORDER BY id DESC LIMIT 500) ORDER BY id').all(id).map(event => ({ id: Number(event.id), runId: String(event.run_id), requestId: event.request_id === null ? null : String(event.request_id), createdAt: String(event.created_at), type: String(event.type), message: String(event.message), ...(event.data ? { data: parseStored(event.data) } : {}) }));
413
+ return { ...run, scope: run.scope ?? { kind: run.graph ? 'graph' : 'chain' }, owner: owner ? { pid: owner.pid, claimedAt: owner.claimed_at } : null, requests: runRequests(id), events };
414
+ }
415
+ function failOwned(runId, token, error) {
416
+ transaction(() => { if (ownerData(runId)?.token === token)
417
+ failWithin(runId, error); });
418
+ changed();
419
+ }
420
+ async function executeOne(requestId, workspace, token, signal) {
421
+ const initial = required(requestData(requestId), 'Request');
422
+ const runId = initial.runId;
423
+ transaction(() => {
424
+ const request = required(requestData(requestId), 'Request');
425
+ if (ownerData(runId)?.token !== token || request.status !== 'QUEUED')
426
+ throw codedError('Review ownership changed before execution.', 'RUN_OWNERSHIP_LOST');
427
+ request.status = 'RUNNING';
428
+ request.startedAt = now();
429
+ request.blockedReason = null;
430
+ saveRequest(request);
431
+ appendEvent(runId, request.id, 'request.started', `${request.title}: reviewing ${workspace.descriptor.mode} workspace.`, { snapshotHash: workspace.descriptor.hash });
432
+ updateRunStatus(runId);
433
+ });
434
+ changed();
435
+ try {
436
+ signal.throwIfAborted();
437
+ await workspace.assertUnchanged();
438
+ const request = required(requestData(requestId), 'Request');
439
+ const runDir = path.join(stateDir, 'runs', runId, request.id);
440
+ fs.mkdirSync(runDir, { recursive: true, mode: 0o700 });
441
+ if (required(runData(runId), 'Run').project) {
442
+ const capability = await requireExecutors().canExecute(copy(request));
443
+ if (!capability?.ok)
444
+ throw codedError(capability?.reason ?? 'No compatible executor.', capability?.code ?? 'EXECUTOR_UNAVAILABLE');
445
+ }
446
+ if (request.profile.kind === 'human') {
447
+ if (typeof requireExecutors().notifyHuman !== 'function')
448
+ throw new Error('Human execution requires a registered alarm method.');
449
+ transaction(() => {
450
+ const current = required(requestData(requestId), 'Request');
451
+ if (current.status !== 'RUNNING')
452
+ throw codedError('Review was canceled before Human notification.', 'REVIEW_CANCELED');
453
+ current.status = 'WAITING_HUMAN';
454
+ saveRequest(current);
455
+ appendEvent(runId, requestId, 'human.waiting', 'Human review is ready; registered alarm delivery is pending.');
456
+ updateRunStatus(runId);
457
+ });
458
+ await requireExecutors().notifyHuman(copy(required(requestData(requestId), 'Request')), { signal });
459
+ signal.throwIfAborted();
460
+ await workspace.assertUnchanged();
461
+ transaction(() => {
462
+ const current = required(requestData(requestId), 'Request');
463
+ if (current.status !== 'WAITING_HUMAN')
464
+ return;
465
+ current.notifiedAt = now();
466
+ saveRequest(current);
467
+ appendEvent(runId, requestId, 'human.notified', 'Registered human alarm methods confirmed delivery.');
468
+ });
469
+ changed();
470
+ return;
471
+ }
472
+ const result = validateResult(await requireExecutors().execute(copy(request), {
473
+ worktreePath: workspace.descriptor.path, workspacePath: workspace.descriptor.path, runDir, signal,
474
+ onEvent(event) {
475
+ if (closed || closing || signal.aborted || !event || requestData(requestId)?.status !== 'RUNNING' || !['executor.started', 'artifact.tools.ready', 'artifact.tool.called', 'executor.completed'].includes(event.type))
476
+ return;
477
+ const safe = {};
478
+ for (const key of ['name', 'provider', 'model', 'kind', 'artifactId', 'path'])
479
+ if (typeof event[key] === 'string')
480
+ safe[key] = event[key].slice(0, 1000);
481
+ if (object(event.observation)) {
482
+ // Persist the same bounded metadata as final results, even if the Provider later fails.
483
+ const observed = storedObservation(event.observation);
484
+ if (observed)
485
+ safe.observation = observed;
486
+ }
487
+ if (Array.isArray(event.tools))
488
+ safe.tools = event.tools.slice(0, 32).map(tool => typeof tool === 'string' ? tool : tool?.name).filter(value => typeof value === 'string');
489
+ appendEvent(runId, requestId, event.type, String(event.message ?? event.type).slice(0, 2000), safe);
490
+ changed();
491
+ },
492
+ }));
493
+ await workspace.assertUnchanged();
494
+ signal.throwIfAborted();
495
+ transaction(() => { if (ownerData(runId)?.token === token)
496
+ finishWithin(requestId, { result }); });
497
+ changed();
498
+ }
499
+ catch (error) {
500
+ const reason = signal.aborted ? signal.reason : error;
501
+ if (signal.aborted || fatalExecutionError(reason))
502
+ throw reason;
503
+ transaction(() => { if (ownerData(runId)?.token === token)
504
+ finishWithin(requestId, { error: reason }); });
505
+ changed();
506
+ }
507
+ }
508
+ async function run(runId, { signal, onStarted } = {}) {
509
+ ensureOpen();
510
+ requireExecutors();
511
+ const token = randomUUID();
512
+ let stored = null;
513
+ const acquired = transaction(() => {
514
+ reconcileWithin(runId);
515
+ stored = runData(runId);
516
+ if (!stored)
517
+ throw new Error('Unknown Run.');
518
+ if (terminal.has(stored.status))
519
+ return false;
520
+ if (!stored.workspace)
521
+ throw new Error('This legacy Run has no workspace descriptor; submit a new review.');
522
+ if (ownerData(runId))
523
+ throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
524
+ db.prepare('INSERT INTO run_owners(run_id,pid,process_identity,token,claimed_at) VALUES (?,?,?,?,?)').run(runId, process.pid, ownProcessIdentity, token, now());
525
+ appendEvent(runId, null, 'worker.started', 'A request-scoped worker owns this Run.', { pid: process.pid });
526
+ return true;
527
+ });
528
+ if (!acquired)
529
+ return getRun(runId);
530
+ const ownedRun = required(stored, 'Run');
531
+ const abort = new AbortController();
532
+ const executionSignal = signal ? AbortSignal.any([abort.signal, signal]) : abort.signal;
533
+ const entry = { runId, token, abort, promise: null };
534
+ active.set(token, entry);
535
+ entry.promise = (async () => {
536
+ let workspace;
537
+ let poll;
538
+ const inFlight = new Map();
539
+ try {
540
+ onStarted?.({ runId, pid: process.pid });
541
+ workspace = await workspaceAdapter.reopenWorkspace(ownedRun.workspace, { signal: executionSignal });
542
+ const reviewSignal = AbortSignal.any([executionSignal, workspace.signal]);
543
+ poll = setInterval(() => {
544
+ if (ownerData(runId)?.token !== token)
545
+ abort.abort(codedError('Review ownership was lost.', 'RUN_OWNERSHIP_LOST'));
546
+ else if (terminal.has(required(runData(runId), 'Run').status))
547
+ abort.abort(codedError('Review is already complete or canceled.', 'REVIEW_CANCELED'));
548
+ }, 100);
549
+ while (true) {
550
+ const current = required(runData(runId), 'Run');
551
+ if (terminal.has(current.status)) {
552
+ if (inFlight.size)
553
+ abort.abort(codedError('Review is already complete or canceled.', 'REVIEW_CANCELED'));
554
+ break;
555
+ }
556
+ reviewSignal.throwIfAborted();
557
+ transaction(() => { refreshReadinessWithin(runId); updateRunStatus(runId); });
558
+ const requests = runRequests(runId);
559
+ let executing = [...inFlight.values()].filter(item => item.kind !== 'human').length;
560
+ for (const queued of requests.filter(request => request.status === 'QUEUED')) {
561
+ if (queued.profile.kind !== 'human' && executing >= MAX_CONCURRENT_EXECUTORS)
562
+ continue;
563
+ if (queued.profile.kind !== 'human')
564
+ executing++;
565
+ const promise = executeOne(queued.id, workspace, token, reviewSignal).catch(error => {
566
+ // A different task may win Promise.race before this rejection. Preserve fatal failure independently of that race.
567
+ abort.abort(error);
568
+ throw error;
569
+ }).finally(() => { inFlight.delete(queued.id); });
570
+ inFlight.set(queued.id, { kind: queued.profile.kind, promise });
571
+ }
572
+ if (inFlight.size) {
573
+ // Human alarms and independent branches progress together; completed dependencies can unblock work immediately.
574
+ await Promise.race([...inFlight.values()].map(item => item.promise).concat(delay(50, undefined, { signal: reviewSignal })));
575
+ continue;
576
+ }
577
+ const waiting = requests.filter(request => request.status === 'WAITING_HUMAN');
578
+ if (waiting.length) {
579
+ if (waiting.some(request => !request.notifiedAt))
580
+ throw new Error('Human notification did not complete; submit a new Run to retry.');
581
+ await workspace.assertUnchanged();
582
+ if (ownedRun.workspace.mode === 'copy') {
583
+ // A result can arrive during input validation. Release only after rechecking all branches atomically.
584
+ const paused = transaction(() => {
585
+ const pending = runRequests(runId).filter(request => !terminal.has(request.status));
586
+ if (!pending.some(request => request.status === 'WAITING_HUMAN') || pending.some(request => request.status !== 'BLOCKED' && (request.status !== 'WAITING_HUMAN' || !request.notifiedAt)))
587
+ return false;
588
+ if (ownerData(runId)?.token !== token)
589
+ throw codedError('Review ownership was lost.', 'RUN_OWNERSHIP_LOST');
590
+ db.prepare('DELETE FROM run_owners WHERE run_id = ? AND token = ?').run(runId, token);
591
+ appendEvent(runId, null, 'worker.paused', 'Copied Human reviews are persisted; no worker is needed while awaiting results.');
592
+ return true;
593
+ });
594
+ if (paused)
595
+ break;
596
+ continue;
597
+ }
598
+ await delay(100, undefined, { signal: reviewSignal });
599
+ continue;
600
+ }
601
+ if (terminal.has(required(runData(runId), 'Run').status))
602
+ break;
603
+ throw codedError('Run has no executable request or pending Human review.', 'REVIEW_GRAPH_INVALID');
604
+ }
605
+ }
606
+ catch (error) {
607
+ const reason = workspace?.signal.aborted ? workspace.signal.reason : executionSignal.aborted ? executionSignal.reason : error;
608
+ abort.abort(reason);
609
+ failOwned(runId, token, reason);
610
+ }
611
+ finally {
612
+ clearInterval(poll);
613
+ await Promise.allSettled([...inFlight.values()].map(item => item.promise));
614
+ try {
615
+ await workspace?.close();
616
+ }
617
+ catch (error) {
618
+ failOwned(runId, token, error);
619
+ appendEvent(runId, null, 'worker.cleanup.error', `Workspace monitoring cleanup failed: ${errorText(error)}`);
620
+ }
621
+ finally {
622
+ transaction(() => { db.prepare('DELETE FROM run_owners WHERE run_id = ? AND token = ?').run(runId, token); });
623
+ active.delete(token);
624
+ changed();
625
+ }
626
+ }
627
+ // close() can be awaiting this promise; read directly before it closes the DB.
628
+ return { ...required(runData(runId), 'Run'), requests: runRequests(runId) };
629
+ })();
630
+ return entry.promise;
631
+ }
632
+ return {
633
+ async submitProject({ mode = 'copy', requesterId = 'cli', selection, recursive = false, force = false }) {
634
+ ensureOpen();
635
+ requireExecutors();
636
+ if (!requesterId.trim() || requesterId.length > 200)
637
+ throw new Error('requesterId is required (maximum 200 characters).');
638
+ if (!['lock', 'copy'].includes(mode))
639
+ throw new Error('mode must be lock or copy.');
640
+ await requireExecutors().validateWorkspace?.(repoPath);
641
+ const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, mode });
642
+ try {
643
+ const { config } = await readWorkspaceConfig(workspace.descriptor.path);
644
+ const snapshot = await createProjectSnapshot(config, workspace.descriptor.path, workspace.descriptor.hash, workspace.signal);
645
+ const ids = includedCritics(snapshot, selection, recursive);
646
+ const templates = [];
647
+ for (const id of ids)
648
+ templates.push(...await prepareReviewRequests({ repoPath: workspace.descriptor.path, repoId, snapshotHash: workspace.descriptor.hash, criticId: id, preparedConfig: config }));
649
+ const id = randomUUID(), createdAt = now();
650
+ const record = { id, repoId, snapshotHash: workspace.descriptor.hash, workspace: workspace.descriptor, requesterId,
651
+ scope: { kind: 'project' }, graph: createGraphDefinition(config), project: { version: 1, snapshot, selection, recursive, force, templates }, status: 'QUEUED', createdAt };
652
+ await workspace.assertUnchanged();
653
+ workspace.signal.throwIfAborted();
654
+ transaction(() => {
655
+ db.prepare('INSERT INTO runs(id,created_at,status,data) VALUES (?,?,?,?)').run(id, createdAt, record.status, JSON.stringify(record));
656
+ appendEvent(id, null, 'run.submitted', 'Project validation requested against fixed input.', { selection, recursive, force });
657
+ refreshReadinessWithin(id);
658
+ updateRunStatus(id);
659
+ });
660
+ changed();
661
+ return required(getRun(id), 'Run');
662
+ }
663
+ finally {
664
+ await workspace.close();
665
+ }
666
+ },
667
+ async submit({ mode = 'copy', requesterId, reviewRequests, criticId, snapshotCommit } = {}) {
668
+ ensureOpen();
669
+ requireExecutors();
670
+ if (snapshotCommit !== undefined)
671
+ throw new Error('snapshotCommit is no longer accepted; use mode copy or lock with the current workspace.');
672
+ if (typeof requesterId !== 'string' || !requesterId.trim() || requesterId.length > 200)
673
+ throw new Error('requesterId is required (maximum 200 characters).');
674
+ if (!['lock', 'copy'].includes(mode))
675
+ throw new Error('mode must be lock or copy.');
676
+ await requireExecutors().validateWorkspace?.(repoPath);
677
+ const workspace = await workspaceAdapter.prepareWorkspace({ repoPath, stateDir, mode });
678
+ try {
679
+ const descriptor = copy(workspace.descriptor);
680
+ const { config } = await readWorkspaceConfig(descriptor.path);
681
+ const graph = createGraphDefinition(config);
682
+ const expected = await prepareReviewRequests({ repoPath: descriptor.path, repoId, snapshotHash: descriptor.hash, criticId, preparedConfig: config });
683
+ const supplied = reviewRequests === undefined ? expected : reviewRequests;
684
+ if (!isDeepStrictEqual(supplied, expected))
685
+ throw new Error('Submitted review request envelopes must exactly match the Artifact definitions, payload, profiles and dependencies in the requested workspace.');
686
+ const id = randomUUID();
687
+ const createdAt = now();
688
+ const requests = copy(supplied).map(envelope => ({
689
+ ...envelope, id: randomUUID(), runId: id, workspace: descriptor, worktreePath: descriptor.path,
690
+ status: 'BLOCKED', createdAt, startedAt: null, completedAt: null,
691
+ result: null, error: null, claimedBy: null, claimedAt: null, notifiedAt: null,
692
+ }));
693
+ for (const request of requests) {
694
+ const dependencies = criticId === undefined ? prerequisiteCriticIds(request, graph) : [];
695
+ request.status = dependencies.length ? 'BLOCKED' : 'QUEUED';
696
+ request.blockedReason = dependencies.length ? `Waiting for ${dependencies.join(', ')} to become GREEN.` : null;
697
+ const capability = await requireExecutors().canExecute(copy(request));
698
+ if (!capability?.ok)
699
+ throw new Error(`Cannot execute ${request.criticId}: ${capability?.reason ?? 'No compatible executor.'}`);
700
+ if (request.profile.kind === 'human' && typeof requireExecutors().notifyHuman !== 'function')
701
+ throw new Error('Human execution requires an alarm method.');
702
+ }
703
+ await workspace.assertUnchanged();
704
+ workspace.signal.throwIfAborted();
705
+ ensureOpen();
706
+ const scope = criticId === undefined ? { kind: 'graph' } : { kind: 'critic', criticId: criticId };
707
+ const record = { id, repoId, snapshotHash: descriptor.hash, workspace: descriptor, requesterId, scope, graph, status: 'QUEUED', createdAt };
708
+ transaction(() => {
709
+ db.prepare('INSERT INTO runs(id,created_at,status,data) VALUES (?,?,?,?)').run(id, createdAt, record.status, JSON.stringify(record));
710
+ requests.forEach((request, index) => db.prepare('INSERT INTO requests(id,run_id,ordinal,status,data) VALUES (?,?,?,?,?)').run(request.id, id, index, request.status, JSON.stringify(request)));
711
+ appendEvent(id, null, 'run.submitted', 'Review persisted; a request-scoped worker can execute it.', { snapshotHash: descriptor.hash, mode, requesterId, scope });
712
+ for (const request of requests)
713
+ if (request.status === 'QUEUED')
714
+ appendEvent(id, request.id, 'request.queued', 'Required Artifact reviews are satisfied; the review is ready.');
715
+ appendEvent(id, null, 'workspace.ready', mode === 'copy' ? 'Content-addressed copied workspace is ready.' : 'Current workspace is fixed for this review; changes invalidate it.', { path: descriptor.path, snapshotHash: descriptor.hash });
716
+ });
717
+ changed();
718
+ return required(getRun(id), 'Run');
719
+ }
720
+ finally {
721
+ await workspace.close();
722
+ }
723
+ },
724
+ run,
725
+ reconcile,
726
+ listRuns() {
727
+ ensureOpen();
728
+ return db.prepare('SELECT id FROM runs ORDER BY created_at DESC, rowid DESC').all().map(row => {
729
+ const record = required(getRun(String(row.id)), 'Run');
730
+ return { ...record, events: undefined, requests: record.requests.map(({ result, ...request }) => ({ ...request, result: result ? { verdict: result.verdict, summary: result.summary } : null })) };
731
+ });
732
+ },
733
+ getRun,
734
+ getRequest(id) {
735
+ ensureOpen();
736
+ const request = requestData(id);
737
+ if (request)
738
+ reconcile(request.runId);
739
+ return requestData(id);
740
+ },
741
+ claimHuman(requestId, reviewerId) {
742
+ ensureOpen();
743
+ if (typeof reviewerId !== 'string' || !reviewerId.trim() || reviewerId.length > 200)
744
+ throw new Error('A reviewerId is required.');
745
+ const existing = requestData(requestId);
746
+ if (existing)
747
+ reconcile(existing.runId);
748
+ transaction(() => {
749
+ const request = requestData(requestId);
750
+ if (!request || request.profile.kind !== 'human' || request.status !== 'WAITING_HUMAN')
751
+ throw new Error('Request is not waiting for a human review.');
752
+ if (request.claimedBy && request.claimedBy !== reviewerId)
753
+ throw new Error('This review is already claimed by another reviewer.');
754
+ if (!request.claimedBy) {
755
+ request.claimedBy = reviewerId;
756
+ request.claimedAt = now();
757
+ saveRequest(request);
758
+ appendEvent(request.runId, request.id, 'human.claimed', `Review claimed by ${reviewerId}.`);
759
+ }
760
+ });
761
+ changed();
762
+ return requestData(requestId);
763
+ },
764
+ async executeHumanTool(requestId, { reviewerId, toolName, arguments: args = {}, signal }) {
765
+ ensureOpen();
766
+ const first = requestData(requestId);
767
+ if (first)
768
+ reconcile(first.runId);
769
+ const assertClaim = () => {
770
+ ensureOpen();
771
+ const current = requestData(requestId);
772
+ if (!current || current.profile.kind !== 'human' || current.status !== 'WAITING_HUMAN')
773
+ throw new Error('Request is not waiting for a human review.');
774
+ if (!reviewerId || current.claimedBy !== reviewerId)
775
+ throw new Error('Only the reviewer who claimed this request can use its tools.');
776
+ if (current.workspace.mode === 'lock' && !ownerAlive(ownerData(current.runId)))
777
+ throw new Error('A lock review requires its monitoring worker to remain alive.');
778
+ return current;
779
+ };
780
+ const request = assertClaim();
781
+ let workspace;
782
+ let inputsValidated = false;
783
+ try {
784
+ workspace = await workspaceAdapter.reopenWorkspace(request.workspace, { signal });
785
+ await workspace.assertUnchanged();
786
+ workspace.signal.throwIfAborted();
787
+ inputsValidated = true;
788
+ if (!request.configManifest) {
789
+ if (Object.values(request.artifactTypes).some(type => type.custom))
790
+ throw codedError('Stored TS Artifact configuration has no tool manifest.', 'WORKSPACE_ARTIFACT_MISMATCH');
791
+ const expected = await readStoredArtifactScope({ repoPath: workspace.descriptor.path, criticId: request.criticId });
792
+ if (!expected || expected.profile.kind !== 'human' || !isDeepStrictEqual(expected.artifactTypes, request.artifactTypes) || !isDeepStrictEqual(expected.artifacts, request.artifacts) || !isDeepStrictEqual(expected.artifactGroups ?? [], request.artifactGroups ?? [])) {
793
+ throw codedError('Stored Artifact tools do not match the reviewed workspace configuration.', 'WORKSPACE_ARTIFACT_MISMATCH');
794
+ }
795
+ }
796
+ const registry = await createReviewTools({
797
+ worktreePath: workspace.descriptor.path, artifacts: request.artifacts, artifactGroups: request.artifactGroups, artifactTypes: request.artifactTypes,
798
+ configManifest: request.configManifest, criticId: request.criticId, audience: 'human',
799
+ runDir: path.resolve(stateDir, 'runs', request.runId, request.id, 'human-tools'), signal: workspace.signal,
800
+ });
801
+ try {
802
+ registry.validateArguments(toolName, args);
803
+ assertClaim();
804
+ const result = await registry.call(toolName, args);
805
+ await workspace.assertUnchanged();
806
+ workspace.signal.throwIfAborted();
807
+ assertClaim();
808
+ const tool = registry.tools.find(tool => tool.name === toolName);
809
+ transaction(() => {
810
+ assertClaim();
811
+ appendEvent(request.runId, requestId, 'human.tool.executed', 'The claimed reviewer executed a registered Artifact tool.', { name: tool.name, artifactId: tool.artifactId, operation: tool.operation });
812
+ });
813
+ changed();
814
+ return result;
815
+ }
816
+ finally {
817
+ await registry.close();
818
+ }
819
+ }
820
+ catch (error) {
821
+ const failure = workspace?.signal.aborted && !signal?.aborted ? workspace.signal.reason ?? error : error;
822
+ if (!signal?.aborted && (!inputsValidated || errorCode(failure)?.startsWith('WORKSPACE_'))) {
823
+ transaction(() => {
824
+ if (requestData(requestId)?.status === 'WAITING_HUMAN')
825
+ failWithin(request.runId, failure);
826
+ });
827
+ changed();
828
+ }
829
+ throw failure;
830
+ }
831
+ finally {
832
+ await workspace?.close();
833
+ }
834
+ },
835
+ async completeHuman(requestId, { reviewerId, result }) {
836
+ ensureOpen();
837
+ const validated = validateResult(result);
838
+ if (!validated.evidence.length || validated.evidence.some(item => !item.trim()))
839
+ throw new Error('Human review requires at least one nonempty evidence entry.');
840
+ const first = requestData(requestId);
841
+ if (first)
842
+ reconcile(first.runId);
843
+ const request = requestData(requestId);
844
+ if (!request || request.profile.kind !== 'human' || request.status !== 'WAITING_HUMAN')
845
+ throw new Error('Request is not waiting for a human review.');
846
+ if (!reviewerId || request.claimedBy !== reviewerId)
847
+ throw new Error('Only the reviewer who claimed this request can submit its result.');
848
+ if (!request.notifiedAt)
849
+ throw new Error('Human alarm delivery is still pending.');
850
+ let workspace;
851
+ let inputsValidated = false;
852
+ try {
853
+ workspace = await workspaceAdapter.reopenWorkspace(request.workspace);
854
+ await workspace.assertUnchanged();
855
+ workspace.signal.throwIfAborted();
856
+ ensureOpen();
857
+ inputsValidated = true;
858
+ transaction(() => {
859
+ const current = required(requestData(requestId), 'Request');
860
+ if (current.status !== 'WAITING_HUMAN')
861
+ throw new Error('This human review has already completed or failed.');
862
+ if (current.claimedBy !== reviewerId)
863
+ throw new Error('Only the reviewer who claimed this request can submit its result.');
864
+ if (current.workspace.mode === 'lock' && !ownerAlive(ownerData(current.runId)))
865
+ throw new Error('A lock review requires its monitoring worker to remain alive.');
866
+ required(workspace, 'Workspace').signal.throwIfAborted();
867
+ finishWithin(requestId, { result: validated });
868
+ });
869
+ }
870
+ catch (error) {
871
+ // A workspace failure invalidates the review; a competing completed result is immutable.
872
+ if (!inputsValidated || errorCode(error)?.startsWith('WORKSPACE_')) {
873
+ transaction(() => { if (requestData(requestId)?.status === 'WAITING_HUMAN')
874
+ failWithin(request.runId, error); });
875
+ changed();
876
+ }
877
+ throw error;
878
+ }
879
+ finally {
880
+ await workspace?.close();
881
+ }
882
+ changed();
883
+ return requestData(requestId);
884
+ },
885
+ failRun(runId, error) {
886
+ ensureOpen();
887
+ transaction(() => {
888
+ reconcileWithin(runId);
889
+ if (!runData(runId))
890
+ throw new Error('Unknown Run.');
891
+ if (ownerData(runId))
892
+ throw codedError('Another process already owns this review Run.', 'RUN_ALREADY_OWNED');
893
+ failWithin(runId, error instanceof Error ? error : new Error(errorText(error)));
894
+ });
895
+ changed();
896
+ return getRun(runId);
897
+ },
898
+ cancel(runId) {
899
+ ensureOpen();
900
+ const error = codedError('Review canceled by requester.', 'REVIEW_CANCELED');
901
+ transaction(() => {
902
+ if (!runData(runId))
903
+ throw new Error('Unknown Run.');
904
+ failWithin(runId, error);
905
+ });
906
+ for (const entry of active.values())
907
+ if (entry.runId === runId)
908
+ entry.abort.abort(error);
909
+ changed();
910
+ return getRun(runId);
911
+ },
912
+ onChange(callback) { listeners.add(callback); return () => listeners.delete(callback); },
913
+ async close() {
914
+ if (closed)
915
+ return;
916
+ if (closing) {
917
+ await Promise.allSettled([...active.values()].map(entry => entry.promise));
918
+ return;
919
+ }
920
+ closing = true;
921
+ const owned = [...active.values()];
922
+ for (const entry of owned)
923
+ entry.abort.abort(codedError('The review worker stopped before completion.', 'WORKER_STOPPED'));
924
+ await Promise.allSettled(owned.map(entry => entry.promise));
925
+ listeners.clear();
926
+ db.close();
927
+ closed = true;
928
+ },
929
+ };
930
+ }
931
+ //# sourceMappingURL=index.js.map