@thebackstoryis/engineering-with-ai 0.2.9 → 0.3.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.
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
@@ -1,9 +1,15 @@
1
- import { accessSync, constants, createWriteStream, mkdirSync } from 'node:fs';
1
+ import { accessSync, constants, createWriteStream, mkdirSync, existsSync, realpathSync, lstatSync, openSync, readSync, closeSync, fstatSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
2
2
  import { delimiter, isAbsolute, resolve } from 'node:path';
3
- import { spawn } from 'node:child_process';
3
+ import { spawn, spawnSync } from 'node:child_process';
4
+ import { createHash } from 'node:crypto';
5
+ import { release, tmpdir } from 'node:os';
6
+ import { createServer } from 'node:http';
4
7
  import { CONTEXT_ESTIMATE_METHOD, estimateContextTokens } from './context-assembly.mjs';
5
8
 
6
9
  export const AFK_PROVIDERS = Object.freeze(['codex', 'claude', 'antigravity']);
10
+ // Retain owned child handles after an uncertain stop; do not turn a missing
11
+ // close event into success or lose the ability to observe a later exit.
12
+ const unsettledProcesses = new Set();
7
13
 
8
14
  export function measureProviderPrompt(prompt) {
9
15
  const measurement = estimateContextTokens(prompt);
@@ -62,6 +68,7 @@ function claudeTools(task, mode) {
62
68
 
63
69
  export function buildProviderInvocation(provider, input = {}) {
64
70
  if (!AFK_PROVIDERS.includes(provider)) throw new Error(`Unsupported AFK provider: ${provider}`);
71
+ assertAfkMode(input.mode);
65
72
  const mode = input.mode === 'review' ? 'review' : 'implementation';
66
73
  const cwd = resolve(input.cwd);
67
74
  const model = String(input.model ?? '').trim();
@@ -109,6 +116,11 @@ export function buildProviderInvocation(provider, input = {}) {
109
116
  }
110
117
 
111
118
  export function invokeProvider(invocation, options = {}) {
119
+ assertAfkMode(invocation.mode);
120
+ if (options.signal !== undefined && !(options.signal instanceof AbortSignal)) throw new Error('Invalid host cancellation signal.');
121
+ if (options.signal?.aborted) return Promise.resolve({ provider: invocation.provider, exitCode: -1,
122
+ signal: '', cancelled: true, timedOut: false, executionStopped: true, processGroupStopped: true,
123
+ durationMs: 0, output: '', logPath: invocation.logPath, providerUsage: null });
112
124
  mkdirSync(resolve(invocation.logPath, '..'), { recursive: true });
113
125
  const output = createWriteStream(invocation.logPath, { flags: 'a' });
114
126
  const startedAt = Date.now();
@@ -120,10 +132,29 @@ export function invokeProvider(invocation, options = {}) {
120
132
  env: options.env ?? process.env,
121
133
  stdio: ['pipe', 'pipe', 'pipe'],
122
134
  shell: false,
135
+ detached: process.platform !== 'win32',
123
136
  });
124
137
  options.onStart?.(child.pid);
125
138
  let captured = '';
126
139
  let timedOut = false;
140
+ let cancelled = false, killTimer, stopDeadline, settled = false;
141
+ const kill = signal => {
142
+ try { if (process.platform === 'win32') child.kill(signal); else process.kill(-child.pid, signal); } catch {}
143
+ };
144
+ const stop = () => {
145
+ if (settled) return;
146
+ kill('SIGTERM'); killTimer ??= setTimeout(() => kill('SIGKILL'), 250);
147
+ stopDeadline ??= setTimeout(() => {
148
+ if (settled) return; settled = true; clearTimeout(timeout); clearTimeout(killTimer);
149
+ options.signal?.removeEventListener('abort', cancel);
150
+ unsettledProcesses.add(child); child.once('close', () => unsettledProcesses.delete(child));
151
+ child.stdin.destroy(); child.stdout.destroy(); child.stderr.destroy(); child.unref(); output.end();
152
+ resolvePromise({ provider: invocation.provider, status: 'unknown', exitCode: -1, signal: '',
153
+ cancelled, timedOut, executionStopped: false, processGroupStopped: false, output: '',
154
+ durationMs: Date.now() - startedAt, logPath: invocation.logPath, providerUsage: null });
155
+ }, 1000);
156
+ };
157
+ const cancel = () => { cancelled = true; stop(); };
127
158
  const capture = (chunk) => {
128
159
  output.write(chunk);
129
160
  captured = `${captured}${chunk.toString('utf8')}`.slice(-maxCapture);
@@ -131,23 +162,34 @@ export function invokeProvider(invocation, options = {}) {
131
162
  child.stdout.on('data', capture);
132
163
  child.stderr.on('data', capture);
133
164
  child.once('error', (error) => {
165
+ settled = true; clearTimeout(timeout); clearTimeout(killTimer); clearTimeout(stopDeadline); options.signal?.removeEventListener('abort', cancel);
134
166
  output.end();
135
167
  reject(error);
136
168
  });
137
169
  const timeout = setTimeout(() => {
138
170
  timedOut = true;
139
- child.kill('SIGTERM');
140
- setTimeout(() => child.kill('SIGKILL'), 5_000).unref();
171
+ stop();
141
172
  }, invocation.timeoutMs);
142
173
  timeout.unref();
143
- child.once('close', (exitCode, signal) => {
144
- clearTimeout(timeout);
174
+ child.once('close', async (exitCode, signal) => {
175
+ if (settled) return;
176
+ settled = true; clearTimeout(timeout); clearTimeout(killTimer); clearTimeout(stopDeadline); options.signal?.removeEventListener('abort', cancel);
177
+ let processGroupStopped = false;
178
+ if (process.platform !== 'win32') {
179
+ try { process.kill(-child.pid, 0); kill('SIGKILL'); }
180
+ catch (error) { processGroupStopped = error.code === 'ESRCH'; }
181
+ }
145
182
  output.end();
146
183
  resolvePromise({
147
184
  provider: invocation.provider,
148
185
  exitCode: exitCode ?? -1,
149
186
  signal: signal ?? '',
150
187
  timedOut,
188
+ cancelled,
189
+ processGroupStopped,
190
+ // AFK tools can launch detached descendants. Group disappearance alone
191
+ // is not proof of full-tree termination after cancellation or timeout.
192
+ executionStopped: !cancelled && !timedOut && processGroupStopped,
151
193
  durationMs: Date.now() - startedAt,
152
194
  logPath: invocation.logPath,
153
195
  output: captured,
@@ -158,6 +200,322 @@ export function invokeProvider(invocation, options = {}) {
158
200
  ),
159
201
  });
160
202
  });
161
- child.stdin.end(invocation.prompt);
203
+ options.signal?.addEventListener('abort', cancel, { once: true });
204
+ if (options.signal?.aborted) cancel();
205
+ child.stdin.on('error', () => {});
206
+ child.stdin.end(cancelled ? '' : invocation.prompt);
207
+ });
208
+ }
209
+
210
+ function assertAfkMode(mode) {
211
+ // Proposal workers have a different authority and output boundary. An
212
+ // unrecognised mode must never silently acquire implementation permissions.
213
+ if (mode === undefined || mode === 'implementation' || mode === 'review') return;
214
+ const error = new Error('This mode is unavailable through the AFK provider transport.');
215
+ error.code = 'phase-provider-mode-unavailable';
216
+ throw error;
217
+ }
218
+
219
+ // Separate from AFK: capabilities are host-created, process-local handles, not
220
+ // serialisable worker/browser supplied executable paths or permission flags.
221
+ const phaseCapabilities = new WeakMap();
222
+ const unavailablePhase = code => ({ status: 'unavailable', code, requiresHuman: true });
223
+ const phaseVersions = Object.freeze({ claude: '2.1.257 (Claude Code)', sandbox: 'macos-seatbelt-phase-v1' });
224
+ const phaseEnv = () => ({ PATH: process.env.PATH ?? '', LANG: 'en_US.UTF-8' });
225
+
226
+ function installedExecutable(command) {
227
+ for (const directory of (process.env.PATH ?? '').split(delimiter).filter(Boolean)) {
228
+ const path = resolve(directory, command);
229
+ try { accessSync(path, constants.X_OK); return realpathSync(path); } catch {}
230
+ }
231
+ throw new Error('phase-provider-missing');
232
+ }
233
+
234
+ function executableDigest(path) {
235
+ const before = lstatSync(path);
236
+ // Package managers may hard-link installed executables. They are trusted
237
+ // host code, unlike untrusted proposal files; bind their bytes and identity.
238
+ if (!before.isFile() || before.size > 512 * 1024 * 1024) throw new Error('phase-provider-identity-invalid');
239
+ const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW), hash = createHash('sha256'), buffer = Buffer.alloc(1024 * 1024);
240
+ try {
241
+ let bytes; while ((bytes = readSync(fd, buffer, 0, buffer.length, null))) hash.update(buffer.subarray(0, bytes));
242
+ const after = fstatSync(fd), current = lstatSync(path);
243
+ if (before.ino !== after.ino || before.dev !== after.dev || before.size !== after.size || before.mtimeMs !== after.mtimeMs
244
+ || after.nlink !== before.nlink || current.ino !== after.ino || current.mtimeMs !== after.mtimeMs) throw new Error('phase-provider-identity-changed');
245
+ return hash.digest('hex');
246
+ } finally { closeSync(fd); }
247
+ }
248
+
249
+ export function preparePhaseProvider(provider) {
250
+ // Additional providers/platforms stay unavailable until the same executable
251
+ // and configuration-isolation conformance has actually been demonstrated.
252
+ if (provider !== 'claude' || process.platform !== 'darwin') return unavailablePhase('phase-provider-mode-unavailable');
253
+ try {
254
+ const executable = installedExecutable('claude'), sandbox = realpathSync('/usr/bin/sandbox-exec');
255
+ const version = path => {
256
+ const result = spawnSync(path, ['--version'], { env: phaseEnv(), encoding: 'utf8', timeout: 5000, maxBuffer: 1024 });
257
+ if (result.status !== 0 || result.error) throw new Error('phase-provider-version-unavailable');
258
+ return result.stdout.trim();
259
+ };
260
+ if (version(executable) !== phaseVersions.claude) return unavailablePhase('phase-provider-version-unverified');
261
+ const hashes = [executableDigest(executable), executableDigest(sandbox)];
262
+ const handle = Object.freeze({ status: 'requires-conformance', provider, version: phaseVersions.claude,
263
+ sandboxVersion: phaseVersions.sandbox,
264
+ identityDigest: createHash('sha256').update(JSON.stringify([hashes, release(), process.arch, phaseVersions.sandbox])).digest('hex') });
265
+ phaseCapabilities.set(handle, { executable, sandbox, hashes, verified: false });
266
+ return handle;
267
+ } catch { return unavailablePhase('phase-provider-identity-unavailable'); }
268
+ }
269
+
270
+ function currentPhaseCapability(handle, requireVerified = true) {
271
+ const capability = phaseCapabilities.get(handle);
272
+ if (!capability || requireVerified && !capability.verified) throw new Error('phase-provider-conformance-required');
273
+ if (executableDigest(capability.executable) !== capability.hashes[0] || executableDigest(capability.sandbox) !== capability.hashes[1]) {
274
+ capability.verified = false; throw new Error('phase-provider-identity-changed');
275
+ }
276
+ return capability;
277
+ }
278
+
279
+ function restrictedPhaseArgs(runtime, command, args, fixture) {
280
+ // One kernel policy covers every restriction; nested Seatbelt initialisation
281
+ // is not supported. Denying fork also denies posix_spawn, so a child cannot
282
+ // detach into a new session and outlive the captured process.
283
+ const port = fixture?.url ? Number(new URL(fixture.url).port) : null;
284
+ const policy = `(version 1)(allow default)(deny file-write*)`
285
+ + `(allow file-write* (subpath ${JSON.stringify(runtime)}))(deny process-fork)`
286
+ + (fixture ? `(deny network*)${port ? `(allow network-outbound (remote ip "localhost:${port}"))` : ''}` : '');
287
+ return ['-p', policy, command, ...args];
288
+ }
289
+
290
+ const restrictedClaudeArgs = () => ['--safe-mode', '--bare', '--restricted', '--tools', '', '--strict-mcp-config',
291
+ '--mcp-config', '{"mcpServers":{}}', '--setting-sources', '', '--no-chrome', '--disable-slash-commands',
292
+ '--no-session-persistence', '--system-prompt-snapshot', 'off', '--permission-mode', 'dontAsk',
293
+ '--max-turns', '1', '--print', '--output-format', 'json'];
294
+
295
+ async function captureRestrictedProcess(capability, input, fixture = null) {
296
+ const cancelledBeforeSpawn = () => ({ status: 'cancelled', exitCode: -1, signal: null,
297
+ executionStopped: true, spawned: false, output: '', bytes: 0 });
298
+ if (input.signal?.aborted) return cancelledBeforeSpawn();
299
+ const directory = realpathSync(mkdtempSync(resolve(tmpdir(), 'ewai-phase-process-')));
300
+ const identity = lstatSync(directory), runtime = resolve(directory, 'runtime');
301
+ mkdirSync(runtime, { mode: 0o700 });
302
+ if (fixture?.isolationProbe) {
303
+ // Deliberately hostile *fixture* configuration. A loaded hook/MCP server
304
+ // would contact the local observer; no real provider endpoint is involved.
305
+ const script = `require("node:http").get(${JSON.stringify(`${fixture.url}/forbidden-hook`)})`;
306
+ const hook = { hooks: { SessionStart: [{ hooks: [{ type: 'command', command: `${JSON.stringify(process.execPath)} -e '${script}'` }] }] } };
307
+ mkdirSync(resolve(directory, '.claude'));
308
+ for (const path of [resolve(directory, '.claude/settings.json'), resolve(runtime, 'settings.json')]) writeFileSync(path, JSON.stringify(hook), { mode: 0o600 });
309
+ writeFileSync(resolve(directory, '.mcp.json'), JSON.stringify({ mcpServers: { forbidden: { type: 'http', url: `${fixture.url}/forbidden-mcp` } } }), { mode: 0o600 });
310
+ writeFileSync(resolve(directory, 'CLAUDE.md'), 'EWAI_UNTRUSTED_INSTRUCTIONS_CANARY', { mode: 0o600 });
311
+ }
312
+ const env = { ...phaseEnv(), CLAUDE_CONFIG_DIR: runtime, CLAUDE_CODE_TMPDIR: runtime, TMPDIR: runtime,
313
+ CLAUDE_CODE_SKIP_PROMPT_HISTORY: '1', CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
314
+ DISABLE_TELEMETRY: '1', DISABLE_ERROR_REPORTING: '1',
315
+ ANTHROPIC_API_KEY: fixture ? 'ewai-fixture-not-a-real-key' : process.env.ANTHROPIC_API_KEY };
316
+ if (fixture?.url) env.ANTHROPIC_BASE_URL = fixture.url;
317
+ const command = fixture?.command ?? capability.executable, args = fixture?.args ?? restrictedClaudeArgs();
318
+ const launchCommand = capability.sandbox;
319
+ const launchArgs = restrictedPhaseArgs(runtime, command, args, fixture);
320
+ let stopped = false;
321
+ try {
322
+ const result = await new Promise(resolvePromise => {
323
+ let child, timer, killTimer, stopDeadline, terminal = null, bytes = 0, output = [], settled = false;
324
+ const kill = signal => { if (child?.pid) try { process.kill(-child.pid, signal); } catch {} };
325
+ const stop = status => {
326
+ if (settled) return;
327
+ terminal ??= status; kill('SIGTERM'); killTimer ??= setTimeout(() => kill('SIGKILL'), 250);
328
+ stopDeadline ??= setTimeout(() => {
329
+ if (settled) return; settled = true; clearTimeout(timer); clearTimeout(killTimer);
330
+ input.signal?.removeEventListener('abort', cancel);
331
+ unsettledProcesses.add(child); child.once('close', () => unsettledProcesses.delete(child));
332
+ child.stdin.destroy(); child.stdout.destroy(); child.stderr.destroy(); child.unref();
333
+ resolvePromise({ status: 'unknown', exitCode: -1, signal: null, executionStopped: false, output: '', bytes });
334
+ }, 1000);
335
+ };
336
+ const cancel = () => stop('cancelled');
337
+ const finish = async (exitCode, signal) => {
338
+ if (settled) return; settled = true; clearTimeout(timer); clearTimeout(killTimer); clearTimeout(stopDeadline);
339
+ input.signal?.removeEventListener('abort', cancel);
340
+ // A surviving process group is not a confirmed stop. Kill it and do
341
+ // not return a proposal; preserve its private workspace for recovery.
342
+ stopped = !child?.pid;
343
+ for (let attempt = 0; child?.pid && attempt < 20; attempt += 1) {
344
+ try { process.kill(-child.pid, 0); kill('SIGKILL'); }
345
+ catch (error) { stopped = error.code === 'ESRCH'; break; }
346
+ // macOS may retain a reparented zombie briefly. A zombie cannot
347
+ // execute or hold file descriptors; do not confuse it with a worker.
348
+ const processes = spawnSync('/bin/ps', ['-o', 'pgid=,stat=', '-g', String(child.pid)],
349
+ { encoding: 'utf8', timeout: 500, maxBuffer: 4096, env: phaseEnv() });
350
+ const rows = (processes.stdout ?? '').trim().split('\n').filter(Boolean);
351
+ if (!processes.error && rows.length && rows.every(row => {
352
+ const [group, state] = row.trim().split(/\s+/); return group === String(child.pid) && /^Z/.test(state ?? '');
353
+ })) { stopped = true; break; }
354
+ await new Promise(resolveWait => setTimeout(resolveWait, 25));
355
+ }
356
+ if (input.signal?.aborted) terminal ??= 'cancelled';
357
+ resolvePromise({ status: !stopped ? 'unknown' : terminal ?? (exitCode === 0 ? 'complete' : 'failed'),
358
+ exitCode: exitCode ?? -1, signal: signal ?? null, executionStopped: stopped,
359
+ output: !terminal && stopped ? Buffer.concat(output).toString('utf8') : '', bytes });
360
+ };
361
+ if (input.signal?.aborted) { stopped = true; resolvePromise(cancelledBeforeSpawn()); return; }
362
+ try { child = spawn(launchCommand, launchArgs, { cwd: directory, env, detached: true, shell: false, stdio: ['pipe', 'pipe', 'pipe'] }); }
363
+ catch { finish(-1, null); return; }
364
+ const capture = (chunk, stdout) => {
365
+ bytes += chunk.length;
366
+ if (bytes > input.maxBytes) { output = []; stop('truncated'); return; }
367
+ if (stdout && !terminal) output.push(chunk);
368
+ };
369
+ child.stdout.on('data', chunk => capture(chunk, true)); child.stderr.on('data', chunk => capture(chunk, false));
370
+ child.stdin.on('error', () => {});
371
+ child.once('error', () => finish(-1, null)); child.once('close', finish);
372
+ timer = setTimeout(() => stop('timed-out'), input.timeoutMs);
373
+ input.signal?.addEventListener('abort', cancel, { once: true });
374
+ if (input.signal?.aborted) cancel();
375
+ child.stdin.end(terminal ? '' : input.prompt);
376
+ });
377
+ return result;
378
+ } finally {
379
+ if (stopped && existsSync(directory)) {
380
+ const current = lstatSync(directory);
381
+ if (current.isDirectory() && current.ino === identity.ino && current.dev === identity.dev) rmSync(directory, { recursive: true });
382
+ }
383
+ }
384
+ }
385
+
386
+ export async function invokeRestrictedPhaseProvider(handle, input = {}) {
387
+ if (!input || typeof input !== 'object' || Array.isArray(input) || typeof input.prompt !== 'string' || Buffer.byteLength(input.prompt) > 256 * 1024
388
+ || !Number.isSafeInteger(input.timeoutMs) || input.timeoutMs < 100 || input.timeoutMs > 600000
389
+ || input.signal !== undefined && !(input.signal instanceof AbortSignal)) return unavailablePhase('phase-provider-input-invalid');
390
+ let capability;
391
+ try { capability = currentPhaseCapability(handle); } catch { return unavailablePhase('phase-provider-conformance-required'); }
392
+ if (!process.env.ANTHROPIC_API_KEY) return unavailablePhase('phase-provider-credential-unavailable');
393
+ const captured = await captureRestrictedProcess(capability, { ...input, maxBytes: 256 * 1024 });
394
+ if (captured.status !== 'complete') return { ...captured, output: '' };
395
+ let envelope;
396
+ try { envelope = JSON.parse(captured.output); } catch { return { status: 'malformed', executionStopped: true, exitCode: 0, output: '' }; }
397
+ if (envelope?.type !== 'result' || envelope.subtype !== 'success' || envelope.is_error !== false || typeof envelope.result !== 'string') {
398
+ return { status: 'failed', executionStopped: true, exitCode: captured.exitCode, output: '' };
399
+ }
400
+ return { status: 'complete', exitCode: 0, executionStopped: true, output: envelope.result,
401
+ providerUsage: normaliseProviderUsage('claude', envelope.usage) };
402
+ }
403
+
404
+ export async function verifyPhaseProviderConformance(handle, control = {}) {
405
+ if (!control || typeof control !== 'object' || Array.isArray(control)
406
+ || Object.getPrototypeOf(control) !== Object.prototype || Object.keys(control).some(key => !['signal', 'timeoutMs'].includes(key))
407
+ || control.signal !== undefined && !(control.signal instanceof AbortSignal)
408
+ || control.timeoutMs !== undefined && (!Number.isSafeInteger(control.timeoutMs) || control.timeoutMs < 100 || control.timeoutMs > 600000)) {
409
+ return { ...unavailablePhase('phase-provider-input-invalid'), executionStopped: true };
410
+ }
411
+ if (control.signal?.aborted) return { ...unavailablePhase('phase-cancelled'), executionStopped: true };
412
+ let capability;
413
+ try { capability = currentPhaseCapability(handle, false); } catch { return unavailablePhase('phase-provider-conformance-required'); }
414
+ capability.verified = false;
415
+ const controller = new AbortController(), cancel = () => controller.abort();
416
+ control.signal?.addEventListener('abort', cancel, { once: true });
417
+ const deadline = setTimeout(cancel, control.timeoutMs ?? 60000);
418
+ let executionStopped = true;
419
+ const capture = async (capability, input, fixture) => {
420
+ if (controller.signal.aborted) throw new Error('phase-cancelled');
421
+ const result = await captureRestrictedProcess(capability, { ...input, signal: controller.signal }, fixture);
422
+ executionStopped &&= result.executionStopped === true;
423
+ if (!executionStopped) throw new Error('phase-execution-unknown');
424
+ if (controller.signal.aborted) throw new Error('phase-cancelled');
425
+ return result;
426
+ };
427
+ const targetRoot = realpathSync(mkdtempSync(resolve(tmpdir(), 'ewai-phase-conformance-'))), target = resolve(targetRoot, 'approval.json');
428
+ const requests = [];
429
+ let forcedTool = false;
430
+ const server = createServer((request, response) => {
431
+ if (request.method === 'GET' && request.url === '/ewai-network-probe') { response.writeHead(204); response.end(); return; }
432
+ if (request.method === 'HEAD' && request.url === '/api/hello') {
433
+ requests.push({ path: '/api/hello', healthProbe: true }); response.writeHead(204); response.end(); return;
434
+ }
435
+ let body = '', bytes = 0;
436
+ request.on('data', chunk => { bytes += chunk.length; if (bytes > 2 * 1024 * 1024) request.destroy(); else body += chunk; });
437
+ request.on('end', () => {
438
+ let input; try { input = JSON.parse(body); } catch { requests.push({ path: request.url }); response.writeHead(400); response.end(); return; }
439
+ requests.push({ path: request.url, tools: (input.tools ?? []).map(tool => tool.name),
440
+ inheritedInstructions: body.includes('EWAI_UNTRUSTED_INSTRUCTIONS_CANARY') });
441
+ response.writeHead(200, { 'content-type': 'text/event-stream' });
442
+ const event = (type, value) => response.write(`event: ${type}\ndata: ${JSON.stringify({ type, ...value })}\n\n`);
443
+ event('message_start', { message: { id: 'msg_ewai_fixture', type: 'message', role: 'assistant', model: 'fixture-model',
444
+ content: [], stop_reason: null, stop_sequence: null, usage: { input_tokens: 1, output_tokens: 0 } } });
445
+ event('content_block_start', { index: 0, content_block: forcedTool
446
+ ? { type: 'tool_use', id: 'toolu_ewai_fixture', name: 'Write', input: {} } : { type: 'text', text: '' } });
447
+ event('content_block_delta', { index: 0, delta: forcedTool
448
+ ? { type: 'input_json_delta', partial_json: JSON.stringify({ file_path: target, content: 'forged approval' }) }
449
+ : { type: 'text_delta', text: 'EWAI_RESTRICTED_FIXTURE_ONLY' } });
450
+ event('content_block_stop', { index: 0 });
451
+ event('message_delta', { delta: { stop_reason: forcedTool ? 'tool_use' : 'end_turn', stop_sequence: null }, usage: { output_tokens: 1 } });
452
+ event('message_stop', {}); response.end();
453
+ });
162
454
  });
455
+ try {
456
+ await new Promise((resolvePromise, reject) => { server.once('error', reject); server.listen(0, '127.0.0.1', resolvePromise); });
457
+ const fixtureUrl = `http://127.0.0.1:${server.address().port}`;
458
+ const network = await capture(capability, { prompt: '', timeoutMs: 5000, maxBytes: 1024 }, {
459
+ url: fixtureUrl, command: process.execPath, args: ['-e',
460
+ 'const http=require("node:http"),net=require("node:net");let a=null,b=null;const done=()=>{if(a!==null&&b!==null)process.stdout.write(JSON.stringify({allowed:a,denied:b}))};http.get(process.argv[1]+"/ewai-network-probe",r=>{a=r.statusCode===204;r.resume();done()}).on("error",()=>{a=false;done()});const s=net.connect({host:"127.0.0.2",port:Number(process.argv[2])});s.on("connect",()=>{b=false;s.destroy();done()});s.on("error",e=>{b=["EPERM","EACCES"].includes(e.code);done()});',
461
+ fixtureUrl, String(server.address().port)] });
462
+ if (network.status !== 'complete' || network.output !== '{"allowed":true,"denied":true}') {
463
+ let observed; try { observed = JSON.parse(network.output); } catch {}
464
+ return { ...unavailablePhase('phase-provider-network-confinement-failed'), checks: { networkIsolated: false,
465
+ allowedLoopback: observed?.allowed === true, deniedOtherAddress: observed?.denied === true,
466
+ processComplete: network.status === 'complete' } };
467
+ }
468
+ const denial = await capture(capability, { prompt: '', timeoutMs: 5000, maxBytes: 1024 }, {
469
+ command: process.execPath, args: ['-e', 'try{require("node:fs").writeFileSync(process.argv[1],"forged");process.stdout.write("UNSAFE")}catch(e){process.stdout.write(e.code)}', target] });
470
+ const overflow = await capture(capability, { prompt: '', timeoutMs: 5000, maxBytes: 1024 }, {
471
+ command: process.execPath, args: ['-e', 'process.stdout.write("x".repeat(65536))'] });
472
+ const timeout = await capture(capability, { prompt: '', timeoutMs: 100, maxBytes: 1024 }, {
473
+ command: process.execPath, args: ['-e', 'setTimeout(() => {}, 10000)'] });
474
+ const fork = await capture(capability, { prompt: '', timeoutMs: 5000, maxBytes: 1024 }, {
475
+ command: process.execPath, args: ['-e',
476
+ 'try{const c=require("node:child_process").spawn(process.execPath,["-e","process.exit()"],{detached:true,stdio:"ignore"});c.on("error",e=>process.stdout.write(e.code));c.on("spawn",()=>process.stdout.write("UNSAFE"))}catch(e){process.stdout.write(e.code)}'] });
477
+ const primitiveChecks = {
478
+ networkIsolated: true,
479
+ canonicalWritePrevented: denial.status === 'complete' && ['EPERM', 'EACCES'].includes(denial.output) && !existsSync(target),
480
+ outputBoundEnforced: overflow.status === 'truncated' && overflow.output === '' && overflow.executionStopped,
481
+ timeoutEnforced: timeout.status === 'timed-out' && timeout.output === '' && timeout.executionStopped,
482
+ childProcessPrevented: fork.status === 'complete' && ['EPERM', 'EACCES'].includes(fork.output),
483
+ };
484
+ if (!Object.values(primitiveChecks).every(value => value === true)) {
485
+ return { ...unavailablePhase('phase-provider-confinement-failed'), checks: primitiveChecks };
486
+ }
487
+ const model = await capture(capability, { prompt: 'Return the fixture token only.', timeoutMs: 15000, maxBytes: 16384 },
488
+ { url: fixtureUrl, isolationProbe: true });
489
+ let envelope; try { envelope = JSON.parse(model.output); } catch {}
490
+ forcedTool = true;
491
+ const forged = await capture(capability, { prompt: 'No tools or approvals are authorised.', timeoutMs: 15000, maxBytes: 16384 },
492
+ { url: fixtureUrl, isolationProbe: true });
493
+ let refused; try { refused = JSON.parse(forged.output); } catch {}
494
+ const checks = {
495
+ ...primitiveChecks,
496
+ structuredDraft: model.status === 'complete' && envelope?.type === 'result' && envelope.subtype === 'success' && envelope.is_error === false
497
+ && envelope.result === 'EWAI_RESTRICTED_FIXTURE_ONLY',
498
+ inheritedCapabilitiesBlocked: requests.filter(request => request.path.startsWith('/v1/messages')).length === 2
499
+ && requests.every(request => request.healthProbe === true
500
+ || request.path.startsWith('/v1/messages') && request.tools?.length === 0 && !request.inheritedInstructions),
501
+ forgedToolRefused: forged.executionStopped && refused?.type === 'result' && refused.is_error === true && !existsSync(target),
502
+ };
503
+ capability.verified = Object.values(checks).every(value => value === true);
504
+ currentPhaseCapability(handle, false);
505
+ return capability.verified ? { status: 'verified', provider: 'claude', identityDigest: handle.identityDigest,
506
+ canonicalWritePrevented: true, outputBoundEnforced: true, timeoutEnforced: true, networkIsolated: true, childProcessPrevented: true, toolsExposed: 0, inheritedCapabilitiesBlocked: true,
507
+ forgedToolRefused: true, fixtureServiceOnly: true,
508
+ authority: 'none' } : { ...unavailablePhase('phase-provider-conformance-failed'), checks,
509
+ requests: requests.map(request => ({ route: request.healthProbe ? 'fixture-health' : request.path.startsWith('/v1/messages') ? 'fixture-model'
510
+ : request.path.includes('forbidden-hook') ? 'forbidden-hook' : request.path.includes('forbidden-mcp') ? 'forbidden-mcp' : 'other',
511
+ toolsExposed: request.tools?.length ?? null, inheritedInstructions: request.inheritedInstructions === true })) };
512
+ } catch (error) {
513
+ capability.verified = false;
514
+ return { ...unavailablePhase(['phase-cancelled', 'phase-execution-unknown'].includes(error.message)
515
+ ? error.message : 'phase-provider-conformance-failed'), executionStopped };
516
+ } finally {
517
+ clearTimeout(deadline); control.signal?.removeEventListener('abort', cancel);
518
+ server.closeAllConnections(); await new Promise(resolvePromise => server.close(resolvePromise));
519
+ if (executionStopped) rmSync(targetRoot, { recursive: true, force: true });
520
+ }
163
521
  }
@@ -5,6 +5,7 @@ import { validatePrototypeManifest } from '../delivery-artifacts.mjs';
5
5
  import { projectPaths } from '../paths.mjs';
6
6
  import { openRuntimeDatabase } from './database.mjs';
7
7
  import { readRuntimeIntent, syncIntentIndex } from './intents.mjs';
8
+ import { withIntentMutation, assertIntentMutation } from './intent-ownership.mjs';
8
9
 
9
10
  const lanes = ['backlog', 'ready', 'active', 'qa', 'blocked', 'done'];
10
11
 
@@ -184,13 +185,17 @@ export function syncWorkItems(projectRoot, suppliedDatabase = null) {
184
185
  title = excluded.title, status = 'active'
185
186
  `);
186
187
 
188
+ // Evidence readers may open their own database connection. Derive before
189
+ // taking the projection writer, avoiding self-contention and false stale
190
+ // results. Canonical mutations revalidate under their intent ownership.
191
+ const derivedItems = intents.map(intent => deriveExecutionState(projectRoot, intentFromRow(intent), {
192
+ intentCatalog: execution.catalog,
193
+ leases: execution.leases.get(intent.id) ?? [],
194
+ }));
187
195
  database.exec('BEGIN IMMEDIATE;');
188
196
  try {
189
197
  intents.forEach((intent, index) => {
190
- const derived = deriveExecutionState(projectRoot, intentFromRow(intent), {
191
- intentCatalog: execution.catalog,
192
- leases: execution.leases.get(intent.id) ?? [],
193
- });
198
+ const derived = derivedItems[index];
194
199
  const defaults = derived.projection;
195
200
  insertItem.run(
196
201
  intent.id,
@@ -394,6 +399,10 @@ export function resolvePrototypeAsset(projectRoot, reference, artefactId, assetP
394
399
  }
395
400
 
396
401
  export function updateWorkItem(projectRoot, reference, input) {
402
+ return withIntentMutation(projectRoot, reference, { action: 'update-projection', input, ownership: input.ownership }, () => updateOwnedWorkItem(projectRoot, reference, input));
403
+ }
404
+
405
+ function updateOwnedWorkItem(projectRoot, reference, input) {
397
406
  return withDatabase(projectRoot, null, (database) => {
398
407
  syncWorkItems(projectRoot, database);
399
408
  const item = findItem(database, reference);
@@ -419,6 +428,7 @@ export function updateWorkItem(projectRoot, reference, input) {
419
428
  if (key === 'completionPercent') return Math.max(0, Math.min(100, Number(value)));
420
429
  return value;
421
430
  });
431
+ assertIntentMutation(projectRoot, reference);
422
432
  database.prepare(`UPDATE work_items SET ${entries.map(([key]) => `${allowed[key]} = ?`).join(', ')}, updated_at = CURRENT_TIMESTAMP WHERE id = ?`)
423
433
  .run(...values, item.id);
424
434
  }
@@ -427,6 +437,10 @@ export function updateWorkItem(projectRoot, reference, input) {
427
437
  }
428
438
 
429
439
  export function setPhase(projectRoot, reference, phaseKey, input = {}) {
440
+ return withIntentMutation(projectRoot, reference, { action: 'update-phase-projection', phase: phaseKey, input, ownership: input.ownership }, () => setOwnedPhase(projectRoot, reference, phaseKey, input));
441
+ }
442
+
443
+ function setOwnedPhase(projectRoot, reference, phaseKey, input) {
430
444
  return withDatabase(projectRoot, null, (database) => {
431
445
  syncWorkItems(projectRoot, database);
432
446
  const item = findItem(database, reference);
@@ -434,6 +448,7 @@ export function setPhase(projectRoot, reference, phaseKey, input = {}) {
434
448
  const existing = database.prepare(`
435
449
  SELECT status, iteration_count FROM pipeline_phases WHERE work_item_id = ? AND phase_key = ?
436
450
  `).get(item.id, phaseKey);
451
+ assertIntentMutation(projectRoot, reference);
437
452
  database.prepare(`
438
453
  INSERT INTO pipeline_phases (work_item_id, phase_key, status, started_at, completed_at, artefact_path, iteration_count, notes)
439
454
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
@@ -525,6 +540,7 @@ function recordActivity(projectRoot, reference, input, mode) {
525
540
  database.exec('BEGIN IMMEDIATE;');
526
541
  try {
527
542
  const existingSession = database.prepare('SELECT * FROM active_sessions WHERE work_item_id = ?').get(item.id);
543
+ if (mode === 'start') assertIntentMutation(projectRoot, reference, database);
528
544
  if (mode === 'start' && existingSession && existingSession.ended_at === null) {
529
545
  const requestedOwner = String(input.ownerId ?? '').trim();
530
546
  const requestedTool = String(input.tool ?? '').trim();
@@ -606,7 +622,7 @@ function recordActivity(projectRoot, reference, input, mode) {
606
622
  }
607
623
 
608
624
  export function startActiveSession(projectRoot, reference, input = {}) {
609
- return recordActivity(projectRoot, reference, input, 'start');
625
+ return withIntentMutation(projectRoot, reference, { action: 'start-session', input, ownership: input.ownership }, () => recordActivity(projectRoot, reference, input, 'start'));
610
626
  }
611
627
 
612
628
  export function addActivityEvent(projectRoot, reference, input = {}) {