Files
stack/agents/dewey/work/wui/mockups/slice1/data.js
T
jason.woltjeandClaude Opus 5.5 1f4882d2b4 docs(wui): Dewey's design work: brand board, five mockups, slice 1 S5 views and checks
Brand board revisions, the five dashboard mockups with their verify
script and evidence, the slice 1 S5 design note (SLICE1-VIEWS.md, rev 3
plus the S2c section 10 update), the Console mockup of inbox, tasks,
agents and trail (mockups/slice1/) and checks/slice1-verify.mjs (16
checks; check 16 pinned to bus at d27042fa). Also the CHAT-03 progress
log and the 2026-09-26 dirty-tree manifest. Design and evidence only;
no package code. Row 40 (#1522), committed on its own at Sage's
direction, 2026-10-09.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 17:54:50 -05:00

185 lines
21 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Fixture for the slice 1 S5 mockup. Rows are shaped like schema v3b
// (agents/darkwing/work/slice1-proto/schema-v3b.sql, 179ffe35, lead decision 60), and the rows the
// broker writes follow its writers in packages/bus/src/broker.mjs (S2c, d27042fa; S2b refused messages
// 101 and 102 and S2c restores them, see SLICE1-VIEWS.md section 10):
// human requests are a message plus a human.input naming it, every human answer is a human.input,
// every decision.raise has its action.allowed context, session.launched comes from bindLaunch.
// checks/slice1-verify.mjs inserts every row into that schema, so its triggers check the fixture too.
// `lookups` stands in for what no Q1 read verb returns yet (business file, kanban view, S3 status);
// mockups/slice1/q1.js labels each use. Nothing here is live data.
window.S1 = (() => {
'use strict';
const NOW = '2026-10-06T15:00:00Z';
const B = 'mosaic-stack';
const id = n => `0d1a${String(n).padStart(4, '0')}-5e7c-4b1a-9c3e-1f2a3b4c5d6e`;
const D = { launch: id(1), prd: id(2), prdOld: id(3), conflict: id(4), scope: id(5), merged: id(6) };
const lookups = {
business: B,
human: 'jason',
tracker: { baseUrl: 'http://127.0.0.1:3456', project: 3, view: 9 },
buckets: { 21: 'todo', 22: 'in-progress', 23: 'in-review', 24: 'blocked', 25: 'done' },
bots: { 7: 'bot-mosaic-stack-pm', 8: 'bot-mosaic-stack-cto', 9: 'bot-mosaic-stack-coder', 10: 'bot-mosaic-stack-reviewer' },
instances: [
{ instance: 'pm', definition: 'pm', harness: 'pi', model: 'claude-opus-5-5', arbiterFor: 'delivery' },
{ instance: 'cto', definition: 'cto', harness: 'claude-code', model: 'claude-opus-5-5', arbiterFor: 'technical' },
{ instance: 'coder', definition: 'coder', harness: 'pi', model: 'claude-sonnet-5-5' },
{ instance: 'reviewer', definition: 'reviewer', harness: 'pi', model: 'claude-sonnet-5-5' },
],
launch: { by: 'pm', instances: ['coder', 'reviewer'], max: { opus: 4, sonnet: 4 } },
families: { 'claude-opus-5-5': 'opus', 'claude-sonnet-5-5': 'sonnet' },
// Proposed in SLICE1-VIEWS.md Q2. The database can't answer this today.
poll: { project: 3, board: '2026-10-06T14:59:41Z', cursor: '2026-10-06T14:59:42Z', error: null, proposed: true },
};
const role_claims = [
{ seq: 1, at: '2026-10-06T08:02:10Z', business: B, role: 'pm', op: 'claim', holder_run: 'run-pm-7f31', harness: 'pi', address: null, by: 'run-pm-7f31', reason: null, decision: null },
{ seq: 2, at: '2026-10-06T08:05:44Z', business: B, role: 'cto', op: 'claim', holder_run: 'run-cto-2b90', harness: 't3', address: 't3:5c1e9a0d', by: 'run-cto-2b90', reason: null, decision: null },
{ seq: 3, at: '2026-10-06T09:46:03Z', business: B, role: 'coder', op: 'claim', holder_run: 'run-coder-a442', harness: 'pi', address: null, by: 'run-coder-a442', reason: null, decision: null },
{ seq: 4, at: '2026-10-06T11:20:00Z', business: B, role: 'reviewer', op: 'claim', holder_run: 'run-rev-0c18', harness: 'pi', address: null, by: 'run-rev-0c18', reason: null, decision: null },
{ seq: 5, at: '2026-10-06T12:41:27Z', business: B, role: 'reviewer', op: 'release', holder_run: 'run-rev-0c18', harness: 'pi', address: null, by: 'run-rev-0c18', reason: 'verdict posted', decision: null },
];
// The broker stamps every write with a strictly later millisecond (#now). Rows one call writes are a millisecond apart here.
const plus = (at, n) => new Date(Date.parse(at) + n).toISOString();
const opt = (...pairs) => JSON.stringify(pairs.map(([key, text]) => ({ key, text })));
const decisions = [
{ seq: 1, id: D.merged, at: '2026-10-06T09:42:00Z', business: B, project: 'stack', raised_by_role: 'pm', raised_by_run: 'run-pm-7f31', class: 'gated', action: 'role.launch', route_to: 'human', question: 'Launch the coder instance for task #41 (bus store)?', options: opt(['yes', 'Launch coder on Sonnet'], ['no', 'Do not launch']), recommendation: 'yes', task_ref: 'vikunja:3/41', requirement_ref: 'REQ-DEC-1', blocking: 1, supersedes: null },
{ seq: 2, id: D.prdOld, at: '2026-10-06T10:02:00Z', business: B, project: 'stack', raised_by_role: 'cto', raised_by_run: 'run-cto-2b90', class: 'gated', action: 'prd.approve', route_to: 'human', question: 'Approve the REQ-TASK-2 wording change?', options: opt(['a', 'Approve'], ['b', 'Reject']), recommendation: 'a', task_ref: null, requirement_ref: 'REQ-TASK-2', blocking: 0, supersedes: null },
{ seq: 3, id: D.prd, at: '2026-10-06T10:31:00Z', business: B, project: 'stack', raised_by_role: 'cto', raised_by_run: 'run-cto-2b90', class: 'gated', action: 'prd.approve', route_to: 'human',
question: 'Approve the REQ-TASK-2 wording change? The draft says **"every 30 seconds"**; probes support it.\nQuoted from the agent, unchanged: <img src=x onerror="window.__pwned=1"><script>window.__pwned=2</script> ‮exe.txt‬ and a bell \u0007.',
options: opt(['a', 'Approve as written'], ['b', 'Approve with "at least every 60 seconds"'], ['c', 'Reject; keep draft 0.3 wording']), recommendation: 'a', task_ref: null, requirement_ref: 'REQ-TASK-2', blocking: 0, supersedes: D.prdOld },
{ seq: 4, id: D.launch, at: '2026-10-06T13:52:00Z', business: B, project: 'stack', raised_by_role: 'pm', raised_by_run: 'run-pm-7f31', class: 'gated', action: 'role.launch', route_to: 'human', question: 'Launch a second coder instance so the Vikunja poller (#44) can move while #42 is in progress?', options: opt(['yes', 'Launch coder-2 on Sonnet'], ['no', 'Keep one coder; #44 waits'], ['later', 'Ask again when #42 is in review']), recommendation: 'yes', task_ref: 'vikunja:3/44', requirement_ref: 'REQ-LAUNCH-1', blocking: 1, supersedes: null },
{ seq: 5, id: D.conflict, at: '2026-10-06T14:31:20Z', business: B, project: 'stack', raised_by_role: 'coder', raised_by_run: 'run-coder-a442', class: 'cross-role', action: 'task.close', route_to: 'pm', question: 'Task #48 is done in Vikunja but in-review in the queue. Which stands?', options: opt(['queue', 'Keep in-review; ask the person to reopen'], ['vikunja', 'Close in the queue after a verdict']), recommendation: 'queue', task_ref: 'vikunja:3/48', requirement_ref: 'REQ-TASK-4', blocking: 0, supersedes: null },
{ seq: 6, id: D.scope, at: '2026-10-06T14:40:05Z', business: B, project: 'stack', raised_by_role: 'coder', raised_by_run: 'run-coder-a442', class: 'cross-role', action: 'task.scope.change', route_to: 'cto', question: 'Task #42 also needs a status read for poll freshness. Add it to the scope?', options: opt(['add', 'Add to #42'], ['split', 'New task for S3']), recommendation: 'split', task_ref: 'vikunja:3/42', requirement_ref: 'REQ-WEB-1', blocking: 0, supersedes: null },
];
const decision_events = [
{ seq: 1, decision: D.merged, at: '2026-10-06T09:44:10Z', op: 'seen', by: 'jason', choice: null, note: null, via: 'cli' },
{ seq: 2, decision: D.merged, at: '2026-10-06T09:44:31Z', op: 'resolved', by: 'jason', choice: 'yes', note: null, via: 'cli' },
{ seq: 3, decision: D.prdOld, at: '2026-10-06T10:30:59.999Z', op: 'withdrawn', by: 'run-cto-2b90', choice: null, note: null, via: null },
{ seq: 4, decision: D.launch, at: '2026-10-06T13:58:02Z', op: 'seen', by: 'jason', choice: null, note: null, via: 'cli' },
];
// A human request is a message from the CLI plus a human.input naming it (broker message.send). The
// broker routes it to the role's holder; the holder's receive and read add deliveries.
const PM = { holder_run: 'run-pm-7f31', address: null };
const later = (at, s) => new Date(Date.parse(at) + s * 1000).toISOString().replace('.000Z', 'Z');
const asks = [[3, '2026-10-06T08:29:00Z', 'Write the identity runbook first.'], [5, '2026-10-06T09:38:00Z', 'Build the bus store next.'],
[35, '2026-10-06T09:48:10Z', 'Then the read verbs, and the inbox and decide commands on top of them.'],
[12, '2026-10-06T09:54:30Z', 'The poller has to catch column moves within one tick.'], [36, '2026-10-06T10:17:40Z', 'Add a task for the task-create path itself.']];
const messages = [
{ seq: 1, id: id(101), at: '2026-10-06T13:52:01Z', business: B, from_role: 'pm', from_run: 'run-pm-7f31', to_role: 'human', class: 'DECISION', in_reply_to: null, decision: D.launch, corrects: null, body: 'Blocking: launch a second coder for #44?' },
{ seq: 2, id: id(102), at: '2026-10-06T09:42:01Z', business: B, from_role: 'pm', from_run: 'run-pm-7f31', to_role: 'human', class: 'DECISION', in_reply_to: null, decision: D.merged, corrects: null, body: 'Blocking: launch the coder for #41?' },
...asks.map(([n, at, body]) => ({ seq: 0, id: id(200 + n), at, business: B, from_role: 'human', from_run: 'human-cli', to_role: 'pm', class: 'INFO', in_reply_to: null, decision: null, corrects: null, body })),
];
// The human holds no claim, so a message to the human is routed with no holder. The DM is the Discord connector's delivery.
const deliveries = [
{ seq: 1, message: id(101), at: plus('2026-10-06T13:52:01Z', 1), op: 'routed', holder_run: null, transport: null, address: null, detail: null },
{ seq: 2, message: id(101), at: '2026-10-06T13:52:03Z', op: 'delivered', holder_run: null, transport: 'discord-dm', address: 'dm', detail: null },
{ seq: 3, message: id(102), at: plus('2026-10-06T09:42:01Z', 1), op: 'routed', holder_run: null, transport: null, address: null, detail: null },
{ seq: 4, message: id(102), at: '2026-10-06T09:42:02Z', op: 'delivered', holder_run: null, transport: 'discord-dm', address: 'dm', detail: null },
...asks.flatMap(([n, at]) => [
{ seq: 0, message: id(200 + n), at: plus(at, 1), op: 'routed', ...PM, transport: 'pi', detail: null },
{ seq: 0, message: id(200 + n), at: later(at, 15), op: 'delivered', ...PM, transport: 'broker', detail: null },
{ seq: 0, message: id(200 + n), at: later(at, 40), op: 'read', ...PM, transport: 'pi', detail: null },
]),
];
// A stand-in digest: 64 hex characters from the ref and time (FNV-1a, eight rounds). Not the real field hash.
const hex = str => Array.from({ length: 8 }, (_, k) => { let h = 0x811c9dc5 ^ k; for (const c of str) h = Math.imul(h ^ c.charCodeAt(0), 0x01000193); return (h >>> 0).toString(16).padStart(8, '0'); }).join('');
const snap = (seq, at, ref, f, src, extra = {}) => ({ seq, at, business: B, task_ref: ref, updated: extra.updated || at, etag: null, digest: extra.digest || hex(ref + at), fields: JSON.stringify(f), source: src, via: extra.via || null, read_at: extra.read_at || null, role: src === 'self' ? (extra.role || 'pm') : null, run: src === 'self' ? (extra.run || 'run-pm-7f31') : null });
const T = (index, title, bucket, o = {}) => ({ project_id: 3, index, title, description: `…\nRequirement: ${o.req || ''}`, done: bucket === 25 ? 1 : 0, due_date: o.due || null, priority: o.prio ?? 2, percent_done: o.pct ?? 0, bucket, labels: o.labels || [], assignees: o.who ? [o.who] : [] });
const task_snapshots = [
snap(1, '2026-10-06T08:30:00Z', 'vikunja:3/45', T(45, 'Install runbook for slice 1 identities', 21, { req: 'REQ-CRED-1', who: 7 }), 'self'),
snap(2, '2026-10-06T09:41:00Z', 'vikunja:3/41', T(41, 'Bus store over node:sqlite', 21, { req: 'REQ-DEC-1', who: 9, prio: 4 }), 'self'),
snap(3, '2026-10-06T09:50:00Z', 'vikunja:3/42', T(42, 'Broker read verbs for the CLI and the WebUI', 21, { req: 'REQ-WEB-1', who: 9, prio: 3 }), 'self'),
snap(4, '2026-10-06T09:52:00Z', 'vikunja:3/43', T(43, '`mosaic inbox` and `mosaic decide`', 21, { req: 'REQ-CLI-1', who: 9, due: '2026-10-09' }), 'self'),
snap(5, '2026-10-06T09:55:00Z', 'vikunja:3/44', T(44, 'Vikunja poller: board read plus cursor read', 21, { req: 'REQ-TASK-2', who: 9, prio: 3 }), 'self'),
snap(6, '2026-10-06T10:20:00Z', 'vikunja:3/48', T(48, 'Field table checks in the task verbs', 22, { req: 'REQ-TASK-1', who: 9 }), 'self', { role: 'coder', run: 'run-coder-a442' }),
snap(7, '2026-10-06T11:05:00Z', 'vikunja:3/41', T(41, 'Bus store over node:sqlite', 23, { req: 'REQ-DEC-1', who: 9, prio: 4, pct: 90 }), 'self', { role: 'coder', run: 'run-coder-a442' }),
// A move between open columns leaves Vikunja's `updated` alone (probe P3), so this self row keeps 09:50.
snap(8, '2026-10-06T14:59:40Z', 'vikunja:3/42', T(42, 'Broker read verbs for the CLI and the WebUI', 22, { req: 'REQ-WEB-1', who: 9, prio: 3 }), 'self', { role: 'coder', run: 'run-coder-a442', updated: '2026-10-06T09:50:00Z' }),
// A board read sent before that move finished and recorded after it. Its digest differs from the latest
// row, so the broker records it (addendum B section 5); task_external_changes ignores it (read_at rule).
snap(8.5, '2026-10-06T14:59:41Z', 'vikunja:3/42', T(42, 'Broker read verbs for the CLI and the WebUI', 21, { req: 'REQ-WEB-1', who: 9, prio: 3 }), 'poll', { via: 'board', read_at: '2026-10-06T14:59:39Z', updated: '2026-10-06T09:50:00Z', digest: '0003'.repeat(16) }),
snap(9, '2026-10-06T12:15:00Z', 'vikunja:3/45', T(45, 'Install runbook for slice 1 identities', 25, { req: 'REQ-CRED-1', who: 7, pct: 100 }), 'self'),
snap(10, '2026-10-06T12:30:11Z', 'vikunja:3/46', { project_id: 3, index: 46, title: 'Check the digest time against Central daylight saving', description: '', done: 0, due_date: null, priority: 1, percent_done: 0, bucket: 21, labels: [], assignees: [] }, 'poll', { via: 'cursor', read_at: '2026-10-06T12:30:10Z' }),
snap(11, '2026-10-06T13:50:00Z', 'vikunja:3/44', T(44, 'Vikunja poller: board read plus cursor read', 24, { req: 'REQ-TASK-2', who: 9, prio: 3 }), 'self', { role: 'coder', run: 'run-coder-a442' }),
snap(12, '2026-10-06T14:20:00Z', 'vikunja:3/47', { gone: 'not-found' }, 'poll', { via: 'task', read_at: '2026-10-06T14:19:59Z' }),
snap(13, '2026-10-06T14:31:12Z', 'vikunja:3/48', T(48, 'Field table checks in the task verbs', 25, { req: 'REQ-TASK-1', who: 9 }), 'poll', { via: 'cursor', read_at: '2026-10-06T14:31:11Z', updated: '2026-10-06T14:31:05Z' }),
snap(14, '2026-10-06T14:44:00Z', 'vikunja:3/43', T(43, '`mosaic inbox` and `mosaic decide`', 22, { req: 'REQ-CLI-1', who: 9, due: '2026-10-09' }), 'poll', { via: 'board', read_at: '2026-10-06T14:43:59Z', updated: '2026-10-06T09:52:00Z' }),
];
// 3/47 had an earlier self snapshot; it's gone now.
task_snapshots.splice(5, 0, snap(0.5, '2026-10-06T10:00:00Z', 'vikunja:3/47', T(47, 'Spike: webhook receiver (dropped)', 21, { req: 'REQ-TASK-2', who: 7 }), 'self'));
// Event shapes. Rows the broker writes follow it exactly: human.input (instruction, answer, admin),
// action.allowed (decision.raise context, authorize, agent message.send), action.refused ({code}),
// launch.revoked/restored ({via}), session.launched (bindLaunch: the launched run is actor and
// subject). Nothing writes task.*, review.*, session.ended or credential.* yet; those keep the shapes
// from SLICE1-VIEWS.md, with the task ref in `subject` (schema v3a onward) and a session.ended shaped
// like session.launched.
const ev = (seq, at, kind, actor_role, actor_run, subject, body) => ({ seq, id: id(500 + seq), at, business: B, kind, actor_role, actor_run, subject, corrects: null, body: JSON.stringify(body) });
const launched = (seq, at, role, run, harness, address = null) => ev(seq, at, 'session.launched', role, run, run, { harness, address, pid: null, startTime: null });
const raise = (seq, d, action, target, approvalChoice) => { const r = decisions.find(x => x.id === d); return ev(seq, plus(r.at, 1), 'action.allowed', r.raised_by_role, r.raised_by_run, r.task_ref || d, { decision: d, action, operation: 'decision.raise', task_ref: r.task_ref, target, approvalChoice, holderRun: null }); };
const answer = (seq, at, decision, op, choice = null) => ev(seq, plus(at, 1), 'human.input', null, null, decision, { kind: 'answer', decision, op, choice });
const events = [
launched(1, '2026-10-06T08:00:00Z', 'pm', 'run-pm-7f31', 'pi'),
launched(2, '2026-10-06T08:04:00Z', 'cto', 'run-cto-2b90', 'claude-code', 't3:5c1e9a0d'),
...asks.map(([n, at]) => ev(n, plus(at, 2), 'human.input', null, null, id(200 + n), { kind: 'instruction', message: id(200 + n) })),
ev(4, '2026-10-06T08:30:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/45', { request: id(503), requirement: 'REQ-CRED-1', index: 45 }),
ev(6, '2026-10-06T09:41:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/41', { request: id(505), requirement: 'REQ-DEC-1', index: 41 }),
ev(7, '2026-10-06T09:41:01Z', 'task.assigned', 'pm', 'run-pm-7f31', 'vikunja:3/41', { role: 'coder' }),
raise(40, D.merged, 'role.launch', 'coder', 'yes'),
// S2c (d27042fa): an agent message.send writes action.allowed first; within-role, so no decision in it.
ev(50, plus('2026-10-06T09:42:01Z', -1), 'action.allowed', 'pm', 'run-pm-7f31', 'human', { action: 'message.send', class: 'within-role', target: 'human' }),
answer(37, '2026-10-06T09:44:10Z', D.merged, 'seen'),
answer(8, '2026-10-06T09:44:31Z', D.merged, 'resolved', 'yes'),
// The launcher checks the resolved decision (authorize), then S6 binds the launch record.
ev(46, '2026-10-06T09:45:00Z', 'action.allowed', 'pm', 'run-pm-7f31', 'coder', { action: 'role.launch', class: 'gated', decision: D.merged, target: 'coder' }),
launched(9, '2026-10-06T09:45:02Z', 'coder', 'run-coder-a442', 'pi'),
ev(10, '2026-10-06T09:50:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/42', { request: id(535), requirement: 'REQ-WEB-1', index: 42 }),
ev(11, '2026-10-06T09:52:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/43', { request: id(535), requirement: 'REQ-CLI-1', index: 43 }),
ev(13, '2026-10-06T09:55:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/44', { request: id(512), requirement: 'REQ-TASK-2', index: 44 }),
ev(14, '2026-10-06T09:55:01Z', 'task.assigned', 'pm', 'run-pm-7f31', 'vikunja:3/44', { role: 'coder' }),
ev(15, '2026-10-06T10:00:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/47', { request: id(512), requirement: 'REQ-TASK-2', index: 47 }),
raise(41, D.prdOld, 'prd.approve', 'REQ-TASK-2', 'a'),
ev(16, '2026-10-06T10:19:00Z', 'task.created', 'pm', 'run-pm-7f31', 'vikunja:3/48', { request: id(536), requirement: 'REQ-TASK-1', index: 48 }),
ev(17, '2026-10-06T10:20:00Z', 'task.state', 'coder', 'run-coder-a442', 'vikunja:3/48', { from: 'todo', to: 'in-progress' }),
raise(42, D.prd, 'prd.approve', 'REQ-TASK-2', 'a'),
ev(18, '2026-10-06T11:05:00Z', 'task.state', 'coder', 'run-coder-a442', 'vikunja:3/41', { from: 'todo', to: 'in-review' }),
ev(19, '2026-10-06T11:05:02Z', 'review.requested', 'coder', 'run-coder-a442', 'vikunja:3/41', { row: 37, round: 1 }),
launched(20, '2026-10-06T11:19:00Z', 'reviewer', 'run-rev-0c18', 'pi'),
ev(21, '2026-10-06T14:59:40Z', 'task.state', 'coder', 'run-coder-a442', 'vikunja:3/42', { from: 'todo', to: 'in-progress' }),
ev(22, '2026-10-06T12:15:00Z', 'task.closed', 'pm', 'run-pm-7f31', 'vikunja:3/45', { verdict: 'approve' }),
ev(23, '2026-10-06T12:30:11Z', 'task.changed.external', null, null, 'vikunja:3/46', { fields: ['created'] }),
ev(24, '2026-10-06T12:40:00Z', 'review.verdict', 'reviewer', 'run-rev-0c18', 'vikunja:3/41', { row: 37, round: 1, verdict: 'approve' }),
ev(25, '2026-10-06T12:41:30Z', 'session.ended', 'reviewer', 'run-rev-0c18', 'run-rev-0c18', { exit: 0, reason: 'verdict posted' }),
ev(26, '2026-10-06T13:10:00Z', 'launch.revoked', null, null, null, { via: 'cli' }),
ev(47, plus('2026-10-06T13:10:00Z', 1), 'human.input', null, null, null, { kind: 'admin', event: id(526) }),
ev(27, '2026-10-06T13:25:00Z', 'launch.restored', null, null, null, { via: 'cli' }),
ev(48, plus('2026-10-06T13:25:00Z', 1), 'human.input', null, null, null, { kind: 'admin', event: id(527) }),
ev(28, '2026-10-06T13:50:00Z', 'task.state', 'coder', 'run-coder-a442', 'vikunja:3/44', { from: 'todo', to: 'blocked' }),
ev(29, '2026-10-06T13:51:00Z', 'action.refused', 'pm', 'run-pm-7f31', 'vikunja:3/44', { code: 'decision-required' }),
raise(43, D.launch, 'role.launch', 'coder', 'yes'),
ev(49, plus('2026-10-06T13:52:01Z', -1), 'action.allowed', 'pm', 'run-pm-7f31', 'human', { action: 'message.send', class: 'within-role', target: 'human' }),
answer(38, '2026-10-06T13:58:02Z', D.launch, 'seen'),
ev(30, '2026-10-06T14:00:00Z', 'credential.expiring', null, null, null, { service: 'vikunja', instance: 'coder', expires: '2026-10-13' }),
ev(31, '2026-10-06T14:20:00Z', 'task.missing', null, null, 'vikunja:3/47', { reason: 'not-found' }),
ev(32, '2026-10-06T14:31:12Z', 'task.changed.external', null, null, 'vikunja:3/48', { fields: ['done', 'bucket'] }),
ev(33, '2026-10-06T14:31:13Z', 'task.conflict', null, null, 'vikunja:3/48', { field: 'state', queue: 'in-review', tracker: 'done', decision: D.conflict }),
// Recommended and approving choices differ here: choosing "queue" authorizes nothing.
raise(44, D.conflict, 'task.close', 'vikunja:3/48', 'vikunja'),
raise(45, D.scope, 'task.scope.change', 'vikunja:3/42', 'add'),
ev(34, '2026-10-06T14:44:00Z', 'task.changed.external', null, null, 'vikunja:3/43', { fields: ['bucket'] }),
];
// The broker writes times as toISOString(), with milliseconds; its Store refuses any other form.
const ms = t => t && t.length === 20 ? t.replace('Z', '.000Z') : t;
for (const t of [role_claims, decisions, decision_events, messages, deliveries, task_snapshots, events]) for (const r of t) { r.at = ms(r.at); if ('read_at' in r) r.read_at = ms(r.read_at); }
// seq follows insertion order, which is time order; the verify script checks this against the schema.
for (const t of [messages, deliveries, task_snapshots, events]) t.sort((a, b) => a.at.localeCompare(b.at)).forEach((r, i) => { r.seq = i + 1; });
return { NOW, lookups, role_claims, decisions, decision_events, messages, deliveries, task_snapshots, events };
})();