@thebackstoryis/engineering-with-ai 0.2.9 → 0.3.0-beta.2
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/Docs/README.md +2 -0
- package/Docs/autonomous-intent-delivery.md +37 -0
- package/Docs/guide-catalogue.md +2 -0
- package/Docs/quality/persona-driven-test-scenarios.md +6 -0
- package/README.md +17 -0
- package/config/autonomy-worker.schema.json +70 -0
- package/config/autonomy.schema.json +25 -0
- package/package.json +3 -3
- package/public/app.js +7 -0
- package/public/autonomy.js +470 -0
- package/public/index.html +26 -0
- package/public/styles.css +64 -0
- package/skills-src/ewai-autonomy/SKILL.md +18 -0
- package/skills-src/ewai-autonomy/agents/openai.yaml +4 -0
- package/skills-src/ewai-deliver/SKILL.md +2 -0
- package/skills-src/ewai-test-scenarios/references/scenario-contract.md +6 -0
- package/src/autonomy-phase-contracts.mjs +88 -0
- package/src/autonomy-worker.mjs +15 -0
- package/src/autonomy.mjs +199 -0
- package/src/checkin.mjs +30 -6
- package/src/cli.mjs +67 -0
- package/src/delivery-gates.mjs +8 -1
- package/src/delivery.mjs +74 -3
- package/src/design-system-application.mjs +28 -0
- package/src/runtime/afk-conductor.mjs +191 -71
- package/src/runtime/autonomy-operations.mjs +250 -0
- package/src/runtime/autonomy-supervisor.mjs +426 -0
- package/src/runtime/autonomy-workers.mjs +333 -0
- package/src/runtime/autonomy-workspace.mjs +310 -0
- package/src/runtime/dashboard-actions.mjs +69 -0
- package/src/runtime/dashboard-server.mjs +25 -0
- package/src/runtime/database.mjs +25 -6
- package/src/runtime/intent-ownership.mjs +272 -0
- package/src/runtime/mcp-server.mjs +38 -0
- package/src/runtime/provider-adapters.mjs +365 -7
- package/src/runtime/work.mjs +21 -5
- package/src/test-scenarios.mjs +173 -20
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
+
import { existsSync, readdirSync, lstatSync, openSync, closeSync, fstatSync, readFileSync, writeFileSync, mkdirSync, fsyncSync, constants } from 'node:fs';
|
|
3
|
+
import { resolve, relative, dirname } from 'node:path';
|
|
4
|
+
import { intentMutationPaths, safeIntentMutationPath, resolveIntentMutationReference, recoverStoppedIntentOwnership, assertIntentMutation } from './intent-ownership.mjs';
|
|
5
|
+
|
|
6
|
+
const idPattern = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
|
|
7
|
+
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
|
8
|
+
const actions = new Set(['begin-harness', 'prepare-phase', 'afk-build', 'record-gate', 'record-validation', 'start-phase', 'complete-phase',
|
|
9
|
+
'resume-shelf', 'ratify-plan', 'ratify-evidence', 'approve-build', 'approve-manual-qa', 'update-projection', 'update-phase-projection', 'start-session']);
|
|
10
|
+
const digest = value => 'sha256:' + createHash('sha256').update(typeof value === 'string' || Buffer.isBuffer(value) ? value : JSON.stringify(value)).digest('hex');
|
|
11
|
+
|
|
12
|
+
function fail(code, statusCode = 409) {
|
|
13
|
+
const error = new Error(`Autonomy operation cannot continue: ${code}.`); error.code = code; error.statusCode = statusCode; throw error;
|
|
14
|
+
}
|
|
15
|
+
function readBytes(paths, path) {
|
|
16
|
+
safeIntentMutationPath(paths.projectRoot, path);
|
|
17
|
+
const before = lstatSync(path);
|
|
18
|
+
if (!before.isFile() || before.size > 16 * 1024 * 1024) fail('autonomy-operation-evidence-limit', 413);
|
|
19
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
20
|
+
try {
|
|
21
|
+
const opened = fstatSync(fd), bytes = readFileSync(fd), after = fstatSync(fd), current = lstatSync(path);
|
|
22
|
+
if (opened.ino !== before.ino || opened.dev !== before.dev || opened.nlink !== 1 || after.nlink !== 1
|
|
23
|
+
|| after.size !== before.size || after.mtimeMs !== before.mtimeMs || current.ino !== before.ino || current.mtimeMs !== before.mtimeMs) fail('autonomy-operation-evidence-changed');
|
|
24
|
+
return bytes;
|
|
25
|
+
} finally { closeSync(fd); }
|
|
26
|
+
}
|
|
27
|
+
function readRecord(paths, path) {
|
|
28
|
+
let record; try { record = JSON.parse(readBytes(paths, path)); } catch (error) { if (error.code?.startsWith('autonomy-')) throw error; fail('autonomy-operation-evidence-invalid', 422); }
|
|
29
|
+
const { digest: expected, ...body } = record ?? {};
|
|
30
|
+
if (!digestPattern.test(expected ?? '') || digest(body) !== expected) fail('autonomy-operation-evidence-invalid', 422);
|
|
31
|
+
return record;
|
|
32
|
+
}
|
|
33
|
+
function writeRecord(paths, path, body) {
|
|
34
|
+
const record = { ...body, digest: digest(body) };
|
|
35
|
+
safeIntentMutationPath(paths.projectRoot, path); mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
36
|
+
safeIntentMutationPath(paths.projectRoot, path);
|
|
37
|
+
const fd = openSync(path, 'wx', 0o600);
|
|
38
|
+
try { writeFileSync(fd, JSON.stringify(record, null, 2) + '\n'); fsyncSync(fd); } finally { closeSync(fd); }
|
|
39
|
+
const directory = openSync(dirname(path), constants.O_RDONLY);
|
|
40
|
+
try { fsyncSync(directory); } finally { closeSync(directory); }
|
|
41
|
+
return record;
|
|
42
|
+
}
|
|
43
|
+
function operationPaths(root, operationId) {
|
|
44
|
+
if (!idPattern.test(operationId ?? '')) fail('autonomy-operation-id-invalid', 422);
|
|
45
|
+
const paths = intentMutationPaths(root);
|
|
46
|
+
const operationsRoot = safeIntentMutationPath(paths.projectRoot, resolve(paths.specsRoot, '3.Evidence/autonomy/operations'));
|
|
47
|
+
return { ...paths, operationsRoot, operationRoot: safeIntentMutationPath(paths.projectRoot, resolve(operationsRoot, operationId)) };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function readIntentMutationSnapshot(root, reference) {
|
|
51
|
+
const paths = intentMutationPaths(root), intentId = resolveIntentMutationReference(root, reference), slug = intentId.split('/')[1];
|
|
52
|
+
const amendmentLease = resolve(paths.buildRoot, slug, '.evidence-amendment.lock');
|
|
53
|
+
const files = [], captured = new Map(), directories = new Map(), missing = []; let total = 0;
|
|
54
|
+
const same = (before, after) => before.ino === after.ino && before.dev === after.dev && before.size === after.size
|
|
55
|
+
&& before.mtimeMs === after.mtimeMs && before.ctimeMs === after.ctimeMs && before.nlink === after.nlink;
|
|
56
|
+
const capture = path => {
|
|
57
|
+
const before = lstatSync(path), bytes = readBytes(paths, path);
|
|
58
|
+
if (!same(before, lstatSync(path))) fail('autonomy-operation-evidence-changed');
|
|
59
|
+
captured.set(path, before); total += bytes.length;
|
|
60
|
+
if (total > 128 * 1024 * 1024) fail('autonomy-operation-evidence-limit', 413);
|
|
61
|
+
return bytes;
|
|
62
|
+
};
|
|
63
|
+
const visit = (path, depth = 0) => {
|
|
64
|
+
safeIntentMutationPath(paths.projectRoot, path);
|
|
65
|
+
// The existing amendment lease is disposable coordination, like SQLite
|
|
66
|
+
// ownership, not canonical evidence whose cleanup requires ratification.
|
|
67
|
+
if (path === amendmentLease) return;
|
|
68
|
+
if (!existsSync(path)) { missing.push(path); return; }
|
|
69
|
+
const stat = lstatSync(path);
|
|
70
|
+
if (depth > 20 || files.length >= 10000) fail('autonomy-operation-evidence-limit', 413);
|
|
71
|
+
if (stat.isDirectory()) {
|
|
72
|
+
const names = readdirSync(path).sort(); directories.set(path, { stat, names });
|
|
73
|
+
for (const entry of names) visit(resolve(path, entry), depth + 1);
|
|
74
|
+
} else {
|
|
75
|
+
files.push({ path: relative(paths.projectRoot, path), digest: digest(capture(path)) });
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
visit(paths.configPath);
|
|
79
|
+
for (const extension of ['md', 'json']) visit(resolve(paths.specsRoot, '2.Purpose/intents', `${intentId}.${extension}`));
|
|
80
|
+
visit(resolve(paths.buildRoot, slug));
|
|
81
|
+
const core = files.filter(file => /\/2\.Purpose\/intents\//.test(file.path) || file.path.endsWith('/delivery-state.json'));
|
|
82
|
+
const identity = digest(paths.projectRoot), locator = existsSync(paths.runtimeProjectPath) ? digest(capture(paths.runtimeProjectPath)) : null;
|
|
83
|
+
if (locator === null) missing.push(paths.runtimeProjectPath);
|
|
84
|
+
for (const [path, stat] of captured) {
|
|
85
|
+
safeIntentMutationPath(paths.projectRoot, path);
|
|
86
|
+
if (!existsSync(path) || !same(stat, lstatSync(path))) fail('autonomy-operation-evidence-changed');
|
|
87
|
+
}
|
|
88
|
+
for (const [path, before] of directories) {
|
|
89
|
+
safeIntentMutationPath(paths.projectRoot, path);
|
|
90
|
+
if (!existsSync(path) || !same(before.stat, lstatSync(path)) || JSON.stringify(readdirSync(path).sort()) !== JSON.stringify(before.names)) fail('autonomy-operation-evidence-changed');
|
|
91
|
+
}
|
|
92
|
+
if (missing.some(path => existsSync(path))) fail('autonomy-operation-evidence-changed');
|
|
93
|
+
return { digest: digest({ identity, locator, files }), coreDigest: digest({ identity, locator, files: core }), files };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function normaliseAutonomyOperation(input = {}) {
|
|
97
|
+
if (!actions.has(input.action)) fail('autonomy-operation-action-invalid', 422);
|
|
98
|
+
const id = input.id ?? randomUUID(), maxAttempts = input.maxAttempts ?? 1;
|
|
99
|
+
if (!idPattern.test(id) || !Number.isSafeInteger(maxAttempts) || maxAttempts < 1 || maxAttempts > 100) fail('autonomy-operation-input-invalid', 422);
|
|
100
|
+
if (input.phase !== undefined && (typeof input.phase !== 'string' || !/^[a-z]+(?:-[a-z]+)*$/.test(input.phase))) fail('autonomy-operation-input-invalid', 422);
|
|
101
|
+
if (input.provider !== undefined && (typeof input.provider !== 'string' || !/^[a-z][a-z0-9-]{0,39}$/.test(input.provider))) fail('autonomy-operation-input-invalid', 422);
|
|
102
|
+
const inputDigest = input.inputDigest ?? digest({ action: input.action, phase: input.phase ?? null, provider: input.provider ?? null, input: input.input ?? null });
|
|
103
|
+
if (!digestPattern.test(inputDigest) || input.grantDigest !== undefined && input.grantDigest !== null && !digestPattern.test(input.grantDigest)
|
|
104
|
+
|| input.expectedPredecessor !== undefined && !digestPattern.test(input.expectedPredecessor)) fail('autonomy-operation-input-invalid', 422);
|
|
105
|
+
return { id, action: input.action, phase: input.phase ?? null, provider: input.provider ?? null, inputDigest, grantDigest: input.grantDigest ?? null, maxAttempts,
|
|
106
|
+
expectedPredecessor: input.expectedPredecessor ?? null, tracked: input.id !== undefined };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function readAttempts(paths, operationId) {
|
|
110
|
+
const directory = safeIntentMutationPath(paths.projectRoot, resolve(paths.operationRoot, 'attempts'));
|
|
111
|
+
if (!existsSync(directory)) return [];
|
|
112
|
+
const names = readdirSync(directory);
|
|
113
|
+
if (names.length > 100 || names.some(name => !/^[1-9]\d*\.json$/.test(name))) fail('autonomy-operation-evidence-invalid', 422);
|
|
114
|
+
const attempts = names.sort((a, b) => parseInt(a) - parseInt(b)).map(name => readRecord(paths, resolve(directory, name)));
|
|
115
|
+
attempts.forEach((attempt, index) => {
|
|
116
|
+
if (attempt.schema !== 'ewai.autonomy-operation/v1' || attempt.operationId !== operationId || attempt.attempt !== index + 1
|
|
117
|
+
|| attempt.previousDigest !== (attempts[index - 1]?.digest ?? null) || !actions.has(attempt.action)
|
|
118
|
+
|| !digestPattern.test(attempt.inputDigest) || attempt.projectIdentity !== digest(paths.projectRoot)
|
|
119
|
+
|| !Number.isSafeInteger(attempt.maxAttempts) || attempt.maxAttempts < 1 || attempt.maxAttempts > 100
|
|
120
|
+
|| !Number.isInteger(attempt.owner?.pid) || attempt.owner.pid < 1 || typeof attempt.owner.processInstance !== 'string') fail('autonomy-operation-evidence-invalid', 422);
|
|
121
|
+
if (index && ['intentId', 'action', 'phase', 'provider', 'inputDigest', 'grantDigest', 'maxAttempts', 'tracked'].some(key => JSON.stringify(attempt[key]) !== JSON.stringify(attempts[0][key]))) fail('autonomy-operation-evidence-invalid', 422);
|
|
122
|
+
});
|
|
123
|
+
return attempts;
|
|
124
|
+
}
|
|
125
|
+
function checkIdentity(attempt, intentId, operation) {
|
|
126
|
+
if (attempt && (attempt.intentId !== intentId || ['action', 'phase', 'provider', 'inputDigest', 'grantDigest', 'maxAttempts', 'tracked'].some(key => attempt[key] !== operation[key])
|
|
127
|
+
|| operation.expectedPredecessor && operation.expectedPredecessor !== attempt.predecessor.digest)) fail('autonomy-operation-conflict');
|
|
128
|
+
}
|
|
129
|
+
function stopped(attempt, outcome) {
|
|
130
|
+
if (outcome?.executionStopped === true) return true;
|
|
131
|
+
try { process.kill(attempt.owner.pid, 0); return false; }
|
|
132
|
+
catch (error) { return error.code === 'ESRCH'; }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function reconcileAutonomyOperation(root, operationId) {
|
|
136
|
+
const paths = operationPaths(root, operationId), attempts = readAttempts(paths, operationId), attempt = attempts.at(-1);
|
|
137
|
+
if (!attempt) return { status: 'not-found', operationId, requiresHuman: true, reasons: ['operation-journal-missing'] };
|
|
138
|
+
const receiptPath = safeIntentMutationPath(paths.projectRoot, resolve(paths.operationRoot, 'receipts', `${attempt.attempt}.json`));
|
|
139
|
+
const outcomePath = safeIntentMutationPath(paths.projectRoot, resolve(paths.operationRoot, 'outcomes', `${attempt.attempt}.json`));
|
|
140
|
+
const current = readIntentMutationSnapshot(root, attempt.intentId);
|
|
141
|
+
const common = { operationId, intentId: attempt.intentId, attempt: attempt.attempt };
|
|
142
|
+
if (existsSync(receiptPath)) {
|
|
143
|
+
const receipt = readRecord(paths, receiptPath);
|
|
144
|
+
if (receipt.schema !== 'ewai.autonomy-operation-receipt/v1' || receipt.operationDigest !== attempt.digest
|
|
145
|
+
|| receipt.operationId !== operationId || receipt.attempt !== attempt.attempt || receipt.inputDigest !== attempt.inputDigest
|
|
146
|
+
|| receipt.grantDigest !== attempt.grantDigest || receipt.intentId !== attempt.intentId) fail('autonomy-operation-evidence-invalid', 422);
|
|
147
|
+
return receipt.result.digest === current.digest
|
|
148
|
+
? { ...common, status: 'completed', requiresHuman: false, reasons: [], receipt }
|
|
149
|
+
: { ...common, status: 'recovery-required', requiresHuman: true, reasons: ['canonical-receipt-changed'] };
|
|
150
|
+
}
|
|
151
|
+
const outcome = existsSync(outcomePath) ? readRecord(paths, outcomePath) : null;
|
|
152
|
+
if (outcome && (outcome.schema !== 'ewai.autonomy-operation-outcome/v1' || outcome.operationDigest !== attempt.digest)) fail('autonomy-operation-evidence-invalid', 422);
|
|
153
|
+
const reasons = [];
|
|
154
|
+
if (current.digest !== attempt.predecessor.digest) reasons.push('canonical-predecessor-changed');
|
|
155
|
+
if (!stopped(attempt, outcome)) reasons.push('execution-not-confirmed-stopped');
|
|
156
|
+
if (attempt.attempt >= attempt.maxAttempts) reasons.push('attempts-exhausted');
|
|
157
|
+
return { ...common, status: reasons.length ? 'recovery-required' : 'retryable', requiresHuman: reasons.length > 0, reasons };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function inspectAutonomyOperation(root, intentId, operation) {
|
|
161
|
+
const paths = operationPaths(root, operation.id), attempts = readAttempts(paths, operation.id);
|
|
162
|
+
checkIdentity(attempts[0], intentId, operation);
|
|
163
|
+
const state = reconcileAutonomyOperation(root, operation.id);
|
|
164
|
+
if (state.status === 'retryable') recoverStoppedIntentOwnership(root, attempts.at(-1).owner, intentId);
|
|
165
|
+
return state;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Recovery may acknowledge an already completed synchronous mutation, but
|
|
169
|
+
// cannot infer provider termination or repeat a side effect from parent death.
|
|
170
|
+
export function recoverCompletedAutonomyOperation(root, operationId, expected) {
|
|
171
|
+
const paths = operationPaths(root, operationId), attempt = readAttempts(paths, operationId).at(-1);
|
|
172
|
+
if (!attempt || expected?.action !== 'begin-harness' || ['action', 'intentId', 'grantDigest'].some(key => attempt[key] !== expected[key])) {
|
|
173
|
+
return { status: 'recovery-required', operationId, requiresHuman: true };
|
|
174
|
+
}
|
|
175
|
+
const reconciled = reconcileAutonomyOperation(root, operationId);
|
|
176
|
+
if (reconciled.status !== 'completed' || reconciled.receipt.action !== attempt.action) return { status: 'recovery-required', operationId, requiresHuman: true };
|
|
177
|
+
recoverStoppedIntentOwnership(root, attempt.owner, attempt.intentId);
|
|
178
|
+
return { status: 'completed', operationId, requiresHuman: false };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// A new identifier is not a recovery decision. Completed receipts may be
|
|
182
|
+
// historical, but an uncertain attempt still owns the intent's recovery work.
|
|
183
|
+
export function assertNoUnresolvedIntentOperation(root, intentId, operationId) {
|
|
184
|
+
const base = operationPaths(root, operationId);
|
|
185
|
+
if (!existsSync(base.operationsRoot)) return;
|
|
186
|
+
const ids = readdirSync(base.operationsRoot);
|
|
187
|
+
if (ids.length > 10000 || ids.some(id => !idPattern.test(id))) fail('autonomy-operation-evidence-invalid', 422);
|
|
188
|
+
for (const id of ids) {
|
|
189
|
+
if (id === operationId) continue;
|
|
190
|
+
const paths = operationPaths(root, id), attempt = readAttempts(paths, id).at(-1);
|
|
191
|
+
if (!attempt || attempt.intentId !== intentId) continue;
|
|
192
|
+
const receiptPath = resolve(paths.operationRoot, 'receipts', `${attempt.attempt}.json`);
|
|
193
|
+
if (existsSync(receiptPath)) {
|
|
194
|
+
const receipt = readRecord(paths, receiptPath);
|
|
195
|
+
if (receipt.schema !== 'ewai.autonomy-operation-receipt/v1' || receipt.operationDigest !== attempt.digest) fail('autonomy-operation-evidence-invalid', 422);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
const outcomePath = resolve(paths.operationRoot, 'outcomes', `${attempt.attempt}.json`);
|
|
199
|
+
const outcome = existsSync(outcomePath) ? readRecord(paths, outcomePath) : null;
|
|
200
|
+
if (outcome && (outcome.schema !== 'ewai.autonomy-operation-outcome/v1' || outcome.operationDigest !== attempt.digest)) fail('autonomy-operation-evidence-invalid', 422);
|
|
201
|
+
// Only allowlisted checker reports are harmless diagnostic changes. Runs,
|
|
202
|
+
// gate ledgers and all other durable evidence retain recovery ownership.
|
|
203
|
+
if (!attempt.tracked && outcome?.executionStopped === true && outcome.diagnosticOnly === true) continue;
|
|
204
|
+
fail('autonomy-recovery-required');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function beginAutonomyOperation(root, intentId, operation, owner) {
|
|
209
|
+
const paths = operationPaths(root, operation.id), attempts = readAttempts(paths, operation.id);
|
|
210
|
+
checkIdentity(attempts[0], intentId, operation);
|
|
211
|
+
const state = reconcileAutonomyOperation(root, operation.id);
|
|
212
|
+
if (!['not-found', 'retryable'].includes(state.status)) fail(state.status === 'completed' ? 'autonomy-operation-conflict' : 'autonomy-recovery-required');
|
|
213
|
+
const predecessor = readIntentMutationSnapshot(root, intentId);
|
|
214
|
+
if (operation.expectedPredecessor && operation.expectedPredecessor !== predecessor.digest) fail('autonomy-predecessor-stale');
|
|
215
|
+
const attempt = attempts.length + 1;
|
|
216
|
+
assertIntentMutation(root, intentId);
|
|
217
|
+
return writeRecord(paths, resolve(paths.operationRoot, 'attempts', `${attempt}.json`), {
|
|
218
|
+
schema: 'ewai.autonomy-operation/v1', operationId: operation.id, intentId, action: operation.action, phase: operation.phase, provider: operation.provider,
|
|
219
|
+
inputDigest: operation.inputDigest, grantDigest: operation.grantDigest, maxAttempts: operation.maxAttempts, tracked: operation.tracked,
|
|
220
|
+
predecessor, attempt, previousDigest: attempts.at(-1)?.digest ?? null, projectIdentity: digest(paths.projectRoot),
|
|
221
|
+
owner: { epoch: owner.epoch, generation: owner.generation, ownerId: owner.ownerId, pid: process.pid, processInstance: owner.processInstance },
|
|
222
|
+
startedAt: new Date().toISOString(),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function completeAutonomyOperation(root, attempt) {
|
|
227
|
+
const paths = operationPaths(root, attempt.operationId), result = readIntentMutationSnapshot(root, attempt.intentId);
|
|
228
|
+
assertIntentMutation(root, attempt.intentId);
|
|
229
|
+
return writeRecord(paths, resolve(paths.operationRoot, 'receipts', `${attempt.attempt}.json`), {
|
|
230
|
+
schema: 'ewai.autonomy-operation-receipt/v1', operationId: attempt.operationId, operationDigest: attempt.digest,
|
|
231
|
+
intentId: attempt.intentId, action: attempt.action, inputDigest: attempt.inputDigest, grantDigest: attempt.grantDigest,
|
|
232
|
+
attempt: attempt.attempt, owner: attempt.owner, result, completedAt: new Date().toISOString(),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function failAutonomyOperation(root, attempt, { executionStopped = true } = {}) {
|
|
237
|
+
const paths = operationPaths(root, attempt.operationId), result = readIntentMutationSnapshot(root, attempt.intentId);
|
|
238
|
+
const before = new Map(attempt.predecessor.files.map(file => [file.path, file.digest]));
|
|
239
|
+
const after = new Map(result.files.map(file => [file.path, file.digest]));
|
|
240
|
+
const changes = [...new Set([...before.keys(), ...after.keys()])].filter(path => before.get(path) !== after.get(path));
|
|
241
|
+
const diagnosticOnly = result.coreDigest === attempt.predecessor.coreDigest && changes.every(path =>
|
|
242
|
+
/\/gates\/[a-z-]+\/(?:phase-gate-check\.md|task-graph-check\.(?:md|json))$/.test(path));
|
|
243
|
+
return writeRecord(paths, resolve(paths.operationRoot, 'outcomes', `${attempt.attempt}.json`), {
|
|
244
|
+
schema: 'ewai.autonomy-operation-outcome/v1', operationId: attempt.operationId, operationDigest: attempt.digest,
|
|
245
|
+
executionStopped, canonicalChanged: result.digest !== attempt.predecessor.digest,
|
|
246
|
+
coreChanged: result.coreDigest !== attempt.predecessor.coreDigest,
|
|
247
|
+
diagnosticOnly,
|
|
248
|
+
resultDigest: result.digest, finishedAt: new Date().toISOString(),
|
|
249
|
+
});
|
|
250
|
+
}
|