@dtlabs/ai-toolkit 0.13.0 → 0.14.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.
- package/CHANGELOG.md +44 -0
- package/bin/cli.js +371 -2
- package/docs/task-executor-bootstrap.md +195 -0
- package/docs/task-executor-cli.md +319 -0
- package/docs/workflow-control-plane.md +95 -0
- package/lib/execution-ledger.js +186 -4
- package/lib/task-executor/claude-process.js +563 -0
- package/lib/task-executor/git.js +1362 -0
- package/lib/task-executor/index.js +3729 -0
- package/lib/task-executor/ownership.js +590 -0
- package/lib/task-executor/plan.js +725 -0
- package/lib/task-executor/store.js +1164 -0
- package/lib/workflow-artifacts.js +102 -0
- package/lib/workflow-control.js +219 -0
- package/lib/workflow-runner.js +39 -0
- package/package.json +1 -1
- package/src/claude/agents/gaia-intervention-documentation-standard.md +9 -9
- package/src/claude/skills/assess-codebase/SKILL.md +9 -103
- package/src/claude/skills/implement-feature/SKILL.md +74 -159
- package/src/claude/workflows/am-phase1.js +43 -294
- package/src/claude/workflows/am-phase2.js +10 -164
- package/src/claude/workflows/pm-phase1.js +45 -358
- package/src/claude/workflows/pm-phase2.js +50 -571
- package/src/claude/workflows/pm-phase3.js +5 -824
|
@@ -0,0 +1,1164 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// Executor state protocol (FTR-018, INFRA-TASK-BE-01).
|
|
4
|
+
//
|
|
5
|
+
// This module owns the versioned on-disk state protocol for the deterministic
|
|
6
|
+
// task executor: a `State` class (schema validation + pure, immutable mutation
|
|
7
|
+
// methods) and the atomic write / readback / recovery primitives for the
|
|
8
|
+
// per-run state document, plus immutable receipt and intent files.
|
|
9
|
+
//
|
|
10
|
+
// Layout under an execution root (<absolute-common-git-dir>/ai-toolkit/execution/,
|
|
11
|
+
// resolved by the caller — this module does not discover Git directories):
|
|
12
|
+
// runs/<runId>/state.json — current validated generation
|
|
13
|
+
// runs/<runId>/state.previous.json — last validated prior generation
|
|
14
|
+
// runs/<runId>/receipts/<id>.json — immutable activity result/usage receipts
|
|
15
|
+
// runs/<runId>/intents/<id>.json — immutable checkpoint/integration descriptors
|
|
16
|
+
//
|
|
17
|
+
// No LLM, scheduling, or Git logic lives here — see lib/task-executor/plan.js,
|
|
18
|
+
// ownership.js and git.js for those responsibilities.
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
|
|
24
|
+
// ── Schema vocabulary ────────────────────────────────────────────────────────
|
|
25
|
+
|
|
26
|
+
const PROTOCOL_VERSION = 1;
|
|
27
|
+
|
|
28
|
+
const RUN_STATUSES = ['running', 'stopping', 'paused', 'blocked', 'completed', 'superseded'];
|
|
29
|
+
|
|
30
|
+
const TASK_STATUSES = ['pending', 'active', 'checkpointed', 'integrated', 'skipped', 'blocked'];
|
|
31
|
+
|
|
32
|
+
const ATTEMPT_STAGES = [
|
|
33
|
+
'prepared', 'dispatching', 'running', 'implementation-recorded',
|
|
34
|
+
'verified', 'reviewed', 'checkpoint-prepared', 'committed', 'integrated',
|
|
35
|
+
'failed', 'interrupted',
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Excludes ':' deliberately: it is a reserved character in Windows filenames
|
|
39
|
+
// (and in NTFS alternate data stream syntax), so it cannot appear in an id
|
|
40
|
+
// that is used verbatim as a filename. Ledger agent keys such as
|
|
41
|
+
// "executor:<runId>:<taskId>:<kind>" are not valid store.js ids as-is —
|
|
42
|
+
// callers must substitute a filesystem-safe separator (e.g. "-").
|
|
43
|
+
const SAFE_ID_PATTERN = /^[A-Za-z0-9_.-]+$/;
|
|
44
|
+
|
|
45
|
+
// ── Internal helpers ─────────────────────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
function _err(code, message) {
|
|
48
|
+
const e = new Error(message);
|
|
49
|
+
e.code = code;
|
|
50
|
+
return e;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function _validateSafeId(id, label) {
|
|
54
|
+
if (typeof id !== 'string' || id.length === 0 || !SAFE_ID_PATTERN.test(id)) {
|
|
55
|
+
throw _err(
|
|
56
|
+
'STATE_VALIDATION_ERROR',
|
|
57
|
+
label + ': id must be a non-empty string matching ' + SAFE_ID_PATTERN + ', got ' + JSON.stringify(id)
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function _sortKeysDeep(value) {
|
|
63
|
+
if (Array.isArray(value)) {
|
|
64
|
+
return value.map(_sortKeysDeep);
|
|
65
|
+
}
|
|
66
|
+
if (value !== null && typeof value === 'object') {
|
|
67
|
+
const sorted = {};
|
|
68
|
+
Object.keys(value).sort().forEach(function (key) {
|
|
69
|
+
sorted[key] = _sortKeysDeep(value[key]);
|
|
70
|
+
});
|
|
71
|
+
return sorted;
|
|
72
|
+
}
|
|
73
|
+
return value;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function _stableStringify(value) {
|
|
77
|
+
return JSON.stringify(_sortKeysDeep(value));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function _sha256Hex(input) {
|
|
81
|
+
return crypto.createHash('sha256').update(input, 'utf8').digest('hex');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function _ensureDirSync(dir) {
|
|
85
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function _fsyncDirSync(dir) {
|
|
89
|
+
let dfd;
|
|
90
|
+
try {
|
|
91
|
+
dfd = fs.openSync(dir, 'r');
|
|
92
|
+
fs.fsyncSync(dfd);
|
|
93
|
+
} catch (_) {
|
|
94
|
+
// Directory-handle fsync is not supported on every platform/filesystem
|
|
95
|
+
// (notably Windows). Best-effort only — file fsync + rename already
|
|
96
|
+
// happened before this call.
|
|
97
|
+
} finally {
|
|
98
|
+
if (dfd !== undefined) {
|
|
99
|
+
try { fs.closeSync(dfd); } catch (_) {}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Same-directory exclusive temp file, complete write, flush, close, rename,
|
|
105
|
+
// directory flush where supported. Mirrors the atomic-write protocol used by
|
|
106
|
+
// lib/execution-ledger.js.
|
|
107
|
+
function _atomicWriteFileSync(filePath, contents) {
|
|
108
|
+
const dir = path.dirname(filePath);
|
|
109
|
+
_ensureDirSync(dir);
|
|
110
|
+
const tmpName = '.' + path.basename(filePath) + '.tmp-' + process.pid + '-' +
|
|
111
|
+
Date.now() + '-' + crypto.randomBytes(4).toString('hex');
|
|
112
|
+
const tmpPath = path.join(dir, tmpName);
|
|
113
|
+
|
|
114
|
+
let fd;
|
|
115
|
+
try {
|
|
116
|
+
fd = fs.openSync(tmpPath, 'w');
|
|
117
|
+
fs.writeSync(fd, contents);
|
|
118
|
+
fs.fsyncSync(fd);
|
|
119
|
+
fs.closeSync(fd);
|
|
120
|
+
fd = undefined;
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (fd !== undefined) {
|
|
123
|
+
try { fs.closeSync(fd); } catch (_) {}
|
|
124
|
+
}
|
|
125
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
126
|
+
throw err;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
fs.renameSync(tmpPath, filePath);
|
|
131
|
+
} catch (err) {
|
|
132
|
+
try { fs.unlinkSync(tmpPath); } catch (_) {}
|
|
133
|
+
throw err;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
_fsyncDirSync(dir);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function _executionPaths(executionRoot, runId) {
|
|
140
|
+
_validateSafeId(runId, '_executionPaths');
|
|
141
|
+
const runDir = path.join(executionRoot, 'runs', runId);
|
|
142
|
+
return {
|
|
143
|
+
runDir: runDir,
|
|
144
|
+
stateFile: path.join(runDir, 'state.json'),
|
|
145
|
+
previousStateFile: path.join(runDir, 'state.previous.json'),
|
|
146
|
+
receiptsDir: path.join(runDir, 'receipts'),
|
|
147
|
+
intentsDir: path.join(runDir, 'intents'),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function _listJsonIds(dir) {
|
|
152
|
+
let entries;
|
|
153
|
+
try {
|
|
154
|
+
entries = fs.readdirSync(dir);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (err.code === 'ENOENT') return [];
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
return entries
|
|
160
|
+
.filter(function (name) { return name.endsWith('.json') && !name.startsWith('.'); })
|
|
161
|
+
.map(function (name) { return name.slice(0, -'.json'.length); })
|
|
162
|
+
.sort();
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── State schema and pure mutation methods ──────────────────────────────────
|
|
166
|
+
|
|
167
|
+
class State {
|
|
168
|
+
constructor(fields) {
|
|
169
|
+
fields = fields || {};
|
|
170
|
+
this.protocolVersion = fields.protocolVersion != null ? fields.protocolVersion : PROTOCOL_VERSION;
|
|
171
|
+
this.runId = fields.runId;
|
|
172
|
+
this.featureId = fields.featureId;
|
|
173
|
+
this.repo = fields.repo;
|
|
174
|
+
this.commonDir = fields.commonDir;
|
|
175
|
+
this.featureRef = fields.featureRef;
|
|
176
|
+
this.baseSha = fields.baseSha;
|
|
177
|
+
this.expectedHead = fields.expectedHead !== undefined ? fields.expectedHead : null;
|
|
178
|
+
this.planDigest = fields.planDigest;
|
|
179
|
+
this.contextHashes = fields.contextHashes || {};
|
|
180
|
+
this.config = fields.config || {};
|
|
181
|
+
this.generation = fields.generation != null ? fields.generation : 0;
|
|
182
|
+
this.previousDigest = fields.previousDigest !== undefined ? fields.previousDigest : null;
|
|
183
|
+
this.runStatus = fields.runStatus || 'running';
|
|
184
|
+
this.tasks = fields.tasks || {};
|
|
185
|
+
this.dispatchSequence = fields.dispatchSequence || [];
|
|
186
|
+
this.integrationSequence = fields.integrationSequence || [];
|
|
187
|
+
this.receiptRefs = fields.receiptRefs || [];
|
|
188
|
+
|
|
189
|
+
const result = State.validate(this.toJSON());
|
|
190
|
+
if (!result.valid) {
|
|
191
|
+
throw _err('STATE_VALIDATION_ERROR', 'State: schema validation failed — ' + result.errors.join('; '));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
toJSON() {
|
|
196
|
+
return {
|
|
197
|
+
protocolVersion: this.protocolVersion,
|
|
198
|
+
runId: this.runId,
|
|
199
|
+
featureId: this.featureId,
|
|
200
|
+
repo: this.repo,
|
|
201
|
+
commonDir: this.commonDir,
|
|
202
|
+
featureRef: this.featureRef,
|
|
203
|
+
baseSha: this.baseSha,
|
|
204
|
+
expectedHead: this.expectedHead,
|
|
205
|
+
planDigest: this.planDigest,
|
|
206
|
+
contextHashes: this.contextHashes,
|
|
207
|
+
config: this.config,
|
|
208
|
+
generation: this.generation,
|
|
209
|
+
previousDigest: this.previousDigest,
|
|
210
|
+
runStatus: this.runStatus,
|
|
211
|
+
tasks: this.tasks,
|
|
212
|
+
dispatchSequence: this.dispatchSequence,
|
|
213
|
+
integrationSequence: this.integrationSequence,
|
|
214
|
+
receiptRefs: this.receiptRefs,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
computeChecksum() {
|
|
219
|
+
return _sha256Hex(_stableStringify(this.toJSON()));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Returns a new State with the given field overrides, generation incremented
|
|
223
|
+
// and previousDigest bound to this instance's checksum. `this` is untouched —
|
|
224
|
+
// every mutation method is pure so callers cannot lose track of a prior
|
|
225
|
+
// in-memory generation by accident.
|
|
226
|
+
_clone(overrides) {
|
|
227
|
+
const merged = Object.assign({}, this.toJSON(), overrides, {
|
|
228
|
+
generation: this.generation + 1,
|
|
229
|
+
previousDigest: this.computeChecksum(),
|
|
230
|
+
});
|
|
231
|
+
return new State(merged);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
addTask(taskId, options) {
|
|
235
|
+
_validateSafeId(taskId, 'addTask');
|
|
236
|
+
if (Object.prototype.hasOwnProperty.call(this.tasks, taskId)) {
|
|
237
|
+
throw _err('STATE_TASK_EXISTS', 'addTask: task "' + taskId + '" already exists');
|
|
238
|
+
}
|
|
239
|
+
options = options || {};
|
|
240
|
+
const dependencies = Array.isArray(options.dependencies) ? options.dependencies.slice() : [];
|
|
241
|
+
const task = { taskId: taskId, dependencies: dependencies, status: 'pending', attempts: [] };
|
|
242
|
+
const tasks = Object.assign({}, this.tasks, {});
|
|
243
|
+
tasks[taskId] = task;
|
|
244
|
+
return this._clone({ tasks: tasks });
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
_requireTask(taskId, caller) {
|
|
248
|
+
const task = this.tasks[taskId];
|
|
249
|
+
if (!task) {
|
|
250
|
+
throw _err('STATE_TASK_NOT_FOUND', caller + ': task "' + taskId + '" not found');
|
|
251
|
+
}
|
|
252
|
+
return task;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
setTaskStatus(taskId, status) {
|
|
256
|
+
const task = this._requireTask(taskId, 'setTaskStatus');
|
|
257
|
+
if (TASK_STATUSES.indexOf(status) === -1) {
|
|
258
|
+
throw _err('STATE_VALIDATION_ERROR', 'setTaskStatus: invalid status "' + status + '"; allowed: ' + TASK_STATUSES.join(', '));
|
|
259
|
+
}
|
|
260
|
+
const tasks = Object.assign({}, this.tasks);
|
|
261
|
+
tasks[taskId] = Object.assign({}, task, { status: status });
|
|
262
|
+
return this._clone({ tasks: tasks });
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
addAttempt(taskId, attemptFields) {
|
|
266
|
+
const task = this._requireTask(taskId, 'addAttempt');
|
|
267
|
+
attemptFields = attemptFields || {};
|
|
268
|
+
const number = task.attempts.length + 1;
|
|
269
|
+
const stage = attemptFields.stage || 'prepared';
|
|
270
|
+
if (ATTEMPT_STAGES.indexOf(stage) === -1) {
|
|
271
|
+
throw _err('STATE_VALIDATION_ERROR', 'addAttempt: invalid stage "' + stage + '"; allowed: ' + ATTEMPT_STAGES.join(', '));
|
|
272
|
+
}
|
|
273
|
+
const attempt = Object.assign({
|
|
274
|
+
number: number,
|
|
275
|
+
id: taskId + '#' + number,
|
|
276
|
+
baseSha: null,
|
|
277
|
+
worktreeRef: null,
|
|
278
|
+
processIdentity: null,
|
|
279
|
+
verificationRefs: [],
|
|
280
|
+
reviewRefs: [],
|
|
281
|
+
intentIds: [],
|
|
282
|
+
originalSha: null,
|
|
283
|
+
integratedSha: null,
|
|
284
|
+
terminalReason: null,
|
|
285
|
+
}, attemptFields, { number: number, stage: stage });
|
|
286
|
+
|
|
287
|
+
const tasks = Object.assign({}, this.tasks);
|
|
288
|
+
tasks[taskId] = Object.assign({}, task, { attempts: task.attempts.concat([attempt]) });
|
|
289
|
+
return this._clone({ tasks: tasks });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
updateAttempt(taskId, attemptNumber, patch) {
|
|
293
|
+
const task = this._requireTask(taskId, 'updateAttempt');
|
|
294
|
+
const idx = task.attempts.findIndex(function (a) { return a.number === attemptNumber; });
|
|
295
|
+
if (idx === -1) {
|
|
296
|
+
throw _err('STATE_ATTEMPT_NOT_FOUND', 'updateAttempt: attempt ' + attemptNumber + ' not found for task "' + taskId + '"');
|
|
297
|
+
}
|
|
298
|
+
patch = patch || {};
|
|
299
|
+
if (patch.stage !== undefined && ATTEMPT_STAGES.indexOf(patch.stage) === -1) {
|
|
300
|
+
throw _err('STATE_VALIDATION_ERROR', 'updateAttempt: invalid stage "' + patch.stage + '"; allowed: ' + ATTEMPT_STAGES.join(', '));
|
|
301
|
+
}
|
|
302
|
+
const attempts = task.attempts.slice();
|
|
303
|
+
attempts[idx] = Object.assign({}, attempts[idx], patch, { number: attemptNumber });
|
|
304
|
+
|
|
305
|
+
const tasks = Object.assign({}, this.tasks);
|
|
306
|
+
tasks[taskId] = Object.assign({}, task, { attempts: attempts });
|
|
307
|
+
return this._clone({ tasks: tasks });
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
recordDispatch(taskId, attemptNumber) {
|
|
311
|
+
this._requireTask(taskId, 'recordDispatch');
|
|
312
|
+
const seq = this.dispatchSequence.length + 1;
|
|
313
|
+
const entry = { seq: seq, taskId: taskId, attemptNumber: attemptNumber, at: new Date().toISOString() };
|
|
314
|
+
return this._clone({ dispatchSequence: this.dispatchSequence.concat([entry]) });
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
recordIntegration(taskId, attemptNumber) {
|
|
318
|
+
this._requireTask(taskId, 'recordIntegration');
|
|
319
|
+
const seq = this.integrationSequence.length + 1;
|
|
320
|
+
const entry = { seq: seq, taskId: taskId, attemptNumber: attemptNumber, at: new Date().toISOString() };
|
|
321
|
+
return this._clone({ integrationSequence: this.integrationSequence.concat([entry]) });
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
setRunStatus(status) {
|
|
325
|
+
if (RUN_STATUSES.indexOf(status) === -1) {
|
|
326
|
+
throw _err('STATE_VALIDATION_ERROR', 'setRunStatus: invalid status "' + status + '"; allowed: ' + RUN_STATUSES.join(', '));
|
|
327
|
+
}
|
|
328
|
+
return this._clone({ runStatus: status });
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Persists a stop request (US-07-TASK-BE-02): moves runStatus to
|
|
332
|
+
// 'stopping' (already part of RUN_STATUSES for exactly this purpose — no
|
|
333
|
+
// new status is invented) and durably records the requested mode inside
|
|
334
|
+
// `config`, the existing free-form state field also used for maxConcurrency/
|
|
335
|
+
// claudePath/taskTimeoutMs/agentBudgetUsd. A dedicated top-level field was
|
|
336
|
+
// considered and rejected: `config` already carries exactly this kind of
|
|
337
|
+
// "caller-supplied, not itself schema-critical" run-level setting, and
|
|
338
|
+
// reusing it avoids widening State.validate/State's constructor signature
|
|
339
|
+
// for one narrow addition. Both the status transition and the config
|
|
340
|
+
// update happen in this ONE _clone call (one generation bump, one caller
|
|
341
|
+
// writeState) — mirrors every other single-purpose mutation method on this
|
|
342
|
+
// class (setTaskStatus, addReceiptRef, ...).
|
|
343
|
+
requestStop(mode) {
|
|
344
|
+
if (mode !== 'graceful' && mode !== 'immediate') {
|
|
345
|
+
throw _err('STATE_VALIDATION_ERROR', 'requestStop: mode must be "graceful" or "immediate", got ' + JSON.stringify(mode));
|
|
346
|
+
}
|
|
347
|
+
return this._clone({
|
|
348
|
+
runStatus: 'stopping',
|
|
349
|
+
config: Object.assign({}, this.config, {
|
|
350
|
+
stopRequest: { mode: mode, requestedAt: new Date().toISOString() },
|
|
351
|
+
}),
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
addReceiptRef(receiptId) {
|
|
356
|
+
if (this.receiptRefs.indexOf(receiptId) !== -1) {
|
|
357
|
+
return this; // idempotent no-op — no generation bump for a repeated reference
|
|
358
|
+
}
|
|
359
|
+
return this._clone({ receiptRefs: this.receiptRefs.concat([receiptId]) });
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
static validate(obj) {
|
|
363
|
+
const errors = [];
|
|
364
|
+
if (obj == null || typeof obj !== 'object' || Array.isArray(obj)) {
|
|
365
|
+
return { valid: false, errors: ['state must be a plain object'] };
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function req(field, type) {
|
|
369
|
+
const value = obj[field];
|
|
370
|
+
if (value === undefined) { errors.push('missing required field "' + field + '"'); return; }
|
|
371
|
+
if (type === 'string' && typeof value !== 'string') errors.push('field "' + field + '" must be a string');
|
|
372
|
+
if (type === 'number' && typeof value !== 'number') errors.push('field "' + field + '" must be a number');
|
|
373
|
+
if (type === 'object' && (typeof value !== 'object' || value === null || Array.isArray(value))) errors.push('field "' + field + '" must be an object');
|
|
374
|
+
if (type === 'array' && !Array.isArray(value)) errors.push('field "' + field + '" must be an array');
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
req('protocolVersion', 'number');
|
|
378
|
+
req('runId', 'string');
|
|
379
|
+
req('featureId', 'string');
|
|
380
|
+
req('repo', 'string');
|
|
381
|
+
req('commonDir', 'string');
|
|
382
|
+
req('featureRef', 'string');
|
|
383
|
+
req('baseSha', 'string');
|
|
384
|
+
req('planDigest', 'string');
|
|
385
|
+
req('contextHashes', 'object');
|
|
386
|
+
req('config', 'object');
|
|
387
|
+
req('generation', 'number');
|
|
388
|
+
req('runStatus', 'string');
|
|
389
|
+
req('tasks', 'object');
|
|
390
|
+
req('dispatchSequence', 'array');
|
|
391
|
+
req('integrationSequence', 'array');
|
|
392
|
+
req('receiptRefs', 'array');
|
|
393
|
+
|
|
394
|
+
if (obj.expectedHead !== undefined && obj.expectedHead !== null && typeof obj.expectedHead !== 'string') {
|
|
395
|
+
errors.push('field "expectedHead" must be a string or null');
|
|
396
|
+
}
|
|
397
|
+
if (obj.previousDigest !== undefined && obj.previousDigest !== null && typeof obj.previousDigest !== 'string') {
|
|
398
|
+
errors.push('field "previousDigest" must be a string or null');
|
|
399
|
+
}
|
|
400
|
+
if (typeof obj.generation === 'number' && (!Number.isInteger(obj.generation) || obj.generation < 0)) {
|
|
401
|
+
errors.push('field "generation" must be a non-negative integer');
|
|
402
|
+
}
|
|
403
|
+
if (typeof obj.runStatus === 'string' && RUN_STATUSES.indexOf(obj.runStatus) === -1) {
|
|
404
|
+
errors.push('field "runStatus" has invalid value "' + obj.runStatus + '"; allowed: ' + RUN_STATUSES.join(', '));
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
if (obj.tasks && typeof obj.tasks === 'object' && !Array.isArray(obj.tasks)) {
|
|
408
|
+
Object.keys(obj.tasks).forEach(function (taskId) {
|
|
409
|
+
const task = obj.tasks[taskId];
|
|
410
|
+
const prefix = 'task "' + taskId + '": ';
|
|
411
|
+
if (task == null || typeof task !== 'object') { errors.push(prefix + 'must be an object'); return; }
|
|
412
|
+
if (task.taskId !== taskId) errors.push(prefix + 'taskId field must equal its map key');
|
|
413
|
+
if (!Array.isArray(task.dependencies)) errors.push(prefix + 'dependencies must be an array');
|
|
414
|
+
if (TASK_STATUSES.indexOf(task.status) === -1) errors.push(prefix + 'invalid status "' + task.status + '"');
|
|
415
|
+
if (!Array.isArray(task.attempts)) { errors.push(prefix + 'attempts must be an array'); return; }
|
|
416
|
+
task.attempts.forEach(function (attempt, i) {
|
|
417
|
+
const aprefix = prefix + 'attempt[' + i + ']: ';
|
|
418
|
+
if (attempt == null || typeof attempt !== 'object') { errors.push(aprefix + 'must be an object'); return; }
|
|
419
|
+
if (typeof attempt.number !== 'number' || !Number.isInteger(attempt.number) || attempt.number < 1) {
|
|
420
|
+
errors.push(aprefix + 'number must be a positive integer');
|
|
421
|
+
}
|
|
422
|
+
if (typeof attempt.id !== 'string' || attempt.id.length === 0) {
|
|
423
|
+
errors.push(aprefix + 'id must be a non-empty string');
|
|
424
|
+
}
|
|
425
|
+
if (ATTEMPT_STAGES.indexOf(attempt.stage) === -1) {
|
|
426
|
+
errors.push(aprefix + 'invalid stage "' + attempt.stage + '"');
|
|
427
|
+
}
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
return { valid: errors.length === 0, errors: errors };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
static create(fields) {
|
|
436
|
+
return new State(fields);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
static fromJSON(obj) {
|
|
440
|
+
return new State(obj);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// ── State file: atomic write, readback, recovery ────────────────────────────
|
|
445
|
+
|
|
446
|
+
function _loadStateFile(filePath) {
|
|
447
|
+
let raw;
|
|
448
|
+
try {
|
|
449
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (err.code === 'ENOENT') return { ok: false, reason: 'missing', error: err };
|
|
452
|
+
return { ok: false, reason: 'read-error', error: err };
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
let payload;
|
|
456
|
+
try {
|
|
457
|
+
payload = JSON.parse(raw);
|
|
458
|
+
} catch (err) {
|
|
459
|
+
return { ok: false, reason: 'corrupt-json', error: err };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (payload == null || typeof payload !== 'object' || typeof payload.checksum !== 'string' || payload.state == null) {
|
|
463
|
+
return { ok: false, reason: 'malformed-envelope', error: new Error('state envelope missing "checksum"/"state"') };
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const recomputed = _sha256Hex(_stableStringify(payload.state));
|
|
467
|
+
if (recomputed !== payload.checksum) {
|
|
468
|
+
return {
|
|
469
|
+
ok: false,
|
|
470
|
+
reason: 'checksum-mismatch',
|
|
471
|
+
error: new Error('checksum mismatch: recorded ' + payload.checksum + ', recomputed ' + recomputed),
|
|
472
|
+
};
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const validation = State.validate(payload.state);
|
|
476
|
+
if (!validation.valid) {
|
|
477
|
+
return { ok: false, reason: 'schema-invalid', error: new Error('schema validation failed: ' + validation.errors.join('; ')) };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
let state;
|
|
481
|
+
try {
|
|
482
|
+
state = new State(payload.state);
|
|
483
|
+
} catch (err) {
|
|
484
|
+
return { ok: false, reason: 'construction-failed', error: err };
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return { ok: true, state: state };
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Persists `state` as the current generation for `runId`. Before writing, the
|
|
491
|
+
// previously-current file (if any and if parseable) is promoted to
|
|
492
|
+
// state.previous.json so a validated prior generation always survives a crash
|
|
493
|
+
// mid-write. The new file is then written atomically and read back; a
|
|
494
|
+
// checksum/schema mismatch on readback throws rather than silently trusting
|
|
495
|
+
// the write.
|
|
496
|
+
function writeState(executionRoot, runId, state) {
|
|
497
|
+
if (!(state instanceof State)) {
|
|
498
|
+
throw _err('STATE_VALIDATION_ERROR', 'writeState: state must be a State instance');
|
|
499
|
+
}
|
|
500
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
501
|
+
_ensureDirSync(paths.runDir);
|
|
502
|
+
|
|
503
|
+
if (fs.existsSync(paths.stateFile)) {
|
|
504
|
+
try {
|
|
505
|
+
const currentRaw = fs.readFileSync(paths.stateFile, 'utf8');
|
|
506
|
+
JSON.parse(currentRaw); // only promote a well-formed prior generation
|
|
507
|
+
_atomicWriteFileSync(paths.previousStateFile, currentRaw);
|
|
508
|
+
} catch (_) {
|
|
509
|
+
// Rotation of a corrupted current file is best-effort; do not block the
|
|
510
|
+
// new write on it, and never overwrite an existing valid previous file
|
|
511
|
+
// with unparseable content (the write above only runs when parse succeeds).
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const checksum = state.computeChecksum();
|
|
516
|
+
const contents = JSON.stringify({ checksum: checksum, state: state.toJSON() }, null, 2);
|
|
517
|
+
_atomicWriteFileSync(paths.stateFile, contents);
|
|
518
|
+
|
|
519
|
+
const readBack = _loadStateFile(paths.stateFile);
|
|
520
|
+
if (!readBack.ok || readBack.state.computeChecksum() !== checksum) {
|
|
521
|
+
throw _err('STATE_READBACK_MISMATCH', 'writeState: readback verification failed for run "' + runId + '"');
|
|
522
|
+
}
|
|
523
|
+
return readBack.state;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// Reads the current generation for `runId`. When the current file is
|
|
527
|
+
// missing, throws STATE_NOT_FOUND (nothing to recover). When it is corrupted
|
|
528
|
+
// or fails schema/checksum validation: if allowRecovery is true (default),
|
|
529
|
+
// falls back to the last validated previous generation; otherwise throws
|
|
530
|
+
// STATE_CORRUPTED immediately. Never fabricates an empty run.
|
|
531
|
+
function readState(executionRoot, runId, opts) {
|
|
532
|
+
opts = opts || {};
|
|
533
|
+
const allowRecovery = opts.allowRecovery !== false;
|
|
534
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
535
|
+
|
|
536
|
+
const current = _loadStateFile(paths.stateFile);
|
|
537
|
+
if (current.ok) {
|
|
538
|
+
return current.state;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
if (current.reason === 'missing') {
|
|
542
|
+
throw _err('STATE_NOT_FOUND', 'readState: no state found for run "' + runId + '" at ' + paths.stateFile);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
if (!allowRecovery) {
|
|
546
|
+
const e = _err(
|
|
547
|
+
'STATE_CORRUPTED',
|
|
548
|
+
'readState: state at ' + paths.stateFile + ' is unreadable (' + current.reason + '): ' + current.error.message
|
|
549
|
+
);
|
|
550
|
+
e.reason = current.reason;
|
|
551
|
+
throw e;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
const previous = _loadStateFile(paths.previousStateFile);
|
|
555
|
+
if (previous.ok) {
|
|
556
|
+
return previous.state;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
const e = _err(
|
|
560
|
+
'STATE_CORRUPTED',
|
|
561
|
+
'readState: current state is unreadable (' + current.reason + ') and no valid previous generation ' +
|
|
562
|
+
'is available for run "' + runId + '" — stopping non-destructively; no data deleted'
|
|
563
|
+
);
|
|
564
|
+
e.currentReason = current.reason;
|
|
565
|
+
e.previousReason = previous.reason;
|
|
566
|
+
throw e;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Read-only diagnostic: reports which generation (current/previous) is valid
|
|
570
|
+
// for `runId` without mutating anything on disk. Throws STATE_UNRECOVERABLE
|
|
571
|
+
// if neither generation is valid.
|
|
572
|
+
function recoverState(executionRoot, runId) {
|
|
573
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
574
|
+
|
|
575
|
+
const current = _loadStateFile(paths.stateFile);
|
|
576
|
+
if (current.ok) {
|
|
577
|
+
return { recovered: false, source: 'current', state: current.state };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const previous = _loadStateFile(paths.previousStateFile);
|
|
581
|
+
if (previous.ok) {
|
|
582
|
+
return { recovered: true, source: 'previous', state: previous.state, currentReason: current.reason };
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
const e = _err(
|
|
586
|
+
'STATE_UNRECOVERABLE',
|
|
587
|
+
'recoverState: neither current nor previous generation is valid for run "' + runId + '" ' +
|
|
588
|
+
'(current: ' + current.reason + ', previous: ' + previous.reason + ') — no automatic recovery possible'
|
|
589
|
+
);
|
|
590
|
+
e.currentReason = current.reason;
|
|
591
|
+
e.previousReason = previous.reason;
|
|
592
|
+
throw e;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
// ── Receipts and intents: immutable, content-addressed-by-id files ─────────
|
|
596
|
+
|
|
597
|
+
function _writeImmutableRecord(dir, id, data, conflictCode, label) {
|
|
598
|
+
_validateSafeId(id, label);
|
|
599
|
+
_ensureDirSync(dir);
|
|
600
|
+
const filePath = path.join(dir, id + '.json');
|
|
601
|
+
const contents = JSON.stringify(data, null, 2);
|
|
602
|
+
|
|
603
|
+
if (fs.existsSync(filePath)) {
|
|
604
|
+
let existing;
|
|
605
|
+
try {
|
|
606
|
+
existing = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
607
|
+
} catch (err) {
|
|
608
|
+
throw _err(conflictCode, label + ': existing record "' + id + '" is corrupted and cannot be compared: ' + err.message);
|
|
609
|
+
}
|
|
610
|
+
if (_stableStringify(existing) === _stableStringify(data)) {
|
|
611
|
+
return existing; // idempotent replay of an identical record
|
|
612
|
+
}
|
|
613
|
+
throw _err(conflictCode, label + ': record "' + id + '" already exists with different content — records are immutable');
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
_atomicWriteFileSync(filePath, contents);
|
|
617
|
+
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
function _readRecord(dir, id, notFoundCode, corruptCode, label) {
|
|
621
|
+
_validateSafeId(id, label);
|
|
622
|
+
const filePath = path.join(dir, id + '.json');
|
|
623
|
+
let raw;
|
|
624
|
+
try {
|
|
625
|
+
raw = fs.readFileSync(filePath, 'utf8');
|
|
626
|
+
} catch (err) {
|
|
627
|
+
if (err.code === 'ENOENT') {
|
|
628
|
+
throw _err(notFoundCode, label + ': no record found for id "' + id + '" at ' + filePath);
|
|
629
|
+
}
|
|
630
|
+
throw err;
|
|
631
|
+
}
|
|
632
|
+
try {
|
|
633
|
+
return JSON.parse(raw);
|
|
634
|
+
} catch (err) {
|
|
635
|
+
throw _err(corruptCode, label + ': record "' + id + '" is corrupted: ' + err.message);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function writeReceipt(executionRoot, runId, activityId, receiptData) {
|
|
640
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
641
|
+
return _writeImmutableRecord(paths.receiptsDir, activityId, receiptData, 'RECEIPT_CONFLICT', 'writeReceipt');
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function readReceipt(executionRoot, runId, activityId) {
|
|
645
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
646
|
+
return _readRecord(paths.receiptsDir, activityId, 'RECEIPT_NOT_FOUND', 'RECEIPT_CORRUPTED', 'readReceipt');
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
function listReceipts(executionRoot, runId) {
|
|
650
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
651
|
+
return _listJsonIds(paths.receiptsDir);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// ── Combined verification+review outcome persistence (US-04-TASK-BE-03) ─────
|
|
655
|
+
//
|
|
656
|
+
// BR-09 ("verification persisted before review; review persisted before
|
|
657
|
+
// checkpoint") is enforced here: this is the single durable-record primitive
|
|
658
|
+
// that closes out US-04 for one task attempt, called only after index.js's
|
|
659
|
+
// runVerifications and (when applicable) runReview have both already
|
|
660
|
+
// returned. It does not dispatch, spawn, or re-run anything — it is pure
|
|
661
|
+
// persistence over already-computed results.
|
|
662
|
+
|
|
663
|
+
function _mergeUniqueIds(existing, additions) {
|
|
664
|
+
const base = Array.isArray(existing) ? existing.slice() : [];
|
|
665
|
+
additions.forEach(function (id) {
|
|
666
|
+
if (base.indexOf(id) === -1) base.push(id);
|
|
667
|
+
});
|
|
668
|
+
return base;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// `verification` is runVerifications' own return shape: { passed, results }
|
|
672
|
+
// (results: array of { command, exitCode, ... , passed }, in run order —
|
|
673
|
+
// only the fields this function needs are read; extra fields are ignored).
|
|
674
|
+
// `review` is runReview's own return shape ({ reviewPassed, criticalFindings,
|
|
675
|
+
// ... }) when a review was actually dispatched, or null/undefined when it was
|
|
676
|
+
// not — the real flow never dispatches review after a failed verification,
|
|
677
|
+
// so `review` is legitimately absent in that case.
|
|
678
|
+
//
|
|
679
|
+
// Stage-transition rules (ATTEMPT_STAGES is the single source of truth; no
|
|
680
|
+
// new stage name is introduced):
|
|
681
|
+
// - verification.passed !== true -> attempt moves straight to 'failed'
|
|
682
|
+
// (terminalReason 'verification-failed'), REGARDLESS of what `review`
|
|
683
|
+
// contains. This is the defensive rule the outcome calls for: a caller
|
|
684
|
+
// that mistakenly supplies a passing review alongside a failed
|
|
685
|
+
// verification can never buy its way to 'reviewed'.
|
|
686
|
+
// - verification.passed === true -> the attempt is first moved to
|
|
687
|
+
// 'verified' (a real, separately-written transition — mirrors
|
|
688
|
+
// dispatchTaskAttempt's own prepared-then-dispatching two-step). A
|
|
689
|
+
// review outcome is then required (null/undefined `review` here is
|
|
690
|
+
// caller misuse, not a valid "not reviewed yet" state, since this
|
|
691
|
+
// function is only ever called after both upstream calls have
|
|
692
|
+
// returned) and decides the terminal step on top of 'verified':
|
|
693
|
+
// - review.reviewPassed === true -> 'reviewed' (terminalReason null)
|
|
694
|
+
// - anything else (false OR null, i.e. the ambiguous/ unparsed verdict
|
|
695
|
+
// _extractReviewVerdict can return) -> 'failed'
|
|
696
|
+
// (terminalReason 'review-failed')
|
|
697
|
+
//
|
|
698
|
+
// Durable records written (both are immutable store.writeReceipt records —
|
|
699
|
+
// re-calling with identical inputs is an idempotent no-op; re-calling with
|
|
700
|
+
// different inputs for the same attempt throws RECEIPT_CONFLICT, same as any
|
|
701
|
+
// other receipt):
|
|
702
|
+
// - `<taskId>-attempt<attemptNumber>-review` (only when `review` is given):
|
|
703
|
+
// the raw review verdict, independent of whether verification passing
|
|
704
|
+
// made it authoritative for the final stage.
|
|
705
|
+
// - `<taskId>-attempt<attemptNumber>-outcome`: the full combined record —
|
|
706
|
+
// verification summary (referencing, not duplicating, the per-command
|
|
707
|
+
// receipts runVerifications already wrote), review summary, final stage
|
|
708
|
+
// and terminal reason.
|
|
709
|
+
// The attempt's own `verificationRefs`/`reviewRefs` evidence arrays (already
|
|
710
|
+
// part of the addAttempt schema; unused by any writer before this task) are
|
|
711
|
+
// updated to reference these receipt ids, merged with whatever was already
|
|
712
|
+
// recorded rather than overwritten, so a repeated call cannot drop evidence.
|
|
713
|
+
function persistVerificationReviewOutcome(executionRoot, runId, taskId, attemptNumber, verification, review) {
|
|
714
|
+
_validateSafeId(taskId, 'persistVerificationReviewOutcome');
|
|
715
|
+
if (!Number.isInteger(attemptNumber) || attemptNumber < 1) {
|
|
716
|
+
throw _err(
|
|
717
|
+
'STATE_VALIDATION_ERROR',
|
|
718
|
+
'persistVerificationReviewOutcome: attemptNumber must be a positive integer, got ' + JSON.stringify(attemptNumber)
|
|
719
|
+
);
|
|
720
|
+
}
|
|
721
|
+
if (verification == null || typeof verification !== 'object' ||
|
|
722
|
+
typeof verification.passed !== 'boolean' || !Array.isArray(verification.results)) {
|
|
723
|
+
throw _err(
|
|
724
|
+
'STATE_VALIDATION_ERROR',
|
|
725
|
+
'persistVerificationReviewOutcome: verification must be an object with a boolean "passed" and an array "results"'
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
if (review !== null && review !== undefined &&
|
|
729
|
+
(typeof review !== 'object' || !Array.isArray(review.criticalFindings))) {
|
|
730
|
+
throw _err(
|
|
731
|
+
'STATE_VALIDATION_ERROR',
|
|
732
|
+
'persistVerificationReviewOutcome: review must be null/undefined or an object with an array "criticalFindings"'
|
|
733
|
+
);
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
const verificationPassed = verification.passed;
|
|
737
|
+
|
|
738
|
+
if (verificationPassed !== true && review == null) {
|
|
739
|
+
// Common real-flow case: verification failed, review was never dispatched.
|
|
740
|
+
} else if (verificationPassed === true && review == null) {
|
|
741
|
+
throw _err(
|
|
742
|
+
'STATE_VALIDATION_ERROR',
|
|
743
|
+
'persistVerificationReviewOutcome: a review outcome is required once verification has passed ' +
|
|
744
|
+
'(this function is only callable after both runVerifications and runReview have returned)'
|
|
745
|
+
);
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
let finalStage;
|
|
749
|
+
let terminalReason;
|
|
750
|
+
if (verificationPassed !== true) {
|
|
751
|
+
finalStage = 'failed';
|
|
752
|
+
terminalReason = 'verification-failed';
|
|
753
|
+
} else if (review.reviewPassed === true) {
|
|
754
|
+
finalStage = 'reviewed';
|
|
755
|
+
terminalReason = null;
|
|
756
|
+
} else {
|
|
757
|
+
finalStage = 'failed';
|
|
758
|
+
terminalReason = 'review-failed';
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
const verificationSummary = verification.results.map(function (result, index) {
|
|
762
|
+
return {
|
|
763
|
+
index: index,
|
|
764
|
+
receiptId: taskId + '-attempt' + attemptNumber + '-verification-' + index,
|
|
765
|
+
command: result && result.command,
|
|
766
|
+
exitCode: result && result.exitCode !== undefined ? result.exitCode : null,
|
|
767
|
+
passed: !!(result && result.passed),
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
const verificationReceiptIds = verificationSummary.map(function (v) { return v.receiptId; });
|
|
771
|
+
|
|
772
|
+
let reviewReceiptId = null;
|
|
773
|
+
if (review != null) {
|
|
774
|
+
reviewReceiptId = taskId + '-attempt' + attemptNumber + '-review';
|
|
775
|
+
writeReceipt(executionRoot, runId, reviewReceiptId, {
|
|
776
|
+
taskId: taskId,
|
|
777
|
+
attemptNumber: attemptNumber,
|
|
778
|
+
reviewPassed: review.reviewPassed === undefined ? null : review.reviewPassed,
|
|
779
|
+
criticalFindings: review.criticalFindings,
|
|
780
|
+
authoritative: verificationPassed === true,
|
|
781
|
+
});
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const outcomeReceiptId = taskId + '-attempt' + attemptNumber + '-outcome';
|
|
785
|
+
writeReceipt(executionRoot, runId, outcomeReceiptId, {
|
|
786
|
+
taskId: taskId,
|
|
787
|
+
attemptNumber: attemptNumber,
|
|
788
|
+
verificationPassed: verificationPassed,
|
|
789
|
+
verification: verificationSummary,
|
|
790
|
+
review: review == null ? null : {
|
|
791
|
+
receiptId: reviewReceiptId,
|
|
792
|
+
reviewPassed: review.reviewPassed === undefined ? null : review.reviewPassed,
|
|
793
|
+
criticalFindings: review.criticalFindings,
|
|
794
|
+
},
|
|
795
|
+
stage: finalStage,
|
|
796
|
+
terminalReason: terminalReason,
|
|
797
|
+
});
|
|
798
|
+
|
|
799
|
+
let state = readState(executionRoot, runId);
|
|
800
|
+
const task = state.tasks[taskId];
|
|
801
|
+
if (!task) {
|
|
802
|
+
throw _err('STATE_TASK_NOT_FOUND', 'persistVerificationReviewOutcome: task "' + taskId + '" not found');
|
|
803
|
+
}
|
|
804
|
+
const existingAttempt = task.attempts.find(function (a) { return a.number === attemptNumber; });
|
|
805
|
+
if (!existingAttempt) {
|
|
806
|
+
throw _err(
|
|
807
|
+
'STATE_ATTEMPT_NOT_FOUND',
|
|
808
|
+
'persistVerificationReviewOutcome: attempt ' + attemptNumber + ' not found for task "' + taskId + '"'
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
const mergedVerificationRefs = _mergeUniqueIds(existingAttempt.verificationRefs, verificationReceiptIds);
|
|
813
|
+
const mergedReviewRefs = _mergeUniqueIds(existingAttempt.reviewRefs, reviewReceiptId ? [reviewReceiptId] : []);
|
|
814
|
+
|
|
815
|
+
if (verificationPassed === true) {
|
|
816
|
+
// Real, separately-written intermediate transition — 'verified' always
|
|
817
|
+
// lands on disk before the terminal 'reviewed'/'failed' step, mirroring
|
|
818
|
+
// dispatchTaskAttempt's own prepared-then-dispatching two-step write.
|
|
819
|
+
state = state.updateAttempt(taskId, attemptNumber, {
|
|
820
|
+
stage: 'verified',
|
|
821
|
+
verificationRefs: mergedVerificationRefs,
|
|
822
|
+
});
|
|
823
|
+
state = writeState(executionRoot, runId, state);
|
|
824
|
+
|
|
825
|
+
state = state.updateAttempt(taskId, attemptNumber, {
|
|
826
|
+
stage: finalStage,
|
|
827
|
+
reviewRefs: mergedReviewRefs,
|
|
828
|
+
terminalReason: terminalReason,
|
|
829
|
+
});
|
|
830
|
+
state = writeState(executionRoot, runId, state);
|
|
831
|
+
} else {
|
|
832
|
+
state = state.updateAttempt(taskId, attemptNumber, {
|
|
833
|
+
stage: finalStage,
|
|
834
|
+
verificationRefs: mergedVerificationRefs,
|
|
835
|
+
reviewRefs: mergedReviewRefs,
|
|
836
|
+
terminalReason: terminalReason,
|
|
837
|
+
});
|
|
838
|
+
state = writeState(executionRoot, runId, state);
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
const allNewReceiptIds = verificationReceiptIds.concat(reviewReceiptId ? [reviewReceiptId] : [], [outcomeReceiptId]);
|
|
842
|
+
allNewReceiptIds.forEach(function (id) {
|
|
843
|
+
state = state.addReceiptRef(id);
|
|
844
|
+
});
|
|
845
|
+
state = writeState(executionRoot, runId, state);
|
|
846
|
+
|
|
847
|
+
return {
|
|
848
|
+
taskId: taskId,
|
|
849
|
+
attemptNumber: attemptNumber,
|
|
850
|
+
stage: finalStage,
|
|
851
|
+
terminalReason: terminalReason,
|
|
852
|
+
verificationReceiptIds: verificationReceiptIds,
|
|
853
|
+
reviewReceiptId: reviewReceiptId,
|
|
854
|
+
outcomeReceiptId: outcomeReceiptId,
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
// ── registerCommitSHA (US-05-TASK-BE-04) ────────────────────────────────────
|
|
859
|
+
//
|
|
860
|
+
// Persists the real, git-confirmed commit SHA that createTaskCommit
|
|
861
|
+
// (US-05-TASK-BE-03, lib/task-executor/git.js) already produced and
|
|
862
|
+
// re-inspected, into executor state OUTSIDE the tracked worktree. This
|
|
863
|
+
// function runs no git commands itself — it is pure state persistence,
|
|
864
|
+
// called only after the caller already holds a real commit SHA. Registering
|
|
865
|
+
// the SHA as a separate step (rather than writing it from inside the commit
|
|
866
|
+
// call) is deliberate: a commit object can never embed its own SHA without a
|
|
867
|
+
// second, recursive commit, so registration necessarily happens after the
|
|
868
|
+
// commit exists, in state, not in Git.
|
|
869
|
+
//
|
|
870
|
+
// originalSha vs integratedSha (Tech-Spec section 8, N=1 vs N>1 branch
|
|
871
|
+
// model): "N=1: task commit is on feature branch and integratedSha=
|
|
872
|
+
// originalSha after verification." / "N>1: each attempt has unique branch
|
|
873
|
+
// ai-toolkit/<runId>/<taskId>/<attempt> ... Commit remains on technical ref
|
|
874
|
+
// until integration." For N>1, integratedSha is only known once a later,
|
|
875
|
+
// separate cherry-pick/integration step has run — that is US-08-TASK-BE-04
|
|
876
|
+
// ("tracks originalSha ... and integratedSha ... separately; updates
|
|
877
|
+
// integration attempt record"), not yet implemented and explicitly out of
|
|
878
|
+
// scope here.
|
|
879
|
+
//
|
|
880
|
+
// This function therefore ALWAYS sets originalSha (the one thing it is
|
|
881
|
+
// unconditionally responsible for), and only ALSO sets integratedSha in the
|
|
882
|
+
// same call when the caller explicitly passes opts.sequential === true — the
|
|
883
|
+
// caller-known signal that this run is N=1 (the task commit lands directly
|
|
884
|
+
// on the feature branch, so no separate integration step is ever coming for
|
|
885
|
+
// this attempt). Default is false/absent, so N>1 callers (and any caller
|
|
886
|
+
// that has not been updated to pass the flag) can never accidentally
|
|
887
|
+
// populate integratedSha here; it stays null until US-08-TASK-BE-04's own
|
|
888
|
+
// integration step writes it.
|
|
889
|
+
//
|
|
890
|
+
// Stage transition: advances the attempt to 'committed'. Allowed only from
|
|
891
|
+
// 'checkpoint-prepared' (the stage persistCheckpointIntent, US-05-TASK-BE-01,
|
|
892
|
+
// leaves it at) or, idempotently, from 'committed' itself — but ONLY when
|
|
893
|
+
// the SHA (and, for a sequential registration, integratedSha) being
|
|
894
|
+
// registered is identical to what is already recorded (a safe replay of the
|
|
895
|
+
// exact same call; no generation bump). Registering a DIFFERENT SHA against
|
|
896
|
+
// an attempt that already has one recorded is rejected (fail closed,
|
|
897
|
+
// SHA_REGISTRATION_CONFLICT) — a commit, once registered, is immutable; this
|
|
898
|
+
// function never silently swaps it for another.
|
|
899
|
+
//
|
|
900
|
+
// This function does not decide whether the task/run is "done" — ledger
|
|
901
|
+
// finalization is a separate, later concern (US-05-TASK-BE-05).
|
|
902
|
+
const SHA_PATTERN = /^[0-9a-f]{40}$/i;
|
|
903
|
+
|
|
904
|
+
function _shaRegistrationFail(field, reason) {
|
|
905
|
+
throw _err('SHA_REGISTRATION_VALIDATION_ERROR', 'registerCommitSHA: ' + field + ' ' + reason);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/**
|
|
909
|
+
* @param {string} executionRoot
|
|
910
|
+
* @param {string} runId
|
|
911
|
+
* @param {string} taskId
|
|
912
|
+
* @param {number} attemptNumber
|
|
913
|
+
* @param {string} commitSha - the real, already-confirmed 40-character hex
|
|
914
|
+
* commit SHA (e.g. createTaskCommit's returned commitSha).
|
|
915
|
+
* @param {{sequential?: boolean}} [opts] - sequential: true also sets
|
|
916
|
+
* integratedSha = commitSha in this same call (N=1 only — see file comment
|
|
917
|
+
* above). Omit/false for N>1 attempts, whose integratedSha is set later by
|
|
918
|
+
* US-08-TASK-BE-04's integration step.
|
|
919
|
+
* @returns {{taskId: string, attemptNumber: number, stage: string,
|
|
920
|
+
* originalSha: string, integratedSha: string|null}}
|
|
921
|
+
* @throws {Error} SHA_REGISTRATION_VALIDATION_ERROR for malformed input;
|
|
922
|
+
* STATE_TASK_NOT_FOUND / STATE_ATTEMPT_NOT_FOUND when the target does not
|
|
923
|
+
* exist; SHA_REGISTRATION_STAGE_ERROR when the attempt is not at
|
|
924
|
+
* 'checkpoint-prepared' (and not an identical replay of an already-
|
|
925
|
+
* 'committed' attempt); SHA_REGISTRATION_CONFLICT when a different SHA is
|
|
926
|
+
* already registered for this attempt.
|
|
927
|
+
*/
|
|
928
|
+
function registerCommitSHA(executionRoot, runId, taskId, attemptNumber, commitSha, opts) {
|
|
929
|
+
opts = opts || {};
|
|
930
|
+
const sequential = opts.sequential === true;
|
|
931
|
+
|
|
932
|
+
_validateSafeId(taskId, 'registerCommitSHA');
|
|
933
|
+
if (!Number.isInteger(attemptNumber) || attemptNumber < 1) {
|
|
934
|
+
_shaRegistrationFail('attemptNumber', 'must be a positive integer');
|
|
935
|
+
}
|
|
936
|
+
if (typeof commitSha !== 'string' || !SHA_PATTERN.test(commitSha)) {
|
|
937
|
+
_shaRegistrationFail('commitSha', 'must be a 40-character hex SHA-1 string');
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
let state = readState(executionRoot, runId);
|
|
941
|
+
const task = state.tasks[taskId];
|
|
942
|
+
if (!task) {
|
|
943
|
+
throw _err('STATE_TASK_NOT_FOUND', 'registerCommitSHA: task "' + taskId + '" not found');
|
|
944
|
+
}
|
|
945
|
+
const attempt = task.attempts.find(function (a) { return a.number === attemptNumber; });
|
|
946
|
+
if (!attempt) {
|
|
947
|
+
throw _err(
|
|
948
|
+
'STATE_ATTEMPT_NOT_FOUND',
|
|
949
|
+
'registerCommitSHA: attempt ' + attemptNumber + ' not found for task "' + taskId + '"'
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
if (attempt.stage === 'committed') {
|
|
954
|
+
const sameOriginal = attempt.originalSha === commitSha;
|
|
955
|
+
const sameIntegrated = sequential ? attempt.integratedSha === commitSha : true;
|
|
956
|
+
if (sameOriginal && sameIntegrated) {
|
|
957
|
+
return {
|
|
958
|
+
taskId: taskId,
|
|
959
|
+
attemptNumber: attemptNumber,
|
|
960
|
+
stage: attempt.stage,
|
|
961
|
+
originalSha: attempt.originalSha,
|
|
962
|
+
integratedSha: attempt.integratedSha,
|
|
963
|
+
}; // idempotent no-op — no generation bump for a repeated identical registration
|
|
964
|
+
}
|
|
965
|
+
throw _err(
|
|
966
|
+
'SHA_REGISTRATION_CONFLICT',
|
|
967
|
+
'registerCommitSHA: attempt ' + attemptNumber + ' of task "' + taskId + '" already has a commit SHA ' +
|
|
968
|
+
'registered (originalSha ' + attempt.originalSha + ', integratedSha ' + attempt.integratedSha + ') that ' +
|
|
969
|
+
'does not match this registration (commitSha ' + commitSha + ', sequential ' + sequential + ') — a ' +
|
|
970
|
+
'registered commit is immutable; nothing overwritten'
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
if (attempt.stage !== 'checkpoint-prepared') {
|
|
975
|
+
throw _err(
|
|
976
|
+
'SHA_REGISTRATION_STAGE_ERROR',
|
|
977
|
+
'registerCommitSHA: attempt ' + attemptNumber + ' of task "' + taskId + '" is at stage "' + attempt.stage +
|
|
978
|
+
'"; a commit SHA may only be registered once the checkpoint intent has been persisted (expected stage ' +
|
|
979
|
+
'"checkpoint-prepared", or "committed" for an idempotent replay of the identical SHA)'
|
|
980
|
+
);
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
const patch = { stage: 'committed', originalSha: commitSha };
|
|
984
|
+
if (sequential) {
|
|
985
|
+
patch.integratedSha = commitSha;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
state = state.updateAttempt(taskId, attemptNumber, patch);
|
|
989
|
+
state = writeState(executionRoot, runId, state);
|
|
990
|
+
|
|
991
|
+
const updatedAttempt = state.tasks[taskId].attempts.find(function (a) { return a.number === attemptNumber; });
|
|
992
|
+
return {
|
|
993
|
+
taskId: taskId,
|
|
994
|
+
attemptNumber: attemptNumber,
|
|
995
|
+
stage: updatedAttempt.stage,
|
|
996
|
+
originalSha: updatedAttempt.originalSha,
|
|
997
|
+
integratedSha: updatedAttempt.integratedSha,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
// ── registerIntegratedSHA (US-08-TASK-BE-04) ────────────────────────────────
|
|
1002
|
+
//
|
|
1003
|
+
// Persists the real, git-confirmed integration commit SHA that integrateAttempt
|
|
1004
|
+
// (US-08-TASK-BE-03, lib/task-executor/git.js) already produced and
|
|
1005
|
+
// re-inspected (tree/parent/trailer verification, CAS ref-advance), into
|
|
1006
|
+
// executor state. This is the N>1 counterpart to registerCommitSHA's own
|
|
1007
|
+
// `opts.sequential` shortcut: for N>1 (Tech-Spec section 8), originalSha (the
|
|
1008
|
+
// technical-branch commit) and integratedSha (the feature-branch commit
|
|
1009
|
+
// produced by cherry-pick) are genuinely different SHAs, and integratedSha is
|
|
1010
|
+
// only known once integrateAttempt has actually run — a separate, later step
|
|
1011
|
+
// than the original commit's own registration. This function runs no git
|
|
1012
|
+
// commands itself and does not verify cherry-pick correctness or reachability
|
|
1013
|
+
// — that already happened, unconditionally, inside integrateAttempt before it
|
|
1014
|
+
// returned. It only persists the already-integrated result.
|
|
1015
|
+
//
|
|
1016
|
+
// Stage transition: advances the attempt to 'integrated' (an existing
|
|
1017
|
+
// ATTEMPT_STAGES value — no new stage is introduced). Allowed only from
|
|
1018
|
+
// 'committed' with a valid, already-registered originalSha (registerCommitSHA's
|
|
1019
|
+
// own postcondition) — or, idempotently, from 'integrated' itself, but ONLY
|
|
1020
|
+
// when the integratedSha being registered is identical to what is already
|
|
1021
|
+
// recorded (a safe replay of the exact same call; no generation bump).
|
|
1022
|
+
// Registering a DIFFERENT integratedSha against an attempt that already has
|
|
1023
|
+
// one recorded is rejected (fail closed, INTEGRATION_SHA_CONFLICT) — an
|
|
1024
|
+
// integrated commit, once registered, is immutable; this function never
|
|
1025
|
+
// silently swaps it for another.
|
|
1026
|
+
//
|
|
1027
|
+
// originalSha is never touched here — it remains exactly what
|
|
1028
|
+
// registerCommitSHA already set; this function only ever writes
|
|
1029
|
+
// attempt.integratedSha and attempt.stage.
|
|
1030
|
+
//
|
|
1031
|
+
// SCOPE BOUNDARY: this function does NOT set task.status. Setting the task's
|
|
1032
|
+
// overall status is a later, index.js-layer "finalize" concern — the same
|
|
1033
|
+
// division of labor registerCommitSHA itself already follows (it never
|
|
1034
|
+
// touches task.status either; only finalizeTaskCheckpoint, at the index.js
|
|
1035
|
+
// layer, calls state.setTaskStatus(taskId, 'checkpointed') once the commit
|
|
1036
|
+
// is verified reachable on disk). No analogous "finalize integration" step
|
|
1037
|
+
// exists yet at the index.js layer for N>1 runs (this task is scoped to
|
|
1038
|
+
// store.js only), so setting task.status = 'integrated' here would be scope
|
|
1039
|
+
// creep ahead of that future caller — left out deliberately, matching
|
|
1040
|
+
// precedent, and documented here so the future caller knows where that
|
|
1041
|
+
// responsibility belongs.
|
|
1042
|
+
function _integrationShaRegistrationFail(field, reason) {
|
|
1043
|
+
throw _err('INTEGRATION_SHA_VALIDATION_ERROR', 'registerIntegratedSHA: ' + field + ' ' + reason);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
/**
|
|
1047
|
+
* @param {string} executionRoot
|
|
1048
|
+
* @param {string} runId
|
|
1049
|
+
* @param {string} taskId
|
|
1050
|
+
* @param {number} attemptNumber
|
|
1051
|
+
* @param {string} integratedSha - the real, already-confirmed 40-character
|
|
1052
|
+
* hex integration commit SHA (e.g. integrateAttempt's returned
|
|
1053
|
+
* integratedSha).
|
|
1054
|
+
* @returns {{taskId: string, attemptNumber: number, stage: string,
|
|
1055
|
+
* originalSha: string, integratedSha: string}}
|
|
1056
|
+
* @throws {Error} INTEGRATION_SHA_VALIDATION_ERROR for malformed input;
|
|
1057
|
+
* STATE_TASK_NOT_FOUND / STATE_ATTEMPT_NOT_FOUND when the target does not
|
|
1058
|
+
* exist; INTEGRATION_SHA_STAGE_ERROR when the attempt is not at
|
|
1059
|
+
* 'committed' with a valid originalSha already registered (and not an
|
|
1060
|
+
* identical replay of an already-'integrated' attempt);
|
|
1061
|
+
* INTEGRATION_SHA_CONFLICT when a different integratedSha is already
|
|
1062
|
+
* registered for this attempt.
|
|
1063
|
+
*/
|
|
1064
|
+
function registerIntegratedSHA(executionRoot, runId, taskId, attemptNumber, integratedSha) {
|
|
1065
|
+
_validateSafeId(taskId, 'registerIntegratedSHA');
|
|
1066
|
+
if (!Number.isInteger(attemptNumber) || attemptNumber < 1) {
|
|
1067
|
+
_integrationShaRegistrationFail('attemptNumber', 'must be a positive integer');
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof integratedSha !== 'string' || !SHA_PATTERN.test(integratedSha)) {
|
|
1070
|
+
_integrationShaRegistrationFail('integratedSha', 'must be a 40-character hex SHA-1 string');
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
let state = readState(executionRoot, runId);
|
|
1074
|
+
const task = state.tasks[taskId];
|
|
1075
|
+
if (!task) {
|
|
1076
|
+
throw _err('STATE_TASK_NOT_FOUND', 'registerIntegratedSHA: task "' + taskId + '" not found');
|
|
1077
|
+
}
|
|
1078
|
+
const attempt = task.attempts.find(function (a) { return a.number === attemptNumber; });
|
|
1079
|
+
if (!attempt) {
|
|
1080
|
+
throw _err(
|
|
1081
|
+
'STATE_ATTEMPT_NOT_FOUND',
|
|
1082
|
+
'registerIntegratedSHA: attempt ' + attemptNumber + ' not found for task "' + taskId + '"'
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
if (attempt.stage === 'integrated') {
|
|
1087
|
+
if (attempt.integratedSha === integratedSha) {
|
|
1088
|
+
return {
|
|
1089
|
+
taskId: taskId,
|
|
1090
|
+
attemptNumber: attemptNumber,
|
|
1091
|
+
stage: attempt.stage,
|
|
1092
|
+
originalSha: attempt.originalSha,
|
|
1093
|
+
integratedSha: attempt.integratedSha,
|
|
1094
|
+
}; // idempotent no-op — no generation bump for a repeated identical registration
|
|
1095
|
+
}
|
|
1096
|
+
throw _err(
|
|
1097
|
+
'INTEGRATION_SHA_CONFLICT',
|
|
1098
|
+
'registerIntegratedSHA: attempt ' + attemptNumber + ' of task "' + taskId + '" already has an integratedSha ' +
|
|
1099
|
+
'registered (' + attempt.integratedSha + ') that does not match this registration (' + integratedSha + ') ' +
|
|
1100
|
+
'— an integrated commit is immutable; nothing overwritten'
|
|
1101
|
+
);
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (attempt.stage !== 'committed' || typeof attempt.originalSha !== 'string' || !SHA_PATTERN.test(attempt.originalSha)) {
|
|
1105
|
+
throw _err(
|
|
1106
|
+
'INTEGRATION_SHA_STAGE_ERROR',
|
|
1107
|
+
'registerIntegratedSHA: attempt ' + attemptNumber + ' of task "' + taskId + '" is at stage "' + attempt.stage +
|
|
1108
|
+
'"; an integratedSha may only be registered once the original commit SHA has been registered (expected ' +
|
|
1109
|
+
'stage "committed" with a valid originalSha, or "integrated" for an idempotent replay of the identical SHA)'
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
state = state.updateAttempt(taskId, attemptNumber, { stage: 'integrated', integratedSha: integratedSha });
|
|
1114
|
+
state = writeState(executionRoot, runId, state);
|
|
1115
|
+
|
|
1116
|
+
const updatedAttempt = state.tasks[taskId].attempts.find(function (a) { return a.number === attemptNumber; });
|
|
1117
|
+
return {
|
|
1118
|
+
taskId: taskId,
|
|
1119
|
+
attemptNumber: attemptNumber,
|
|
1120
|
+
stage: updatedAttempt.stage,
|
|
1121
|
+
originalSha: updatedAttempt.originalSha,
|
|
1122
|
+
integratedSha: updatedAttempt.integratedSha,
|
|
1123
|
+
};
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function writeIntent(executionRoot, runId, intentId, intentData) {
|
|
1127
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
1128
|
+
return _writeImmutableRecord(paths.intentsDir, intentId, intentData, 'INTENT_CONFLICT', 'writeIntent');
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function readIntent(executionRoot, runId, intentId) {
|
|
1132
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
1133
|
+
return _readRecord(paths.intentsDir, intentId, 'INTENT_NOT_FOUND', 'INTENT_CORRUPTED', 'readIntent');
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
function listIntents(executionRoot, runId) {
|
|
1137
|
+
const paths = _executionPaths(executionRoot, runId);
|
|
1138
|
+
return _listJsonIds(paths.intentsDir);
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
module.exports = {
|
|
1142
|
+
PROTOCOL_VERSION,
|
|
1143
|
+
RUN_STATUSES,
|
|
1144
|
+
TASK_STATUSES,
|
|
1145
|
+
ATTEMPT_STAGES,
|
|
1146
|
+
State,
|
|
1147
|
+
writeState,
|
|
1148
|
+
readState,
|
|
1149
|
+
recoverState,
|
|
1150
|
+
writeReceipt,
|
|
1151
|
+
readReceipt,
|
|
1152
|
+
listReceipts,
|
|
1153
|
+
persistVerificationReviewOutcome,
|
|
1154
|
+
registerCommitSHA,
|
|
1155
|
+
registerIntegratedSHA,
|
|
1156
|
+
writeIntent,
|
|
1157
|
+
readIntent,
|
|
1158
|
+
listIntents,
|
|
1159
|
+
_executionPaths,
|
|
1160
|
+
_loadStateFile,
|
|
1161
|
+
_atomicWriteFileSync,
|
|
1162
|
+
_stableStringify,
|
|
1163
|
+
_sha256Hex,
|
|
1164
|
+
};
|