harness: a new PM run reads current launch state and open tasks before each turn, and old messages show their age, row 109 (#1603)

The broker gains a read-only state verb (launch state with the event that set it, open tasks capped). The runner fetches it before each turn and puts a "Current state" block at the top of the request; a failed read fails the turn. turnRequest() adds the message time. roles/pm.md gets one rule: the block is the truth for launch state and task status. Seat session, seat sandbox and cli launcher tests re-anchored to the new request shape.

Built by Researcher, packet 875c515e9. Filbert approved round 3, comment 28333, candidate manifest 0a62225e (11 paths). Sage gate on 9774d70db plus row109.patch: every package node suite and every scripts/test-*.sh green on the first run, skipped 0 in bus, harness and seat. Vikunja #259.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
2026-10-13 19:12:54 -05:00
co-authored by Claude Opus 5.5
parent 848ceb68a4
commit badfa52b9d
11 changed files with 299 additions and 11 deletions
+113
View File
@@ -0,0 +1,113 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { Store } from '../src/store.mjs';
import { Broker, TASK_PAGE } from '../src/broker.mjs';
// Row 109 (#1603): `state` is what a run reads before it plans: the launch state with the time and
// id of the event that set it, and the open tasks from the tracker snapshot, capped.
const authority = { withinRole: ['message.send', 'role.launch'], crossRole: [] };
const roles = { pm: { authority }, coder: { authority } };
const businesses = {
demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, launch: { by: 'pm', instances: ['coder'], max: { opus: 2, sonnet: 2 } }, roles },
};
const at = '2026-10-12T12:00:00.000Z';
const digest = 'd'.repeat(64);
const ref = (i) => `vikunja:3/${i}`;
const fields = (i, done = false) => ({ project_id: 3, title: `Task ${i}`, description: `<p>${i}</p>`, done, bucket: 11 });
function setup(t) {
const root = mkdtempSync(join(tmpdir(), 'bus-state-'));
const store = new Store(root);
const b = new Broker({ store, businesses });
t.after(() => {
store.close();
rmSync(root, { recursive: true, force: true });
});
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const pm = b.bindLaunch({ business: 'demo', role: 'pm', run: 'demo-pm', harness: 'claude-code' });
b.request(pm, { verb: 'role.claim' });
const snap = (i, over = {}, read_at = at) => ({ task_ref: ref(i), updated: read_at, digest, fields: { ...fields(i), ...over }, via: 'board', read_at });
const record = (...snapshots) => b.recordTask({ business: 'demo', snapshots });
const state = (cap = pm, args = {}) => b.request(cap, { verb: 'state', args });
return { b, store, human, pm, snap, record, state, reader: b.bindReader({ business: 'demo' }) };
}
test('a business that never switched launches reads allowed with no time and no event', (t) => {
const { state } = setup(t);
assert.deepEqual(state().launch, { state: 'allowed', at: null, event: null });
});
test('launches revoked and then restored read allowed, with the restore time and event id', (t) => {
const { b, store, human, pm, state } = setup(t);
const revoked = b.request(human, { verb: 'launch.revoke' }).id;
assert.deepEqual(
{ state: state().launch.state, event: state().launch.event },
{ state: 'revoked', event: revoked },
);
const restored = b.request(human, { verb: 'launch.restore' }).id;
const row = store.get('SELECT at FROM events WHERE id=?', restored);
assert.deepEqual(state(pm).launch, { state: 'allowed', at: row.at, event: restored });
assert.notEqual(restored, revoked);
});
test('a task closed on the tracker leaves the open list, and total counts the open ones', (t) => {
const { snap, record, state } = setup(t);
record(snap(1), snap(2), snap(3));
assert.deepEqual(state().tasks, {
open: [1, 2, 3].map((i) => ({ task_ref: ref(i), title: `Task ${i}` })),
total: 3,
cap: TASK_PAGE,
});
record(snap(2, { done: true }, '2026-10-12T13:00:00.000Z'));
const { open, total } = state().tasks;
assert.deepEqual(open.map((x) => x.task_ref), [ref(1), ref(3)]);
assert.equal(total, 2);
});
test('a tombstone is not open', (t) => {
const { snap, record, state } = setup(t);
record(snap(1), { ...snap(2), fields: { gone: 'no-access' }, via: 'task' });
assert.deepEqual(state().tasks.open.map((x) => x.task_ref), [ref(1)]);
});
test('the open list stops at the cap and says so; total still counts every open task', (t) => {
const { snap, record, state } = setup(t);
record(...Array.from({ length: TASK_PAGE + 5 }, (_, k) => snap(k + 1)));
const { open, total, cap } = state().tasks;
assert.equal(open.length, TASK_PAGE);
assert.equal(total, TASK_PAGE + 5);
assert.equal(cap, TASK_PAGE);
});
test('a role and a reader session read state; arguments, and an unbound cap, refuse', (t) => {
const { reader, pm, state } = setup(t);
assert.deepEqual(state(reader), state(pm));
assert.throws(() => state(pm, { business: 'other' }), { code: 'invalid-request' });
assert.throws(() => state('f'.repeat(64)), /unauthenticated/);
});
test('a human session reads the same state, with the same cap', (t) => {
const { snap, record, human, pm, state } = setup(t);
record(...Array.from({ length: TASK_PAGE + 2 }, (_, k) => snap(k + 1)));
const read = state(human);
assert.deepEqual(read, state(pm));
assert.equal(read.tasks.cap, TASK_PAGE);
assert.equal(read.tasks.open.length, TASK_PAGE);
assert.equal(read.tasks.total, TASK_PAGE + 2);
});
test('reading state writes nothing: every table keeps its row count', (t) => {
const { b, store, human, pm, reader, snap, record, state } = setup(t);
b.request(human, { verb: 'launch.revoke' });
record(snap(1), snap(2));
const counts = () => Object.fromEntries(
store.all("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'")
.map(({ name }) => [name, store.get(`SELECT COUNT(*) AS n FROM "${name}"`).n]),
);
const before = counts();
assert.ok(before.events > 0 && before.task_snapshots > 0);
for (const cap of [pm, reader, human]) state(cap);
assert.deepEqual(counts(), before);
});