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
+1
View File
@@ -157,6 +157,7 @@ an operator inspect content and delivery evidence after an uncertain result.
| `decisions` | `{list}`: `roles` or `recent`; human and reader sessions only. Open decisions routed to a role, or the last 50 closed. |
| `credentials` | No args; human and reader sessions only. `[{service,account,role,dateKind,date,state}]` for the capability's business, never a token, file path or variable name. |
| `requirements` | No args; role sessions only, as `task.create` is (row 90, #1584). The approved PRD's requirement ids, `{prd,version,requirements:[{id,title}]}`, from `packages/tasks/requirements.json`, loaded when the broker starts; a bad file refuses the start. A human session refuses `agent-required`, a reader `read-only`. |
| `state` | No args; role, human and reader sessions (row 109, #1603). `{launch:{state,at,event},tasks:{open:[{task_ref,title}],total,cap}}` for the capability's business. `launch` is the latest `launch.revoked` or `launch.restored` event: `allowed` with `at` and `event` null when none exists. `open` is the tracker snapshot's open tasks in `task_ref` order, at most `cap` (the `tasks` page size, 50); `total` counts every open task. The harness runner reads it before each turn. Writes nothing. |
| `replies` | `{id}`: a message UUID; human and reader sessions only. The messages whose `in_reply_to` is that id, oldest first, at most 50. Writes nothing. |
### Agent state (row 59, #1548)
+18 -1
View File
@@ -654,7 +654,7 @@ export class Broker {
if (!object(args)) fail('invalid-request');
this.#secretCheck(args);
return this.#store.transaction(() => {
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages'].includes(request.verb))
if (s.reader && !['inbox', 'agents', 'tasks', 'trail', 'decisions', 'credentials', 'replies', 'messages', 'state'].includes(request.verb))
fail('read-only');
const result = this.#dispatch(s, request.verb, args);
this.#secretCheck(result);
@@ -1086,6 +1086,23 @@ export class Broker {
this.#agent(s);
return this.#requirements;
}
// Row 109 (#1603): what a run needs to know before it plans. The business's launch state with
// the time and id of the event that set it (`allowed`, no time and no id when none has), and
// the open tasks as the tracker snapshot shows them: refs and titles, at most `cap` of them,
// `total` counting every open task. A read for role, reader and human sessions; nothing is stored.
case 'state': {
keys(a, []);
if (!s.human && !s.reader) this.#agent(s);
const set = this.#store.get(
"SELECT id,kind,at FROM events WHERE business=? AND kind IN ('launch.revoked','launch.restored') ORDER BY seq DESC LIMIT 1",
s.business,
);
const open = this.#tasks(s.business, { done: false });
return {
launch: { state: set?.kind === 'launch.revoked' ? 'revoked' : 'allowed', at: set?.at ?? null, event: set?.id ?? null },
tasks: { open: open.tasks.map(({ task_ref, title }) => ({ task_ref, title })), total: open.total, cap: TASK_PAGE },
};
}
case 'inbox':
case 'agents':
case 'tasks':
+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);
});