@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.
Files changed (37) hide show
  1. package/Docs/README.md +2 -0
  2. package/Docs/autonomous-intent-delivery.md +37 -0
  3. package/Docs/guide-catalogue.md +2 -0
  4. package/Docs/quality/persona-driven-test-scenarios.md +6 -0
  5. package/README.md +17 -0
  6. package/config/autonomy-worker.schema.json +70 -0
  7. package/config/autonomy.schema.json +25 -0
  8. package/package.json +3 -3
  9. package/public/app.js +7 -0
  10. package/public/autonomy.js +470 -0
  11. package/public/index.html +26 -0
  12. package/public/styles.css +64 -0
  13. package/skills-src/ewai-autonomy/SKILL.md +18 -0
  14. package/skills-src/ewai-autonomy/agents/openai.yaml +4 -0
  15. package/skills-src/ewai-deliver/SKILL.md +2 -0
  16. package/skills-src/ewai-test-scenarios/references/scenario-contract.md +6 -0
  17. package/src/autonomy-phase-contracts.mjs +88 -0
  18. package/src/autonomy-worker.mjs +15 -0
  19. package/src/autonomy.mjs +199 -0
  20. package/src/checkin.mjs +30 -6
  21. package/src/cli.mjs +67 -0
  22. package/src/delivery-gates.mjs +8 -1
  23. package/src/delivery.mjs +74 -3
  24. package/src/design-system-application.mjs +28 -0
  25. package/src/runtime/afk-conductor.mjs +191 -71
  26. package/src/runtime/autonomy-operations.mjs +250 -0
  27. package/src/runtime/autonomy-supervisor.mjs +426 -0
  28. package/src/runtime/autonomy-workers.mjs +333 -0
  29. package/src/runtime/autonomy-workspace.mjs +310 -0
  30. package/src/runtime/dashboard-actions.mjs +69 -0
  31. package/src/runtime/dashboard-server.mjs +25 -0
  32. package/src/runtime/database.mjs +25 -6
  33. package/src/runtime/intent-ownership.mjs +272 -0
  34. package/src/runtime/mcp-server.mjs +38 -0
  35. package/src/runtime/provider-adapters.mjs +365 -7
  36. package/src/runtime/work.mjs +21 -5
  37. package/src/test-scenarios.mjs +173 -20
@@ -0,0 +1,426 @@
1
+ import { resolve, relative } from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { spawn } from 'node:child_process';
4
+ import { performance } from 'node:perf_hooks';
5
+ import { readAutonomyPolicy, previewAutonomy, revokeAutonomyGrant } from '../autonomy.mjs';
6
+ import { beginDelivery } from '../delivery.mjs';
7
+ import { finishActiveSession } from './work.mjs';
8
+ import { autonomyPaths, autonomyFiles, autonomyDigest, autonomyError, readAutonomyFile,
9
+ writeAutonomyRecord, withAutonomyLock, readAutonomyRepositoryState, readAutonomySnapshot } from './autonomy-workspace.mjs';
10
+ import { withIntentMutation, acquireIntentOwnership, assertIntentOwnership, releaseIntentOwnership } from './intent-ownership.mjs';
11
+ import { readIntentMutationSnapshot, recoverCompletedAutonomyOperation } from './autonomy-operations.mjs';
12
+ import { prepareAutonomyPhase, invokePhaseProposal, acceptPhaseProposal } from './autonomy-workers.mjs';
13
+ import { preparePhaseProvider, verifyPhaseProviderConformance } from './provider-adapters.mjs';
14
+ import { preflightAfkRun, startAfkRun, resumeAfkRun, pauseAfkRun, cancelAfkRun, afkRunStatus } from './afk-conductor.mjs';
15
+
16
+ const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/;
17
+ const digestPattern = /^sha256:[a-f0-9]{64}$/;
18
+ const terminal = new Set(['completed', 'cancelled', 'revoked', 'expired', 'blocked']);
19
+ const statuses = new Set([...terminal, 'starting', 'running', 'paused', 'awaiting-human', 'cancel-requested', 'recovery-required']);
20
+ const live = new Map();
21
+ const failureCode = error => /^(?:autonomy|phase)-[a-z0-9-]+$/.test(error?.code ?? '') ? error.code : 'autonomy-operation-failed';
22
+ function inputKeys(input, allowed) {
23
+ if (!input || typeof input !== 'object' || Array.isArray(input) || Object.getPrototypeOf(input) !== Object.prototype
24
+ || Object.keys(input).some(key => !allowed.includes(key))) autonomyError('autonomy-invalid-input', 400);
25
+ }
26
+ function named(value) { return typeof value === 'string' && value.trim() === value && value.length > 0 && value.length <= 120 && !/[\x00-\x1f\x7f]/.test(value); }
27
+ function pathsFor(root, id) {
28
+ if (id !== undefined && !uuid.test(id)) autonomyError('autonomy-run-id-invalid', 400);
29
+ // Runtime control is durable across projection rebuilds, but is not source
30
+ // code or authority. Canonical effects retain their separate SPECS receipts.
31
+ const paths = autonomyPaths(root), runsRoot = resolve(paths.controlRoot, 'supervisor/runs');
32
+ return { ...paths, runsRoot, runRoot: id ? resolve(runsRoot, id) : null };
33
+ }
34
+ function record(paths, path, body) { const value = { ...body, digest: autonomyDigest(body) }; writeAutonomyRecord(paths.projectRoot, path, value); return value; }
35
+ function readRecord(paths, path) {
36
+ let value;
37
+ try { value = JSON.parse(readAutonomyFile(paths.projectRoot, path)); }
38
+ catch { autonomyError('autonomy-run-evidence-invalid'); }
39
+ if (!value || typeof value !== 'object' || Array.isArray(value)) autonomyError('autonomy-run-evidence-invalid');
40
+ const { digest, ...body } = value;
41
+ if (digest !== autonomyDigest(body)) autonomyError('autonomy-run-evidence-invalid');
42
+ return value;
43
+ }
44
+ function readRun(root, id) {
45
+ const paths = pathsFor(root, id), files = autonomyFiles(paths.projectRoot, resolve(paths.runRoot, 'revisions'));
46
+ if (!files.length) autonomyError('autonomy-run-not-found', 404);
47
+ if (files.length > 4096) autonomyError('autonomy-run-evidence-limit');
48
+ const records = files.map(path => ({ path, value: readRecord(paths, path) })).sort((a, b) => a.value.revision - b.value.revision);
49
+ records.forEach(({ path, value }, index) => {
50
+ inputKeys(value, ['schema', 'id', 'revision', 'previousDigest', 'projectIdentity', 'mode', 'provider', 'grantDigest', 'status',
51
+ 'desiredState', 'startedAt', 'updatedAt', 'lastClock', 'counters', 'owner', 'current', 'lastResult', 'questions', 'code',
52
+ 'cancellation', 'executionStopped', 'requiresHuman', 'ownedFiles', 'repositoryIdentity', 'digest']);
53
+ if (value.schema !== 'ewai.autonomy-run/v1' || value.id !== id || value.revision !== index + 1
54
+ || value.previousDigest !== (records[index - 1]?.value.digest ?? null)
55
+ || value.projectIdentity !== autonomyDigest(paths.projectRoot)
56
+ || !path.endsWith(`/${value.revision}-${value.digest.slice(7)}.json`)) autonomyError('autonomy-run-evidence-invalid');
57
+ if (!statuses.has(value.status) || !['once', 'service'].includes(value.mode) || !['running', 'paused', 'cancelled', 'revoked'].includes(value.desiredState)
58
+ || !digestPattern.test(value.grantDigest ?? '') || !['claude', 'codex', 'antigravity'].includes(value.provider)
59
+ || !Number.isSafeInteger(value.owner?.pid) || value.owner.pid < 1 || !uuid.test(value.owner?.instance ?? '')
60
+ || !Number.isSafeInteger(value.lastClock) || !Number.isFinite(Date.parse(value.startedAt)) || !Number.isFinite(Date.parse(value.updatedAt))
61
+ || !value.counters || Object.keys(value.counters).sort().join(',') !== 'actions,elapsedMs,providerAttempts,reviewCycles'
62
+ || Object.values(value.counters).some(count => !Number.isSafeInteger(count) || count < 0)
63
+ || typeof value.executionStopped !== 'boolean' || typeof value.requiresHuman !== 'boolean'
64
+ || !Array.isArray(value.questions) || value.questions.length > 100
65
+ || !Array.isArray(value.ownedFiles) || value.ownedFiles.length > 4096
66
+ || value.ownedFiles.some(file => typeof file.path !== 'string' || file.path.startsWith('/') || file.path.split('/').includes('..') || !digestPattern.test(file.digest ?? ''))
67
+ || value.repositoryIdentity !== null && !digestPattern.test(value.repositoryIdentity ?? '')) autonomyError('autonomy-run-evidence-invalid');
68
+ const intentId = value => typeof value === 'string' && /^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/.test(value);
69
+ if (value.current !== null) {
70
+ inputKeys(value.current, ['intentId', 'phase', 'action', 'sortKey', 'reasons', 'operationId', 'workerOperationId', 'afkRunId']);
71
+ if (!intentId(value.current.intentId) || !['begin-harness', 'prepare-phase', 'afk-build'].includes(value.current.action)
72
+ || !uuid.test(value.current.operationId ?? '')) autonomyError('autonomy-run-evidence-invalid');
73
+ }
74
+ if (value.lastResult !== null) {
75
+ inputKeys(value.lastResult, ['status', 'operationId', 'intentId', 'phase', 'phaseCompleted', 'afkRunId']);
76
+ if (!intentId(value.lastResult.intentId) || !['harness-started', 'draft-staged', 'afk-settled', 'reconciled'].includes(value.lastResult.status)) autonomyError('autonomy-run-evidence-invalid');
77
+ }
78
+ for (const q of value.questions) {
79
+ inputKeys(q, ['id', 'intentId', 'phase', 'owner', 'sourceRevision', 'codes', 'authority']);
80
+ if (!/^[a-f0-9]{64}$/.test(q.id ?? '') || !intentId(q.intentId) || !named(q.owner) || q.authority !== 'none'
81
+ || !digestPattern.test(q.sourceRevision ?? '') || !Array.isArray(q.codes) || q.codes.some(code => !/^[a-z0-9-]{1,100}$/.test(code))) autonomyError('autonomy-run-evidence-invalid');
82
+ }
83
+ if (index && (value.grantDigest !== records[0].value.grantDigest || value.mode !== records[0].value.mode
84
+ || Object.keys(value.counters).some(key => value.counters[key] < records[index - 1].value.counters[key]))) autonomyError('autonomy-run-history-invalid');
85
+ });
86
+ return records.at(-1).value;
87
+ }
88
+ function listRuns(root) {
89
+ const paths = pathsFor(root), ids = new Set(autonomyFiles(paths.projectRoot, paths.runsRoot).map(path => path.slice(paths.runsRoot.length + 1).split('/')[0]));
90
+ if (ids.size > 256) autonomyError('autonomy-run-evidence-limit');
91
+ return [...ids].map(id => readRun(root, id));
92
+ }
93
+ function save(paths, previous, changes) {
94
+ const { digest: oldDigest, ...old } = previous;
95
+ const body = { ...old, ...changes, revision: previous.revision + 1, previousDigest: oldDigest,
96
+ updatedAt: new Date().toISOString() };
97
+ return record(paths, resolve(paths.runRoot, 'revisions', `${body.revision}-${autonomyDigest(body).slice(7)}.json`), body);
98
+ }
99
+ function update(root, id, callback) {
100
+ const paths = pathsFor(root, id);
101
+ return withAutonomyLock(paths, () => { const current = readRun(root, id); return save(paths, current, callback(current)); });
102
+ }
103
+ function safeRun(run) {
104
+ return { schema: run.schema, id: run.id, revision: run.revision, digest: run.digest, mode: run.mode, status: run.status,
105
+ desiredState: run.desiredState, grantDigest: run.grantDigest, provider: run.provider, startedAt: run.startedAt,
106
+ updatedAt: run.updatedAt, counters: run.counters, usage: { providerReported: null, hardMoneyCap: false },
107
+ current: run.current, lastResult: run.lastResult, questions: run.questions, code: run.code,
108
+ cancellation: run.cancellation, executionStopped: run.executionStopped, requiresHuman: run.requiresHuman,
109
+ lifetime: { kind: run.mode, ownerProcessId: run.owner.pid, ownerProcessAlive: ownerAlive(run) },
110
+ authority: 'bounded-grant-only', phaseCompleted: false };
111
+ }
112
+ function ownerAlive(run) { try { process.kill(run.owner.pid, 0); return true; } catch (error) { return error.code !== 'ESRCH'; } }
113
+ export function readAutonomyRun(root, id) {
114
+ const run = readRun(root, id), safe = safeRun(run);
115
+ return !terminal.has(run.status) && ['starting', 'running', 'cancel-requested'].includes(run.status) && !ownerAlive(run)
116
+ ? { ...safe, status: 'recovery-required', requiresHuman: true, code: 'autonomy-owner-lost' } : safe;
117
+ }
118
+
119
+ function currentGrant(root, expected) {
120
+ const policy = readAutonomyPolicy(root);
121
+ if (policy.status !== 'current' || policy.grant?.digest !== expected) autonomyError(`autonomy-grant-${policy.status === 'current' ? 'stale' : policy.status}`);
122
+ if (Date.now() < Date.parse(policy.grant.approvedAt)) autonomyError('autonomy-clock-rollback');
123
+ const paths = autonomyPaths(root);
124
+ if (autonomyDigest(readAutonomyFile(paths.projectRoot, paths.configPath)) !== policy.grant.configDigest) autonomyError('autonomy-grant-policy-changed');
125
+ return policy.grant;
126
+ }
127
+ function createRun(root, input, mode) {
128
+ inputKeys(input, mode === 'service' ? ['expectedDigest', 'provider', 'confirmed'] : ['expectedDigest', 'provider']);
129
+ if (!digestPattern.test(input.expectedDigest) || mode === 'service' && input.confirmed !== true) autonomyError('autonomy-run-confirmation-required');
130
+ const paths = pathsFor(root);
131
+ return withAutonomyLock(paths, () => {
132
+ const grant = currentGrant(root, input.expectedDigest);
133
+ if (!grant.scope.providers.includes(input.provider)) autonomyError('autonomy-provider-not-permitted');
134
+ if (listRuns(root).some(run => !terminal.has(run.status))) autonomyError('autonomy-supervisor-busy');
135
+ const id = randomUUID(), at = Date.now();
136
+ const run = { schema: 'ewai.autonomy-run/v1', id, revision: 1, previousDigest: null,
137
+ projectIdentity: autonomyDigest(paths.projectRoot), mode, provider: input.provider, grantDigest: grant.digest,
138
+ status: 'starting', desiredState: 'running', startedAt: new Date(at).toISOString(), updatedAt: new Date(at).toISOString(),
139
+ lastClock: at, counters: { elapsedMs: 0, providerAttempts: 0, reviewCycles: 0, actions: 0 },
140
+ owner: { pid: process.pid, instance: randomUUID() }, current: null, lastResult: null, questions: [],
141
+ code: null, cancellation: null, executionStopped: true, requiresHuman: false };
142
+ run.ownedFiles = []; run.repositoryIdentity = null;
143
+ return record(paths, resolve(paths.runsRoot, id, 'revisions', `1-${autonomyDigest(run).slice(7)}.json`), run);
144
+ });
145
+ }
146
+ function budget(root, run, elapsed = run.counters.elapsedMs) {
147
+ const grant = currentGrant(root, run.grantDigest), prior = listRuns(root).filter(other => other.id !== run.id && other.grantDigest === run.grantDigest);
148
+ const used = prior.reduce((sum, other) => ({ elapsedMs: sum.elapsedMs + other.counters.elapsedMs,
149
+ providerAttempts: sum.providerAttempts + other.counters.providerAttempts }), { elapsedMs: elapsed, providerAttempts: run.counters.providerAttempts });
150
+ if (Date.now() < Math.max(run.lastClock, ...prior.map(other => other.lastClock))) autonomyError('autonomy-clock-rollback');
151
+ if (used.elapsedMs >= grant.scope.limits.maxRuntimeMs) autonomyError('autonomy-runtime-exhausted');
152
+ return { grant, remainingMs: grant.scope.limits.maxRuntimeMs - used.elapsedMs,
153
+ remainingAttempts: grant.scope.limits.maxAttempts - used.providerAttempts };
154
+ }
155
+ function repositoryReady(root, run) {
156
+ const repository = readAutonomyRepositoryState(root, run.ownedFiles);
157
+ if (repository.status !== 'ready') autonomyError(repository.reasons[0]);
158
+ if (run.repositoryIdentity && run.repositoryIdentity !== repository.digest) autonomyError('autonomy-repository-changed');
159
+ return repository;
160
+ }
161
+ function question(root, run, candidate, codes) {
162
+ const paths = pathsFor(root, run.id), snapshot = readIntentMutationSnapshot(root, candidate.intentId), grant = readAutonomyPolicy(root).grant;
163
+ const body = { schema: 'ewai.autonomy-question/v1', intentId: candidate.intentId, phase: candidate.phase,
164
+ grantDigest: run.grantDigest, sourceRevision: snapshot.digest, owner: grant.approvedBy,
165
+ codes: [...new Set(codes)].sort(), authority: 'none' };
166
+ const id = autonomyDigest(body).slice(7);
167
+ record(paths, resolve(paths.authorityRoot, 'supervisor/questions', `${id}.json`), { ...body, id });
168
+ return { id, intentId: body.intentId, phase: body.phase, owner: body.owner, sourceRevision: body.sourceRevision, codes: body.codes, authority: 'none' };
169
+ }
170
+ export function answerAutonomyQuestion(root, input) {
171
+ inputKeys(input, ['runId', 'questionId', 'expectedRevision', 'answeredBy', 'answer']);
172
+ if (!/^[a-f0-9]{64}$/.test(input.questionId ?? '') || !named(input.answeredBy)
173
+ || typeof input.answer !== 'string' || !input.answer.trim() || Buffer.byteLength(input.answer) > 16384) autonomyError('autonomy-answer-invalid', 400);
174
+ const paths = pathsFor(root, input.runId);
175
+ return withAutonomyLock(paths, () => {
176
+ const run = readRun(root, input.runId);
177
+ if (run.revision !== input.expectedRevision) autonomyError('autonomy-run-stale');
178
+ const q = run.questions.find(value => value.id === input.questionId);
179
+ if (!q || q.owner !== input.answeredBy) autonomyError('autonomy-question-owner-required');
180
+ if (readIntentMutationSnapshot(root, q.intentId).digest !== q.sourceRevision) autonomyError('autonomy-question-stale');
181
+ const body = { schema: 'ewai.autonomy-answer/v1', questionId: q.id, sourceRevision: q.sourceRevision,
182
+ answeredBy: input.answeredBy, answer: input.answer, recordedAt: new Date().toISOString(), authority: 'none' };
183
+ const result = record(paths, resolve(paths.authorityRoot, 'supervisor/answers', q.id, `${autonomyDigest(body).slice(7)}.json`), body);
184
+ return { status: 'recorded', questionId: q.id, digest: result.digest, authority: 'none', approvalRecorded: false };
185
+ });
186
+ }
187
+
188
+ export function controlAutonomyRun(root, input) {
189
+ inputKeys(input, ['runId', 'action', 'expectedRevision', 'confirmed', 'revokedBy']);
190
+ if (!['pause', 'cancel', 'resume', 'revoke', 'recover'].includes(input.action) || !Number.isSafeInteger(input.expectedRevision)) autonomyError('autonomy-control-invalid', 400);
191
+ const paths = pathsFor(root, input.runId);
192
+ // Grant revocation owns its own authority lock; never nest that lock.
193
+ if (input.action === 'revoke') {
194
+ const run = readRun(root, input.runId);
195
+ if (run.revision !== input.expectedRevision) autonomyError('autonomy-run-stale');
196
+ revokeAutonomyGrant(root, { expectedDigest: run.grantDigest, confirmed: input.confirmed, revokedBy: input.revokedBy });
197
+ }
198
+ const result = withAutonomyLock(paths, () => {
199
+ const run = readRun(root, input.runId);
200
+ if (input.action !== 'revoke' && run.revision !== input.expectedRevision) autonomyError('autonomy-run-stale');
201
+ if (input.action === 'revoke' && terminal.has(run.status)) return run;
202
+ if (terminal.has(run.status)) autonomyError('autonomy-run-terminal');
203
+ if (input.action === 'recover') {
204
+ if (live.has(`${paths.projectRoot}:${run.id}`) || ownerAlive(run)) autonomyError('autonomy-owner-still-active');
205
+ const operation = run.current;
206
+ // Parent death is not proof that a provider or its descendants stopped.
207
+ // Only a matching canonical receipt for synchronous begin is conclusive.
208
+ const recovered = operation?.action === 'begin-harness'
209
+ ? recoverCompletedAutonomyOperation(root, operation.operationId, { intentId: operation.intentId, grantDigest: run.grantDigest, action: operation.action }) : null;
210
+ const complete = recovered?.status === 'completed';
211
+ return save(paths, run, { status: complete ? 'completed' : 'recovery-required', requiresHuman: !complete,
212
+ executionStopped: complete, current: complete ? null : run.current, code: complete ? null : 'autonomy-execution-unknown',
213
+ lastResult: complete ? { status: 'reconciled', operationId: operation.operationId, intentId: operation.intentId } : run.lastResult,
214
+ counters: { ...run.counters, elapsedMs: run.counters.elapsedMs + Math.max(0, Date.now() - run.lastClock) }, lastClock: Math.max(Date.now(), run.lastClock) });
215
+ }
216
+ if (input.action === 'resume') {
217
+ if (!['paused', 'awaiting-human'].includes(run.status) || run.executionStopped !== true
218
+ || live.has(`${paths.projectRoot}:${run.id}`) || run.owner.pid !== process.pid && ownerAlive(run)) autonomyError('autonomy-recovery-required');
219
+ budget(root, run);
220
+ if (run.questions.some(q => readIntentMutationSnapshot(root, q.intentId).digest === q.sourceRevision)) autonomyError('autonomy-canonical-decision-required');
221
+ const repository = readAutonomyRepositoryState(root);
222
+ if (repository.status !== 'ready') autonomyError(repository.reasons[0]);
223
+ return save(paths, run, { status: 'starting', desiredState: 'running', owner: { pid: process.pid, instance: randomUUID() },
224
+ repositoryIdentity: repository.digest, ownedFiles: [], lastClock: Date.now(), questions: [], code: null });
225
+ }
226
+ const desiredState = input.action === 'pause' ? 'paused' : input.action === 'cancel' ? 'cancelled' : 'revoked';
227
+ const status = run.executionStopped ? desiredState : input.action === 'pause' ? 'running' : 'cancel-requested';
228
+ return save(paths, run, { desiredState, status, cancellation: input.action === 'pause' ? run.cancellation
229
+ : { status: run.executionStopped ? 'confirmed' : 'requested', requestedAt: new Date().toISOString() } });
230
+ });
231
+ const active = live.get(`${paths.projectRoot}:${input.runId}`);
232
+ if (['cancel', 'revoke'].includes(input.action)) active?.controller?.abort();
233
+ const afkId = active?.afkId ?? result.current?.afkRunId;
234
+ if (afkId && ['pause', 'cancel', 'revoke'].includes(input.action)) {
235
+ try {
236
+ const afk = afkRunStatus(root, afkId);
237
+ if (afk.autonomy?.runId !== result.id || afk.autonomy?.grantDigest !== result.grantDigest) autonomyError('autonomy-afk-owner-mismatch');
238
+ if (input.action === 'pause') pauseAfkRun(root, afkId);
239
+ else cancelAfkRun(root, afkId);
240
+ } catch (error) { autonomyError(failureCode(error)); }
241
+ }
242
+ if (input.action === 'resume') return result.mode === 'service' ? launchService(root, result) : executeAutonomyRun(root, result.id);
243
+ return safeRun(result);
244
+ }
245
+
246
+ function assertBuild(root, candidate, initial = false) {
247
+ const paths = autonomyPaths(root), directory = resolve(paths.buildRoot, candidate.intentId.split('/')[1]);
248
+ const state = JSON.parse(readAutonomyFile(paths.projectRoot, resolve(directory, 'delivery-state.json')));
249
+ const approval = JSON.parse(readAutonomyFile(paths.projectRoot, resolve(directory, 'gates/build/build-approval.json')));
250
+ if (approval.schema !== 'ewai.build-approval/v1' || approval.decision !== 'approved' || !named(approval.approvedBy)
251
+ || !approval.scope || state.approvals?.build?.approvedAt !== approval.approvedAt
252
+ || state.approvals?.build?.approvedBy !== approval.approvedBy || state.approvals?.build?.scope !== approval.scope) autonomyError('autonomy-build-approval-stale');
253
+ const item = readAutonomySnapshot(root).items.find(item => item.intentId === candidate.intentId);
254
+ if (!item?.accepted || !item.execution.valid || !item.execution.actions.enterBuild.permitted || initial && !item.execution.actions.acquireTask.permitted) autonomyError('autonomy-build-not-ready');
255
+ }
256
+
257
+ export async function executeAutonomyRun(root, id) {
258
+ const initial = readRun(root, id), paths = pathsFor(root, id), key = `${paths.projectRoot}:${id}`;
259
+ if (initial.status !== 'starting' || initial.owner.pid !== process.pid || live.has(key)) autonomyError('autonomy-run-owner-required');
260
+ const active = { controller: null, afkId: null, executionStopped: true, proposal: null, operationDeadline: null }, start = performance.now(), initialElapsed = initial.counters.elapsedMs;
261
+ live.set(key, active);
262
+ const elapsed = () => initialElapsed + Math.max(0, performance.now() - start, Date.now() - initial.lastClock);
263
+ const check = () => {
264
+ const run = readRun(root, id);
265
+ if (run.owner.instance !== initial.owner.instance || run.owner.pid !== process.pid) autonomyError('autonomy-run-owner-stale');
266
+ if (['cancelled', 'revoked'].includes(run.desiredState)) { active.controller?.abort(); autonomyError(`autonomy-run-${run.desiredState}`); }
267
+ if (active.operationDeadline !== null && performance.now() >= active.operationDeadline) { active.controller?.abort(); autonomyError('autonomy-operation-timeout'); }
268
+ return { run, ...budget(root, run, elapsed()) };
269
+ };
270
+ const settle = (changes = {}) => update(root, id, run => ({ ...changes, counters: { ...run.counters, elapsedMs: Math.ceil(elapsed()) }, lastClock: Math.max(run.lastClock, Date.now()) }));
271
+ let timer;
272
+ try {
273
+ settle({ status: 'running' });
274
+ while (true) {
275
+ const { run, grant, remainingMs, remainingAttempts } = check();
276
+ if (run.desiredState === 'paused') return safeRun(settle({ status: 'paused', executionStopped: true }));
277
+ const preview = previewAutonomy(root), candidate = run.current?.afkRunId ? run.current : preview.executable[0];
278
+ if (!candidate) {
279
+ const pending = [...preview.humanDecisions, ...preview.blocked].find(item => grant.scope.intentIds.includes(item.intentId));
280
+ const questions = pending ? [question(root, run, pending, pending.reasons.map(reason => reason.code))] : [];
281
+ return safeRun(settle({ status: questions.length ? 'awaiting-human' : 'completed', questions, requiresHuman: questions.length > 0, executionStopped: true }));
282
+ }
283
+ const repository = repositoryReady(root, run);
284
+ const operationId = candidate.afkRunId ? candidate.operationId : randomUUID();
285
+ settle({ current: { ...candidate, operationId }, executionStopped: false, repositoryIdentity: repository.digest });
286
+ active.controller = new AbortController();
287
+ active.operationDeadline = performance.now() + Math.min(remainingMs, grant.scope.limits.maxOperationMs);
288
+ timer = setInterval(() => { try { check(); } catch { active.controller?.abort(); } }, 50);
289
+ let result;
290
+ if (candidate.action === 'begin-harness') {
291
+ const predecessor = readIntentMutationSnapshot(root, candidate.intentId).digest;
292
+ check();
293
+ const mutation = withIntentMutation(root, candidate.intentId, { id: operationId, action: 'begin-harness', provider: run.provider,
294
+ grantDigest: grant.digest, expectedPredecessor: predecessor }, () => {
295
+ check();
296
+ const begun = beginDelivery(root, candidate.intentId.split('/')[1], { tool: run.provider });
297
+ finishActiveSession(root, candidate.intentId, { ownerId: begun.run.id, tool: run.provider, summary: 'Autonomous bounded harness start complete.' });
298
+ return { status: 'harness-started', operationId, intentId: candidate.intentId, phaseCompleted: false };
299
+ });
300
+ result = mutation.result;
301
+ const operationFiles = autonomyFiles(paths.projectRoot, resolve(paths.authorityRoot, 'operations', operationId));
302
+ update(root, id, value => ({ ownedFiles: [...new Map([...value.ownedFiles, ...mutation.receipt.result.files,
303
+ ...operationFiles.map(path => ({ path: relative(paths.projectRoot, path), digest: autonomyDigest(readAutonomyFile(paths.projectRoot, path)) }))]
304
+ .map(file => [file.path, file])).values()] }));
305
+ } else if (candidate.action === 'prepare-phase') {
306
+ if (remainingAttempts < 1) autonomyError('autonomy-attempts-exhausted');
307
+ if (remainingMs < grant.scope.limits.maxOperationMs) autonomyError('autonomy-runtime-reservation-unavailable');
308
+ const adapter = preparePhaseProvider(run.provider);
309
+ if (adapter.status === 'unavailable') autonomyError(adapter.code);
310
+ active.executionStopped = false;
311
+ const verified = await verifyPhaseProviderConformance(adapter, { signal: active.controller.signal,
312
+ timeoutMs: Math.max(100, Math.min(600000, Math.floor(active.operationDeadline - performance.now()))) });
313
+ active.executionStopped = verified.executionStopped !== false;
314
+ if (verified.status !== 'verified') autonomyError(verified.code ?? 'autonomy-provider-unavailable');
315
+ check(); repositoryReady(root, readRun(root, id));
316
+ const contract = prepareAutonomyPhase(root, candidate.intentId, candidate.phase);
317
+ if (contract.status !== 'prepared') autonomyError(contract.code);
318
+ update(root, id, current => ({ current: { ...current.current, workerOperationId: contract.operationId },
319
+ counters: { ...current.counters, providerAttempts: current.counters.providerAttempts + 1 } }));
320
+ active.executionStopped = false;
321
+ const proposed = await invokePhaseProposal(contract, adapter, { signal: active.controller.signal });
322
+ active.executionStopped = proposed.executionStopped !== false;
323
+ if (proposed.status === 'draft-ready') active.proposal = { operationId: contract.operationId, result: proposed };
324
+ if (proposed.executionStopped === false) autonomyError('autonomy-execution-unknown');
325
+ check();
326
+ if (proposed.status !== 'draft-ready') autonomyError(proposed.code ?? 'autonomy-phase-not-ready');
327
+ // Worker drafts are isolated evidence, not writes into application code.
328
+ const workerFiles = autonomyFiles(paths.projectRoot, resolve(paths.authorityRoot, 'phase-workers', contract.operationId))
329
+ .map(path => ({ path: relative(paths.projectRoot, path), digest: autonomyDigest(readAutonomyFile(paths.projectRoot, path)) }));
330
+ if (!uuid.test(proposed.reservationOperationId ?? '')) autonomyError('autonomy-reservation-evidence-missing');
331
+ workerFiles.push(...autonomyFiles(paths.projectRoot, resolve(paths.authorityRoot, 'operations', proposed.reservationOperationId))
332
+ .map(path => ({ path: relative(paths.projectRoot, path), digest: autonomyDigest(readAutonomyFile(paths.projectRoot, path)) })));
333
+ const acceptanceRun = readRun(root, id);
334
+ repositoryReady(root, { ...acceptanceRun, ownedFiles: [...acceptanceRun.ownedFiles, ...workerFiles] });
335
+ result = acceptPhaseProposal(root, contract.operationId, proposed);
336
+ active.proposal = null;
337
+ if (result.status !== 'draft-staged') autonomyError(result.code ?? 'autonomy-phase-not-accepted');
338
+ const questions = [question(root, readRun(root, id), candidate, result.questions.map(q => q.code))];
339
+ update(root, id, value => ({ counters: { ...value.counters, actions: value.counters.actions + 1 } }));
340
+ return safeRun(settle({ status: readRun(root, id).desiredState === 'paused' ? 'paused' : 'awaiting-human', current: null, lastResult: { status: result.status,
341
+ operationId: result.operationId, intentId: result.intentId, phase: result.phase }, questions, executionStopped: true, requiresHuman: true }));
342
+ } else if (candidate.action === 'afk-build') {
343
+ if (!grant.scope.actions.includes('afk-build') || !grant.scope.intentIds.includes(candidate.intentId)) autonomyError('autonomy-afk-scope-invalid');
344
+ assertBuild(root, candidate, !candidate.afkRunId);
345
+ const preflight = preflightAfkRun(root, candidate.intentId.split('/')[1], { provider: run.provider, maxParallel: 1, ignoreRunId: candidate.afkRunId });
346
+ if (preflight.status !== 'ready') autonomyError('autonomy-afk-preflight-blocked');
347
+ if (preflight.readyTasks.length && remainingAttempts < 2) autonomyError('autonomy-attempts-exhausted');
348
+ const ownership = acquireIntentOwnership(root, candidate.intentId, { ownerId: `supervisor-${id}`, durationMs: Math.min(86400000, Math.ceil(remainingMs) + 60000) });
349
+ try {
350
+ const options = { provider: run.provider, maxParallel: 1, foreground: true,
351
+ timeoutMs: Math.min(grant.scope.limits.maxOperationMs, Math.floor(remainingMs)), autonomy: { runId: id, grantDigest: grant.digest },
352
+ dependencies: { authority: ({ stage, mode, transitions }) => {
353
+ const current = check(); assertIntentOwnership(root, ownership); assertBuild(root, candidate);
354
+ if (stage === 'worktree' && current.run.desiredState === 'paused') autonomyError('autonomy-run-paused');
355
+ if (stage === 'integrate' || stage === 'dispatch' && mode !== 'command') {
356
+ repositoryReady(root, { ...current.run, ownedFiles: [], repositoryIdentity: active.afkRepository.digest });
357
+ }
358
+ if (stage === 'integrated') {
359
+ const records = active.afkRepository.records.map(record => ({ ...record }));
360
+ for (const change of transitions) {
361
+ const record = records.find(record => record.root === relative(paths.projectRoot, change.root));
362
+ if (!record || record.identity !== `${change.before}\n${change.branch}\n`) autonomyError('autonomy-repository-changed');
363
+ record.identity = `${change.after}\n${change.branch}\n`;
364
+ }
365
+ active.afkRepository = repositoryReady(root, { ...current.run, ownedFiles: [], repositoryIdentity: autonomyDigest(records) });
366
+ update(root, id, () => ({ repositoryIdentity: active.afkRepository.digest }));
367
+ }
368
+ if (stage === 'dispatch' && mode !== 'command') {
369
+ if (current.remainingAttempts < (mode === 'implementation' ? 2 : 1)) autonomyError('autonomy-attempts-exhausted');
370
+ update(root, id, value => ({ counters: { ...value.counters, providerAttempts: value.counters.providerAttempts + 1,
371
+ reviewCycles: value.counters.reviewCycles + (mode === 'review' ? 1 : 0) } }));
372
+ }
373
+ }, onRun: afk => {
374
+ active.afkId = afk.id;
375
+ active.afkRepository = repositoryReady(root, { ...readRun(root, id), repositoryIdentity: null, ownedFiles: [] });
376
+ const control = update(root, id, value => ({ current: { ...value.current, afkRunId: afk.id } }));
377
+ if (control.desiredState === 'paused') pauseAfkRun(root, afk.id);
378
+ else if (['cancelled', 'revoked'].includes(control.desiredState)) cancelAfkRun(root, afk.id);
379
+ active.executionStopped = false;
380
+ } } };
381
+ result = await (candidate.afkRunId ? resumeAfkRun(root, candidate.afkRunId, options)
382
+ : startAfkRun(root, candidate.intentId.split('/')[1], options));
383
+ active.executionStopped = result.executionStopped === true;
384
+ } finally { if (active.executionStopped) try { releaseIntentOwnership(root, ownership); } catch {} }
385
+ if (result.executionStopped !== true) autonomyError('autonomy-execution-unknown');
386
+ check();
387
+ if (!['completed', 'paused'].includes(result.status)) autonomyError('autonomy-afk-blocked');
388
+ if (result.status === 'paused') return safeRun(settle({ status: 'paused', executionStopped: true, requiresHuman: false }));
389
+ result = { status: 'afk-settled', afkRunId: result.id, intentId: candidate.intentId };
390
+ } else autonomyError('autonomy-action-unavailable');
391
+ clearInterval(timer); timer = null; active.controller = null; active.afkId = null; active.operationDeadline = null;
392
+ const settled = settle({ current: null, lastResult: result, executionStopped: true });
393
+ update(root, id, value => ({ counters: { ...value.counters, actions: value.counters.actions + 1 } }));
394
+ if (initial.mode === 'once') return safeRun(settle({ status: settled.desiredState === 'paused' ? 'paused' : 'completed' }));
395
+ }
396
+ } catch (error) {
397
+ if (active.proposal) {
398
+ // Revoked, expired, cancelled or stale proposals must release their local
399
+ // acceptance capability without publishing an assessment.
400
+ active.controller.abort();
401
+ acceptPhaseProposal(root, active.proposal.operationId, active.proposal.result);
402
+ }
403
+ const run = readRun(root, id), policy = readAutonomyPolicy(root);
404
+ const cancelled = ['cancelled', 'revoked'].includes(run.desiredState);
405
+ const status = !active.executionStopped ? 'recovery-required' : cancelled ? run.desiredState : error.code === 'autonomy-run-paused' ? 'paused'
406
+ : policy.status === 'expired' ? 'expired' : policy.status === 'revoked' ? 'revoked' : 'blocked';
407
+ return safeRun(settle({ status, code: failureCode(error), current: active.executionStopped ? null : run.current, executionStopped: active.executionStopped,
408
+ requiresHuman: !cancelled || !active.executionStopped, cancellation: cancelled ? { ...run.cancellation, status: active.executionStopped ? 'confirmed' : 'unknown' } : run.cancellation }));
409
+ } finally { clearInterval(timer); live.delete(key); }
410
+ }
411
+
412
+ export async function runAutonomyOnce(root, input) { const run = createRun(root, input, 'once'); return executeAutonomyRun(root, run.id); }
413
+ export function startAutonomyService(root, input) {
414
+ return launchService(root, createRun(root, input, 'service'));
415
+ }
416
+ function launchService(root, run) {
417
+ const paths = pathsFor(root, run.id);
418
+ const child = spawn(process.execPath, [new URL('../autonomy-worker.mjs', import.meta.url).pathname, '--project', paths.projectRoot, '--run', run.id],
419
+ { cwd: paths.projectRoot, detached: true, stdio: ['ignore', 'ignore', 'ignore', 'ipc'] });
420
+ const unknown = () => { try { update(root, run.id, () => ({ status: 'recovery-required', code: 'autonomy-service-start-unknown', requiresHuman: true, executionStopped: false })); } catch {} };
421
+ child.once('error', unknown);
422
+ if (!Number.isSafeInteger(child.pid)) { unknown(); return safeRun(readRun(root, run.id)); }
423
+ const started = update(root, run.id, current => ({ owner: { ...current.owner, pid: child.pid } }));
424
+ child.send({ start: true }, error => { if (error) unknown(); if (child.connected) child.disconnect(); }); child.unref();
425
+ return safeRun(started);
426
+ }