tasks, bus, harness, seat: the approved requirement ids as data (#1584)
Row 90, round 2. Dewey built it, Filbert approved it (comment 28068) on manifest 95611274 (21 paths, baseb8539480). The approved PRD's requirement ids live in packages/tasks/requirements.json. task_create refuses an id outside it, and the PM session gets a list_requirements tool. Round 2 adds requirements.mjs to the sandbox's RUNNER_FILES (decision 127). Sage gate onc047d028plus the patch, 23:50:16Z to 23:54:26Z: tasks 89, bus 111, harness 66, webui 101, cli 123, seat 33, business 60, control-board 124, queue 150, conversation 182, discord 178, ledger 78, runs 43, mosaic 69, all fail 0. Every scripts/test-*.sh exited 0. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -116,7 +116,7 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
| `role.claim` | No args; claims capability's role/run. One current holder per business/instance; a revoked run cannot reclaim that role, including after restart. |
|
||||
| `role.release` | No args; only its current holder releases. |
|
||||
| `role.revoke` | `{role,decision}`; needs human-approved gated decision for this exact target and holder incarnation. |
|
||||
| `decision.raise` | `{action,question,options:[{key,text}],recommendation,blocking,domain?,target?,project?,task_ref?,requirement_ref?,supersedes?,choice?,approvalChoice?}`. Returns decision with decoded options and authorization context. |
|
||||
| `decision.raise` | `{action,question,options:[{key,text}],recommendation,blocking,domain?,target?,project?,task_ref?,requirement_ref?,supersedes?,choice?,approvalChoice?}`. Returns decision with decoded options and authorization context. A blocking decision holds a task: `blocking` true with no `task_ref` refuses `task-required`. A non-blocking one needs no task, and a gated action such as `prd.approve` or `role.revoke` still routes to the human's inbox. |
|
||||
| `decision.resolve` | `{id,choice,note?}`; option must exist, resolver must be routed role or outside-agent human CLI. |
|
||||
| `decision.seen` | `{id,note?}`; acknowledgement by resolver, not a resolution. |
|
||||
| `decision.withdraw` | `{id,note?}`; raiser or human. Never rewrites the decision. |
|
||||
@@ -131,6 +131,7 @@ an operator inspect content and delivery evidence after an uncertain result.
|
||||
| `trail` | `{subject}`: task ref, decision UUID or message UUID. Linked records in broker write order. |
|
||||
| `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`. |
|
||||
| `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)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { randomUUID, randomBytes } from 'node:crypto';
|
||||
import { loadRequirements } from '../../tasks/src/requirements.mjs';
|
||||
|
||||
// Kept closed while S1 lands; its vocabulary is the integration authority.
|
||||
export const ACTIONS = Object.freeze([
|
||||
@@ -93,6 +94,7 @@ export class Broker {
|
||||
#ended = new Set();
|
||||
#secretCheck;
|
||||
#credentialStatus;
|
||||
#requirements;
|
||||
#clock = 0;
|
||||
// credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use().
|
||||
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [] }) {
|
||||
@@ -101,6 +103,8 @@ export class Broker {
|
||||
this.#store = store;
|
||||
this.#secretCheck = secretCheck;
|
||||
this.#credentialStatus = credentialStatus;
|
||||
// Row 90 (#1584): the approved PRD's requirement ids. A bad requirements.json refuses here.
|
||||
this.#requirements = loadRequirements();
|
||||
for (const table of [
|
||||
'events',
|
||||
'role_claims',
|
||||
@@ -894,6 +898,14 @@ export class Broker {
|
||||
),
|
||||
};
|
||||
}
|
||||
// Row 90 (#1584): the approved PRD's requirement ids, which task.create takes. A read for
|
||||
// every session that can create tasks: task.create is an agent verb, so a human refuses
|
||||
// agent-required here as there, and a reader read-only.
|
||||
case 'requirements': {
|
||||
keys(a, []);
|
||||
this.#agent(s);
|
||||
return this.#requirements;
|
||||
}
|
||||
case 'inbox':
|
||||
case 'agents':
|
||||
case 'tasks':
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
// Row 90 (#1584): `requirements`, the approved PRD's ids that task.create takes, for the role
|
||||
// sessions that create tasks; and the PM's two no-task decisions, a request no id fits
|
||||
// (prd.approve) and a run it thinks should stop (role.revoke), which reach the human's Inbox.
|
||||
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 } from '../src/broker.mjs';
|
||||
import { loadRequirements } from '../../tasks/src/requirements.mjs';
|
||||
|
||||
const options = [
|
||||
{ key: 'yes', text: 'Approve' },
|
||||
{ key: 'no', text: 'Decline' },
|
||||
];
|
||||
const roles = {
|
||||
pm: { authority: { withinRole: ['message.send', 'task.create'], crossRole: [] } },
|
||||
coder: { authority: { withinRole: ['message.send'], crossRole: [] } },
|
||||
};
|
||||
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'pm', delivery: 'pm' }, roles } };
|
||||
function setup(t) {
|
||||
const root = mkdtempSync(join(tmpdir(), 'bus-requirements-'));
|
||||
const store = new Store(root);
|
||||
const b = new Broker({ store, businesses });
|
||||
t.after(() => {
|
||||
store.close();
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
const agent = (role) => {
|
||||
const cap = b.bindLaunch({ business: 'demo', role, run: `demo-${role}-run`, harness: 'pi', address: role });
|
||||
b.request(cap, { verb: 'role.claim' });
|
||||
return cap;
|
||||
};
|
||||
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
|
||||
const reader = b.bindReader({ business: 'demo' });
|
||||
return { b, agent, human, reader };
|
||||
}
|
||||
const call = (b, cap, verb, args = {}) => b.request(cap, { verb, args });
|
||||
const raise = (b, cap, action, extra = {}) =>
|
||||
call(b, cap, 'decision.raise', { action, question: `${action}?`, options, recommendation: 'no', blocking: false, ...extra });
|
||||
|
||||
test('requirements returns the committed list to a holding role session, and no one else', (t) => {
|
||||
const { b, agent, human, reader } = setup(t);
|
||||
const pm = agent('pm');
|
||||
const list = call(b, pm, 'requirements');
|
||||
assert.deepEqual(list, loadRequirements());
|
||||
assert.equal(list.requirements.length, 24);
|
||||
assert.deepEqual(call(b, agent('coder'), 'requirements'), list);
|
||||
assert.throws(() => call(b, human, 'requirements'), /agent-required/);
|
||||
assert.throws(() => call(b, reader, 'requirements'), /read-only/);
|
||||
// A second launch of the role that never claimed it holds nothing.
|
||||
const stale = b.bindLaunch({ business: 'demo', role: 'pm', run: 'demo-pm-other', harness: 'pi', address: 'pm2' });
|
||||
assert.throws(() => call(b, stale, 'requirements'), /not-holder/);
|
||||
assert.throws(() => call(b, pm, 'requirements', { prd: 'docs/prd/mosaic-stack.md' }), /invalid-request/);
|
||||
});
|
||||
|
||||
test("the PM's prd.approve with no task, when no id fits, reaches the human's Inbox", (t) => {
|
||||
const { b, agent, human, reader } = setup(t);
|
||||
const pm = agent('pm');
|
||||
const d = raise(b, pm, 'prd.approve', { target: 'mosaic-stack-prd' });
|
||||
assert.deepEqual([d.class, d.route_to, d.blocking, d.task_ref], ['gated', 'human', false, null]);
|
||||
for (const cap of [human, reader]) assert.deepEqual(call(b, cap, 'inbox').map((r) => r.id), [d.id]);
|
||||
assert.deepEqual(call(b, pm, 'inbox'), []);
|
||||
// Blocking needs a task to hold.
|
||||
assert.throws(() => raise(b, pm, 'prd.approve', { blocking: true }), /task-required/);
|
||||
});
|
||||
|
||||
test("the PM's non-blocking role.revoke with no task reaches the human's Inbox", (t) => {
|
||||
const { b, agent, human } = setup(t);
|
||||
const pm = agent('pm');
|
||||
agent('coder');
|
||||
const d = raise(b, pm, 'role.revoke', { target: 'coder' });
|
||||
assert.deepEqual([d.class, d.route_to, d.blocking, d.task_ref], ['gated', 'human', false, null]);
|
||||
assert.deepEqual(d.authorization, { action: 'role.revoke', target: 'coder', approvalChoice: 'yes' });
|
||||
assert.deepEqual(call(b, human, 'inbox').map((r) => r.id), [d.id]);
|
||||
assert.throws(() => raise(b, pm, 'role.revoke', { target: 'coder', blocking: true }), /task-required/);
|
||||
});
|
||||
Reference in New Issue
Block a user