git mv packages/business -> packages/organization (@mosaic/organization). The old package stays as one-line re-export shims of each moved module, with src/business.mjs re-exporting src/organization-config.mjs and src/cli.mjs forwarding to the moved executable. BusinessError is OrganizationConfigError (one constructor, name kept BusinessError); OrganizationError stays distinct. The bus adapter gains organization-context.mjs (busOrganization), with business.mjs (busBusiness) as the same function. Import consumers, manifests, the lockfile, scripts/mosaic, stack-copy.sh COPY_PATHS (both roots), seat sandbox RUNNER_FILES and the copy closure walker (organization-imports.mjs, both roots required) move to the new path. v1 serialization, v1 writers and the stage A fixtures are unchanged; the stage A contract tests stay where they were and pass as before.
307 lines
24 KiB
JavaScript
307 lines
24 KiB
JavaScript
// Scripted model responses only. Production tools, authority and evidence writers do the work.
|
|
import { readFileSync, writeFileSync, mkdirSync, existsSync, appendFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { execFile } from 'node:child_process';
|
|
import { promisify } from 'node:util';
|
|
import { DatabaseSync } from 'node:sqlite';
|
|
import { startBroker } from '../../../bus/src/runtime.mjs';
|
|
import { createRequestStore } from '../../../bus/src/request-store.mjs';
|
|
import { bootConfig, loadSystem } from '../../src/config.mjs';
|
|
import { loadBusiness, resolveInstance, systemVars } from '../../../organization/src/index.mjs';
|
|
import { limitHit } from '../../../organization/src/launch.mjs';
|
|
import { tasksAdapter } from '../../../tasks/src/adapter.mjs';
|
|
import { createGitSocket } from '../../src/git-socket.mjs';
|
|
import { pushWorking, fetchReview } from '../../../harness/src/git-tools.mjs';
|
|
import { callTool } from '../../../harness/src/tools.mjs';
|
|
import { toolServer } from '../../../harness/src/runner.mjs';
|
|
import { buildBundle } from '../../../harness/src/bundle.mjs';
|
|
import { HARNESS } from '../../../harness/tests/helpers.mjs';
|
|
import { ensureCheckouts } from '../../../seat/src/checkout.mjs';
|
|
import { newRun, Registry, readSessions, writeSessionFile, workspaceDir } from '../../../seat/src/session.mjs';
|
|
import { startTimeOf } from '../../src/host.mjs';
|
|
import { git } from '../../../project/tests/prd-fixture.mjs';
|
|
|
|
export const REQUIREMENT = 'REQ-POMO-1';
|
|
export const PROPOSAL = 'Agent proposal: 25-minute work, 5-minute break, start/pause/reset, a phase-end visual and audible cue after a user gesture. One offline HTML file, no network or external assets.';
|
|
export const HTML = `<!doctype html>
|
|
<html lang="en"><meta charset="utf-8"><meta name="viewport" content="width=device-width">
|
|
<title>Pomodoro</title><style>body{font:20px system-ui;text-align:center;background:#182523;color:#f5edce;margin:10vh auto}output{display:block;font-size:5rem;font-variant-numeric:tabular-nums}button{font:inherit;margin:.4em;padding:.6em 1em}#cue{min-height:2em}</style>
|
|
<h1>Pomodoro</h1><h2 id="phase">Work</h2><output id="clock" aria-live="off">25:00</output>
|
|
<button id="start">Start</button><button id="pause">Pause</button><button id="reset">Reset</button><p id="cue" role="status"></p>
|
|
<script>
|
|
(() => {
|
|
const clock=document.querySelector('#clock'), phase=document.querySelector('#phase'), cue=document.querySelector('#cue');
|
|
let work=true, left=25*60*1000, running=false, last=0, audio=null;
|
|
const render=()=>{const s=Math.ceil(left/1000);clock.textContent=String(Math.floor(s/60)).padStart(2,'0')+':'+String(s%60).padStart(2,'0');phase.textContent=work?'Work':'Break';};
|
|
const signal=()=>{cue.textContent=work?'Break finished. Time to work.':'Work finished. Take a break.';if(audio){const o=audio.createOscillator(),g=audio.createGain();o.connect(g);g.connect(audio.destination);g.gain.value=.12;o.frequency.value=660;o.start();o.stop(audio.currentTime+.2);}};
|
|
const tick=()=>{if(!running)return;const now=Date.now();left-=Math.max(0,now-last);last=now;while(left<=0){work=!work;left+=(work?25:5)*60*1000;signal();}render();};
|
|
document.querySelector('#start').addEventListener('click',()=>{if(!audio){const A=window.AudioContext||window.webkitAudioContext;if(A)audio=new A();}audio?.resume();if(!running){last=Date.now();running=true;}});
|
|
document.querySelector('#pause').addEventListener('click',()=>{tick();running=false;});
|
|
document.querySelector('#reset').addEventListener('click',()=>{running=false;work=true;left=25*60*1000;cue.textContent='';render();});
|
|
setInterval(tick,100);render();
|
|
})();
|
|
</script></html>`;
|
|
|
|
// Executed independently by coder and reviewer against the actual HTML bytes.
|
|
export function verifyHTML(bytes) {
|
|
if (!bytes.includes('25:00') || /https?:\/\/|<script[^>]+src=|<link[^>]+href=/i.test(bytes)) throw new Error('offline acceptance failed');
|
|
return 'Passed offline single-file checks, 25/5 proposal, start/pause/reset and phase-end cue; browser acceptance is recorded separately by the integration gate.';
|
|
}
|
|
|
|
const TIMER_TEST = `import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {readFileSync} from 'node:fs';
|
|
import vm from 'node:vm';
|
|
test('approved offline timer starts, pauses, resumes, changes phase and resets',()=>{
|
|
const bytes=readFileSync(new URL('./pomodoro.html',import.meta.url),'utf8');
|
|
assert.doesNotMatch(bytes,/https?:\\/\\//);
|
|
const elements=Object.fromEntries(['clock','phase','cue','start','pause','reset'].map(id=>[id,{textContent:'',addEventListener(_,fn){this.click=fn;}}]));
|
|
let now=0,tick;const context={document:{querySelector:s=>elements[s.slice(1)],addEventListener(){}},window:{},Date:{now:()=>now},setInterval:fn=>{tick=fn;}};
|
|
vm.runInNewContext(bytes.match(/<script>([\\s\\S]*?)<\\/script>/)[1],context);
|
|
assert.equal(elements.clock.textContent,'25:00');elements.start.click();now=1000;tick();assert.equal(elements.clock.textContent,'24:59');
|
|
elements.pause.click();now=20000;tick();assert.equal(elements.clock.textContent,'24:59');elements.start.click();now+=1499000;tick();assert.equal(elements.phase.textContent,'Break');assert.equal(elements.clock.textContent,'05:00');
|
|
elements.reset.click();assert.equal(elements.phase.textContent,'Work');assert.equal(elements.clock.textContent,'25:00');
|
|
});`;
|
|
|
|
const run = promisify(execFile), SELF = fileURLToPath(import.meta.url);
|
|
if (process.argv[2] === '--tool-call') {
|
|
try { process.stdout.write(JSON.stringify(await callTool(process.argv[3], process.argv[4], JSON.parse(process.argv[5]))) + '\n'); }
|
|
catch (e) { process.stderr.write(e.message + '\n'); process.exitCode = 1; }
|
|
} else if (process.argv[2] === '--runtime') await runtime();
|
|
|
|
async function runtime() {
|
|
const root = process.env.REQUEST_FIXTURE_ROOT, dataRoot = join(root, 'data'), metaFile = join(root, 'scenario.json');
|
|
const system = loadSystem(), config = bootConfig({ system, businessId: 'demo', warn: () => {} });
|
|
let armed = process.env.REQUEST_CRASH_POINT ?? null;
|
|
const checkpoint = async point => {
|
|
if (armed !== point) return;
|
|
process.send({ checkpoint: point });
|
|
await new Promise(() => {});
|
|
};
|
|
const old = readSessions(dataRoot), roles = {}, servers = [], registry = new Registry(dataRoot);
|
|
let adapter;
|
|
const factory = tasksAdapter({ trackers: config.trackers, autostart: false, log: () => {} });
|
|
const bus = await startBroker({ ...config, requestServices: { barrier: checkpoint },
|
|
tasks: async args => (adapter = await factory(args)) });
|
|
await adapter.start('demo');
|
|
if (adapter.status()[0].state !== 'ready') throw new Error('fixture tracker not ready');
|
|
for (const s of old) bus.recoverLaunch(s.launch_record);
|
|
const db = new DatabaseSync(join(dataRoot, 'bus/bus.sqlite'), { readOnly: true });
|
|
const store = { get: (sql, ...args) => db.prepare(sql).get(...args), all: (sql, ...args) => db.prepare(sql).all(...args) };
|
|
const journal = createRequestStore(store);
|
|
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
|
const call = (role, verb, args = {}) => bus.broker.request(role === 'human' ? human : roles[role].cap, { verb, args });
|
|
const metadata = () => existsSync(metaFile) ? JSON.parse(readFileSync(metaFile)) : {};
|
|
const save = obj => writeFileSync(metaFile, JSON.stringify({ ...metadata(), ...obj }) + '\n');
|
|
const state = () => journal.readRequest('demo', metadata().request);
|
|
const rows = (kind, task) => store.all('SELECT * FROM events WHERE business=? AND kind=? ORDER BY seq', 'demo', kind)
|
|
.map(e => ({ ...e, body: JSON.parse(e.body) })).filter(e => !task || e.subject === task);
|
|
let socket;
|
|
const refreshGit = async () => {
|
|
await socket?.close();
|
|
const b = loadBusiness('demo', { dir: dirname(process.env.MOSAIC_CONFIG), rolesDir: process.env.MOSAIC_ROLES_DIR });
|
|
socket = createGitSocket({ host: { op: async r => {
|
|
if (r.op === 'identity') return bus.identity(r.cap);
|
|
if (r.op === 'checkPush') return bus.checkPush(r.cap, r.task_ref, r.project);
|
|
if (r.op === 'recordPush') return bus.recordPush(r.cap, r.push);
|
|
if (r.op === 'reviewFetch') return bus.reviewFetch(r.cap, r.args);
|
|
if (r.op === 'refuse') return bus.refuse(r.cap, r.code, r.subject);
|
|
throw new Error('unexpected git operation');
|
|
} }, business: b, dataRoot, env: process.env });
|
|
await socket.listen();
|
|
};
|
|
async function launch(role, thread = null) {
|
|
if (roles[role]) return roles[role];
|
|
const b = loadBusiness('demo', { dir: dirname(process.env.MOSAIC_CONFIG), rolesDir: process.env.MOSAIC_ROLES_DIR });
|
|
const resolved = resolveInstance({ system: systemVars(system), business: b, instance: role });
|
|
if (!['chief','assistant'].includes(role)) bus.authorizeLaunch(roles.chief.cap, role);
|
|
if (limitHit(registry.sessions, b.id, b.launch.limits, { provider: 'zai', model: resolved.vars.model })) throw new Error('fixture launch capacity refused');
|
|
const may = await bus.mayLaunch({ business: 'demo', instance: role, provider: 'zai', model: resolved.vars.model });
|
|
if (may?.allow !== true) throw new Error('fixture launch limit refused: ' + may?.code);
|
|
const { run: id, runDir } = newRun(dataRoot, 'demo'), workspace = workspaceDir(dataRoot, 'demo', role);
|
|
mkdirSync(workspace, { recursive: true });
|
|
const projects = Object.fromEntries(Object.entries(b.projects).filter(([project, p]) => p.kind !== 'library' && bus.admission({ business: 'demo', project }).ready));
|
|
const checkouts = await ensureCheckouts({ workspace, projects });
|
|
const launch_record = { business: 'demo', role, run: id, harness: 'pi', pid: process.pid, startTime: startTimeOf(process.pid), ...(thread ? { thread } : {}) };
|
|
const cap = bus.bindLaunch(launch_record).cap;
|
|
roles[role] = { cap, run: id, workspace, runDir };
|
|
call(role, 'role.claim');
|
|
const bundle = buildBundle({ dir: join(runDir, 'bundle'), resolved, contract: readFileSync(resolved.contract, 'utf8'),
|
|
arbiter: role === 'cto', tracker: Boolean(resolved.tracker), credentials: [], skillsDir: join(dataRoot, 'skills-enabled'), harness: HARNESS.pi,
|
|
harnessVersion: '0.85.1', session: { run: id, workspace, checkouts, toolSocket: join(runDir, 'tools.sock'), turnMarker: join(runDir, 'turn.done') } });
|
|
const client = { call: async (verb, args) => {
|
|
const result = verb.startsWith('task.') ? await bus.broker.requestTask(cap, { verb, args }, adapter.handle) : await call(role, verb, args);
|
|
if (verb === 'decision.resolve') await checkpoint('prd-resolution');
|
|
if (verb === 'review.verdict') await checkpoint('review-verdict');
|
|
if (verb === 'request.deliver') await checkpoint('export-before-notification');
|
|
return result;
|
|
} };
|
|
const local = {
|
|
'working.push': async args => { const r = await pushWorking({ path: socket.path, cap, workspace, ...args, tmp: runDir, env: process.env }); await checkpoint('working-push'); return r; },
|
|
'working.fetch': args => fetchReview({ path: socket.path, cap, workspace, ...args, tmp: runDir, env: process.env }),
|
|
};
|
|
const server = await toolServer({ path: join(runDir, 'tools.sock'), tools: bundle.tools, client, local }); servers.push(server);
|
|
const entry = { business: 'demo', instance: role, run: id, workspace, runDir, pid: process.pid, startTime: startTimeOf(process.pid), harness: 'pi', provider: 'zai', model: resolved.vars.model,
|
|
manifest: bundle.tools.map(t => t.name), launch_record };
|
|
writeSessionFile(runDir, entry); registry.add(entry);
|
|
return roles[role];
|
|
}
|
|
async function tool(role, name, args = {}) {
|
|
const session = await launch(role, role === 'assistant' ? metadata().thread : null);
|
|
// The scripted model process receives only its own typed socket, never a tracker token or other role capability.
|
|
const { stdout } = await run(process.execPath, [SELF, '--tool-call', join(session.runDir, 'tools.sock'), name, JSON.stringify(args)],
|
|
{ env: { PATH: process.env.PATH, TMPDIR: process.env.TMPDIR }, timeout: 120000, maxBuffer: 4 * 1024 * 1024 });
|
|
appendFileSync(join(root, 'model-calls.jsonl'), JSON.stringify({ role, run: session.run, tool: name, args }) + '\n');
|
|
return JSON.parse(stdout);
|
|
}
|
|
await refreshGit(); await launch('chief'); await launch('pm'); await launch('cto');
|
|
if (metadata().thread) await launch('assistant', metadata().thread);
|
|
async function startRequest(text, attach = false) {
|
|
const project = call('human', 'project.create', { name: 'Intake ' + Date.now() }).id;
|
|
const thread = call('human', 'thread.create', { project, name: 'Pomodoro' }).id;
|
|
if (roles.assistant) { registry.remove('demo', roles.assistant.run); bus.endLaunch({ business: 'demo', run: roles.assistant.run, reason: 'exit', exitCode: 0 }); delete roles.assistant; }
|
|
await launch('assistant', thread);
|
|
const source = call('human', 'thread.send', { thread, body: text });
|
|
save({ request: source.request, source: source.id, thread, attach });
|
|
const relay = await tool('assistant', 'send_message', { to: 'chief', in_reply_to: source.id, body: 'Handle original request ' + source.request });
|
|
await tool('chief', 'send_message', { to: 'pm', in_reply_to: relay.id, body: 'Open original request ' + source.request });
|
|
let s = await tool('pm', 'intake_open', { request: source.request });
|
|
s = await tool('pm', 'intake_propose', { request: s.request, revision: s.revision, op: 'resource-plan', source_project: 'stack', project: attach ? 'unused' : 'timer', name: attach ? 'Unused' : 'timer', tracker_project: 2 });
|
|
return s;
|
|
}
|
|
async function draft(s) {
|
|
const submitted = store.all("SELECT body FROM request_events WHERE business='demo' AND json_extract(body,'$.candidate.project')=? AND kind='prd.submitted' ORDER BY seq", s.project).map(r => JSON.parse(r.body));
|
|
const version = `1.${submitted.length}`;
|
|
const proposal = PROPOSAL + (journal.intent('demo', s.request).text.includes('keyboard shortcut') ? ' Add an R keyboard shortcut for reset.' : '') + ` PRD revision ${version}.`;
|
|
await tool('pm', 'write_record', { project: s.project, path: `docs/prd-${version}.md`, record: '# Pomodoro ' + version + '\n\n' + proposal + (submitted.length ? '\nRevision: explicitly satisfy the owner answer and rejected draft feedback.\n' : '\n') });
|
|
const requirement = await tool('pm', 'write_record', { project: s.project, path: `requirements/${REQUIREMENT}.json`, record: { recordVersion: 1, id: REQUIREMENT, title: 'Offline Pomodoro', text: proposal, status: 'draft' } });
|
|
return tool('pm', 'submit_prd', { request: s.request, revision: s.revision, op: 'prd-' + s.revision, prd_version: version,
|
|
records_commit: requirement.commit, prd_path: `docs/prd-${version}.md`, requirement_ids: [REQUIREMENT] });
|
|
}
|
|
async function drive() {
|
|
await bus.requestLifecycle.recover();
|
|
for (let count = 0; count < 24; count++) {
|
|
let s = state(); if (!s) return null;
|
|
if (s.phase === 'cancelled') return s;
|
|
if (s.phase === 'selection') {
|
|
if (s.wait?.code === 'request-attachment-selected') {
|
|
const project = s.wait.subject;
|
|
const entries = await tool('pm', 'intake_projects', { request: s.request });
|
|
const entry = entries.find(e => e.project === project);
|
|
await tool('pm', 'intake_propose', { request: s.request, revision: s.revision, op: 'existing-plan', source_project: project, project, name: entry.name, tracker_project: entry.tracker.project });
|
|
return state();
|
|
}
|
|
return s;
|
|
}
|
|
if (s.phase === 'bootstrap') { await bus.requestLifecycle.resume('demo', s.request); continue; }
|
|
if (s.phase === 'discovery') {
|
|
if (s.mode === 'guided' && !s.question) {
|
|
await tool('pm', 'intake_question', { request: s.request, revision: s.revision, op: 'platform-question', text: 'Use an offline browser and allow a cue after Start?', options: [{ key: 'offline', text: 'Offline browser with a cue' }] });
|
|
return state();
|
|
}
|
|
if (s.question?.answer === null) return s;
|
|
await draft(s); continue;
|
|
}
|
|
if (s.phase === 'prd-review') {
|
|
if (s.mode === 'guided') return s;
|
|
await tool('cto', 'intake_status', { request: s.request, path: s.candidate.prd_path });
|
|
await tool('cto', 'intake_status', { request: s.request, path: s.candidate.requirements[0].path });
|
|
await tool('cto', 'resolve_decision', { id: s.wait.subject, choice: 'yes', note: 'The exact agent proposal fits the confirmed original offline timer intent.' });
|
|
await bus.requestLifecycle.resume('demo', s.request); continue;
|
|
}
|
|
if (s.phase === 'planning') {
|
|
await adapter.reconcileCreates('demo');
|
|
let task = rows('task.created').find(e => e.body.request === s.request);
|
|
if (!task) {
|
|
try {
|
|
await tool('pm', 'task_create', { title: 'Implement reviewed offline Pomodoro', request: s.request, requirement: REQUIREMENT,
|
|
project_key: s.project, requirement_snapshot: s.snapshot, op: 'pomodoro-' + s.request });
|
|
} catch (e) { if (/task-create-uncertain|tracker-|write-uncertain/.test(e.message)) return state(); throw e; }
|
|
task = rows('task.created').find(e => e.body.request === s.request);
|
|
}
|
|
if (!rows('task.assigned', task.subject).length) await tool('pm', 'task_assign', { task_ref: task.subject, role: 'coder' });
|
|
await tool('pm', 'advance_request', { request: s.request, revision: s.revision, op: 'implement' });
|
|
await refreshGit(); await launch('coder'); await launch('reviewer'); continue;
|
|
}
|
|
if (s.phase === 'implementation') {
|
|
await refreshGit(); await launch('coder'); await launch('reviewer');
|
|
const task = s.tasks[0], clone = join(roles.coder.workspace, s.project), path = `products/${s.project}/pomodoro.html`;
|
|
const boundHTML = HTML.replace('</title>', `</title><meta name="mosaic-snapshot" content="${s.snapshot}"><meta name="mosaic-request" content="${s.request}">`);
|
|
const expected = journal.intent('demo', s.request).text.includes('keyboard shortcut')
|
|
? boundHTML.replace('</script>', "document.addEventListener('keydown',e=>{if(e.key.toLowerCase()==='r')document.querySelector('#reset').click();});</script>") : boundHTML;
|
|
if (!existsSync(join(clone, path)) || readFileSync(join(clone, path), 'utf8') !== expected) {
|
|
git(clone, 'checkout', '-qb', 'pomodoro-' + s.request.slice(0, 8));
|
|
mkdirSync(dirname(join(clone, path)), { recursive: true }); writeFileSync(join(clone, path), expected);
|
|
writeFileSync(join(clone, dirname(path), 'pomodoro.test.mjs'), TIMER_TEST);
|
|
verifyHTML(readFileSync(join(clone, path), 'utf8'));
|
|
const tested = await run(process.execPath, ['--test', join(dirname(path), 'pomodoro.test.mjs')], { cwd: clone, env: { PATH: process.env.PATH, TMPDIR: process.env.TMPDIR } });
|
|
appendFileSync(join(root, 'coder-tests.log'), tested.stdout);
|
|
git(clone, 'add', '--', dirname(path)); git(clone, 'commit', '--author', 'coder <[email protected]>', '-qm', 'Build the approved offline Pomodoro');
|
|
}
|
|
const commit = git(clone, 'rev-parse', 'HEAD'), branch = git(clone, 'branch', '--show-current');
|
|
const pushed = rows('action.allowed', task).find(e => e.body.action === 'git.push.working' && e.body.commit === commit);
|
|
if (!pushed) await tool('coder', 'push_working', { task_ref: task, project: s.project, branch });
|
|
if (!rows('review.requested', task).some(e => e.body.commit === commit)) await tool('coder', 'request_review', { task_ref: task, commit, to: 'reviewer' });
|
|
await tool('pm', 'advance_request', { request: s.request, revision: s.revision, op: 'review' }); continue;
|
|
}
|
|
if (s.phase === 'review') {
|
|
await refreshGit(); await launch('reviewer');
|
|
const task = s.tasks[0], review = rows('review.requested', task).at(-1);
|
|
if (!rows('review.verdict', task).some(e => e.body.request === review.id)) {
|
|
const fetched = await tool('reviewer', 'fetch_review', { task_ref: task, request: review.id });
|
|
const path = `products/${s.project}/pomodoro.html`;
|
|
const bytes = git(fetched.clone, 'show', `${fetched.commit}:${path}`);
|
|
const results = verifyHTML(bytes);
|
|
git(fetched.clone, 'checkout', '--detach', fetched.commit);
|
|
const tested = await run(process.execPath, ['--test', join(dirname(path), 'pomodoro.test.mjs')], { cwd: fetched.clone, env: { PATH: process.env.PATH, TMPDIR: process.env.TMPDIR } });
|
|
appendFileSync(join(root, 'reviewer-tests.log'), tested.stdout);
|
|
const verification = await tool('reviewer', 'write_record', { project: s.project, path: 'docs/verification-' + s.request.slice(0, 8) + '.md',
|
|
record: JSON.stringify({ version: 1, task_ref: task, review_request: review.id, commit: fetched.commit, results }) });
|
|
await tool('reviewer', 'review_verdict', { task_ref: task, request: review.id, verdict: 'approve', body: JSON.stringify({ verification_record: { path: verification.path, commit: verification.commit }, results }) });
|
|
}
|
|
await tool('pm', 'advance_request', { request: s.request, revision: s.revision, op: 'deliver' }); continue;
|
|
}
|
|
if (s.phase === 'delivery') {
|
|
const task = s.tasks[0], verdict = rows('review.verdict', task).at(-1), record = JSON.parse(store.get('SELECT body FROM messages WHERE id=?', verdict.body.message).body.split('\n\n').slice(1).join('\n\n')).verification_record;
|
|
if (!rows('task.closed', task).length) await tool('pm', 'task_close', { task_ref: task, verdict: verdict.id });
|
|
await tool('pm', 'deliver_request', { request: s.request, revision: s.revision, op: 'delivery', task_ref: task, verdict: verdict.id,
|
|
path: `products/${s.project}/pomodoro.html`, verification_record: record }); continue;
|
|
}
|
|
if (s.phase === 'complete') {
|
|
const messages = store.all('SELECT * FROM messages WHERE business=? ORDER BY seq', 'demo');
|
|
if (!messages.some(m => m.from_role === 'chief' && m.to_role === 'assistant' && m.in_reply_to === s.source_message)) {
|
|
await tool('chief', 'send_message', { to: 'assistant', in_reply_to: s.source_message,
|
|
body: 'Reviewed local HTML is ready: ' + JSON.stringify(s.delivery_evidence) });
|
|
}
|
|
if (!messages.some(m => m.from_role === 'assistant' && m.to_role === 'human' && m.in_reply_to === s.source_message)) {
|
|
await tool('assistant', 'send_message', { to: 'human', in_reply_to: s.source_message, body: 'Download the reviewed Pomodoro HTML. ' + s.delivery });
|
|
}
|
|
await bus.requestLifecycle.recover(); return state();
|
|
}
|
|
throw new Error('unexpected phase ' + s.phase);
|
|
}
|
|
throw new Error('fixture made no bounded progress');
|
|
}
|
|
const handlers = {
|
|
start: ({ text, attach }) => startRequest(text, attach),
|
|
drive,
|
|
arm: ({ point }) => { armed = point; return true; },
|
|
console: async ({ verb, args, subject }) => {
|
|
const cap = bus.broker.bindConsole({ business: 'demo', human: 'jason', subject });
|
|
try {
|
|
const result = await bus.broker.request(cap, { verb, args });
|
|
if (verb === 'decision.resolve.console') await checkpoint('selection-resolution');
|
|
return result;
|
|
} finally { bus.broker.disconnect(cap); }
|
|
},
|
|
human: ({ verb, args }) => call('human', verb, args),
|
|
close: async () => { for (const server of servers) await new Promise(resolve => server.close(resolve)); await socket?.close(); db.close(); await bus.close(); return true; },
|
|
};
|
|
process.on('message', async m => {
|
|
try { const value = await handlers[m.op](m.args ?? {}); process.send({ id: m.id, value }); if (m.op === 'close') process.disconnect(); }
|
|
catch (e) { process.send({ id: m.id, error: e.code ?? e.message, detail: e.stack }); }
|
|
});
|
|
process.send({ ready: true });
|
|
}
|