@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,272 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
3
|
+
import { existsSync, realpathSync, lstatSync, readdirSync, readFileSync, mkdirSync, openSync, closeSync, fstatSync, writeFileSync, fsyncSync, unlinkSync } from 'node:fs';
|
|
4
|
+
import { resolve, relative, isAbsolute } from 'node:path';
|
|
5
|
+
import YAML from 'yaml';
|
|
6
|
+
import { projectPaths } from '../paths.mjs';
|
|
7
|
+
import { openRuntimeDatabase } from './database.mjs';
|
|
8
|
+
import { readAutonomyPolicy } from '../autonomy.mjs';
|
|
9
|
+
import { autonomyDigest, readAutonomyFile } from './autonomy-workspace.mjs';
|
|
10
|
+
import { normaliseAutonomyOperation, inspectAutonomyOperation, assertNoUnresolvedIntentOperation, beginAutonomyOperation, completeAutonomyOperation, failAutonomyOperation } from './autonomy-operations.mjs';
|
|
11
|
+
|
|
12
|
+
const context = new AsyncLocalStorage();
|
|
13
|
+
const processInstance = randomUUID();
|
|
14
|
+
const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
15
|
+
const intentPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*\/[a-z0-9]+(?:-[a-z0-9]+)*$/;
|
|
16
|
+
const tokenHash = value => createHash('sha256').update(value).digest('hex');
|
|
17
|
+
|
|
18
|
+
export function intentOwnershipError(code, statusCode = 409) {
|
|
19
|
+
const error = new Error(`Intent mutation cannot proceed: ${code}.`);
|
|
20
|
+
error.code = code; error.statusCode = statusCode; throw error;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function safeIntentMutationPath(root, path) {
|
|
24
|
+
const local = relative(root, path);
|
|
25
|
+
if (local.startsWith('..') || isAbsolute(local)) intentOwnershipError('intent-mutation-unsafe-path', 403);
|
|
26
|
+
let cursor = root;
|
|
27
|
+
for (const part of local.split('/').filter(Boolean)) {
|
|
28
|
+
cursor = resolve(cursor, part);
|
|
29
|
+
let stat; try { stat = lstatSync(cursor); } catch (error) { if (error.code === 'ENOENT') continue; throw error; }
|
|
30
|
+
if (stat.isSymbolicLink() || !stat.isDirectory() && (!stat.isFile() || stat.nlink !== 1)) intentOwnershipError('intent-mutation-unsafe-path', 403);
|
|
31
|
+
}
|
|
32
|
+
return path;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function intentMutationPaths(root) {
|
|
36
|
+
const realRoot = realpathSync(root), locatorPath = safeIntentMutationPath(realRoot, resolve(realRoot, '.ewai-pipeline/project.json'));
|
|
37
|
+
let locator = {};
|
|
38
|
+
if (existsSync(locatorPath)) {
|
|
39
|
+
try { locator = JSON.parse(readFileSync(locatorPath, 'utf8')); } catch { intentOwnershipError('intent-project-invalid', 422); }
|
|
40
|
+
let recordedRoot = realRoot;
|
|
41
|
+
try { if (locator?.projectRoot !== undefined) recordedRoot = realpathSync(locator.projectRoot); }
|
|
42
|
+
catch { intentOwnershipError('intent-project-invalid', 422); }
|
|
43
|
+
if (locator?.schema !== 'ewai.runtime-project/v1' || locator.specsRoot !== undefined && (typeof locator.specsRoot !== 'string' || !locator.specsRoot.trim())
|
|
44
|
+
|| recordedRoot !== realRoot) intentOwnershipError('intent-project-invalid', 422);
|
|
45
|
+
}
|
|
46
|
+
const paths = projectPaths(realRoot, { specsRoot: locator.specsRoot ?? 'SPECS' });
|
|
47
|
+
for (const path of [paths.specsRoot, resolve(paths.runtimeRoot, 'data/pipeline.sqlite')]) safeIntentMutationPath(realRoot, path);
|
|
48
|
+
return { ...paths, ownershipRoot: resolve(paths.runtimeRoot, 'runtime/intent-ownership') };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function resolveIntentMutationReference(root, reference) {
|
|
52
|
+
const paths = intentMutationPaths(root), intentsRoot = resolve(paths.specsRoot, '2.Purpose/intents');
|
|
53
|
+
if (typeof reference !== 'string' || reference.length > 160 || !(intentPattern.test(reference) || slugPattern.test(reference))) intentOwnershipError('intent-reference-invalid', 422);
|
|
54
|
+
const ids = intentPattern.test(reference) ? [reference] : readdirSync(safeIntentMutationPath(paths.projectRoot, intentsRoot), { withFileTypes: true })
|
|
55
|
+
.filter(entry => entry.isDirectory() && slugPattern.test(entry.name)).map(entry => `${entry.name}/${reference}`);
|
|
56
|
+
const matches = ids.filter(id => ['md', 'json'].some(extension => existsSync(safeIntentMutationPath(paths.projectRoot, resolve(intentsRoot, `${id}.${extension}`)))));
|
|
57
|
+
if (matches.length !== 1) intentOwnershipError(matches.length ? 'intent-reference-ambiguous' : 'intent-reference-unknown', 422);
|
|
58
|
+
const id = matches[0], sidecar = resolve(intentsRoot, `${id}.json`);
|
|
59
|
+
if (existsSync(sidecar)) {
|
|
60
|
+
const raw = readAutonomyFile(paths.projectRoot, sidecar);
|
|
61
|
+
let source; try { source = JSON.parse(raw); } catch { intentOwnershipError('intent-state-invalid', 422); }
|
|
62
|
+
if (source?.schema !== 'ewai.intent-state/v1' || `${source.domain}/${source.slug}` !== id) intentOwnershipError('intent-state-invalid', 422);
|
|
63
|
+
} else {
|
|
64
|
+
// Legacy registered Markdown intents remain usable for interactive work;
|
|
65
|
+
// their canonical delivery checks still decide whether advancement is safe.
|
|
66
|
+
const source = readAutonomyFile(paths.projectRoot, resolve(intentsRoot, `${id}.md`));
|
|
67
|
+
const match = source.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
68
|
+
let metadata; try { metadata = match && YAML.parse(match[1], { maxAliasCount: 100 }); } catch { intentOwnershipError('intent-state-invalid', 422); }
|
|
69
|
+
const [domain, slug] = id.split('/');
|
|
70
|
+
if (metadata?.schema !== 'ewai.intent/v1' || metadata.slug !== slug || metadata.domain !== undefined && metadata.domain !== domain) intentOwnershipError('intent-state-invalid', 422);
|
|
71
|
+
}
|
|
72
|
+
return id;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function key(root, intentId) { return `${root}\n${intentId}`; }
|
|
76
|
+
function withMutex(paths, intentId, operation, owner = null) {
|
|
77
|
+
if (context.getStore()?.open && context.getStore()?.key === key(paths.projectRoot, intentId)) return operation();
|
|
78
|
+
const directory = safeIntentMutationPath(paths.projectRoot, paths.ownershipRoot);
|
|
79
|
+
mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
80
|
+
const path = safeIntentMutationPath(paths.projectRoot, resolve(directory, `${tokenHash(intentId)}.lock`));
|
|
81
|
+
let fd;
|
|
82
|
+
// A claimant checking the lease may briefly hold this mutex even though the
|
|
83
|
+
// current owner will reject its claim. Give only that verified owner bounded
|
|
84
|
+
// lock-acquisition patience; no callback or journal attempt is repeated.
|
|
85
|
+
for (let attempt = 0; fd === undefined; attempt += 1) {
|
|
86
|
+
try { fd = openSync(path, 'wx', 0o600); }
|
|
87
|
+
catch (error) {
|
|
88
|
+
if (error.code !== 'EEXIST') throw error;
|
|
89
|
+
if (!owner || attempt >= 20) intentOwnershipError('intent-mutation-conflict');
|
|
90
|
+
assertIntentOwnership(paths.projectRoot, owner);
|
|
91
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const identity = fstatSync(fd);
|
|
95
|
+
try {
|
|
96
|
+
writeFileSync(fd, JSON.stringify({ schema: 'ewai.intent-mutex/v1', intentId, pid: process.pid, processInstance })); fsyncSync(fd);
|
|
97
|
+
return operation();
|
|
98
|
+
} finally {
|
|
99
|
+
closeSync(fd);
|
|
100
|
+
const current = lstatSync(path);
|
|
101
|
+
if (current.ino !== identity.ino || current.dev !== identity.dev) intentOwnershipError('intent-mutation-lock-changed');
|
|
102
|
+
unlinkSync(path);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function withDatabase(paths, operation) {
|
|
107
|
+
const db = openRuntimeDatabase(paths.projectRoot);
|
|
108
|
+
try { return operation(db); } finally { db.close(); }
|
|
109
|
+
}
|
|
110
|
+
function duration(input) {
|
|
111
|
+
const value = input.durationMs ?? 600000;
|
|
112
|
+
if (!Number.isSafeInteger(value) || value < 1000 || value > 86400000) intentOwnershipError('intent-ownership-duration-invalid', 422);
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
function epoch(database) { return database.prepare("SELECT value FROM runtime_meta WHERE key = 'intent_ownership_epoch'").get()?.value; }
|
|
116
|
+
function assertHandle(database, handle, intentId = handle?.intentId) {
|
|
117
|
+
if (!handle || !intentPattern.test(intentId ?? '') || typeof handle.token !== 'string' || handle.token.length > 100
|
|
118
|
+
|| handle.intentId !== intentId || !Number.isSafeInteger(handle.generation)) intentOwnershipError('intent-ownership-stale');
|
|
119
|
+
const row = database.prepare('SELECT * FROM intent_ownership WHERE intent_id = ?').get(intentId);
|
|
120
|
+
if (!row || epoch(database) !== handle.epoch || row.epoch !== handle.epoch || row.generation !== handle.generation
|
|
121
|
+
|| row.owner_id !== handle.ownerId || row.token_hash !== tokenHash(handle.token) || row.status !== 'active' || row.expires_at <= Date.now()) intentOwnershipError('intent-ownership-stale');
|
|
122
|
+
return row;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function acquireIntentOwnership(root, reference, input = {}) {
|
|
126
|
+
const paths = intentMutationPaths(root), intentId = resolveIntentMutationReference(paths.projectRoot, reference), ttl = duration(input);
|
|
127
|
+
const ownerId = input.ownerId ?? `interactive-${processInstance}`;
|
|
128
|
+
if (typeof ownerId !== 'string' || !/^[a-zA-Z0-9][a-zA-Z0-9_.:-]{0,119}$/.test(ownerId)) intentOwnershipError('intent-owner-invalid', 422);
|
|
129
|
+
return withMutex(paths, intentId, () => withDatabase(paths, database => {
|
|
130
|
+
database.exec('BEGIN IMMEDIATE;');
|
|
131
|
+
try {
|
|
132
|
+
const previous = database.prepare('SELECT * FROM intent_ownership WHERE intent_id = ?').get(intentId), currentEpoch = epoch(database);
|
|
133
|
+
if (previous?.status === 'active' && previous.expires_at > Date.now()) intentOwnershipError('intent-ownership-conflict');
|
|
134
|
+
const token = randomUUID(), generation = (previous?.generation ?? 0) + 1, expiresAt = Date.now() + ttl;
|
|
135
|
+
database.prepare(`INSERT INTO intent_ownership (intent_id, epoch, generation, owner_id, token_hash, expires_at, status, pid, process_instance)
|
|
136
|
+
VALUES (?, ?, ?, ?, ?, ?, 'active', ?, ?) ON CONFLICT(intent_id) DO UPDATE SET
|
|
137
|
+
epoch=excluded.epoch, generation=excluded.generation, owner_id=excluded.owner_id, token_hash=excluded.token_hash,
|
|
138
|
+
expires_at=excluded.expires_at, status=excluded.status, pid=excluded.pid, process_instance=excluded.process_instance`)
|
|
139
|
+
.run(intentId, currentEpoch, generation, ownerId, tokenHash(token), expiresAt, process.pid, processInstance);
|
|
140
|
+
database.exec('COMMIT;');
|
|
141
|
+
return { intentId, epoch: currentEpoch, generation, ownerId, token, expiresAt };
|
|
142
|
+
} catch (error) { database.exec('ROLLBACK;'); throw error; }
|
|
143
|
+
}));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function assertIntentOwnership(root, handle) {
|
|
147
|
+
const paths = intentMutationPaths(root);
|
|
148
|
+
return withDatabase(paths, database => { assertHandle(database, handle); return { intentId: handle.intentId, epoch: handle.epoch, generation: handle.generation }; });
|
|
149
|
+
}
|
|
150
|
+
export function renewIntentOwnership(root, handle, input = {}) {
|
|
151
|
+
const paths = intentMutationPaths(root), ttl = duration(input);
|
|
152
|
+
return withMutex(paths, handle?.intentId ?? '', () => withDatabase(paths, database => {
|
|
153
|
+
assertHandle(database, handle); const expiresAt = Date.now() + ttl;
|
|
154
|
+
database.prepare('UPDATE intent_ownership SET expires_at = ? WHERE intent_id = ?').run(expiresAt, handle.intentId);
|
|
155
|
+
return { ...handle, expiresAt };
|
|
156
|
+
}));
|
|
157
|
+
}
|
|
158
|
+
export function releaseIntentOwnership(root, handle) {
|
|
159
|
+
const paths = intentMutationPaths(root);
|
|
160
|
+
return withMutex(paths, handle?.intentId ?? '', () => withDatabase(paths, database => {
|
|
161
|
+
assertHandle(database, handle);
|
|
162
|
+
database.prepare("UPDATE intent_ownership SET status = 'released' WHERE intent_id = ?").run(handle.intentId);
|
|
163
|
+
return { intentId: handle.intentId, generation: handle.generation, status: 'released' };
|
|
164
|
+
}));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function assertIntentMutation(root, reference, database = null) {
|
|
168
|
+
const paths = intentMutationPaths(root), intentId = resolveIntentMutationReference(paths.projectRoot, reference), active = context.getStore();
|
|
169
|
+
if (!active || active.key !== key(paths.projectRoot, intentId)) intentOwnershipError('intent-mutation-context-required');
|
|
170
|
+
if (!active.open) intentOwnershipError('intent-mutation-context-expired');
|
|
171
|
+
assertMutationAuthority(paths, intentId, active.operation);
|
|
172
|
+
if (database) { assertHandle(database, active.handle); return; }
|
|
173
|
+
return assertIntentOwnership(paths.projectRoot, active.handle);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const humanActions = new Set(['approve-build', 'approve-manual-qa', 'ratify-plan', 'ratify-evidence']);
|
|
177
|
+
const preparablePhases = new Set(['intent', 'reconcile', 'plan', 'pattern-validation', 'test-plan']);
|
|
178
|
+
function assertMutationAuthority(paths, intentId, operation, nested = null) {
|
|
179
|
+
if (nested?.grantDigest && nested.grantDigest !== operation.grantDigest) intentOwnershipError('autonomy-grant-context-mismatch', 403);
|
|
180
|
+
if (!operation.grantDigest) return;
|
|
181
|
+
const policy = readAutonomyPolicy(paths.projectRoot), grant = policy.grant;
|
|
182
|
+
if (policy.status !== 'current' || grant?.digest !== operation.grantDigest) intentOwnershipError('autonomy-grant-not-current');
|
|
183
|
+
if (autonomyDigest(readAutonomyFile(paths.projectRoot, paths.configPath)) !== grant.configDigest) intentOwnershipError('autonomy-grant-policy-changed');
|
|
184
|
+
if (!grant.scope.intentIds.includes(intentId)) intentOwnershipError('autonomy-intent-not-permitted', 403);
|
|
185
|
+
const provider = nested?.provider ?? operation.provider;
|
|
186
|
+
if (provider && !grant.scope.providers.includes(provider)
|
|
187
|
+
|| nested?.action === 'begin-harness' && !grant.scope.providers.includes(nested.provider)) intentOwnershipError('autonomy-provider-not-permitted', 403);
|
|
188
|
+
const requested = nested ?? operation;
|
|
189
|
+
if (humanActions.has(requested.action)) intentOwnershipError('autonomy-human-decision-required', 403);
|
|
190
|
+
if (!grant.scope.actions.includes(operation.action) || operation.maxAttempts > grant.scope.limits.maxAttempts) intentOwnershipError('autonomy-action-not-permitted', 403);
|
|
191
|
+
if (operation.action === 'prepare-phase' && !preparablePhases.has(operation.phase)
|
|
192
|
+
|| operation.action === 'afk-build' && operation.phase !== 'build') intentOwnershipError('autonomy-phase-not-permitted', 403);
|
|
193
|
+
if (!nested) return;
|
|
194
|
+
if (['update-projection', 'update-phase-projection', 'start-session'].includes(nested.action)) return;
|
|
195
|
+
if (operation.action === 'begin-harness' && nested.action === 'begin-harness') return;
|
|
196
|
+
if (['prepare-phase', 'afk-build'].includes(operation.action)
|
|
197
|
+
&& ['record-gate', 'record-validation', 'start-phase', 'complete-phase'].includes(nested.action)
|
|
198
|
+
&& nested.phase === operation.phase) return;
|
|
199
|
+
intentOwnershipError('autonomy-action-not-permitted', 403);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function recoverStoppedIntentOwnership(root, owner, intentId) {
|
|
203
|
+
if (!owner || !Number.isInteger(owner.pid) || owner.pid < 1 || !intentPattern.test(intentId ?? '')) intentOwnershipError('intent-recovery-invalid', 422);
|
|
204
|
+
try { process.kill(owner.pid, 0); return; } catch (error) { if (error.code !== 'ESRCH') return; }
|
|
205
|
+
const paths = intentMutationPaths(root), path = safeIntentMutationPath(paths.projectRoot, resolve(paths.ownershipRoot, `${tokenHash(intentId)}.lock`));
|
|
206
|
+
if (existsSync(path)) {
|
|
207
|
+
const before = lstatSync(path), raw = readAutonomyFile(paths.projectRoot, path);
|
|
208
|
+
let lock; try { lock = JSON.parse(raw); } catch { intentOwnershipError('intent-recovery-invalid', 422); }
|
|
209
|
+
if (lock?.schema !== 'ewai.intent-mutex/v1' || lock.intentId !== intentId || lock.pid !== owner.pid || lock.processInstance !== owner.processInstance) intentOwnershipError('intent-recovery-conflict');
|
|
210
|
+
const after = lstatSync(path);
|
|
211
|
+
if (before.ino !== after.ino || before.mtimeMs !== after.mtimeMs) intentOwnershipError('intent-recovery-conflict');
|
|
212
|
+
unlinkSync(path);
|
|
213
|
+
}
|
|
214
|
+
withDatabase(paths, database => database.prepare("UPDATE intent_ownership SET status = 'released' WHERE intent_id = ? AND epoch = ? AND generation = ? AND pid = ? AND process_instance = ?")
|
|
215
|
+
.run(intentId, owner.epoch, owner.generation, owner.pid, owner.processInstance));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Synchronous canonical transitions only. Workers propose outside this critical
|
|
219
|
+
// section; acceptance enters it with a still-current ownership capability.
|
|
220
|
+
export function withIntentMutation(root, reference, operation, callback) {
|
|
221
|
+
const paths = intentMutationPaths(root), intentId = resolveIntentMutationReference(paths.projectRoot, reference);
|
|
222
|
+
if (typeof callback !== 'function' || !operation || typeof operation !== 'object') intentOwnershipError('intent-mutation-invalid', 422);
|
|
223
|
+
if (callback.constructor?.name === 'AsyncFunction') intentOwnershipError('intent-mutation-must-be-synchronous', 422);
|
|
224
|
+
const active = context.getStore();
|
|
225
|
+
if (active && !active.open) intentOwnershipError('intent-mutation-context-expired');
|
|
226
|
+
if (active?.operation.grantDigest && active.key !== key(paths.projectRoot, intentId)) intentOwnershipError('autonomy-intent-not-permitted', 403);
|
|
227
|
+
if (active?.key === key(paths.projectRoot, intentId)) {
|
|
228
|
+
assertMutationAuthority(paths, intentId, active.operation, normaliseAutonomyOperation(operation));
|
|
229
|
+
if (operation.ownership) assertIntentOwnership(paths.projectRoot, operation.ownership);
|
|
230
|
+
assertIntentMutation(paths.projectRoot, intentId); return synchronousResult(active, callback);
|
|
231
|
+
}
|
|
232
|
+
const description = normaliseAutonomyOperation(operation);
|
|
233
|
+
const existing = inspectAutonomyOperation(paths.projectRoot, intentId, description);
|
|
234
|
+
if (existing.status === 'completed') return { ...existing, replayed: true };
|
|
235
|
+
if (existing.status === 'recovery-required') intentOwnershipError('autonomy-recovery-required');
|
|
236
|
+
assertMutationAuthority(paths, intentId, description);
|
|
237
|
+
const handle = operation.ownership ?? acquireIntentOwnership(paths.projectRoot, intentId, operation);
|
|
238
|
+
const scope = { key: key(paths.projectRoot, intentId), handle, operation: description, open: true, executionStopped: true };
|
|
239
|
+
try {
|
|
240
|
+
return withMutex(paths, intentId, () => context.run(scope, () => {
|
|
241
|
+
try {
|
|
242
|
+
assertIntentMutation(paths.projectRoot, intentId);
|
|
243
|
+
if (handle.intentId !== intentId) intentOwnershipError('intent-ownership-stale');
|
|
244
|
+
assertNoUnresolvedIntentOperation(paths.projectRoot, intentId, description.id);
|
|
245
|
+
const attempt = beginAutonomyOperation(paths.projectRoot, intentId, description, { ...handle, processInstance });
|
|
246
|
+
try {
|
|
247
|
+
const result = synchronousResult(scope, callback);
|
|
248
|
+
assertIntentMutation(paths.projectRoot, intentId);
|
|
249
|
+
const receipt = completeAutonomyOperation(paths.projectRoot, attempt);
|
|
250
|
+
return description.tracked ? { status: 'completed', replayed: false, receipt, result } : result;
|
|
251
|
+
} catch (error) {
|
|
252
|
+
// Raw exceptions and worker output never enter the journal. Unknown
|
|
253
|
+
// asynchronous outcomes cannot be presented as stopped execution.
|
|
254
|
+
try { failAutonomyOperation(paths.projectRoot, attempt, { executionStopped: scope.executionStopped }); } catch {}
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
} finally { scope.open = false; }
|
|
258
|
+
}), handle);
|
|
259
|
+
} finally {
|
|
260
|
+
if (!operation.ownership) {
|
|
261
|
+
try { releaseIntentOwnership(paths.projectRoot, handle); }
|
|
262
|
+
catch (error) { if (error.code !== 'intent-ownership-stale') throw error; }
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function synchronousResult(scope, callback) {
|
|
268
|
+
const result = callback();
|
|
269
|
+
if (result?.then) scope.executionStopped = false;
|
|
270
|
+
if (!scope.executionStopped) intentOwnershipError('intent-mutation-must-be-synchronous', 422);
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { McpServer, ResourceTemplate } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
3
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
4
|
import { z } from 'zod/v4';
|
|
5
|
+
import { AUTONOMY_INTERFACE_FIELDS, autonomyInterfaceAction, safeAutonomyInterfaceError } from './dashboard-actions.mjs';
|
|
5
6
|
import { readFileSync } from 'node:fs';
|
|
6
7
|
import { resolve } from 'node:path';
|
|
7
8
|
import { premiumPersonaRoot } from '../checkin.mjs';
|
|
@@ -157,6 +158,43 @@ function result(value) {
|
|
|
157
158
|
};
|
|
158
159
|
}
|
|
159
160
|
|
|
161
|
+
const autonomyFieldGuidance = Object.freeze({
|
|
162
|
+
runId: 'UUID of an existing supervisor run. Status accepts this alone to read one run.',
|
|
163
|
+
proposal: 'Closed object: intentIds (nonempty DOMAIN/SLUG string array), actions (nonempty array of begin-harness, prepare-phase, afk-build), providers (nonempty array of codex, claude, antigravity), expiresAt (future ISO UTC timestamp with milliseconds), limits (object with maxConcurrentIntents: 1, maxRuntimeMs: positive integer up to 86400000, maxOperationMs: positive integer no greater than maxRuntimeMs, maxAttempts: integer from 1 to 100). No extra keys.',
|
|
164
|
+
record: 'Boolean. true persists the preview as evidence; this never approves the proposal.',
|
|
165
|
+
confirmed: 'Boolean. true explicitly confirms a mutation, including a recorded preview.',
|
|
166
|
+
expectedDigest: 'Current sha256: followed by 64 lowercase hexadecimal characters, obtained from the preview or grant.',
|
|
167
|
+
approvedBy: 'Nonempty human name, trimmed, at most 120 characters, without control characters.',
|
|
168
|
+
revokedBy: 'Nonempty human name, trimmed, at most 120 characters, without control characters.',
|
|
169
|
+
provider: 'One of codex, claude, antigravity; must be within the current grant.',
|
|
170
|
+
action: 'One of pause, resume, cancel, revoke, recover; subject to the current run state.',
|
|
171
|
+
expectedRevision: 'Current positive safe integer revision of the run, read from status.',
|
|
172
|
+
questionId: '64 lowercase hexadecimal characters identifying an open human question.',
|
|
173
|
+
answeredBy: 'Nonempty human name, trimmed, at most 120 characters, without control characters.',
|
|
174
|
+
answer: 'Nonempty private answer string, at most 16384 UTF-8 bytes. Never place it in a shell argument.',
|
|
175
|
+
});
|
|
176
|
+
const autonomyRequiredGuidance = Object.freeze({
|
|
177
|
+
status: 'none; runId is optional', preview: 'none; proposal is needed for a new grant and record: true needs confirmed: true',
|
|
178
|
+
approve: 'expectedDigest, approvedBy, confirmed', revoke: 'expectedDigest, revokedBy, confirmed',
|
|
179
|
+
run: 'expectedDigest, provider, confirmed', service: 'expectedDigest, provider, confirmed',
|
|
180
|
+
control: 'runId, action, expectedRevision, confirmed',
|
|
181
|
+
answer: 'runId, questionId, expectedRevision, answeredBy, answer, confirmed',
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
for (const [action, fields] of Object.entries(AUTONOMY_INTERFACE_FIELDS)) {
|
|
185
|
+
server.registerTool(`ewai_autonomy_${action}`, {
|
|
186
|
+
title: `EWAI autonomy ${action}`,
|
|
187
|
+
description: `Use the configured project only. Required fields: ${autonomyRequiredGuidance[action]}. ${action === 'status' || action === 'preview' ? 'Reading or previewing cannot launch a provider or approve work.' : 'Mutations require confirmed: true; they never grant Build, Manual QA or release approval.'} Unknown fields are rejected by the shared engine; refresh status or preview before acting on a current digest or revision.`,
|
|
188
|
+
// Preserve the complete input for the shared closed validator, including
|
|
189
|
+
// unknown fields. SDK stripping would hide attempted root overrides.
|
|
190
|
+
inputSchema: z.object(Object.fromEntries(fields.map(key => [key, z.unknown().optional().describe(autonomyFieldGuidance[key])]))).passthrough(),
|
|
191
|
+
annotations: { readOnlyHint: action === 'status', destructiveHint: false, openWorldHint: false },
|
|
192
|
+
}, async input => {
|
|
193
|
+
try { return result(await autonomyInterfaceAction(paths.projectRoot, action, input)); }
|
|
194
|
+
catch (error) { return { ...result(safeAutonomyInterfaceError(error)), isError: true }; }
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
160
198
|
function portfolioPersonas() {
|
|
161
199
|
return listPersonas([
|
|
162
200
|
resolve(import.meta.dirname, '../../packs/personas/core/personas'),
|