Compare commits

...
7 Commits
Author SHA1 Message Date
jason.woltje e9a079fcc4 fix(organization): validate narrowed arbiter resolve authority (#1873) 2026-11-10 16:51:03 -06:00
jason.woltje 7de9cfccc3 feat(organization): enforce arbiter domain resolve rights (#1873) 2026-11-10 16:33:21 -06:00
jason.woltje c3c192677d test(webui): the layout reload test loads the page through navigate (#1873)
The mark from the previous commit kept the wait off the old page, but
the wait could still start on it and be killed by the reload
("Inspected target navigated or closed", once in ten runs). navigate()
waits for the load; a query makes the same URL load again.
2026-11-10 16:01:01 -06:00
jason.woltje 95ee19cc71 fix(webui): a task dialog closed during the conflict read stays closed (#1873)
Review fix. dialog.close() clears `open` at once but queues the close
event that empties the dialog. A conflict read that resumed in between
still found the old form connected and reopened the dialog, and the
queued event then emptied it: an open modal with nothing in it. The
check is now the dialog's own `open` state and that it still holds the
form. A browser test closes the dialog and releases the read's body in
one task; it fails on the previous commit.
2026-11-10 16:01:01 -06:00
jason.woltje 07dba7f2f5 test(webui): the layout reload test waits for the new page (#1873)
The wait after location.reload() could pass on the board that was
unloading, and the next read then found no board (seen once in ten
runs under load). A mark set before the reload goes with the old page,
so the wait now passes only on the reloaded one.
2026-11-10 16:01:01 -06:00
jason.woltje ceee80d5b7 fix(webui): task board conflict shows the refusal only with the current task (#1873)
A product race, not only a test race. On task-conflict the dialog wrote
the refusal into the old form, turned its buttons back on and only then
read the current task. For the length of that read the form showed the
refusal beside the old task and a Save went out against the old digest.
The conflict test read the title in that window and failed under load.

The old form now stays disabled with "Reading the current task…" until
the read answers; the refusal and the reopened form appear together. A
read a poll overtook no longer reopens on the board's old read; the
dialog reads the task itself. If the task cannot be read the form keeps
only Cancel and takes no save. A dialog closed during the read stays
closed, and a form whose Save is disabled refuses a submit sent past it.
2026-11-10 16:01:01 -06:00
jason.woltje fa9bedb09a fix(webui): prefix project ids in side-by-side panes (#1873)
The requirements page drew project-request-select, project-north-star
and project-prd with fixed ids, so the same project in both panes put
each id in the document twice. mountContent takes the pane's idPrefix
(pane-a-, pane-b-) and every id and id lookup on the page follows it;
the full view passes none and keeps its ids.

The dashboard was already prefixed and the Agents page draws no ids.
A new browser test opens the same dashboard, Agents page and project
pages in both panes and checks for duplicate ids and for labels, ARIA
references and in-page links that leave their pane.
2026-11-10 16:01:01 -06:00
95 changed files with 628 additions and 223 deletions

No files matched your search

+22 -2
View File
@@ -50,6 +50,7 @@ export const ACTIONS = Object.freeze([
'policy.change',
'prd.approve',
'decision.resolve.technical',
'decision.resolve.delivery',
// Row 285 (C2-08b, #1790): routed under PRD 1.16 REQ-WAIT-3 and REQ-LIB-6, below.
'library.write',
]);
@@ -78,6 +79,7 @@ const ROUTED = new Set([
'policy.change',
'prd.approve',
'decision.resolve.technical',
'decision.resolve.delivery',
// PRD 1.7 REQ-COS-3 (row 194, #1692): the chief's hire and fire, within the bounds the broker checks.
'role.hire',
'role.fire',
@@ -88,6 +90,8 @@ const ROUTED = new Set([
export const HARD_WAITS = Object.freeze(ACTIONS.filter((action) => !ROUTED.has(action)));
const GATED = new Set(HARD_WAITS);
const CLOSED = "('resolved','withdrawn','expired')";
// RR (#1873): these actions never take their domain from the raising agent.
const DECISION_DOMAINS = Object.freeze({ 'git.merge.protected': 'technical', 'policy.change': 'delivery' });
// How many closed decisions `decisions` with list 'recent' returns, newest close first.
export const RECENT = 50;
// How many replies `replies` returns, oldest first.
@@ -5061,6 +5065,20 @@ export class Broker {
);
return evidence ? JSON.parse(evidence.body) : null;
}
#checkResolver(s, d) {
const b = this.#business(s.business), role = b.roles[s.role];
const domain = d.action === 'prd.approve' ? 'technical' : DECISION_DOMAINS[d.action] ?? this.#raised(s.business, d.id)?.domain;
// Pre-RR evidence has no domain. Assignments may have changed since raise,
// so require both possible rights rather than infer one from today's file.
const domains = domain === undefined ? ['technical', 'delivery'] : [domain];
for (const kind of domains) {
if (!['technical', 'delivery'].includes(kind)) fail('not-resolver');
const right = `decision.resolve.${kind}`;
if (!role?.authority?.withinRole.includes(right)) {
throw new BusError('not-resolver', `instance ${s.role}, role ${role?.definition ?? s.role}, lacks ${right}`);
}
}
}
// Whether an executor already used the decision. decision.raise records context, not permission
// consumed by an executor.
#spent(business, decision) {
@@ -7064,8 +7082,8 @@ export class Broker {
if (!choices.includes(a.recommendation)) fail('invalid-recommendation');
const approvalChoice = a.approvalChoice ?? 'yes';
if (!choices.includes(approvalChoice)) fail('invalid-approval-choice');
const domain = a.domain ?? 'delivery';
if (!['technical', 'delivery'].includes(domain)) fail('invalid-domain');
if (a.domain !== undefined && !['technical', 'delivery'].includes(a.domain)) fail('invalid-domain');
const domain = DECISION_DOMAINS[a.action] ?? a.domain ?? 'delivery';
// PRD 1.5 REQ-DEC-2 (row 139, #1633): a gated raise citing the project's current spec commit
// goes to the arbiter for its domain, like a cross-role one. An uncited gated raise and any
// hard wait go to the human. readSpec already refused a stale or unconfirmed citation.
@@ -7147,6 +7165,7 @@ export class Broker {
decision: did,
action: a.action,
operation: 'decision.raise',
domain,
task_ref: a.task_ref ?? null,
target: a.target ?? null,
approvalChoice,
@@ -7213,6 +7232,7 @@ export class Broker {
else {
this.#agent(s);
if (s.role !== d.route_to) fail('not-resolver');
if (verb === 'decision.resolve') this.#checkResolver(s, d);
}
const op = verb === 'decision.resolve' || viaConsole ? 'resolved' : verb === 'decision.seen' ? 'seen' : 'withdrawn';
if (op === 'resolved' && !JSON.parse(d.options).some((o) => o.key === a.choice))
+2 -2
View File
@@ -282,7 +282,7 @@ test('REQ-LIMIT-1: the chief of staff is logged as allowed, and organization.set
const outside = [
{ launch: { by: 'coder' } },
{ human: 'mallory' },
{ arbiters: { technical: 'coder', delivery: 'coder' } },
{ arbiters: { technical: 'chief', delivery: 'chief' } },
{ roles: {} },
{ actor: 'jason' },
{ 'launch.by': 'coder' },
@@ -1026,7 +1026,7 @@ test('row 194 T8, T9, T18: through the runtime the chief hires and fires with th
const root = mkdtempSync(join(tmpdir(), 'bus-chief-hire-'));
const rolesDir = rolesCopy(root);
// A hireable scratch definition: version 2, no credentials, every action on HIRE_ALLOWED.
writeFileSync(join(rolesDir, 'aide.md'), '# Aide\n');
writeFileSync(join(rolesDir, 'aide.md'), '# Aide\n\n## Authority\n');
writeJson(join(rolesDir, 'aide.json'), {
roleVersion: 2, name: 'aide', title: 'Aide', contract: 'aide.md', tools: ['read'], network: 'api-only',
authority: { withinRole: ['message.send'], crossRole: [] }, credentials: [],
+2 -2
View File
@@ -18,8 +18,8 @@ export function citationWorld(t, { definition = 'coder', library = true, guard:
writeFileSync(settings, JSON.stringify({ replyLimits: { requireCitation: false, citationsPerReply: 20 } }));
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
...(library ? { projects: { books: map } } : {}),
arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition, web: true, authority: { withinRole: sendAuthority ? ['message.send'] : [], crossRole: [] } } } };
arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition, web: true, authority: { withinRole: sendAuthority ? ['message.send'] : [], crossRole: [] } } } };
const limits = () => {
try { return resolveReplyLimits({ readable: true, document: JSON.parse(readFileSync(settings, 'utf8')) }); }
catch { return resolveReplyLimits({ readable: false }); }
+2 -2
View File
@@ -18,8 +18,8 @@ test('C2-06 runtime reads changed and unreadable reply limits without restarting
// Runtime takes the host's broker projection, not the business validator's unresolved role entries.
// No agent action is needed here: use the same empty-authority shape as limits-runtime.test.mjs.
const business = { id: 'acme', human: 'jason', projects: checked.projects,
arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
const runtime = await startBroker({ dataRoot, businesses: { acme: business }, organization: { dir, rolesDir, system: systemFor(root) } });
t.after(() => runtime.close());
const human = runtime.broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
+3 -3
View File
@@ -48,8 +48,8 @@ function setup(t) {
human: 'jason',
arbiters: { technical: 'pm', delivery: 'pm' },
roles: {
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send', 'decision.resolve.delivery'], crossRole: [] } },
pm: { definition: 'pm', authority: { withinRole: ['message.send', 'decision.resolve.delivery'], crossRole: [] } },
},
vars: { 'bus.tester': true },
};
@@ -227,7 +227,7 @@ test('ordinary PRD approval and nine non-yes/no choices use the same protocol wi
test('ordinary role routes refuse Console resolution and agent answers retain the actual role/run', (t) => {
const f = setup(t);
for (const route of ['pm', 'chief']) {
const id = grantDecision(f.store, { action: 'prd.approve', route, close: null });
const id = grantDecision(f.store, { action: 'policy.change', route, close: null });
refusesSync(() => f.call('console', 'decision.preview.console', { id }), 'decision-not-eligible');
refusesSync(() => f.call('console', 'decision.resolve.console', { id, choice: 'yes', digest: '0'.repeat(64) }), 'decision-not-eligible');
f.call(route, 'decision.resolve', { id, choice: 'yes' });
+6 -6
View File
@@ -30,8 +30,8 @@ async function setup(t, { storeRoot = true } = {}) {
const store = (agent, service) => join(root, 'secrets', 'agents', 'demo', agent, service);
// The bus's resolved business carries the same references; a store one names its derived file.
const demo = {
id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, vars: { 'bus.tester': true },
roles: {
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { ...role, credentials: { gitea: { store: true, file: store('coder', 'gitea'), rotateBy: DATE }, vikunja: doc.roles.coder.credentials.vikunja } },
reviewer: { ...role, credentials: doc.roles.reviewer.credentials },
},
@@ -58,7 +58,7 @@ test('row 199: the lifecycle verbs refuse every session but the human, and the t
const calls = [];
const b = new Broker({
store: s,
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, vars: { 'bus.tester': true }, roles: { coder: role } } },
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: role } } },
credentialVerbs: (id, verb, args, actor) => (calls.push([verb, actor]), { ok: true }),
});
const human = b.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
@@ -97,7 +97,7 @@ test('row 199: the lifecycle verbs refuse every session but the human, and the t
b.request(tester, { verb: 'credentials.revoke', args: args['credentials.revoke'] });
assert.deepEqual(calls, [['credentials.issue', 'jason'], ['credentials.revoke', 'jason'], ['credentials.delete', 'jason'], ['credentials.revoke', 'tester']]);
// No lifecycle without the runtime's store.
const bare = new Broker({ store: s, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: role } } } });
const bare = new Broker({ store: s, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: role } } } });
const h2 = bare.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
assert.throws(() => bare.request(h2, { verb: 'credentials.revoke', args: args['credentials.revoke'] }), /not-configured/);
});
@@ -182,8 +182,8 @@ test('#1886: a store root that exists with loose modes still refuses start', asy
mkdirSync(join(root, 'secrets', 'agents'), { recursive: true, mode: 0o750 });
chmodSync(join(root, 'secrets', 'agents'), 0o750);
const demo = {
id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, vars: {},
roles: { coder: { ...role, credentials: { gitea: { store: true, file: join(root, 'secrets', 'agents', 'demo', 'coder', 'gitea'), rotateBy: DATE } } } },
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: {},
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { ...role, credentials: { gitea: { store: true, file: join(root, 'secrets', 'agents', 'demo', 'coder', 'gitea'), rotateBy: DATE } } } },
};
await assert.rejects(startBroker({ dataRoot: join(root, 'data'), businesses: { demo }, organization: { dir: root, rolesDir, system: systemFor(root) } }), /credential-file/);
});
@@ -24,8 +24,8 @@ async function setup(t, limit = 0) {
writeJson(join(root, 'businesses/demo.json'), doc);
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'),
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: ROLES, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } },
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, ...ROLES }, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } },
organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
return { bus, human: bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
@@ -76,8 +76,8 @@ test('REQ-LIMIT-4 bindLaunch keeps the account off session.launched, refuses one
const root = mkdtempSync(join(tmpdir(), 'mosaic-daily-bind-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const broker = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: ROLES, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } } });
const broker = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, ...ROLES }, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } } });
const base = { business: 'demo', role: 'coder', harness: 'claude-code', provider: 'anthropic', model: 'opus' };
for (const [i, account] of ['Jason Max', '', 5, {}, '-x'].entries())
assert.throws(() => broker.bindLaunch({ ...base, run: `bad${i}`, account }), { code: 'invalid-request' }, JSON.stringify(account));
+2 -2
View File
@@ -13,8 +13,8 @@ const options = [
{ key: 'no', text: 'Decline' },
];
const roles = {
pm: { authority: { withinRole: ['message.send'], crossRole: [] } },
cto: { authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { authority: { withinRole: ['decision.resolve.delivery', 'message.send'], crossRole: [] } },
cto: { authority: { withinRole: ['decision.resolve.technical', 'message.send'], crossRole: [] } },
coder: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } },
};
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' }, roles } };
+2 -2
View File
@@ -22,8 +22,8 @@ const options = [
];
const pusher = { authority: { withinRole: ['message.send'], crossRole: ['git.push.working'] } };
const roles = {
pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { definition: 'pm', authority: { withinRole: ['message.send', 'decision.resolve.technical'], crossRole: [] } },
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send', 'decision.resolve.delivery'], crossRole: [] } },
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
// Hears from pm, the technical arbiter, but not from the chief of staff, the delivery arbiter.
coder: { definition: 'coder', ...pusher },
+2 -2
View File
@@ -13,8 +13,8 @@ const options = [
{ key: 'no', text: 'Decline' },
];
const roles = {
pm: { authority: { withinRole: ['message.send', 'task.create'], crossRole: [] } },
cto: { authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { authority: { withinRole: ['decision.resolve.delivery', 'message.send', 'task.create'], crossRole: [] } },
cto: { authority: { withinRole: ['decision.resolve.technical', 'message.send'], crossRole: [] } },
coder: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } },
};
const businesses = {
+1 -1
View File
@@ -48,7 +48,7 @@ function setup(t, { keygen, check, hang = false } = {}) {
arbiters: { technical: 'pm', delivery: 'pm' },
roles: {
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { definition: 'pm', authority: { withinRole: ['message.send', 'decision.resolve.delivery'], crossRole: [] } },
},
vars: { 'bus.tester': true },
};
@@ -17,8 +17,8 @@ test('row 298 runtime: human scope approval writes the file and organization his
const file = writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
mkdirSync(join(root, 'data'), { mode: 0o700 });
runtime = await startBroker({ dataRoot: join(root, 'data'),
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } },
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } },
organization: { dir: root, rolesDir, system: systemFor(root) },
});
const human = runtime.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
+21 -21
View File
@@ -27,8 +27,8 @@ test('REQ-WAIT-2: a business granting a hard wait at either class is refused; th
const root = mkdtempSync(join(tmpdir(), 'hard-waits-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const business = authority => ({ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority } } } });
const business = authority => ({ demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority } } } });
for (const action of WAITS) {
assert.throws(() => new Broker({ store, businesses: business({ withinRole: [action], crossRole: [] }) }), /invalid-authority/, action);
assert.throws(() => new Broker({ store, businesses: business({ withinRole: [], crossRole: [action] }) }), /invalid-authority/, action);
@@ -54,19 +54,19 @@ test('REQ-DEC-2: every hard wait goes to the human, cited or not, and the arbite
}
}
const d = await f.raise({ action: 'data.delete', ...cite(f) });
assert.throws(() => f.call(f.reviewer, 'decision.resolve', { id: d.id, choice: 'yes' }), /human-required/);
assert.throws(() => f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' }), /human-required/);
f.call(f.human, 'decision.resolve', { id: d.id, choice: 'yes' });
});
test('REQ-DEC-2: a gated raise citing the current spec goes to the arbiter for its domain; uncited, to the human', async t => {
const f = await fixture(t, { arbiters: { technical: 'cto', delivery: 'reviewer' },
roles: { cto: { definition: 'cto', authority: { withinRole: ['message.send'], crossRole: [] } } } });
const f = await fixture(t, { arbiters: { technical: 'cto', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, cto: { definition: 'cto', authority: { withinRole: ['message.send', 'decision.resolve.technical'], crossRole: [] } } } });
f.agent('cto');
for (const action of LEFT) {
const raise = { action, ...(action === 'role.revoke' ? { target: 'reviewer' } : {}) };
assert.deepEqual(routed(f, f.raise(raise)), { class: 'gated', route_to: 'human' }, action);
assert.deepEqual(routed(f, await f.raise({ ...raise, ...cite(f), domain: 'technical' })), { class: 'gated', route_to: 'cto' }, action);
assert.deepEqual(routed(f, await f.raise({ ...raise, ...cite(f) })), { class: 'gated', route_to: 'reviewer' }, action);
assert.deepEqual(routed(f, await f.raise({ ...raise, ...cite(f), domain: 'technical' })), { class: 'gated', route_to: action === 'policy.change' ? 'chief' : 'cto' }, action);
assert.deepEqual(routed(f, await f.raise({ ...raise, ...cite(f) })), { class: 'gated', route_to: action === 'git.merge.protected' ? 'cto' : 'chief' }, action);
}
});
@@ -74,15 +74,15 @@ test('REQ-DEC-2: an arbiter-approved cited gated decision authorizes the raiser,
const f = await fixture(t, { roles: { pm: { definition: 'pm', authority: { withinRole: ['message.send'], crossRole: [] } } } });
f.agent('pm');
const d = await f.raise({ action: 'role.revoke', target: 'pm', ...cite(f) });
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'reviewer' });
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'chief' });
assert.throws(() => f.call(f.human, 'decision.resolve', { id: d.id, choice: 'yes' }), /agent-required/);
f.call(f.reviewer, 'decision.resolve', { id: d.id, choice: 'yes' });
f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' });
assert.deepEqual(f.call(f.coder, 'role.revoke', { role: 'pm', decision: d.id }), { revoked: true });
});
test('REQ-DEC-2: a cited merge raised by merge-gate goes to the technical arbiter; a merge raised without the grant routes by citation', async t => {
const f = await fixture(t, { arbiters: { technical: 'cto', delivery: 'reviewer' }, roles: {
cto: { definition: 'cto', authority: { withinRole: ['message.send'], crossRole: [] } },
const f = await fixture(t, { arbiters: { technical: 'cto', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
cto: { definition: 'cto', authority: { withinRole: ['message.send', 'decision.resolve.technical'], crossRole: [] } },
'merge-gate': { definition: 'merge-gate', authority: { withinRole: [], crossRole: ['git.merge.protected'] } },
} });
const cto = f.agent('cto'), gate = f.agent('merge-gate');
@@ -106,7 +106,7 @@ test('REQ-DEC-2: a stale or unconfirmed citation refuses without a decision; an
await assert.rejects(f.raise({ action: 'policy.change', ...old, spec_commit: edit }), { code: 'spec-unconfirmed' });
assert.equal(f.store.get('SELECT count(*) n FROM decisions').n, 0);
const current = f.save('# Current\n');
const own = await f.call(f.reviewer, 'decision.raise', { action: 'policy.change', question: 'Change?',
const own = await f.call(f.chief, 'decision.raise', { action: 'policy.change', question: 'Change?',
options: [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }], recommendation: 'yes', blocking: false,
project: 'stack', spec_path: 'spec/north-star.md', spec_commit: current });
assert.deepEqual(routed(f, own), { class: 'gated', route_to: 'human' });
@@ -136,7 +136,7 @@ const allowedCount = f => f.store.get("SELECT count(*) n FROM events WHERE kind=
test('Q169: the chief of staff\'s end of the assistant stops every thread run; its own run and another ender\'s are refused', async t => {
const f = await fixture(t, { roles: { ...seats,
chief: { definition: 'chief-of-staff', ...grants(['role.end']) }, lead: { definition: 'lead', ...grants(['role.end']) } } });
const chief = f.agent('chief'), lead = f.agent('lead');
const chief = f.chief, lead = f.agent('lead');
// No thread is held yet, so there is nothing to end.
assert.throws(() => f.call(chief, 'role.revoke', { role: 'assistant', reason: 'Q169' }), /not-held/);
const a = held(f, 'a-run'), b = held(f, 'b-run');
@@ -158,11 +158,11 @@ test('Q169: the chief of staff\'s end of the assistant stops every thread run; i
test('Q169: a revoke of the assistant the chief of staff raises goes to the human, cited or not, and no arbiter resolves it', async t => {
const f = await fixture(t, { roles: { ...seats, chief: { definition: 'chief-of-staff', ...grants(['role.end']) } } });
const chief = f.agent('chief');
const chief = f.chief;
for (const args of [{}, cite(f), { ...cite(f), domain: 'technical' }, { ...cite(f), domain: 'delivery' }]) {
const d = await revoke(f, chief, 'assistant', args);
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'human' }, JSON.stringify(args));
assert.throws(() => f.call(f.reviewer, 'decision.resolve', { id: d.id, choice: 'yes' }), /human-required/);
assert.throws(() => f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' }), /human-required/);
}
// Resolved by the human, the chief's revoke citing it stops the thread run, once.
const a = held(f, 'a-run');
@@ -181,8 +181,8 @@ test('Q169: another role\'s cited revoke of the assistant, resolved by an arbite
// One raised while the instance's definition wasn't protected went to the arbiter. The same
// store under a business that now makes it the assistant still refuses it.
const d = await revoke(f, f.coder, 'pm', cite(f));
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'reviewer' });
f.call(f.reviewer, 'decision.resolve', { id: d.id, choice: 'yes' });
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'chief' });
f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' });
const now = { ...f.business, roles: { ...f.business.roles, pm: { definition: 'assistant', ...grants([]) } } };
const later = new Broker({ store: f.store, businesses: { demo: now } });
const coder = later.bindLaunch({ business: 'demo', role: 'coder', run: 'coder-run', harness: 'pi', address: 'coder' });
@@ -192,7 +192,7 @@ test('Q169: another role\'s cited revoke of the assistant, resolved by an arbite
test('Q169: a revoke of the assistant citing a decision the human resolved stops its thread run', async t => {
const f = await fixture(t, { roles: seats });
const d = await revoke(f, f.coder, 'assistant', cite(f));
assert.throws(() => f.call(f.reviewer, 'decision.resolve', { id: d.id, choice: 'yes' }), /human-required/);
assert.throws(() => f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' }), /human-required/);
f.call(f.human, 'decision.resolve', { id: d.id, choice: 'yes' });
// Row 155: the recorded holder run (null) and the role_claims one (none) compare equal, so the
// revoke reaches the thread claims and stops the held one.
@@ -205,8 +205,8 @@ test('Q169: a cited revoke of a role not in END_PROTECTED still goes to the arbi
const f = await fixture(t, { roles: seats });
f.agent('pm');
const d = await revoke(f, f.coder, 'pm', cite(f));
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'reviewer' });
f.call(f.reviewer, 'decision.resolve', { id: d.id, choice: 'yes' });
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'chief' });
f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' });
assert.deepEqual(f.call(f.coder, 'role.revoke', { role: 'pm', decision: d.id }), { revoked: true });
});
@@ -216,7 +216,7 @@ test('Q169: a role granted role.revoke at either class still sends a revoke of t
boss: { definition: 'boss', ...grants(['role.revoke']) } } });
const lead = f.agent('lead'), boss = f.agent('boss');
assert.deepEqual(routed(f, await revoke(f, lead, 'assistant', cite(f))), { class: 'gated', route_to: 'human' });
assert.deepEqual(routed(f, await revoke(f, lead, 'pm', cite(f))), { class: 'cross-role', route_to: 'reviewer' });
assert.deepEqual(routed(f, await revoke(f, lead, 'pm', cite(f))), { class: 'cross-role', route_to: 'chief' });
const own = await revoke(f, boss, 'assistant', { choice: 'yes' });
assert.deepEqual(routed(f, own), { class: 'gated', route_to: 'human' });
assert.equal(f.store.get('SELECT count(*) n FROM decision_events WHERE decision=?', own.id).n, 0);
+2 -2
View File
@@ -12,7 +12,7 @@ export const HUMAN_YES = { op: 'resolved', by: 'jason', choice: 'yes', via: 'cli
// A credential.mint decision with `grant` in its raise body. `close` is the decision_events row, or null
// for an open decision.
export function grantDecision(store, { business = 'demo', action = 'credential.mint', route = 'human', grant, close = HUMAN_YES }) {
export function grantDecision(store, { business = 'demo', action = 'credential.mint', route = 'human', domain = 'delivery', grant, close = HUMAN_YES }) {
const id = randomUUID();
store.run(
'INSERT INTO decisions(id,at,business,raised_by_role,raised_by_run,class,action,route_to,question,options,recommendation,blocking) VALUES(?,?,?,?,?,?,?,?,?,?,?,?)',
@@ -21,7 +21,7 @@ export function grantDecision(store, { business = 'demo', action = 'credential.m
store.run(
"INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,?,'action.allowed',?,?,?,?)",
randomUUID(), AT, business, 'chief', 'chief-run', id,
JSON.stringify({ decision: id, action, operation: 'decision.raise', approvalChoice: 'yes', ...(grant ? { grant } : {}) }),
JSON.stringify({ decision: id, action, operation: 'decision.raise', ...(domain === null ? {} : { domain }), approvalChoice: 'yes', ...(grant ? { grant } : {}) }),
);
if (close)
store.run('INSERT INTO decision_events(decision,at,op,by,choice,via) VALUES(?,?,?,?,?,?)', id, AT, close.op, close.by, close.choice ?? null, close.via ?? null);
+4 -4
View File
@@ -572,8 +572,8 @@ test('T27: the bus start sweeps <configDir>/secrets/run before it listens, and a
await new Promise((r) => dead.on('close', r));
writeFileSync(join(run, 'h-dddddd', 'pids'), `${dead.pid} x\n`);
const demo = {
id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { authority: { withinRole: ['message.send'], crossRole: [] }, credentials: {} } },
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: ['message.send'], crossRole: [] }, credentials: {} } },
};
const start = () => startBroker({ dataRoot: join(root, 'data'), businesses: { demo }, organization: { dir: root, rolesDir, system: systemFor(root) } });
const bus = await start();
@@ -640,8 +640,8 @@ test('M3: a host_grants record gives Credentials a store reference at start, and
mkdirSync(join(root, 'secrets', 'agents'), { recursive: true, mode: 0o700 });
chmodSync(join(root, 'secrets'), 0o700);
const demo = {
id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { authority: { withinRole: ['message.send'], crossRole: [] }, credentials: {} } },
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: ['message.send'], crossRole: [] }, credentials: {} } },
};
const start = () => startBroker({ dataRoot: join(root, 'data'), businesses: { demo }, organization: { dir: root, rolesDir, system: systemFor(root) } });
const hostKeys = (bus) => bus.credentials.status().filter((c) => c.service.startsWith('host.'));
+1 -1
View File
@@ -65,7 +65,7 @@ test('row 165 runtime: an add and a change reach the organization file with the
const rolesDir = rolesCopy(root);
writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const file = join(root, 'businesses/demo.json');
+5 -5
View File
@@ -26,7 +26,7 @@ const A = 'a'.repeat(40);
const B = 'b'.repeat(40);
const C = 'c'.repeat(40);
const roles = {
pm: { authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { authority: { withinRole: ['message.send', 'decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { authority: { withinRole: ['git.push.working', 'review.request', 'message.send'], crossRole: [] } },
reviewer: { authority: { withinRole: ['review.verdict', 'message.send'], crossRole: [] } },
'merge-gate': { authority: { withinRole: [], crossRole: [MERGE] } },
@@ -221,15 +221,15 @@ test('REQ-LAND-1: a within-role merge grant refuses not-authorized at the land',
assert.deepEqual([...w.merges(), ...w.events('action.refused')], []);
});
test('a cross-role merge that routes to the human, because merge-gate is its own arbiter, lands on the human\'s CLI answer', (t) => {
const w = setup(t, { arbiters: { technical: 'merge-gate', delivery: 'merge-gate' } });
test('a cross-role merge that routes to the human, because the cto lander is its own arbiter, lands on the human\'s CLI answer', (t) => {
const w = setup(t, { arbiters: { technical: 'cto', delivery: 'pm' }, roles: { ...roles, cto: { authority: { withinRole: ['decision.resolve.technical'], crossRole: [MERGE] } } } });
w.push(A);
w.approve(A);
const d = w.raise();
const d = w.raise('cto');
assert.deepEqual(w.routed(d), { class: 'cross-role', route_to: 'human' });
assert.throws(() => w.resolve(d), /human-required/);
w.resolve(d, 'yes', 'human');
const { ticket, by, via } = w.check({ task_ref: TASK, decision: d.id });
const { ticket, by, via } = w.b.checkLand(w.caps.cto, { task_ref: TASK, decision: d.id });
assert.deepEqual([by, via], ['jason', 'cli']);
w.b.recordLand(ticket, w.land);
assert.equal(w.merges()[0].body.class, 'cross-role');
@@ -119,7 +119,7 @@ const TASK = 'vikunja:1/2';
test('row 302 a run landRelease released answers launcher-released, and claims nothing again', (t) => {
const w = setup(t, {
pm: { authority: { withinRole: ['message.send'], crossRole: [] } },
pm: { authority: { withinRole: ['message.send', 'decision.resolve.technical'], crossRole: [] } },
coder: { authority: { withinRole: ['git.push.working', 'review.request', 'message.send'], crossRole: [] } },
reviewer: { authority: { withinRole: ['review.verdict', 'message.send'], crossRole: [] } },
'merge-gate': { authority: { withinRole: [], crossRole: [MERGE] } },
@@ -43,9 +43,9 @@ async function until(fn, ms = 10000) {
}
const makeBusiness = ({ root, chief = true, instances = null } = {}) => ({
id: 'acme', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
id: 'acme', human: 'jason', arbiters: { technical: 'cto', delivery: 'pm' },
// sage-4097 / mos-1584: the chief's declared read credential; no-chief negative fixture stays absent.
roles: { ...(chief ? { chief: { definition: 'chief-of-staff', ...role, credentials: { gitea: giteaReference(join(root, 'secrets', 'agents'), 'acme', 'chief', '2099-01-01') } } } : {}), coder: { definition: 'coder', ...role } },
roles: { cto: { definition: 'cto', authority: { withinRole: ['decision.resolve.technical'], crossRole: [] } }, pm: { definition: 'pm', authority: { withinRole: ['decision.resolve.delivery'], crossRole: [] } }, ...(chief ? { chief: { definition: 'chief-of-staff', ...role, credentials: { gitea: giteaReference(join(root, 'secrets', 'agents'), 'acme', 'chief', '2099-01-01') } } } : {}), coder: { definition: 'coder', ...role } },
projects: { books: { ...mappingEntry(), user: 'books-bot' } },
...(instances ? { launch: { instances } } : {}),
});
@@ -77,7 +77,7 @@ function world(t, options) {
// chief-of-staff instance, fired by boss. The real read grant targets the library project books.
function verbDoc(url, root) {
return {
id: 'acme', human: 'jason', arbiters: { technical: 'boss', delivery: 'boss' },
id: 'acme', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {
boss: { definition: 'pm', authority: chiefAuthority },
// sage-4097 / mos-1584: fixture projection carries the chief's own Gitea store reference.
+4 -4
View File
@@ -31,8 +31,8 @@ async function world(t, { projects, adapter, claimed = true, now = () => NOW, fi
const f = scratchRepo(t, files), dataRoot = join(f.root, 'data');
mkdirSync(dataRoot);
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
projects: projects ?? { books: mappingEntry() }, arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
projects: projects ?? { books: mappingEntry() }, arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
const context = { business, project: 'books', dataRoot };
let mirror = null, state = null, fetches = 0;
if (business.projects.books?.kind === 'library') {
@@ -222,8 +222,8 @@ test('real socket awaits lookup results and rechecks rejected query arguments',
test('runtime actually injects the lookup adapter, without exposing human library.file', { timeout: 30_000 }, async t => {
const f = scratchRepo(t, TEXTS), dataRoot = join(f.root, 'runtime'); mkdirSync(dataRoot);
const business = { id: 'acme', human: 'jason', plugins: { harness: ['pi'] }, projects: { books: mappingEntry() },
arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } } };
const runtime = await startBroker({ dataRoot, businesses: { acme: business } }); t.after(() => runtime.close());
const mirror = library.mirrorPath({ business, project: 'books', dataRoot });
git(f.repo, '-c', 'protocol.file.allow=always', 'push', '-q', mirror, 'HEAD:refs/heads/served');
+2 -2
View File
@@ -21,7 +21,7 @@ function world(t, adapter) {
const root = mkdtempSync(join(tmpdir(), 'bus-library-sync-')), store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
projects: { books: { ...mappingEntry(), user: 'books-bot' } }, arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } };
projects: { books: { ...mappingEntry(), user: 'books-bot' } }, arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } };
const broker = new Broker({ store, businesses: { acme: business }, library: adapter });
const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
const agent = broker.bindLaunch({ business: 'acme', role: 'coder', run: 'library-sync-agent', harness: 'pi' });
@@ -81,7 +81,7 @@ test('C2-03a runtime: the timer syncs each entry with a user at start and on eac
mkdirSync(dataRoot, { mode: 0o700 });
mkdirSync(join(configDir, 'secrets', 'acme'), { recursive: true, mode: 0o700 });
writeFileSync(join(configDir, 'secrets', 'acme', library.SLOT), TOKEN + '\n', { mode: 0o600 });
const business = { id: 'acme', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } },
const business = { id: 'acme', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } },
projects: { books: { ...mappingEntry(), user: 'books-bot' }, quiet: mappingEntry() } };
const calls = [];
// A git that succeeds without fetching: the remote-tracking ref stays absent, so each pass fails.
+2 -2
View File
@@ -234,8 +234,8 @@ test('C2-02 F2/E6 runtime: human saves/reverts use editor.user; no login never u
const pushes = [];
const command = (kind, url, options) => { if (kind === 'push') pushes.push(options.user); return bare.command(kind, url, options); };
const initial = (await bare.state()).lastFetch;
const business = { ...bare.context.business, human: 'jason', roles: { coder: { authority: { withinRole: [], crossRole: [] } } },
arbiters: { technical: 'coder', delivery: 'coder' } };
const business = { ...bare.context.business, human: 'jason', roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } },
arbiters: { technical: 'chief', delivery: 'chief' } };
const runtime = await startBroker({ dataRoot: bare.context.dataRoot, businesses: { acme: business },
librarySync: { configDir, command, intervalMs: 60000 } });
try {
+4 -4
View File
@@ -16,7 +16,7 @@ function world(t, adapter = {}) {
const root = mkdtempSync(join(tmpdir(), 'bus-library-')), store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const business = { id: 'acme', human: 'jason', vars: { 'bus.tester': true },
projects: { books: mappingEntry() }, arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } };
projects: { books: mappingEntry() }, arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } };
const broker = new Broker({ store, businesses: { acme: business }, library: adapter });
const human = broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
const agent = broker.bindLaunch({ business: 'acme', role: 'coder', run: 'library-agent', harness: 'pi' });
@@ -68,7 +68,7 @@ test('C2-04 runtime: protectedRoots skips library but retains real code-root cre
const f = repo(t), dataRoot = join(f.root, 'data'), token = join(f.root, 'fixture-token');
mkdirSync(dataRoot, { mode: 0o700 }); writeFileSync(token, 'made-up-fixture-token-123456', { mode: 0o600 });
const business = { id: 'acme', human: 'jason', projects: { books: mappingEntry(), stack: { root: f.repo } },
arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] }, credentials: { gitea: { file: token, rotateBy: '2099-01-01' } } } } };
arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] }, credentials: { gitea: { file: token, rotateBy: '2099-01-01' } } } } };
const runtime = await startBroker({ dataRoot, businesses: { acme: business } });
await runtime.close();
const mirror = library.mirrorPath({ business, project: 'books', dataRoot });
@@ -81,7 +81,7 @@ test('C2-04 runtime: protectedRoots skips library but retains real code-root cre
test('C2-04 runtime: unsafe existing mirror refuses boot and never publishes a socket', async t => {
const f = repo(t), dataRoot = join(f.root, 'data');
mkdirSync(dataRoot, { mode: 0o700 });
const business = { id: 'acme', human: 'jason', projects: { books: mappingEntry() }, arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } };
const business = { id: 'acme', human: 'jason', projects: { books: mappingEntry() }, arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } };
const mirror = join(dataRoot, 'projects/acme/books'); mkdirSync(mirror, { recursive: true });
writeFileSync(join(mirror, 'keep'), 'made-up note');
await assert.rejects(startBroker({ dataRoot, businesses: { acme: business } }), { code: 'library-git' });
@@ -92,7 +92,7 @@ test('C2-04 runtime: unsafe existing mirror refuses boot and never publishes a s
test('C2-04 runtime: real library adapters reach the socket and lock waits leave other bus reads responsive', { timeout: 15000 }, async t => {
const f = repo(t), dataRoot = join(f.root, 'data');
mkdirSync(dataRoot, { mode: 0o700 });
const business = { id: 'acme', human: 'jason', projects: { books: mappingEntry() }, arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } };
const business = { id: 'acme', human: 'jason', projects: { books: mappingEntry() }, arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } };
const runtime = await startBroker({ dataRoot, businesses: { acme: business } });
t.after(() => runtime.close());
const cap = runtime.broker.bindHuman({ business: 'acme', human: 'jason', via: 'cli', outsideAgent: true });
+2 -2
View File
@@ -26,7 +26,7 @@ const doc = (extra = {}) => ({
retired: ['old'],
...extra,
});
const OTHER = { id: 'other', human: 'jason', arbiters: { technical: 'only-there', delivery: 'only-there' }, roles: { 'only-there': none } };
const OTHER = { id: 'other', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, 'only-there': none } };
function setup(t, { organization = null, testerOn = undefined, processing = undefined } = {}) {
const root = mkdtempSync(join(tmpdir(), 'lifecycle-verbs-'));
@@ -349,7 +349,7 @@ test('row 194 V5 retry: a retired name deletes again only after workspace-rename
const { transaction } = f.b.lifecycleDelete({ business: 'demo', request });
f.done(request, { transaction, refusal: 'workspace-rename-failed' });
// The bus reloads after the commit, as the runtime does.
const next = doc({ retired: ['old', 'pm'], arbiters: { technical: 'coder', delivery: 'chief' } });
const next = doc({ retired: ['old', 'pm'], arbiters: { technical: 'chief', delivery: 'chief' } });
delete next.roles.pm;
f.b.reloadBusiness('demo', next);
const writes = w.calls.length;
+2 -2
View File
@@ -13,8 +13,8 @@ import { Broker } from '../src/broker.mjs';
const shipped = name => JSON.parse(readFileSync(fileURLToPath(new URL(`../../../roles/${name}.json`, import.meta.url)), 'utf8'));
const businesses = () => ({ demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: {
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { definition: 'coder', authority: { withinRole: ['message.send'], crossRole: [] } },
assistant: { definition: 'assistant', authority: { withinRole: ['message.send'], crossRole: [] } },
},
+2 -2
View File
@@ -20,8 +20,8 @@ async function setup(t, plugins, limit = 0) {
writeJson(join(root, 'businesses/demo.json'), doc);
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'),
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } }, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } },
businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } }, definitions: { coder: {} }, projects: { stack: { root: '/nonexistent' } } } },
organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
return { bus, human: bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true }) };
+3 -3
View File
@@ -8,7 +8,7 @@ import {Broker} from '../src/broker.mjs';
import {Store} from '../src/store.mjs';
test('limits binds actor from human proof and rejects role, reader, a tester set and forged actor',t=>{
const root=mkdtempSync(join(tmpdir(),'mosaic-limits-bus-')),store=new Store(root);t.after(()=>{store.close();rmSync(root,{recursive:true,force:true});});const calls=[];
const b=new Broker({store,businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},vars:{'bus.tester':true},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:(...args)=>{calls.push(args);return {revision:'b'.repeat(64)};}});
const b=new Broker({store,businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},vars:{'bus.tester':true},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:(...args)=>{calls.push(args);return {revision:'b'.repeat(64)};}});
const human=b.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true}),role=b.bindLaunch({business:'demo',role:'coder',run:'coder-run',harness:'pi',address:'coder-run'}),reader=b.bindReader({business:'demo'}),tester=b.bindTester({business:'demo'}),patch={revision:'a'.repeat(64),limits:{total:0}};
for(const cap of [role,reader])for(const verb of ['limits','limits.set'])assert.throws(()=>b.request(cap,{verb,args:verb==='limits'?{}:patch}),/human-required/);
// Row 175 (#1673): the tester reads limits; limits.set stays the human's.
@@ -19,7 +19,7 @@ test('limits binds actor from human proof and rejects role, reader, a tester set
test('runtime wires verified human settings to the actual shared file writer and actor receipt',async t=>{
const {startBroker}=await import('../src/runtime.mjs');const {businessDoc,rolesCopy,systemFor,writeJson}=await import('../../organization/tests/helpers.mjs');const {readFileSync}=await import('node:fs');
const root=mkdtempSync(join(tmpdir(),'mosaic-limits-runtime-')),rolesDir=rolesCopy(root),doc=businessDoc(root,'demo');writeJson(join(root,'businesses/demo.json'),doc);mkdirSync(join(root,'data'),{mode:0o700});
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)}});
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)}});
t.after(async()=>{await bus.close();rmSync(root,{recursive:true,force:true});});const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const view=bus.broker.request(human,{verb:'limits'});const done=bus.broker.request(human,{verb:'limits.set',args:{revision:view.revision,limits:{total:0}}});assert.equal(done.limits.total,0);assert.equal(JSON.parse(readFileSync(join(root,'businesses/demo.json'),'utf8')).launch.limits.total,0);const receipt=JSON.parse(readFileSync(join(root,'businesses/.history/demo/changes.jsonl'),'utf8').trim().split('\n').at(-1));assert.equal(receipt.actor,'jason');assert.equal(receipt.state,'committed');
});
@@ -28,7 +28,7 @@ test('uncertain organization receipts leave bus startup and reads alone but refu
const root=mkdtempSync(join(tmpdir(),'mosaic-limits-recovery-')),rolesDir=rolesCopy(root),doc=businessDoc(root,'demo'),file=writeJson(join(root,'businesses/demo.json'),doc);mkdirSync(join(root,'data'),{mode:0o700});
const writer=organizationWriter({dir:root,rolesDir}),original=readFileSync(file,'utf8'),revision=writer.read('demo').revision,next=structuredClone(doc);next.launch.limits.total=7;
const interrupted=organizationWriter({dir:root,rolesDir,barrier:()=>{throw Error('fixture interruption');}});assert.throws(()=>interrupted.write('demo',next,{revision,actor:'jason'}));const external=structuredClone(doc);external.launch.limits.total=12;writeJson(file,external);const bytes=readFileSync(file,'utf8');
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)}});t.after(async()=>{await bus.close();rmSync(root,{recursive:true,force:true});});
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)}});t.after(async()=>{await bus.close();rmSync(root,{recursive:true,force:true});});
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});const view=bus.broker.request(human,{verb:'limits'});assert.equal(view.limits.total,12);assert.throws(()=>bus.broker.request(human,{verb:'limits.set',args:{revision:view.revision,limits:{total:1}}}),/organization-uncertain/);assert.equal(readFileSync(file,'utf8'),bytes);
// The operator reconciles the fixture to the recorded old whole document.
writeJson(file,original);const reconciled=bus.broker.request(human,{verb:'limits'});const result=bus.broker.request(human,{verb:'limits.set',args:{revision:reconciled.revision,limits:{total:1}}});assert.equal(result.limits.total,1);
+1 -1
View File
@@ -15,7 +15,7 @@ function fixture(t, cross = false) {
arbiters: { technical: 'cto', delivery: 'pm' },
roles: {
pm: role(cross ? [] : ['message.send'], cross ? ['message.send'] : []),
cto: role(),
cto: role(['decision.resolve.technical']),
coder: role(),
},
});
+2 -2
View File
@@ -46,7 +46,7 @@ test('launch admission refreshes selection and refuses a removed model and incom
writeJson(join(root,'businesses/demo.json'),doc); mkdirSync(join(root,'data'),{mode:0o700});
let present=true, reads=0;
const service={invalidate(){},read:async(_,{fresh})=>{assert.equal(fresh,true);reads++;return {status:'ready',models:present?[{module:'cliproxy',upstream:'zai',model:'fixture-id',available:true,harnesses:['pi']}]:[]};}};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:service});t.after(()=>bus.close());
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:service});t.after(()=>bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const revision=bus.broker.request(human,{verb:'limits'}).revision;
const {checkedTransport}=await import('../../harness/src/bundle.mjs');
@@ -78,7 +78,7 @@ test('a different enabled fixture provider supplies legacy mapping and admission
const makeProviderLoader=()=>({load:async()=>({refused:[]}),snapshot:()=>[{name:'second-fixture',module}]});
const offered={key:'fixture-model',module:'second-fixture',upstream:'fixture',provider:'fixture',model:'fixture-model',label:'Fixture',available:true,reason:null,harnesses:['pi']};
const modelCatalogue={read:async()=>({status:'ready',models:[offered],notices:[]}),invalidate(){}};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue,makeProviderLoader});t.after(()=>bus.close());
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue,makeProviderLoader});t.after(()=>bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});const view=await bus.broker.request(human,{verb:'routes'});
assert.equal(view.catalogue.legacy['fixture-legacy'].module,'second-fixture');
const answer=await bus.modelAdmission({business:'demo',instance:'coder',modelKey:'fixture-legacy',harness:'pi',revision:view.revision,entry:doc.launch.models['fixture-legacy'],provider:'fixture',model:'fixture-model',account:null,route:'primary',transport});
@@ -27,7 +27,7 @@ export async function ownershipFixture(t,{launches=true,listed=true,keep=false,r
t.after(async()=>{const failures=[];for(const g of gates)g.release();try{await waitFor(()=>active.size===0,'owned operations did not settle',5000);}catch{failures.push('owned-operation-close');}for(const close of [...closers].reverse()){try{await close();}catch{failures.push('owned-close');}}try{server?.closeAllConnections();if(server?.listening)await new Promise((resolve,reject)=>server.close(e=>e?reject(e):resolve()));}catch{failures.push('owned-server-close');}try{checkHistory({logs,proxyReceipts,trace});}catch{failures.push('value-free-receipts');}try{checkHistory({launch:launchLog()});}catch{failures.push('owned-launch-log-guard');}if(stderr)process.stderr.write=stderr;try{fs.rmSync(root,{recursive:true,force:true});}catch{failures.push('owned-root-close');}if(failures.length)throw new AggregateError(failures.map(code=>Error(code)),'owned managed fixture cleanup failed');});
const f=fixture(root,'acme',doc=>{doc.plugins={...doc.plugins,harness:['pi']};for(const role of Object.values(doc.roles))role.vars={...role.vars,harness:'pi'};doc.roles.chief.vars.model='claude-opus-5-5';doc.roles.aide={definition:'aide',vars:{harness:'pi',model:'claude-opus-5-5',thinking:'high'}};if(listed)doc.launch.instances.push('aide');if(keep)doc.launch.keep=['aide'];if(retired)doc.retired=['alpha'];if(noHire)doc.launch.noHire=['aide'];return doc;});
// The established scratch aide pattern only; repository roles/policy stay untouched.
for(const name of ['aide','helper']){fs.writeFileSync(join(f.roles,name+'.md'),'# Scratch aide\n');writeJson(join(f.roles,name+'.json'),{roleVersion:2,name,title:'Aide',contract:name+'.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});}
for(const name of ['aide','helper']){fs.writeFileSync(join(f.roles,name+'.md'),'# Scratch aide\n\n## Authority\n');writeJson(join(f.roles,name+'.json'),{roleVersion:2,name,title:'Aide',contract:name+'.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});}
const chiefFile=join(f.roles,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide','helper']});
const env={PATH:process.env.PATH,HOME:process.env.HOME,TMPDIR:process.env.TMPDIR,MOSAIC_CONFIG:f.config,MOSAIC_ROLES_DIR:f.roles};
const configDir=join(root,'config'),prefix=`msk-${id(configDir)}-`,sentinel=prefix+'sentinel-'+randomBytes(32).toString('base64url');history.add(sentinel);
+1 -1
View File
@@ -176,7 +176,7 @@ test('proxy-stop-failed: old proxy key and retiring record survive a failed grac
async function bootBusiness(f, binding = {}) {
const rolesDir = rolesCopy(f.root), system = systemFor(f.root), doc = businessDoc(f.root, 'demo');
// Aide is needed because shipped chief hireable:[] and security-reviewer has review.verdict outside HIRE_ALLOWED.
fs.writeFileSync(join(rolesDir, 'aide.md'), '# Aide\n');
fs.writeFileSync(join(rolesDir, 'aide.md'), '# Aide\n\n## Authority\n');
writeJson(join(rolesDir, 'aide.json'), {
roleVersion: 2, name: 'aide', title: 'Aide', contract: 'aide.md', tools: ['read'], network: 'api-only',
authority: { withinRole: ['message.send'], crossRole: [] }, credentials: [],
@@ -15,7 +15,7 @@ async function hireFixture(t,{revoking=false}={}){
if(revoking){const r=old.credentials.modelRecord('demo/orphan');old.credentials.writeModel('demo/orphan',{...r,state:'revoking',revokingAt:'2026-10-25T00:00:00.000Z'});}old.credentials.close();
const rolesDir=rolesCopy(f.root),system=systemFor(f.root),doc=businessDoc(f.root,'demo');
// Scratch aide: shipped chief hireable:[] and review.verdict is outside HIRE_ALLOWED.
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n\n## Authority\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});
const chiefFile=join(rolesDir,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide']});doc.plugins={...doc.plugins,harness:['pi']};
const file=writeJson(join(f.configDir,'businesses','demo.json'),doc);fs.writeFileSync(join(f.configDir,'secrets','proxy-management.key'),'fx651-manage',{mode:0o600});
const business=loadBusiness('demo',{dir:f.configDir,rolesDir}),resolved={};for(const instance of Object.keys(business.roles))resolved[instance]=resolveInstance({system,business,instance});
+1 -1
View File
@@ -22,7 +22,7 @@ export async function pendingFixture(t,{twoBusinesses=false}={}){
const configDir=join(root,'config'),dataRoot=join(root,'data'),rolesDir=rolesCopy(root),system=systemFor(root);fs.mkdirSync(dataRoot,{mode:0o700});
const doc=businessDoc(root,'demo');doc.plugins={...doc.plugins,harness:['pi']};
// Existing row331 scratch aide pattern only; repository role authority unchanged.
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});const chiefFile=join(rolesDir,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide']});
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n\n## Authority\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});const chiefFile=join(rolesDir,'chief-of-staff.json');writeJson(chiefFile,{...JSON.parse(fs.readFileSync(chiefFile,'utf8')),hireable:['aide']});
const ids=twoBusinesses?['demo','other']:['demo'],files={},businesses={};for(const name of ids){const document={...structuredClone(doc),id:name};files[name]=writeJson(join(configDir,'businesses',name+'.json'),document);const business=loadBusiness(name,{dir:configDir,rolesDir}),resolved={};for(const instance of Object.keys(business.roles))resolved[instance]=resolveInstance({system,business,instance});businesses[name]=busOrganization(business,resolved);}
fs.mkdirSync(join(configDir,'secrets'),{recursive:true,mode:0o700});fs.writeFileSync(join(configDir,'secrets','proxy-management.key'),'fx654-manage',{mode:0o600});fs.mkdirSync(join(dataRoot,'bus-host'),{recursive:true,mode:0o700});writeJson(sessionsFile(dataRoot),{sessionsVersion:SESSIONS_VERSION,sessions:[]});
const sentinel=`msk-${hash(configDir).slice(0,12)}-sentinel-${randomBytes(32).toString('base64url')}`;state.keys.push(sentinel);history.add(sentinel);
+1 -1
View File
@@ -7,7 +7,7 @@ import { join } from 'node:path';
import { Broker } from '../src/broker.mjs';
import { Store } from '../src/store.mjs';
const role={authority:{withinRole:['message.send'],crossRole:[]}};
function fixture(t,proxyInit){const store=new Store(mkdtempSync(join(tmpdir(),'p1-bus-')));t.after(()=>store.close());const b=new Broker({store,proxyInit,businesses:{demo:{id:'demo',human:'owner',arbiters:{technical:'coder',delivery:'coder'},vars:{'bus.tester':true},roles:{coder:role}}}});return {store,b,human:b.bindHuman({business:'demo',human:'owner',via:'cli',outsideAgent:true}),tester:b.bindTester({business:'demo'}),reader:b.bindReader({business:'demo'}),agent:b.bindLaunch({business:'demo',role:'coder',run:'p1-run',harness:'pi'})};}
function fixture(t,proxyInit){const store=new Store(mkdtempSync(join(tmpdir(),'p1-bus-')));t.after(()=>store.close());const b=new Broker({store,proxyInit,businesses:{demo:{id:'demo',human:'owner',arbiters:{technical:'chief',delivery:'chief'},vars:{'bus.tester':true},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:role}}}});return {store,b,human:b.bindHuman({business:'demo',human:'owner',via:'cli',outsideAgent:true}),tester:b.bindTester({business:'demo'}),reader:b.bindReader({business:'demo'}),agent:b.bindLaunch({business:'demo',role:'coder',run:'p1-run',harness:'pi'})};}
test('proxy.init only accepts path-bound outside-agent human; refuses agent/tester/reader',async t=>{const calls=[];const f=fixture(t,async a=>{calls.push(a);return {port:a.port,binary:'/fixture'};});for(const cap of [f.tester,f.agent])assert.throws(()=>f.b.request(cap,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='human-required');assert.throws(()=>f.b.request(f.reader,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='read-only');assert.equal(calls.length,0);assert.deepEqual(await f.b.request(f.human,{verb:'proxy.init',args:{key:'p1-management',port:18432}}),{port:18432,binary:'/fixture'});assert.equal(JSON.stringify(f.store.all('SELECT body FROM events')).includes('p1-management'),false);});
for(const [code,args]of [['proxy-key-invalid',{key:'a\nb'}],['proxy-config-invalid',{key:'p1-management',port:8317}],['invalid-request',{key:'p1-management',binary:'/forged'}]])test(`proxy.init ${code}`,t=>{const f=fixture(t,()=>{throw Error('must not call');});assert.throws(()=>f.b.request(f.human,{verb:'proxy.init',args}),e=>e.code===code);});
test('proxy.init not-configured',t=>{const f=fixture(t,null);assert.throws(()=>f.b.request(f.human,{verb:'proxy.init',args:{key:'p1-management'}}),e=>e.code==='not-configured');});
@@ -51,7 +51,7 @@ export async function revocationFixture(t,{runtime=false,onRoot=()=>{},missingBo
let boot,writer,chief,human,busBoots=0;
if(runtime){
const rolesDir=rolesCopy(root),system=systemFor(root),doc=businessDoc(root,'demo');doc.plugins={...doc.plugins,harness:['pi']};doc.roles.aide={definition:'aide',vars:{harness:'pi',model:'claude-opus-5-5',thinking:'high'}};doc.launch.instances.push('aide');
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});writeJson(join(configDir,'businesses','demo.json'),doc);
fs.writeFileSync(join(rolesDir,'aide.md'),'# Aide\n\n## Authority\n');writeJson(join(rolesDir,'aide.json'),{roleVersion:2,name:'aide',title:'Aide',contract:'aide.md',tools:['read'],network:'api-only',authority:{withinRole:['message.send'],crossRole:[]},credentials:[]});writeJson(join(configDir,'businesses','demo.json'),doc);
const business=loadBusiness('demo',{dir:configDir,rolesDir}),resolved={};for(const name of Object.keys(business.roles))resolved[name]=resolveInstance({system,business,instance:name});
boot={dataRoot,businesses:{demo:busOrganization(business,resolved)},organization:{dir:configDir,rolesDir,system},proxy:{port,binary:'/fixture-unused'}};writer=organizationWriter({dir:configDir,rolesDir});
stderr=process.stderr.write;process.stderr.write=function(chunk,...args){if(String(chunk).startsWith('model-proxy: ')){receipts.push(String(chunk));args.find(a=>typeof a==='function')?.();return true;}return stderr.call(this,chunk,...args);};
+1 -1
View File
@@ -24,7 +24,7 @@ const demo = () => ({
demo: {
id: 'demo',
human: 'jason',
arbiters: { technical: 'lead', delivery: 'chief' },
arbiters: { technical: 'chief', delivery: 'chief' },
roles,
definitions: { reviewer: {} },
vars: { 'bus.tester': true },
+1 -1
View File
@@ -9,7 +9,7 @@ import { createLimits, modelUsageKey } from '../src/limits.mjs';
const key='012345abcdef';
test('P5 broker preserves original plugin subject and derives internal run context from authenticated native caller',async t=>{
const root=mkdtempSync(join(tmpdir(),'p5-context-')),store=new Store(root),asked=[];t.after(()=>{store.close();rmSync(root,{recursive:true,force:true});});
const b=new Broker({store,businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},
const b=new Broker({store,businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},
limits:{mustStop:async(...args)=>{asked.push(args);return {stop:false};}},usage:{collect:async()=>({recorded:true}),view:()=>({})}});
const human=b.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const cap=b.bindLaunch({business:'demo',role:'coder',run:'native-run',harness:'pi',provider:'fixture',model:'fake'});b.request(cap,{verb:'role.claim'});
+2 -2
View File
@@ -30,7 +30,7 @@ test('P5 real runtime binds stack usage to issued key, daily stops at stack coun
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));let bus;
t.after(async()=>{await bus?.close();await new Promise(r=>server.close(r));rmSync(root,{recursive:true,force:true});});
bus=await startBroker({dataRoot:data,businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
bus=await startBroker({dataRoot:data,businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
organization:{dir:config,rolesDir,system:systemFor(config)},proxy:{port:server.address().port,binary:'/fixture-unused'}});
assert.equal(calls.some(p=>p.includes('usage/queue')),false);
const model=await bus.modelPreflight({business:'demo',role:'coder'}),keyId=hash(model.key);
@@ -77,7 +77,7 @@ test('R4 real runtime: a quota spent on A and moved to B with no reset still ref
});
await new Promise(r=>server.listen(0,'127.0.0.1',r));let bus;
t.after(async()=>{await bus?.close();await new Promise(r=>server.close(r));rmSync(root,{recursive:true,force:true});});
bus=await startBroker({dataRoot:data,businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
bus=await startBroker({dataRoot:data,businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
organization:{dir:config,rolesDir,system:systemFor(config)},proxy:{port:server.address().port,binary:'/fixture-unused'}});
const stored=()=>JSON.parse(readFileSync(join(config,'businesses/demo.json'),'utf8')).launch.limits.tokens.keys;
const may=async()=>(await bus.mayLaunch({business:'demo',instance:'coder',provider:'fixture',model:'fake'})).allow;
+1 -1
View File
@@ -64,7 +64,7 @@ export async function telemetryFixture(t, { foreign = false, hold = false, waitM
});
await new Promise(resolve=>server.listen(0,'127.0.0.1',resolve));
let bus=null,startPromise=null,closePromise=null;
const start=()=>startPromise??=(startBroker({dataRoot:data,...(orgMode ? {instanceRoot:root} : {}),businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
const start=()=>startPromise??=(startBroker({dataRoot:data,...(orgMode ? {instanceRoot:root} : {}),businesses:{demo:{id:'demo',human:'jason',plugins:doc.plugins,arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{definition:'coder',authority:{withinRole:[],crossRole:[]}}}}},
organization:{dir:config,rolesDir,system:systemFor(config)},proxy:{port:server.address().port,binary:'/fixture-unused'}}).then(value=>(bus=value)));
const release=(fail=false)=>{state.hold=false;for(const res of state.held.splice(0)){if(res.destroyed)continue;if(fail){res.writeHead(503);res.end('{}');}else answerQueue(res);}};
const close=()=>bus?(closePromise??=bus.close()):Promise.resolve();
+3 -3
View File
@@ -37,7 +37,7 @@ test('runtime: the switch changes the organization file, the receipt carries the
const rolesDir = rolesCopy(root);
writeJson(join(root, 'businesses/demo.json'), businessDoc(root, 'demo'));
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const file = join(root, 'businesses/demo.json');
@@ -72,7 +72,7 @@ test('vikunja:19/309 through the broker, a patch switches the kinds it names and
const rolesDir = rolesCopy(root);
writeJson(join(root, 'businesses/demo.json'), { ...businessDoc(root, 'demo'), plugins: { 'usage-source': ['meter'] } });
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const log = () => readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
@@ -97,7 +97,7 @@ test('plugins read advertises loadable harnesses even when switched off', async
const root=mkdtempSync(join(tmpdir(),'plugins-available-'));t.after(()=>rmSync(root,{recursive:true,force:true}));
const rolesDir=rolesCopy(root);const doc=businessDoc(root,'demo');doc.plugins={harness:['claude-code']};
writeJson(join(root,'businesses/demo.json'),doc);mkdirSync(join(root,'data'),{mode:0o700});
const host=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)}});t.after(()=>host.close());
const host=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)}});t.after(()=>host.close());
const human=host.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const view=host.broker.request(human,{verb:'plugins'});
assert.deepEqual(view.plugins.harness,['claude-code']);assert.ok(view.available.harness.includes('pi'));assert.ok(view.available.harness.includes('claude-code'));
@@ -10,8 +10,8 @@ import { Client } from '../src/client.mjs';
import { registerRuntime, updateRuntime, processIdentity } from '../../processing-control/src/runtimes.mjs';
import { readControl } from '../../processing-control/src/state.mjs';
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' },
roles: { coder: { authority: { withinRole: ['message.send'], crossRole: [] } } } } };
const businesses = { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: ['message.send'], crossRole: [] } } } } };
async function broker(root) {
const r = await startBroker({ dataRoot: root, businesses, readers: ['demo'] });
const cap = r.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
+1 -1
View File
@@ -13,7 +13,7 @@ const state = await import('../../processing-control/src/state.mjs').catch(e =>
throw e;
});
const businesses = { demo: { id: 'demo', human: 'jason', vars: { 'bus.tester': true },
arbiters: { technical: 'coder', delivery: 'coder' }, roles: {
arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { definition: 'coder', authority: { withinRole: ['message.send', 'record.write'], crossRole: [] } },
} } };
function fixture(t, extra = {}) {
+1 -1
View File
@@ -17,7 +17,7 @@ import { Broker, TESTER_VERBS, USER_VERBS, VIEWER_VERBS } from '../src/broker.mj
const shipped = (name) => JSON.parse(readFileSync(new URL(`../../../roles/${name}.json`, import.meta.url), 'utf8'));
const AT = '2026-11-05T00:00:00.000Z';
const business = {
id: 'demo', human: 'jason', arbiters: { technical: 'continuity', delivery: 'continuity' },
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' },
roles: {
continuity: { definition: 'continuity', authority: { withinRole: ['message.send'], crossRole: [] } },
chief: { definition: 'chief-of-staff', authority: { withinRole: ['message.send'], crossRole: [] } },
+1 -1
View File
@@ -10,7 +10,7 @@ function fixture(t, project, secretCheck = () => {}) {
const root = mkdtempSync(join(tmpdir(), 'project-await-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const business = { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: {
const business = { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { definition: 'coder', authority: { withinRole: ['record.write'], crossRole: [] } },
} };
const broker = new Broker({ store, businesses: { demo: business }, project, secretCheck });
+1 -1
View File
@@ -64,7 +64,7 @@ test('real startBroker injects the project adapter for socket reads and bound-au
t.after(async () => { await runtime?.close(); rmSync(root, { recursive: true, force: true }); });
// Row 65: the bound run's harness is switched on.
const business = { id: 'demo', human: 'jason', plugins: { harness: ['pi'] }, projects: { stack: { root: join(root, 'code') } },
arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: {
arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: {
definition: 'coder', authority: { withinRole: ['record.write'], crossRole: [] },
} } };
const initialized = await initProject({ business, project: 'stack', dataRoot: root });
@@ -15,7 +15,7 @@ test('role must not materialize a new catalogue key by submitting legacy entry s
const offered={key:'zai-fixture-new',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-new',label:'Fixture',available:true,reason:null,harnesses:['pi']};
let status='ready';
const modelCatalogue={invalidate(){},read:async()=>({status,models:[offered],notices:[]})};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}},chief:{definition:'chief-of-staff',authority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue});t.after(()=>bus.close());
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{coder:{authority:{withinRole:[],crossRole:[]}},chief:{definition:'chief-of-staff',authority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue});t.after(()=>bus.close());
const db=new DatabaseSync(join(root,'data/bus/bus.sqlite'),{readOnly:true});t.after(()=>db.close());
const allowed=()=>db.prepare("SELECT count(*) AS n FROM events WHERE kind='action.allowed' AND json_extract(body,'$.action')='organization.change'").get().n;
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
@@ -20,7 +20,7 @@ async function fixture(t, options={}) {
if(options.fallback)Object.assign(doc.roles.coder.vars,{'fallback.harness':'pi','fallback.model':'existing-alias','fallback.thinking':'low'});
writeJson(file,doc);fs.mkdirSync(join(root,'data'),{mode:0o700});
const modelCatalogue={invalidate(){},read:async()=>({status:'ready',models:[offer],notices:[]})};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{...Object.fromEntries(Object.keys(doc.roles).map(key=>[key,{authority:{withinRole:[],crossRole:[]}}])),chief:{definition:'chief-of-staff',authority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue});t.after(()=>bus.close());
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{...Object.fromEntries(Object.keys(doc.roles).map(key=>[key,{authority:{withinRole:[],crossRole:[]}}])),chief:{definition:'chief-of-staff',authority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue});t.after(()=>bus.close());
const db=new DatabaseSync(join(root,'data/bus/bus.sqlite'),{readOnly:true});t.after(()=>db.close());
const count=()=>db.prepare("SELECT count(*) AS n FROM events WHERE kind='action.allowed' AND json_extract(body,'$.action')='organization.change'").get().n;
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
+2 -2
View File
@@ -18,8 +18,8 @@ function setup(t, proxyPage = async () => ({ ok: true })) {
access: () => ({ accessVersion: 1, revision: 1, users: [], grants: [
{ id: 'g-1', user: 'alice', business: 'demo', projects: ['*'], instances: ['coder'], level: 'collaborator' },
] }),
businesses: { demo: { id: 'demo', human: 'owner', arbiters: { technical: 'coder', delivery: 'coder' },
vars: { 'bus.tester': true }, roles: { coder: { definition: 'coder', authority: { withinRole: ['message.send'], crossRole: [] } } } } },
businesses: { demo: { id: 'demo', human: 'owner', arbiters: { technical: 'chief', delivery: 'chief' },
vars: { 'bus.tester': true }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: { withinRole: ['message.send'], crossRole: [] } } } } },
});
const caps = {
human: broker.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }),
+2 -2
View File
@@ -24,9 +24,9 @@ function businesses(upstream = 'https://git.example/mosaic/stack') {
demo: {
id: 'demo',
human: 'jason',
arbiters: { technical: 'reviewer', delivery: 'reviewer' },
arbiters: { technical: 'chief', delivery: 'chief' },
projects: { stack: { root: '/nonexistent', remote: upstream }, books: { root: '/nonexistent-books' } },
roles: {
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: { authority: pusher, identity: identity('acme-coder'), publish: ['stack'] },
second: { authority: pusher, identity: identity('acme-second'), publish: ['stack'] },
plain: { authority: pusher },
+2 -2
View File
@@ -211,13 +211,13 @@ test('T14 reloadBusiness refuses an unknown business, a changed human and a docu
const root = mkdtempSync(join(tmpdir(), 'reload-unit-'));
t.after(() => rmSync(root, { recursive: true, force: true }));
const role = { authority: { withinRole: [], crossRole: [] } };
const demo = { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: role } };
const demo = { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: role } };
const b = new Broker({ store: new Store(root), businesses: { demo } });
assert.equal(code(() => b.reloadBusiness('other', demo)), 'unknown-business');
assert.equal(code(() => b.reloadBusiness('demo', { ...demo, human: 'mos' })), 'invalid-business');
assert.equal(code(() => b.reloadBusiness('demo', { ...demo, roles: { coder: role, tester: role } })), 'invalid-business');
assert.equal(code(() => b.reloadBusiness('demo', { ...demo, roles: { coder: { authority: { withinRole: ['spend'], crossRole: [] } } } })), 'invalid-authority');
assert.equal(code(() => b.reloadBusiness('demo', null)), 'invalid-business');
b.reloadBusiness('demo', { ...demo, roles: { coder: role, 'sec-1': role } });
b.reloadBusiness('demo', { ...demo, roles: { ...demo.roles, 'sec-1': role } });
assert.ok(b.bindLaunch({ business: 'demo', role: 'sec-1', run: 'run-1', harness: 'pi' }));
});
@@ -0,0 +1,82 @@
// RR (#1873): resolution checks current role rights, and callers cannot choose a merge/policy arbiter.
import test from 'node:test';
import assert from 'node:assert/strict';
import { loadRole } from '../../organization/src/index.mjs';
import { REPO_ROLES } from '../../organization/tests/helpers.mjs';
import { fixture } from './spec-fixtures.mjs';
import { grantDecision } from './host-fixtures.mjs';
async function setup(t, { shared = false } = {}) {
const roles = Object.fromEntries(['cto', 'pm', 'chief-of-staff'].map(definition => {
const role = loadRole(REPO_ROLES, definition);
return [definition === 'chief-of-staff' ? 'chief' : definition, { definition, authority: structuredClone(role.authority) }];
}));
roles.coder = { definition: 'coder', authority: { withinRole: ['message.send', 'record.write'], crossRole: ['task.scope.change', 'git.merge.protected', 'policy.change'] } };
const f = await fixture(t, { roles, arbiters: { technical: 'cto', delivery: shared ? 'cto' : 'pm' } });
return { ...f, roles, cto: f.agent('cto'), pm: f.agent('pm'), chief: f.chief };
}
for (const [domain, resolver] of [['technical', 'cto'], ['delivery', 'pm']]) {
test(`RR: ${domain} arbiter that loses its right refuses not-resolver`, async t => {
const f = await setup(t);
const d = f.raise({ action: 'task.scope.change', domain });
f.business.roles[resolver].authority.withinRole = ['message.send'];
f.broker.reloadBusiness('demo', f.business);
assert.throws(() => f.call(f[resolver], 'decision.resolve', { id: d.id, choice: 'yes' }), error => {
assert.equal(error.code, 'not-resolver');
assert.match(error.reason, new RegExp(`${resolver}.*${resolver}.*decision\\.resolve\\.${domain}`));
return true;
});
assert.equal(f.store.get('SELECT count(*) n FROM decision_events WHERE decision=?', d.id).n, 0);
});
}
test('RR: a shared arbiter cannot resolve a delivery decision using only its technical right', async t => {
const f = await setup(t, { shared: true });
const d = f.raise({ action: 'task.scope.change', domain: 'delivery' });
assert.equal(d.route_to, 'cto');
assert.throws(() => f.call(f.cto, 'decision.resolve', { id: d.id, choice: 'yes' }), { code: 'not-resolver' });
});
test('RR: merge raised with delivery still routes to CTO and resolves (acceptance 314 step 5)', async t => {
const f = await setup(t);
const d = await f.raise({ action: 'git.merge.protected', domain: 'delivery', project: 'stack', spec_path: 'spec/north-star.md', spec_commit: f.commit });
assert.equal(d.route_to, 'cto');
assert.throws(() => f.call(f.pm, 'decision.resolve', { id: d.id, choice: 'yes' }), { code: 'not-resolver' });
f.call(f.cto, 'decision.resolve', { id: d.id, choice: 'yes' });
assert.equal(f.store.get('SELECT op FROM decision_events WHERE decision=?', d.id).op, 'resolved');
});
for (const resolver of ['chief', 'pm']) {
test(`RR: ${resolver} named delivery arbiter resolves policy.change even when raised technical`, async t => {
const f = await setup(t); f.business.arbiters.delivery = resolver;
f.broker.reloadBusiness('demo', f.business);
const d = f.raise({ action: 'policy.change', domain: 'technical' });
assert.equal(d.route_to, resolver);
f.call(f[resolver], 'decision.resolve', { id: d.id, choice: 'yes' });
assert.equal(f.store.get('SELECT by FROM decision_events WHERE decision=?', d.id).by, `${resolver}-run`);
});
}
test('RR: the chief holds both rights when named for both domains', async t => {
const f = await setup(t); f.business.arbiters = { technical: 'chief', delivery: 'chief' };
f.broker.reloadBusiness('demo', f.business);
for (const action of ['git.merge.protected', 'policy.change']) {
const d = f.raise({ action }); assert.equal(d.route_to, 'chief');
f.call(f.chief, 'decision.resolve', { id: d.id, choice: 'yes' });
}
});
// Old raise evidence did not preserve domain. Current assignments cannot prove the old domain.
test('RR: remapping arbiters cannot let a legacy decision resolve with only the other domain right', async t => {
const f = await setup(t);
const id = grantDecision(f.store, { action: 'task.scope.change', route: 'pm', domain: null, close: null });
f.business.arbiters = { technical: 'pm', delivery: 'cto' };
f.business.roles.pm.authority.withinRole = ['message.send', 'decision.resolve.technical'];
f.broker.reloadBusiness('demo', f.business);
assert.throws(() => f.call(f.pm, 'decision.resolve', { id, choice: 'yes' }), { code: 'not-resolver' });
assert.equal(f.store.get('SELECT count(*) n FROM decision_events WHERE decision=?', id).n, 0);
// Human recovery closes the old route by withdrawing it, before the raiser creates a new decision.
f.call(f.human, 'decision.withdraw', { id });
assert.equal(f.store.get('SELECT op FROM decision_events WHERE decision=?', id).op, 'withdrawn');
});
+1 -1
View File
@@ -28,7 +28,7 @@ test('C2-10c check hydration preserves unchecked rows and one broadcast with two
const messaging = JSON.parse(readFileSync(new URL('../../../roles/companion.json', import.meta.url))).messaging;
const role = { definition: 'companion', authority: { withinRole: ['message.send'], crossRole: [] } };
const broker = new Broker({ store, businesses: { demo: { id: 'demo', human: 'jason', vars: {},
arbiters: { technical: 'bean', delivery: 'bean' }, roles: { bean: role, chet: role }, definitions: { companion: { messaging } } } } });
arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, bean: role, chet: role }, definitions: { companion: { messaging } } } } });
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const call = (verb, args) => broker.request(human, { verb, args });
const project = call('project.create', { name: 'Room' }).id;
+4 -4
View File
@@ -110,7 +110,7 @@ test('row 65 runtime: routes and routes.set reach the organization file with the
const file = join(root, 'businesses/demo.json');
writeJson(file, doc);
mkdirSync(join(root, 'data'), { mode: 0o700 });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(() => bus.close());
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const log = () => readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').filter(Boolean).map(JSON.parse);
@@ -157,7 +157,7 @@ test('fixture provider catalogue appears in actual routes and materializes the s
const file = join(root, 'businesses/demo.json'); writeJson(file, doc); mkdirSync(join(root, 'data'), { mode: 0o700 });
const offered={ key:'zai-fixture-id',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'fixture-id',available:true,reason:null,harnesses:['pi'] };
const catalogues={ read:async()=>({status:'ready',observedAt:'2026-11-06T00:00:00.000Z',models:[offered],notices:[]}),invalidate:()=>{} };
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}} } }, organization:{dir:root,rolesDir,system:systemFor(root)}, modelCatalogue:catalogues });
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: { id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}} } }, organization:{dir:root,rolesDir,system:systemFor(root)}, modelCatalogue:catalogues });
t.after(() => bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const before=await bus.broker.request(human,{verb:'routes'});
@@ -179,7 +179,7 @@ for (const writer of ['limits.set','organization.set','instances.set']) test(`${
let mode='absent';
const offered={key:'fixture-persisted',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'fixture',available:true,reason:null,harnesses:['pi']};
const catalogue={read:async()=>({status:'ready',observedAt:'2026-11-06T00:00:00.000Z',models:mode==='absent'?[]:[{...offered,harnesses:mode==='incompatible'?['claude-code']:['pi']}],notices:[]}),invalidate(){}};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}},chief:{definition:'chief-of-staff',authority:chiefAuthority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:catalogue});t.after(()=>bus.close());
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{coder:{authority:{withinRole:[],crossRole:[]}},chief:{definition:'chief-of-staff',authority:chiefAuthority}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:catalogue});t.after(()=>bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});
const cap=writer==='organization.set'?bus.broker.bindLaunch({business:'demo',role:'chief',run:'provider-chief',harness:'pi'}):human;
if(writer==='organization.set')bus.broker.request(cap,{verb:'role.claim'});
@@ -201,7 +201,7 @@ test('legacy routes follow enabled module mappings while unchanged unavailable s
writeJson(join(root,'businesses/demo.json'),doc);mkdirSync(join(root,'data'),{mode:0o700});
let state='ready';const offered={key:'zai-fixture-two',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-two',label:'fixture',available:true,reason:null,harnesses:['pi']};
const catalogue={read:async()=>({status:state,observedAt:'2026-11-06T00:00:00.000Z',models:[offered],notices:[]}),invalidate(){}};
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:catalogue});t.after(()=>bus.close());
const bus=await startBroker({dataRoot:join(root,'data'),businesses:{demo:{id:'demo',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir:root,rolesDir,system:systemFor(root)},modelCatalogue:catalogue});t.after(()=>bus.close());
const human=bus.broker.bindHuman({business:'demo',human:'jason',via:'cli',outsideAgent:true});const request=(verb,args)=>bus.broker.request(human,{verb,args});
let view=await request('routes');assert.deepEqual(view.catalogue.legacy['legacy-one'],{module:'cliproxy',upstream:'claude',model:'fixture-one'});
const pick=(harness,model,fallback=null)=>({harness,model,thinking:'low',fallback});
+2 -2
View File
@@ -12,8 +12,8 @@ const businesses = {
arbiters: { technical: 'cto', delivery: 'pm' },
roles: {
coder: { authority: { withinRole: ['message.send'], crossRole: ['task.scope.change'] } },
cto: { authority: { withinRole: [], crossRole: [] } },
pm: { authority: { withinRole: [], crossRole: [] } },
cto: { authority: { withinRole: ['decision.resolve.technical'], crossRole: [] } },
pm: { authority: { withinRole: ['decision.resolve.delivery'], crossRole: [] } },
},
},
};
+4 -3
View File
@@ -7,7 +7,7 @@ import { Broker } from '../src/broker.mjs';
import { initProject, readSpec, writeRecord, listProjectFiles, readProjectFile, writeHumanFile, ProjectError } from '../../project/src/index.mjs';
export const git = (repo, ...args) => execFileSync('git', ['-c', 'core.hooksPath=/dev/null', '-c', 'maintenance.auto=false', '-c', 'gc.auto=0', '-C', repo, ...args], { encoding: 'utf8' }).trim();
const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }];
export async function fixture(t, { adapter = true, roles = {}, arbiters = { technical: 'reviewer', delivery: 'reviewer' }, access = null } = {}) {
export async function fixture(t, { adapter = true, roles = {}, arbiters = { technical: 'chief', delivery: 'chief' }, access = null } = {}) {
const root = mkdtempSync(join(tmpdir(), 'bus-spec-'));
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
@@ -16,6 +16,7 @@ export async function fixture(t, { adapter = true, roles = {}, arbiters = { tech
projects: { stack: { root: join(root, 'code'), repo: join(root, 'records') } }, roles: {
coder: { definition: 'coder', authority: authority(['message.send', 'record.write']) },
reviewer: { definition: 'reviewer', authority: authority(['message.send']) },
chief: { definition: 'chief-of-staff', authority: authority(['message.send', 'decision.resolve.technical', 'decision.resolve.delivery']) },
...roles,
} };
const { repo, commit } = await initProject({ business, project: 'stack', dataRoot: root });
@@ -31,7 +32,7 @@ export async function fixture(t, { adapter = true, roles = {}, arbiters = { tech
const cap = broker.bindLaunch({ business: 'demo', role, run: role + '-run', harness: 'pi', address: role });
broker.request(cap, { verb: 'role.claim' }); return cap;
};
const coder = agent('coder'), reviewer = agent('reviewer');
const coder = agent('coder'), reviewer = agent('reviewer'), chief = agent('chief');
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const call = (cap, verb, args = {}) => broker.request(cap, { verb, args });
const raise = args => call(coder, 'decision.raise', { action: 'deploy', question: 'Ship?', options,
@@ -42,5 +43,5 @@ export async function fixture(t, { adapter = true, roles = {}, arbiters = { tech
git(repo, '-c', `user.name=${author}`, '-c', '[email protected]', 'commit', '-m', 'Spec');
return git(repo, 'rev-parse', 'HEAD');
};
return { root, store, business, broker, repo, commit, agent, coder, reviewer, human, call, raise, save };
return { root, store, business, broker, repo, commit, agent, coder, reviewer, chief, human, call, raise, save };
}
+1 -1
View File
@@ -19,7 +19,7 @@ async function setup(t, { running = ['chief', 'pm', 'cto'], thread = true } = {}
cto: { definition: 'cto', ...within }, assistant: { definition: 'assistant', ...within },
} });
f.business.definitions = { assistant: { messaging: shipped('assistant').messaging } };
f.cap = Object.fromEntries(running.map(role => [role, f.agent(role)]));
f.cap = Object.fromEntries(running.map(role => [role, role === 'chief' ? f.chief : f.agent(role)]));
if (thread) {
const project = f.call(f.human, 'project.create', { name: 'stack work', repo: 'stack' }).id;
f.threadId = f.call(f.human, 'thread.create', { project, name: 'alpha' }).id;
+3 -3
View File
@@ -139,7 +139,7 @@ test('REQ-COS-1: the request goes to the business launcher, and only when that i
// launcher `chief`, so a check on the name passes there; these use names that differ from the definitions.
test('REQ-COS-1: the sender is judged by definition: an instance `lead` of the pm definition can ask, an instance `pm` of the coder definition can\'t', (t) => {
const lead = setup(t, {
arbiters: { technical: 'lead', delivery: 'chief' },
arbiters: { technical: 'chief', delivery: 'chief' },
roles: {
chief: { definition: 'chief-of-staff', authority: authority.chief },
lead: { definition: 'pm', authority: authority.pm },
@@ -172,9 +172,9 @@ test('REQ-COS-1: the sender is judged by definition: an instance `lead` of the p
test('REQ-COS-1: the launcher is judged by definition: an instance `boss` of the chief-of-staff definition receives, a `chief` of the coder definition gives no-chief-of-staff', (t) => {
const boss = setup(t, {
arbiters: { technical: 'pm', delivery: 'boss' },
arbiters: { technical: 'pm', delivery: 'chief' },
launch: { by: 'boss', instances: ['coder'], max: { opus: 2, sonnet: 2 } },
roles: {
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
boss: { definition: 'chief-of-staff', authority: authority.chief },
pm: { definition: 'pm', authority: authority.pm },
coder: { definition: 'coder', ...none },
+1 -1
View File
@@ -91,7 +91,7 @@ test('runtime reads the current organization file for project selections and con
doc.plugins = { 'system-type': ['coding'] };
writeJson(join(root, 'businesses/demo.json'), doc);
mkdirSync(join(root, 'data'), { mode: 0o700 });
const business = { id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } };
const business = { id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } };
const bus = await startBroker({ dataRoot: join(root, 'data'), businesses: { demo: business }, organization: { dir: root, rolesDir, system: systemFor(root) } });
t.after(async () => { await bus.close(); rmSync(root, { recursive: true, force: true }); });
const human = bus.broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
+4 -4
View File
@@ -46,8 +46,8 @@ async function runtime(t) {
dataRoot: join(root, 'data'),
businesses: {
demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, vars: { 'bus.tester': true },
roles: { coder: { definition: 'coder', ...none }, assistant: { definition: 'assistant', ...none } },
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, vars: { 'bus.tester': true },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', ...none }, assistant: { definition: 'assistant', ...none } },
},
},
organization: { dir: root, rolesDir, system: systemFor(root) },
@@ -166,8 +166,8 @@ function broker(t, vars = { 'bus.tester': true }) {
store,
businesses: {
demo: {
id: 'demo', human: 'jason', arbiters: { technical: 'lead', delivery: 'lead' }, ...(vars ? { vars } : {}),
roles: { dev: { definition: 'coder', ...none }, lead: { definition: 'pm', ...none } },
id: 'demo', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, ...(vars ? { vars } : {}),
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, dev: { definition: 'coder', ...none }, lead: { definition: 'pm', ...none } },
},
},
});
+1 -1
View File
@@ -8,7 +8,7 @@ import { Broker } from '../src/broker.mjs';
test('REQ-LIMIT-4 usage protocol binds run identity, requires a delivered turn and isolates businesses', async t => {
const root = mkdtempSync(join(tmpdir(), 'usage-broker-'));
const store = new Store(root); t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const businesses = { demo: { id: 'demo', human: 'jason', roles: { coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } };
const businesses = { demo: { id: 'demo', human: 'jason', roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'chief', delivery: 'chief' } } };
const recorded = [];
const broker = new Broker({ store, businesses, usage: {
collect: async (...args) => { recorded.push(args); return { recorded: true }; },
+1 -1
View File
@@ -12,7 +12,7 @@ test('REQ-PLUG-1 runtime reaches transcript only through loader, over the real b
t.after(async () => { await runtime?.close(); rmSync(root, { recursive: true, force: true }); });
const dir = join(root, 'launches/demo/run1/session'); mkdirSync(dir, { recursive: true });
const business = { id: 'demo', human: 'jason', plugins: { 'usage-source': ['transcript'], harness: ['pi'] },
roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } };
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'chief', delivery: 'chief' } };
runtime = await startBroker({ dataRoot: root, businesses: { demo: business }, launches: [{
// Row 65: the launch names where its harness keeps the session files.
business: 'demo', role: 'coder', run: 'run1', harness: 'pi', provider: 'other-provider', model: 'other-model', transcript: dir,
+2 -2
View File
@@ -65,10 +65,10 @@ test("merge-gate's technical raise is cross-role, goes to the cto, and only merg
assert.throws(() => b.authorize(caps['merge-gate'], 'git.merge.protected', { decision: d.id, target: TASK }), /decision-consumed/);
});
test("merge-gate has no message.send, and without a domain its raise goes to the delivery arbiter", (t) => {
test("merge-gate has no message.send, and without a domain its merge raise still goes to the technical arbiter", (t) => {
const { call, raise } = setup(t);
assert.throws(() => call('merge-gate', 'message.send', { to: 'cto', body: 'landed' }), /decision-required/);
assert.equal(raise('merge-gate', 'git.merge.protected', { target: TASK }).route_to, 'pm');
assert.equal(raise('merge-gate', 'git.merge.protected', { target: TASK }).route_to, 'cto');
// Every other action is gated for merge-gate, so an uncited raise goes to the human.
const push = raise('merge-gate', 'git.push.working', { domain: 'technical', target: TASK });
assert.deepEqual([push.class, push.route_to], ['gated', 'human']);
+1 -1
View File
@@ -290,7 +290,7 @@ test("the real human-cli, run from agent ancestry, refuses and writes nothing",
const config = join(root, "config");
const bus = await startBroker({
dataRoot: f.dataRoot,
businesses: { acme: { id: "acme", human: "jason", arbiters: { technical: "coder", delivery: "coder" }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } },
businesses: { acme: { id: "acme", human: "jason", arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } },
organization: { dir: config, rolesDir: f.roles, system: systemFor(root) },
});
t.after(() => bus.close());
+1 -1
View File
@@ -33,7 +33,7 @@ async function setup(t, { stub = false, hook = null } = {}) {
const bus = await startBroker({
dataRoot: f.dataRoot,
businesses: stub
? { acme: { id: "acme", human: "jason", arbiters: { technical: "coder", delivery: "coder" }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } }
? { acme: { id: "acme", human: "jason", arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } }
: boot.businesses,
organization: stub ? { dir, rolesDir: f.roles, system: systemFor(root) } : boot.organization,
});
+1 -1
View File
@@ -445,7 +445,7 @@ test("each launch makes sure the workspace has a clone of the project: cloned, f
test("a role the broker says holds a merge decision gets land_work, its prompt line, its landing and the git socket; without it, none", { skip, timeout: 30000 }, async (t) => {
const asked = [];
const held = [{ instance: "chief", decision: "d1", task_ref: "vikunja:1/2" }, { instance: "coder", decision: "d2", task_ref: "vikunja:1/3" }];
const answering = (holders) => ({ wrap: (host) => Object.create(host, { op: { value: async (m) => (m.op === "landHolders" ? (asked.push(m), holders) : host.op(m)) } }) });
const answering = (holders) => ({ threadTickMs: 3600000, wrap: (host) => Object.create(host, { op: { value: async (m) => (m.op === "landHolders" ? (asked.push(m), holders) : host.op(m)) } }) });
const landing = "- Landing: once your merge decision is approved, land_work it with the task.";
const read = (f, run, file) => readFileSync(join(f.dataRoot, "launches", "acme", run, file), "utf8");
+2 -2
View File
@@ -90,9 +90,9 @@ async function setup(t, { publishDeadlineMs } = {}) {
credentials: { gitea: { store: true, file: join(root, "secrets", "agents", "demo", user, "gitea"), rotateBy: "2099-01-01" } },
});
const demo = {
id: "demo", human: "jason", arbiters: { technical: "reviewer", delivery: "reviewer" },
id: "demo", human: "jason", arbiters: { technical: 'chief', delivery: 'chief' },
projects: { stack: { root: project, remote: `${gitea.origin}/mosaic/stack` } },
roles: {
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } },
coder: agent("coder"),
second: agent("second"),
reviewer: { authority: { withinRole: ["message.send"], crossRole: [] } },
+2 -2
View File
@@ -18,8 +18,8 @@ const none = { authority: { withinRole: ["message.send"], crossRole: [] } };
const inAgent = (env) => io({ ...env, CLAUDECODE: "1", CLAUDE_CODE_ENTRYPOINT: "sdk-ts" });
const bus = (vars = { "bus.tester": true }) => ({
acme: {
id: "acme", human: "jason", arbiters: { technical: "coder", delivery: "coder" }, ...(vars ? { vars } : {}),
roles: { coder: { definition: "coder", ...none }, assistant: { definition: "assistant", ...none } },
id: "acme", human: "jason", arbiters: { technical: 'chief', delivery: 'chief' }, ...(vars ? { vars } : {}),
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: "coder", ...none }, assistant: { definition: "assistant", ...none } },
},
});
+2 -2
View File
@@ -281,8 +281,8 @@ function mosaic(args, env) {
test("row 184: scripts/mosaic runs thread create, threads, talk --thread and thread read as the tester through tester-cli.mjs", async (t) => {
const { root, f, org } = await orgFixture(t);
const none = { authority: { withinRole: ["message.send"], crossRole: [] } };
const businesses = { acme: { id: "acme", human: "jason", arbiters: { technical: "coder", delivery: "coder" }, vars: { "bus.tester": true },
roles: { coder: { definition: "coder", ...none }, assistant: { definition: "assistant", ...none } } } };
const businesses = { acme: { id: "acme", human: "jason", arbiters: { technical: 'chief', delivery: 'chief' }, vars: { "bus.tester": true },
roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: "coder", ...none }, assistant: { definition: "assistant", ...none } } } };
const rt = await startBroker({ dataRoot: org.root, businesses, organization: { dir: join(root, ".config", "mosaic-test"), rolesDir: f.roles, system: systemFor(root) } });
t.after(() => rt.close());
const human = rt.broker.bindHuman({ business: "acme", human: "jason", via: "cli", outsideAgent: true });
+5 -5
View File
@@ -193,17 +193,17 @@ test("a chief-of-staff bundle lists launch among the bus tools and the manifest'
// Row 132 (#1628, REQ-ROLE-2, REQ-COS-1): the 3d re-pin window checks this manifest: both tools typed.
// Row 141 (#1635, REQ-MSG-2) granted instance.name, and row 155 (#1649) gives it set_name, so nothing
// the chief holds is untyped and the prompt has no untyped line. Row 194 (#1692, PRD 1.7 REQ-COS-3)
// granted role.hire and role.fire, with hire and fire.
test("a chief-of-staff bundle lists end_run, change_organization and set_name as typed and nothing untyped, for either harness", (t) => {
// the chief held then was untyped. Row 194 (#1692, PRD 1.7 REQ-COS-3)
// granted role.hire and role.fire, with hire and fire. RR adds two resolve rights; TI owns their tools.
test("a chief-of-staff bundle lists end_run, change_organization and set_name as typed and resolve rights as untyped, for either harness", (t) => {
for (const harness of ["pi", "claude-code"]) {
const r = resolvedFor(t, "chief", (d) => (d.roles.chief.vars = { harness, model: "claude-opus-5-5" }));
const b = build(t, r);
const prefix = harness === "pi" ? "" : "mcp__mosaic__";
assert.deepEqual(b.manifest.typed, ["send_message", "launch", "end_run", "hire", "fire", "organization_revision", "change_organization", "set_name", "read_spec", "write_record", "raise_decision", "list_agents", "inbox", "trail", "list_tasks", "get_task"].map((n) => prefix + n), harness);
assert.deepEqual(b.manifest.untyped, [], harness);
assert.deepEqual(b.manifest.untyped, ["decision.resolve.technical", "decision.resolve.delivery"], harness);
assert.match(readFileSync(b.files.prompt, "utf8"), /Bus tools: send_message, launch, end_run, hire, fire, organization_revision, change_organization, set_name, read_spec, write_record, raise_decision, /);
assert.doesNotMatch(readFileSync(b.files.prompt, "utf8"), /Actions you hold that have no bus tool yet/);
assert.match(readFileSync(b.files.prompt, "utf8"), /Actions you hold that have no bus tool yet/);
}
});
@@ -55,7 +55,7 @@ for (const harness of ['pi', 'claude-code']) {
}
test('PM intake writes and staff request follow the existing message.send grant', t => {
const muted = bundle(t, 'pm', 'pi', doc => { doc.roles.pm.vars['limits.authority'] = ['task.assign']; });
const muted = bundle(t, 'pm', 'pi', doc => { doc.roles.pm.vars['limits.authority'] = ['task.assign', 'decision.resolve.delivery']; });
assert.deepEqual(muted.tools.filter(intake).map(t => t.name).sort(), reads.toSorted());
assert.ok(!muted.tools.some(t => t.name === 'request_staff'));
});
+8 -6
View File
@@ -23,15 +23,15 @@ const businesses = {
demo: {
id: "demo",
human: "jason",
arbiters: { technical: "pm", delivery: "pm" },
arbiters: { technical: "chief", delivery: "pm" },
launch: { by: "pm", instances: ["coder"], models:{}, limits:{} },
roles: {
pm: { authority: { withinRole: ["message.send", "role.launch"], crossRole: [] } },
pm: { authority: { withinRole: ["message.send", "role.launch", "decision.resolve.delivery"], crossRole: [] } },
coder: { authority: { withinRole: ["message.send"], crossRole: [] } },
// Row 132 (#1628): the chief of staff's grants and the assistant. Since row 139 (Q169) the
// chief may end the assistant; with no thread run holding a claim the end refuses not-held.
// Row 155 (#1649) adds row 141's instance.name for set_name.
chief: { definition: "chief-of-staff", authority: { withinRole: ["message.send", "role.end", "organization.change", "instance.name"], crossRole: [] } },
chief: { definition: "chief-of-staff", authority: { withinRole: ["message.send", "role.end", "organization.change", "instance.name", "decision.resolve.technical", "decision.resolve.delivery"], crossRole: [] } },
assistant: { definition: "assistant", authority: { withinRole: ["message.send"], crossRole: [] } },
},
},
@@ -829,8 +829,10 @@ test("a run in a landing turn isn't released for a second held decision until it
const earlier = ctx.broker.bindLaunch({ business: "demo", role: "coder", run: "coder-0", harness: "pi" });
ctx.call(earlier, "role.claim");
const d1 = ctx.call(earlier, "decision.raise", merge("vikunja:1/2"));
assert.equal(d1.route_to, "pm");
ctx.call(ctx.pm, "decision.resolve", { id: d1.id, choice: "yes" });
const chief = ctx.broker.bindLaunch({ business: "demo", role: "chief", run: "chief-run", harness: "pi" });
ctx.call(chief, "role.claim");
assert.equal(d1.route_to, "chief");
ctx.call(chief, "decision.resolve", { id: d1.id, choice: "yes" });
ctx.broker.endLaunch({ business: "demo", run: "coder-0", reason: "exited" });
const file = join(ctx.runDir, "session.json");
writeFileSync(file, JSON.stringify({ ...JSON.parse(readFileSync(file, "utf8")), landing: [{ decision: d1.id, task_ref: "vikunja:1/2" }] }));
@@ -843,7 +845,7 @@ test("a run in a landing turn isn't released for a second held decision until it
await until(() => existsSync(started));
// d2 is resolved during the landing turn, so the landing pass lists it for the running holder.
const d2 = ctx.call(ctx.cap, "decision.raise", merge("vikunja:1/3"));
ctx.call(ctx.pm, "decision.resolve", { id: d2.id, choice: "yes" });
ctx.call(chief, "decision.resolve", { id: d2.id, choice: "yes" });
assert.deepEqual(ctx.broker.landHolders("demo").map((h) => [h.instance, h.decision]), [["coder", d1.id], ["coder", d2.id]]);
const release = () => ctx.broker.landRelease({ business: "demo", run: "coder-run", decision: d2.id });
assert.deepEqual(release(), { released: false, landing: true });
+4 -3
View File
@@ -58,7 +58,8 @@ test("the chief of staff gets launch, send_message, end_run, hire, fire, change_
// Row 132 (#1628): role.end and organization.change have their tools. Row 141 (#1635) granted
// instance.name, and row 155 (#1649) gives it set_name, so no held action is untyped. Row 194 (#1692,
// PRD 1.7 REQ-COS-3) granted role.hire and role.fire, with hire and fire.
assert.deepEqual(untypedActions(chief, tools), []);
// RR grants resolve rights; TI owns their typed tool exposure.
assert.deepEqual(untypedActions(chief, tools), ["decision.resolve.technical", "decision.resolve.delivery"]);
});
// Row 194 (#1692, PRD 1.7 REQ-COS-3): hire and fire send the broker's argument keys, reason required,
@@ -104,7 +105,7 @@ test("request_staff is the pm's alone: staff.request over message.send, roles an
const assistant = resolvedFor(t, "assistant", (d) => (d.roles.assistant = { definition: "assistant", holder: "mos" }));
assert.ok(!names(typedTools({ resolved: assistant })).includes("request_staff"));
// A pm without message.send is offered no request_staff, as for send_message.
const mute = resolvedFor(t, "pm", (d) => (d.roles.pm.vars["limits.authority"] = ["task.assign"]));
const mute = resolvedFor(t, "pm", (d) => (d.roles.pm.vars["limits.authority"] = ["task.assign", "decision.resolve.delivery"]));
assert.ok(!names(typedTools({ resolved: mute, tracker: true })).includes("request_staff"));
// The pm still has no launch tool, and the chief of staff's launch offer is its limits, not a request.
assert.ok(!names(tools).includes("launch"));
@@ -240,7 +241,7 @@ test("list_requirements reads the requirement ids, for a task.create holder with
assert.match(list.description, /task_create takes only these/);
assert.match(list.description, /Product intake always needs an approved product snapshot/);
assert.ok(!names(typedTools({ resolved: pm, arbiter: true })).includes("list_requirements"));
const noCreate = resolvedFor(t, "pm", (d) => (d.roles.pm.vars["limits.authority"] = ["message.send", "task.assign"]));
const noCreate = resolvedFor(t, "pm", (d) => (d.roles.pm.vars["limits.authority"] = ["message.send", "task.assign", "decision.resolve.delivery"]));
const without = names(typedTools({ resolved: noCreate, tracker: true }));
assert.ok(without.includes("task_assign") && !without.includes("task_create") && !without.includes("list_requirements"));
const create = tools.find((x) => x.name === "task_create").parameters.properties;
@@ -267,6 +267,12 @@ export function validateOrganizationDocument(document, file, { rolesDir }) {
if (definitions[definition].messaging) {
refuse(`${file} arbiters.${kind} names ${name}, whose role definition ${definition} declares messaging lists`);
}
const right = `decision.resolve.${kind}`;
const narrowed = [vars, roles[name].vars].some(layer =>
layer["limits.authority"] !== undefined && !layer["limits.authority"].includes(right));
if (!definitions[definition].authority.withinRole.includes(right) || narrowed) {
refuse(`${file} arbiters.${kind} names ${name}, whose role definition ${definition} lacks ${right}`);
}
arbiters[kind] = name;
}
+4 -1
View File
@@ -4,7 +4,7 @@
// ceilings. Version 1 files still load: they carry no authority, so every
// vocabulary action is gated for them.
import { existsSync, lstatSync } from "node:fs";
import { existsSync, lstatSync, readFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { refuse } from "./errors.mjs";
import {
@@ -155,6 +155,9 @@ function checkContract(contract, file) {
refuse(`role contract not found: ${path}`);
}
if (!stat.isFile() || stat.isSymbolicLink() || stat.size === 0) refuse(`role contract must be a non-empty regular file: ${path}`);
if (!/^## Authority[\t ]*\r?$/m.test(readFileSync(path, "utf8"))) {
refuse(`role contract ${path} requires a ## Authority section`);
}
return path;
}
+2
View File
@@ -58,6 +58,7 @@ export const ACTIONS = Object.freeze([
"policy.change",
"prd.approve",
"decision.resolve.technical",
"decision.resolve.delivery",
// Row 285 (C2-08b, #1790): a creative agent writes a supporting Markdown file in the human's library.
// Added as a hard wait and routed under PRD 1.16 REQ-WAIT-3 and REQ-LIB-6; role.mjs grants it within-role
// to the creative roles only.
@@ -93,6 +94,7 @@ export const ROUTED = Object.freeze([
"policy.change",
"prd.approve",
"decision.resolve.technical",
"decision.resolve.delivery",
// PRD 1.7 REQ-COS-3 (row 194, #1692): the chief's hire and fire, within the bounds the broker checks.
"role.hire",
"role.fire",
@@ -15,11 +15,11 @@ import { businessDoc, launchDoc, REPO_ROLES, systemFor, tmp, writeJson } from ".
// Decision 104's list plus role.launch (decision 139) plus role.end and
// organization.change (row 123, REQ-COS-1) and record.write (row 138), plus instance.name
// (row 141, REQ-MSG-2, sage-0589), plus role.hire and role.fire (row 194, PRD 1.7 REQ-COS-3,
// sage-1471, mos-1089). A role file that differs from it fails.
// sage-1471, mos-1089), plus both resolve rights (RR, #1873). A role file that differs fails.
const APPROVED = Object.freeze({
tools: ["read", "grep", "find", "ls"],
network: "api-only",
authority: Object.freeze({ withinRole: Object.freeze(["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire"]), crossRole: Object.freeze([]) }),
authority: Object.freeze({ withinRole: Object.freeze(["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire", "decision.resolve.technical", "decision.resolve.delivery"]), crossRole: Object.freeze([]) }),
// Row 285 (#1790, mos-1309 item 5, sage-2915): gitea for the library read grant, read scopes only.
credentials: Object.freeze([Object.freeze({ service: "gitea", scopes: Object.freeze(["read:repository", "read:user"]) })]),
// Row 194 (#1692, Q4, sage-1374): empty until a policy commit after the PRD 1.7 resolve.
@@ -91,7 +91,7 @@ test("the resolved record matches the approved list, so a widened file fails", (
test("the chief of staff holds role.launch, message.send, role.end, organization.change, record.write, instance.name, role.hire and role.fire; every other action is gated", () => {
const record = resolved(loadFixture());
const held = ["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire"];
const held = ["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire", "decision.resolve.technical", "decision.resolve.delivery"];
for (const action of ACTIONS) {
assert.equal(classify(record, action), held.includes(action) ? "within" : "gated", action);
}
@@ -137,14 +137,21 @@ test("role.hire and role.fire refuse", () => {
// Step 5.
test("a decision.resolve action comes back as a flagged line", () => {
const { flagged } = run(base({ authority: { withinRole: ["message.send"], crossRole: ["decision.resolve.technical"] } }));
assert.deepEqual(flagged, [
{ field: "authority.crossRole", value: "decision.resolve.technical", reasons: [FLAG_ABOVE_CHIEF, FLAG_RESOLVE] },
]);
// Held by the chief: still flagged as a resolve right, and only that.
const chief = { ...CHIEF, authority: { withinRole: [...CHIEF.authority.withinRole, "decision.resolve.technical"], crossRole: [] } };
const held = run(base({ authority: { withinRole: ["decision.resolve.technical"], crossRole: [] } }), CORE, chief);
assert.deepEqual(held.flagged, [{ field: "authority.withinRole", value: "decision.resolve.technical", reasons: [FLAG_RESOLVE] }]);
for (const domain of ["technical", "delivery"]) {
const action = `decision.resolve.${domain}`;
const without = { ...CHIEF, authority: { ...CHIEF.authority,
withinRole: CHIEF.authority.withinRole.filter(value => value !== action) } };
const input = base({ authority: { withinRole: ["message.send"], crossRole: [action] } });
assert.deepEqual(run(input, CORE, without).flagged, [
{ field: "authority.crossRole", value: action, reasons: [FLAG_ABOVE_CHIEF, FLAG_RESOLVE] },
]);
// RR gives the shipped chief both rights. Each still flags, without above-chief.
assert.deepEqual(run(input).flagged, [
{ field: "authority.crossRole", value: action, reasons: [FLAG_RESOLVE] },
]);
const held = run(base({ authority: { withinRole: [action], crossRole: [] } }));
assert.deepEqual(held.flagged, [{ field: "authority.withinRole", value: action, reasons: [FLAG_RESOLVE] }]);
}
});
// Step 6.
+1 -1
View File
@@ -89,7 +89,7 @@ test("limits can narrow a C-suite role but never widen it", () => {
test("any unknown key, tool, action or version in a role file is refused", () => {
const base = JSON.parse(readFileSync(join(REPO_ROLES, "chief-executive-officer.json"), "utf8"));
const root = tmp();
writeFileSync(join(root, "chief-executive-officer.md"), "# contract\n");
writeFileSync(join(root, "chief-executive-officer.md"), "# contract\n\n## Authority\n");
const check = (extra) => validateRoleDocument({ ...base, ...extra }, join(root, "chief-executive-officer.json"));
assert.equal(check({}).name, "chief-executive-officer");
for (const extra of [{ arbiter: "technical" }, { launch: {} }, { tools: ["read", "browser"] }, { roleVersion: 3 }, { authority: { withinRole: ["message.send", "deploy.prod"], crossRole: [] } }]) {
@@ -13,7 +13,7 @@ const WAITS = ["spend", "data.delete", "network.expose", "account.create", "cred
const LEFT = ["git.merge.protected", "policy.change", "prd.approve", "role.revoke"];
const role = (authority) => {
const dir = tmp();
writeFileSync(join(dir, "r.md"), "# contract\n");
writeFileSync(join(dir, "r.md"), "# contract\n\n## Authority\n");
const doc = { roleVersion: 2, name: "r", title: "R", contract: "r.md", tools: ["read"], network: "none", authority,
credentials: [{ service: "gitea", scopes: ["read:issue"] }] };
return validateRoleDocument(doc, join(dir, "r.json"));
@@ -0,0 +1,86 @@
// RR (#1873): the file writer refuses arbiters without their domain's right.
import test from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, rmSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { organizationWriter } from '../src/organization.mjs';
import { loadRole, validateBusinessDocument } from '../src/index.mjs';
import { businessDoc, REPO_ROLES, rolesCopy, tmp, writeJson } from './helpers.mjs';
for (const [domain, instance, definition] of [['technical', 'cto', 'cto'], ['delivery', 'pm', 'pm']]) {
test(`RR: naming ${instance} without decision.resolve.${domain} refuses when written`, t => {
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
const rolesDir = rolesCopy(root), file = join(root, 'businesses/acme.json');
writeJson(file, businessDoc(root));
const writer = organizationWriter({ dir: root, rolesDir });
const { document, revision } = writer.read('acme'), before = readFileSync(file, 'utf8');
const roleFile = join(rolesDir, `${definition}.json`), role = JSON.parse(readFileSync(roleFile, 'utf8'));
role.authority.withinRole = role.authority.withinRole.filter(a => a !== `decision.resolve.${domain}`);
writeJson(roleFile, role);
assert.throws(() => writer.write('acme', document, { revision, actor: 'jason' }), error => {
assert.match(error.message, new RegExp(`arbiters\\.${domain}.*${instance}.*role definition ${definition}.*decision\\.resolve\\.${domain}`));
return true;
});
assert.equal(readFileSync(file, 'utf8'), before);
});
}
test('RR: a coder named as arbiter refuses when written', t => {
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
const file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root));
const writer = organizationWriter({ dir: root, rolesDir: REPO_ROLES });
const { document, revision } = writer.read('acme'), before = readFileSync(file, 'utf8');
document.arbiters.delivery = 'coder';
assert.throws(() => writer.write('acme', document, { revision, actor: 'jason' }), /arbiters\.delivery.*coder.*role definition coder.*decision\.resolve\.delivery/);
assert.equal(readFileSync(file, 'utf8'), before);
});
test('RR: a cross-role resolve grant cannot qualify an arbiter', t => {
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
const rolesDir = rolesCopy(root), roleFile = join(rolesDir, 'cto.json');
const role = JSON.parse(readFileSync(roleFile, 'utf8'));
role.authority.withinRole = role.authority.withinRole.filter(a => a !== 'decision.resolve.technical');
role.authority.crossRole.push('decision.resolve.technical'); writeJson(roleFile, role);
assert.throws(() => validateBusinessDocument(businessDoc(root), join(root, 'businesses/acme.json'), { rolesDir }), /cto.*decision\.resolve\.technical/);
});
test('RR: a version 2 contract without its Authority section refuses', t => {
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
const rolesDir = rolesCopy(root);
writeFileSync(join(rolesDir, 'coder.md'), '# Role contract: coder\n\n## Duties\nRead only.\n');
assert.throws(() => loadRole(rolesDir, 'coder'), /coder.*## Authority/);
});
for (const [domain, instance] of [['technical', 'cto'], ['delivery', 'pm']]) {
for (const layer of ['agent', 'business', 'intersection']) {
test(`RR: ${layer} limits removing ${instance}'s ${domain} resolve right refuse when written`, t => {
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
const file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root));
const writer = organizationWriter({ dir: root, rolesDir: REPO_ROLES });
const { document, revision } = writer.read('acme'), before = readFileSync(file, 'utf8');
const right = `decision.resolve.${domain}`;
const rights = ['message.send', 'decision.resolve.technical', 'decision.resolve.delivery'];
if (layer === 'agent') document.roles[instance].vars['limits.authority'] = ['message.send'];
else document.vars['limits.authority'] = rights.filter(value => value !== right);
if (layer === 'intersection') document.roles[instance].vars['limits.authority'] = rights;
assert.throws(() => writer.write('acme', document, { revision, actor: 'jason' }), error => {
assert.match(error.message, new RegExp(`arbiters\\.${domain}.*${instance}.*role definition ${instance}.*decision\\.resolve\\.${domain}`));
return true;
});
assert.equal(readFileSync(file, 'utf8'), before);
});
}
}
test('RR: narrowed arbiters retaining their domain rights can be written', t => {
const root = tmp(); t.after(() => rmSync(root, { recursive: true, force: true }));
const file = join(root, 'businesses/acme.json'); writeJson(file, businessDoc(root));
const writer = organizationWriter({ dir: root, rolesDir: REPO_ROLES });
const { document, revision } = writer.read('acme');
document.vars['limits.authority'] = ['message.send', 'decision.resolve.technical', 'decision.resolve.delivery'];
for (const [domain, instance] of [['technical', 'cto'], ['delivery', 'pm']]) {
document.roles[instance].vars['limits.authority'] = ['message.send', `decision.resolve.${domain}`];
}
writer.write('acme', document, { revision, actor: 'jason' });
assert.deepEqual(writer.read('acme').document.arbiters, document.arbiters);
});
+2 -2
View File
@@ -138,10 +138,10 @@ test("limits.authority without role.launch leaves the launcher with no launch bl
test("limits.authority narrows cross-role actions too", () => {
// pm, the role with two cross-role actions: the limit keeps one and drops the other.
const s = setup((d) => {
d.roles.pm.vars["limits.authority"] = ["task.create", "task.close", "task.priority.change"];
d.roles.pm.vars["limits.authority"] = ["task.create", "task.close", "task.priority.change", "decision.resolve.delivery"];
});
const pm = resolve(s, "pm");
assert.deepEqual(pm.limits.authority, { withinRole: ["task.create", "task.close"], crossRole: ["task.priority.change"] });
assert.deepEqual(pm.limits.authority, { withinRole: ["task.create", "task.close", "decision.resolve.delivery"], crossRole: ["task.priority.change"] });
assert.equal(classify(pm, "task.priority.change"), "cross");
assert.equal(classify(pm, "task.scope.change"), "gated");
assert.equal(classify(pm, "task.assign"), "gated");
+3 -3
View File
@@ -15,7 +15,7 @@ function refuses(fn, pattern, exitCode = 2) {
});
}
function scratchRole(overrides = {}, contract = "# contract\n") {
function scratchRole(overrides = {}, contract = "# contract\n\n## Authority\n") {
const dir = tmp();
if (contract !== null) writeFileSync(join(dir, "r.md"), contract);
const doc = {
@@ -243,7 +243,7 @@ test("the six creative roles load: read tools, message.send and library.write, g
test("M1: a creative role file wider than read and answer is refused", () => {
const creative = (name, overrides) => {
const dir = tmp();
writeFileSync(join(dir, `${name}.md`), "# contract\n");
writeFileSync(join(dir, `${name}.md`), "# contract\n\n## Authority\n");
const doc = {
roleVersion: 2, name, title: "C", contract: `${name}.md`, tools: ["read"], network: "none",
authority: { withinRole: ["message.send"], crossRole: [] }, credentials: [],
@@ -284,7 +284,7 @@ const REFUSED_FOR_HIRE = ACTIONS.filter((action) => !HIRE_ALLOWED.includes(actio
function hireDir(definitions) {
const dir = tmp();
for (const [name, doc] of Object.entries(definitions)) {
writeFileSync(join(dir, `${name}.md`), "# contract\n");
writeFileSync(join(dir, `${name}.md`), "# contract\n\n## Authority\n");
writeJson(join(dir, `${name}.json`), doc === 1 ? { roleVersion: 1, name, tools: ["read"] } : {
roleVersion: 2, name, title: name, contract: `${name}.md`, tools: ["read"], network: "none",
authority: { withinRole: doc.withinRole ?? ["message.send"], crossRole: doc.crossRole ?? [] }, credentials: [],
+1 -1
View File
@@ -269,7 +269,7 @@
restore(p, body, kept);
}
const content = body.querySelector('[data-project-content]');
if (content) await window.MosaicProjects.mountContent({ container: content, project, threads, section: what.section, canSend: false, org, pane: true, views: viewsOf(p, org) });
if (content) await window.MosaicProjects.mountContent({ container: content, project, threads, section: what.section, canSend: false, org, pane: true, views: viewsOf(p, org), idPrefix: `pane-${p.key}-` });
return what.section === 'overview' ? project.name || project.id : `${project.name || project.id} · ${sectionName}`;
}
// A pane's request choice and PRD disclosure, per organization, kept on the pane so a repaint
+12 -8
View File
@@ -205,14 +205,15 @@
// state lives as long as the container.
const paneViews = new WeakMap();
const viewState=(container,pane,views)=>{if(!pane) return {selected:selectedRequests,expanded:expandedPrds}; if(views?.selected instanceof Map && views.expanded instanceof Set) return views; if(!paneViews.has(container)) paneViews.set(container,{selected:new Map(),expanded:new Set()}); return paneViews.get(container);};
function requirementView(data,project,selected,expandedSet=expandedPrds) {
function requirementView(data,project,selected,expandedSet=expandedPrds,idPrefix='') {
const pid=esc(idPrefix);
const options=data.requests.filter(r=>r.draft), r=options.find(x=>x.id===selected)||options[0], d=r?.draft;
const north=typeof data.north?.content==='string'?data.north.content:null;
const select=options.length>1?`<label>Request <select id="project-request-select">${options.map(x=>`<option value="${clean(x.id)}"${x===r?' selected':''}>${clean(x.id)} · ${clean(x.thread.name)}</option>`).join('')}</select></label>`:'';
const select=options.length>1?`<label>Request <select id="${pid}project-request-select">${options.map(x=>`<option value="${clean(x.id)}"${x===r?' selected':''}>${clean(x.id)} · ${clean(x.thread.name)}</option>`).join('')}</select></label>`:'';
const candidate=d?.candidate, expanded=r && expandedSet.has(project.id+':'+r.id);
const docPath=candidate?.prd_path, docLink=project.repo && typeof docPath==='string' && /^docs\/[A-Za-z0-9][A-Za-z0-9._-]{0,127}\.md$/.test(docPath) ? `#/docs/${encodeURIComponent(project.repo)}/${encodeURIComponent(docPath)}` : null;
const rows=candidate ? [['PRD version',candidate.prd_version],['Approved snapshot',d.snapshot.id || 'None'],['PRD file',docPath],['PRD blob',candidate.prd_blob],['Records commit',candidate.records_commit],['Confirmed intent commit',candidate.spec_commit],['Mode revision',candidate.mode_revision]].map(([label,value])=>`<div class="project-provenance-row"><span>${label}</span>${label==='PRD file' && docLink?`<a href="${docLink}"><code>${clean(value)}</code></a>`:`<code>${clean(value)}</code>`}</div>`).join('') : '<p>Unavailable until a PRD is read.</p>';
return `${r?`<div class="project-panel project-source">${select || `Request <code>${clean(r.id)}</code>`} · ${chip(r.state)} · from ${clean(r.thread.name)} <a href="${conversationLink(r.thread)}">Open conversation</a></div>`:select}<div class="project-columns"><section><h2>Project direction</h2><h3 id="project-north-star">North star</h3>${north!==null?`<div data-north-star></div><a class="project-text-link" href="#/docs/${encodeURIComponent(project.repo)}/${encodeURIComponent('spec/north-star.md')}">Open north star in Docs</a>`:data.northRecordsMissing?`<p>${window.MosaicRecords.markup(project.repo)}</p>`:data.northMissing?'<p>No north star yet.</p>':project.repo?'<p>North star unavailable. Docs could not read this project’s repository.</p>':`<p>This project has no linked repository. The north star and project documents live in a repository. Linking one to an existing project isn't available yet.</p>`}<h3 id="project-prd">${candidate?`PRD ${clean(candidate.prd_version)}`:'PRD'} ${candidate?`<span class="project-chip ${d.snapshot.id?'success':''}">${d.snapshot.id?'Approved':'Draft'}</span>`:''}</h3>${d?`<p>${d.snapshot.id?'Approved snapshot '+clean(d.snapshot.id):'This candidate has no active approved snapshot.'}</p><div class="project-panel project-prd-panel"><div class="project-prd-preview${expanded?' expanded':''}" data-prd></div><button type="button" class="btn project-prd-toggle" aria-expanded="${!!expanded}">${expanded?'Show less':'Show full PRD'}</button></div><h3>Requirements <span class="project-count">${d.requirements.length}</span></h3><div class="project-panel">${d.requirements.map((req,i)=>`<details data-restore-key="req:${clean(r.id)}:${clean(req.id)}"><summary>${clean(req.id)} · ${clean(req.title)}</summary><p>${clean(req.text)}</p><p>Requirement identity: <code>${clean(candidate.requirements[i]?.path)}</code> · <code>${clean(candidate.requirements[i]?.blob)}</code></p></details>`).join('')}</div>`:`<p>${(data.failed || data.requests.some(x=>x.materialFailed))?'Request or PRD unavailable. Refresh to try again.':'No current PRD candidate or approved snapshot. Waiting for a PRD submission.'}</p>`}${d && data.requests.some(x=>x.materialFailed)?'<p role="status">Some request PRDs could not be read. Refresh to try again.</p>':''}</section><aside class="project-panel project-provenance"><details data-restore-key="provenance"${matchMedia('(min-width:700px)').matches?' open':''}><summary>Version and provenance</summary>${rows}${candidate?`<p>Approval and revocation live in the conversation's Request details.</p><a href="${conversationLink(r.thread)}">Open conversation</a>`:''}</details></aside></div>`;
return `${r?`<div class="project-panel project-source">${select || `Request <code>${clean(r.id)}</code>`} · ${chip(r.state)} · from ${clean(r.thread.name)} <a href="${conversationLink(r.thread)}">Open conversation</a></div>`:select}<div class="project-columns"><section><h2>Project direction</h2><h3 id="${pid}project-north-star">North star</h3>${north!==null?`<div data-north-star></div><a class="project-text-link" href="#/docs/${encodeURIComponent(project.repo)}/${encodeURIComponent('spec/north-star.md')}">Open north star in Docs</a>`:data.northRecordsMissing?`<p>${window.MosaicRecords.markup(project.repo)}</p>`:data.northMissing?'<p>No north star yet.</p>':project.repo?'<p>North star unavailable. Docs could not read this project’s repository.</p>':`<p>This project has no linked repository. The north star and project documents live in a repository. Linking one to an existing project isn't available yet.</p>`}<h3 id="${pid}project-prd">${candidate?`PRD ${clean(candidate.prd_version)}`:'PRD'} ${candidate?`<span class="project-chip ${d.snapshot.id?'success':''}">${d.snapshot.id?'Approved':'Draft'}</span>`:''}</h3>${d?`<p>${d.snapshot.id?'Approved snapshot '+clean(d.snapshot.id):'This candidate has no active approved snapshot.'}</p><div class="project-panel project-prd-panel"><div class="project-prd-preview${expanded?' expanded':''}" data-prd></div><button type="button" class="btn project-prd-toggle" aria-expanded="${!!expanded}">${expanded?'Show less':'Show full PRD'}</button></div><h3>Requirements <span class="project-count">${d.requirements.length}</span></h3><div class="project-panel">${d.requirements.map((req,i)=>`<details data-restore-key="req:${clean(r.id)}:${clean(req.id)}"><summary>${clean(req.id)} · ${clean(req.title)}</summary><p>${clean(req.text)}</p><p>Requirement identity: <code>${clean(candidate.requirements[i]?.path)}</code> · <code>${clean(candidate.requirements[i]?.blob)}</code></p></details>`).join('')}</div>`:`<p>${(data.failed || data.requests.some(x=>x.materialFailed))?'Request or PRD unavailable. Refresh to try again.':'No current PRD candidate or approved snapshot. Waiting for a PRD submission.'}</p>`}${d && data.requests.some(x=>x.materialFailed)?'<p role="status">Some request PRDs could not be read. Refresh to try again.</p>':''}</section><aside class="project-panel project-provenance"><details data-restore-key="provenance"${matchMedia('(min-width:700px)').matches?' open':''}><summary>Version and provenance</summary>${rows}${candidate?`<p>Approval and revocation live in the conversation's Request details.</p><a href="${conversationLink(r.thread)}">Open conversation</a>`:''}</details></aside></div>`;
}
function overview(data,project,canSend,receipt='') {
const active=data.requests.filter(r=>r.state && !ended(r.state)), waiting=active.filter(r=>needs(r.state)), ordered=[...data.requests].sort(requestOrder), first=ordered[0], threads=[...data.threads];
@@ -255,7 +256,9 @@
const focus=f.id ? root.querySelector('#'+CSS.escape(f.id)) : f.details ? [...root.querySelectorAll('details[data-restore-key]')].find(el=>el.dataset.restoreKey===f.details)?.querySelector(':scope > summary') : root.querySelectorAll(FOCUSABLE)[f.index];
focus?.focus({preventScroll:true});
}
async function mountContent({container,project,threads,section,canSend,restore,roomReceipt,org=null,pane=false,views:paneState=null}) {
// A Side by side pane passes `idPrefix` (pane-a-, pane-b-), so the same project in both panes
// draws distinct ids; the full view passes none and keeps its ids.
async function mountContent({container,project,threads,section,canSend,restore,roomReceipt,org=null,pane=false,views:paneState=null,idPrefix=''}) {
if(!container || !project) return;
const generation=epoch, run=(contentRuns.get(container) || 0)+1;
contentRuns.set(container,run);
@@ -267,7 +270,7 @@
const selected=selectedRequests.get(project.id);
if(selected && !data.requests.some(r=>r.id===selected && r.draft)) selectedRequests.delete(project.id);
const selection=selectedRequests.get(project.id);
const view = section==='requirements'?requirementView(data,project,selection,expandedPrds):section==='files'?filesView(data,project):section==='tasks'?tasksView(data):overview(data,project,canSend,roomReceipt);
const view = section==='requirements'?requirementView(data,project,selection,expandedPrds,idPrefix):section==='files'?filesView(data,project):section==='tasks'?tasksView(data):overview(data,project,canSend,roomReceipt);
const selectedRequest=data.requests.find(r=>r.id===(selection||data.requests.find(x=>x.draft)?.id));
// The generated presentation and separately mounted Markdown determine visible content.
const signature=JSON.stringify([view, section==='requirements' ? selectedRequest?.draft?.prd : null, ['requirements','overview'].includes(section) ? data.north?.content : null, data.failed && ['files','tasks'].includes(section)]);
@@ -285,12 +288,13 @@
const liveRestore=contentPaints.has(container) && container.children.length ? captureView(container) : null;
const draw=selected=>{
const {data,project,section,canSend,roomReceipt}=contentModels.get(container);
container.innerHTML=section==='requirements'?requirementView(data,project,selected,expandedPrds):section==='files'?filesView(data,project):section==='tasks'?tasksView(data):overview(data,project,canSend,roomReceipt);
container.innerHTML=section==='requirements'?requirementView(data,project,selected,expandedPrds,idPrefix):section==='files'?filesView(data,project):section==='tasks'?tasksView(data):overview(data,project,canSend,roomReceipt);
if(data.failed && ['files','tasks'].includes(section)) container.insertAdjacentHTML('afterbegin','<p role="status">Some request data could not be read; this list may be incomplete.</p>');
const selectedRequest=data.requests.find(r=>r.id===(selected||data.requests.find(x=>x.draft)?.id));
const md=container.querySelector('[data-prd]'); if(md && selectedRequest?.draft) { md.replaceChildren(window.MosaicMarkdown?.render(selectedRequest.draft.prd) || document.createTextNode(selectedRequest.draft.prd)); const toggle=container.querySelector('.project-prd-toggle'); toggle.hidden=!md.classList.contains('expanded') && md.scrollHeight<=md.clientHeight; toggle.addEventListener('click',()=>{const key=project.id+':'+selectedRequest.id; if(expandedPrds.has(key)) expandedPrds.delete(key); else expandedPrds.add(key); const open=expandedPrds.has(key); md.classList.toggle('expanded',open); toggle.setAttribute('aria-expanded',String(open));toggle.textContent=open?'Show less':'Show full PRD';}); }
const north=container.querySelector('[data-north-star]'); if(north && typeof data.north?.content==='string') { const text=section==='overview' && data.north.content.length>500 ? previewCut(data.north.content,500) : data.north.content; north.replaceChildren(window.MosaicMarkdown?.render(text) || document.createTextNode(text)); }
container.querySelector('#project-request-select')?.addEventListener('change',e=>{selectedRequests.set(project.id,e.target.value);draw(e.target.value);container.querySelector('#project-request-select')?.focus({preventScroll:true});});
const requestSelect='#'+CSS.escape(idPrefix+'project-request-select');
container.querySelector(requestSelect)?.addEventListener('change',e=>{selectedRequests.set(project.id,e.target.value);draw(e.target.value);container.querySelector(requestSelect)?.focus({preventScroll:true});});
};
draw(selection);
restore=liveRestore || restore;
@@ -298,7 +302,7 @@
restoreView(container,restore);
}
const sectionTarget=pane ? null : new URLSearchParams(location.hash.split('?')[1] || '').get('section');
if(section==='requirements' && ['prd','north-star'].includes(sectionTarget)) container.querySelector('#project-'+sectionTarget)?.scrollIntoView({block:'start'});
if(section==='requirements' && ['prd','north-star'].includes(sectionTarget)) container.querySelector('#'+CSS.escape(idPrefix+'project-'+sectionTarget))?.scrollIntoView({block:'start'});
}
// Cut at a word boundary, unless that would leave only a markdown marker (a long heading).
function previewCut(content,max) {
+25 -7
View File
@@ -373,6 +373,8 @@
}
async function submit(form) {
const kind = form.dataset.tbForm, btns = form.querySelectorAll('button');
// A form whose Save is disabled is sending, or waiting on the current task: it takes no second save.
if (form.querySelector('[data-tb-submit]')?.disabled) return;
if (!form.reportValidity()) return;
let body, t = null;
if (kind === 'create') {
@@ -388,16 +390,32 @@
}
btns.forEach(b => { b.disabled = true; }); formStatus(form, 'Sending…');
const r = await write(body);
// The task changed under the form: the form reopens on the current task and keeps the refusal.
// Until the current task is read the old form stays disabled and does not show the refusal, so
// the refusal never stands beside the old task and no save goes out against its digest.
if (!r.ok && r.error === 'task-conflict' && t) {
formStatus(form, 'Reading the current task…');
// A read a poll overtook leaves the board's old read in place, so only a read that landed counts.
const fresh = await reload().catch(() => false);
const now = (fresh && find(key(t))) || await current(t);
// Closed (Escape, Cancel) while the task was read: the dialog stays closed. close() clears `open`
// at once but queues the event that empties the dialog, so `open` is the check that holds.
const d = dialogEl();
if (!d.open || !d.contains(form)) { paint(); return; }
if (now) { openDialog(editForm(now), dialogEl().dataset.return, now); const f2 = dialogEl().querySelector('form'); if (f2) formStatus(f2, r.message); }
else {
// The current task could not be read: the form still holds the old one, so it takes no save.
opened = null;
formStatus(form, 'The task changed after the board read it. Nothing was written, and the current task could not be read; close this form and open the task again.');
form.querySelectorAll('[data-tb-cancel]').forEach(b => { b.disabled = false; });
}
paint();
return;
}
btns.forEach(b => { b.disabled = false; });
if (!r.ok) {
formStatus(form, r.message);
// The task changed under the form: the form shows the current task and keeps the refusal.
if (r.error === 'task-conflict' && t) {
try { await reload(); } catch {}
const now = find(key(t)) || await current(t);
if (now) { const msg = r.message; openDialog(editForm(now), dialogEl().dataset.return, now); const f2 = dialogEl().querySelector('form'); if (f2) formStatus(f2, msg); }
paint();
} else if (r.unknown) { try { await reload(); } catch {} paint(); }
if (r.unknown) { try { await reload(); } catch {} paint(); }
return;
}
const label = orgOf(body.org).label;
@@ -30,7 +30,7 @@ test('a failed archive lookup cannot hide the real broker refusal of an archived
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const roles = { assistant: { definition:'assistant', authority:{withinRole:['message.send'],crossRole:[]} } };
const broker = new Broker({ store, businesses:{ demo:{ id:'demo', human:'jason', arbiters:{technical:'assistant',delivery:'assistant'}, roles, definitions:{assistant:{}}, projects:{} } } });
const broker = new Broker({ store, businesses:{ demo:{ id:'demo', human:'jason', arbiters:{technical:'chief',delivery:'chief'}, roles: { chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, ...roles }, definitions:{assistant:{}}, projects:{} } } });
const cap = broker.bindHuman({ business:'demo', human:'jason', via:'cli', outsideAgent:true });
const call = (verb, args = {}) => broker.request(cap, { verb, args });
const project = call('project.create', { name:'archive test' }).id;
+2 -2
View File
@@ -19,7 +19,7 @@ test('API-key canary reaches fake once and zero responses/logs/receipts/store fi
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args),
businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } });
businesses: { demo: { id: 'demo', human: 'owner', roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'chief', delivery: 'chief' } } } });
const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
const logs = [], output = [];
const auth = await createSsoSession(t, { host: 'console.example', owner: { sub: 'owner-sub', name: 'owner' } });
@@ -57,7 +57,7 @@ test('OAuth URLs, state and code stay out of broker records and management recei
const store = new Store(root);
t.after(() => { store.close(); rmSync(root, { recursive: true, force: true }); });
const b = new Broker({ store, proxyPage: (...args) => fake.page.request(...args),
businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } } } });
businesses: { demo: { id: 'demo', human: 'owner', roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'chief', delivery: 'chief' } } } });
const cap = b.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true });
const call = (verb, args = {}) => b.request(cap, { verb, args });
const login = await call('proxy.login.start', { provider: 'claude' });
+4 -4
View File
@@ -31,7 +31,7 @@ test('row 65 Settings: the routing table lists every instance, saves a harness,
const dir=join(root,'config'),file=join(dir,'businesses','acme.json');
const host=await startBroker({
dataRoot:f.dataRoot,
businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},
businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},
organization:{dir,rolesDir:f.roles,system:systemFor(root)},
});
t.after(()=>host.close());
@@ -161,7 +161,7 @@ test('Routing offers catalogue models only for compatible harnesses and saves a
const dir=join(root,'config'),file=join(dir,'businesses','acme.json');
const offered={key:'zai-fixture-id',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-id',label:'z.ai / fixture-id',available:true,reason:null,harnesses:['pi']};
const modelCatalogue={read:async()=>({status:'ready',observedAt:'2026-11-06T00:00:00.000Z',models:[offered],notices:[]}),invalidate(){}};
const host=await startBroker({dataRoot:f.dataRoot,businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir,rolesDir:f.roles,system:systemFor(root)},modelCatalogue});t.after(()=>host.close());
const host=await startBroker({dataRoot:f.dataRoot,businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir,rolesDir:f.roles,system:systemFor(root)},modelCatalogue});t.after(()=>host.close());
const human=host.broker.bindHuman({business:'acme',human:'jason',via:'cli',outsideAgent:true});
const call=async(verb,args)=>structuredClone(await host.broker.request(human,{verb,args}));
const bus={messages:async()=>({messages:[],unread:0}),usage:async()=>{throw Error('no usage');},routes:()=>call('routes')};
@@ -187,7 +187,7 @@ test('Routing offers catalogue models only for compatible harnesses and saves a
test('Routing switch writes the model-provider setting and keeps other modules', {timeout:60000}, async t => {
const root=tmp(t,'provider-switch-'), f=fixture(root,'acme');
const dir=join(root,'config'),file=join(dir,'businesses','acme.json');
const host=await startBroker({dataRoot:f.dataRoot,businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir,rolesDir:f.roles,system:systemFor(root)}});t.after(()=>host.close());
const host=await startBroker({dataRoot:f.dataRoot,businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir,rolesDir:f.roles,system:systemFor(root)}});t.after(()=>host.close());
const human=host.broker.bindHuman({business:'acme',human:'jason',via:'cli',outsideAgent:true});
const call=async(verb,args)=>structuredClone(await host.broker.request(human,{verb,args}));
const bus={messages:async()=>({messages:[],unread:0}),usage:async()=>{throw Error('no usage');},routes:()=>call('routes')};
@@ -230,7 +230,7 @@ test('enabling a discovered harness refreshes Routing and offers its compatible
const dir=join(root,'config');
const offered={key:'fixture-pi',module:'cliproxy',upstream:'zai',provider:'zai',model:'fixture-pi',label:'Fixture pi',available:true,reason:null,harnesses:['pi']};
const modelCatalogue={read:async()=>({status:'ready',observedAt:'2026-11-07T02:24:02.021Z',models:[offered],notices:[]}),invalidate(){}};
const host=await startBroker({dataRoot:f.dataRoot,businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'coder',delivery:'coder'},roles:{coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir,rolesDir:f.roles,system:systemFor(root)},modelCatalogue});t.after(()=>host.close());
const host=await startBroker({dataRoot:f.dataRoot,businesses:{acme:{id:'acme',human:'jason',arbiters:{technical:'chief',delivery:'chief'},roles:{chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder:{authority:{withinRole:[],crossRole:[]}}}}},organization:{dir,rolesDir:f.roles,system:systemFor(root)},modelCatalogue});t.after(()=>host.close());
const human=host.broker.bindHuman({business:'acme',human:'jason',via:'cli',outsideAgent:true});const call=async(verb,args)=>structuredClone(await host.broker.request(human,{verb,args}));
const web=await startServer({port:0,board:'http://127.0.0.1:9',bus:{messages:async()=>({messages:[],unread:0}),usage:async()=>{throw Error('fixture')},routes:()=>call('routes')},reads:{settings:()=>({business:'acme',release:'fixture',notifier:{}})},log:()=>{}});t.after(()=>close(web));
const b=await browser();t.after(()=>b.close());const settings=await call('plugins');
+1 -1
View File
@@ -45,7 +45,7 @@ test('row 65 Console: the signed-in human saves a route through a real broker; e
const dir = join(root, 'config');
const host = await startBroker({
dataRoot: f.dataRoot,
businesses: { acme: { id: 'acme', human: 'jason', arbiters: { technical: 'coder', delivery: 'coder' }, roles: { coder: { authority: { withinRole: [], crossRole: [] } } } } },
businesses: { acme: { id: 'acme', human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { authority: { withinRole: [], crossRole: [] } } } } },
organization: { dir, rolesDir: f.roles, system: systemFor(root) },
});
t.after(() => host.close());
@@ -259,14 +259,16 @@ test('the same project in both panes keeps a request choice per pane', async t =
await b.evaluate('window.MosaicPanes.refresh()');
await wait(`document.querySelector('#pane-a-pick option[value="ddigital/projects/${P}"]')`);
await b.evaluate(`location.hash = '#/orgs/ddigital/side-by-side?a=ddigital/projects/${P}/requirements&b=forge/projects/${P}/requirements'`);
const values = `[...document.querySelectorAll('[data-pane-body] #project-request-select')].map(e => e.value)`;
await wait(`document.querySelectorAll('[data-pane-body] #project-request-select').length === 2`);
const values = `[...document.querySelectorAll('[data-pane-body] select[id$="-project-request-select"]')].map(e => e.value)`;
await wait(`document.querySelectorAll('[data-pane-body] select[id$="-project-request-select"]').length === 2`);
assert.deepEqual(await b.evaluate(values), ['request-1', 'request-1']);
await b.evaluate(`(() => { const e = document.querySelector('#pane-a #project-request-select'); e.value = 'request-2'; e.dispatchEvent(new Event('change', { bubbles: true })); })()`);
await b.evaluate(`(() => { const e = document.querySelector('#pane-a-project-request-select'); e.value = 'request-2'; e.dispatchEvent(new Event('change', { bubbles: true })); })()`);
await wait(`${values}[0] === 'request-2'`);
// The redrawn select gets focus back through the pane's own id.
assert.equal(await b.evaluate('document.activeElement.id'), 'pane-a-project-request-select');
await b.evaluate('window.MosaicPanes.refresh()');
await settled(b);
await wait(`document.querySelectorAll('[data-pane-body] #project-request-select').length === 2`);
await wait(`document.querySelectorAll('[data-pane-body] select[id$="-project-request-select"]').length === 2`);
assert.deepEqual(await b.evaluate(values), ['request-2', 'request-1']);
});
@@ -279,10 +281,10 @@ test('a pane keeps its request choice and PRD disclosure when a new conversation
await wait(`document.querySelector('#pane-a-pick option[value="ddigital/projects/${P}"]')`);
// The same organization and project in both panes.
await b.evaluate(`location.hash = '#/orgs/ddigital/side-by-side?a=ddigital/projects/${P}/requirements&b=ddigital/projects/${P}/requirements'`);
await wait(`document.querySelectorAll('[data-pane-body] #project-request-select').length === 2 && document.querySelectorAll('[data-pane-body] .project-prd-toggle').length === 2`);
const state = `({ selected: [...document.querySelectorAll('[data-pane-body] #project-request-select')].map(e => e.value), expanded: [...document.querySelectorAll('[data-pane-body] [data-prd]')].map(e => e.classList.contains('expanded')) })`;
await b.evaluate(`(() => { const e = document.querySelector('#pane-a #project-request-select'); e.value = 'request-2'; e.dispatchEvent(new Event('change', { bubbles: true })); })()`);
await wait(`document.querySelector('#pane-a #project-request-select').value === 'request-2'`);
await wait(`document.querySelectorAll('[data-pane-body] select[id$="-project-request-select"]').length === 2 && document.querySelectorAll('[data-pane-body] .project-prd-toggle').length === 2`);
const state = `({ selected: [...document.querySelectorAll('[data-pane-body] select[id$="-project-request-select"]')].map(e => e.value), expanded: [...document.querySelectorAll('[data-pane-body] [data-prd]')].map(e => e.classList.contains('expanded')) })`;
await b.evaluate(`(() => { const e = document.querySelector('#pane-a-project-request-select'); e.value = 'request-2'; e.dispatchEvent(new Event('change', { bubbles: true })); })()`);
await wait(`document.querySelector('#pane-a-project-request-select').value === 'request-2'`);
await b.evaluate(`document.querySelector('#pane-a .project-prd-toggle').click()`);
await wait(`document.querySelector('#pane-a [data-prd]')?.classList.contains('expanded')`);
// A new conversation changes the header's count, so each pane replaces its project content.
@@ -290,7 +292,7 @@ test('a pane keeps its request choice and PRD disclosure when a new conversation
await b.evaluate(`window.fixtureThreads.push({ ...window.fixtureThreads[0], id: '20000000-0000-4000-8000-000000000004', name: 'A new conversation' }); window.MosaicPanes.refresh()`);
await wait(`(() => { const t = document.querySelector('#pane-a .project-header p')?.textContent || ''; return t !== ${JSON.stringify(before)} && /4 conversations/.test(t) && /4 conversations/.test(document.querySelector('#pane-b .project-header p')?.textContent || ''); })()`);
await settled(b);
await wait(`document.querySelectorAll('[data-pane-body] #project-request-select').length === 2 && document.querySelectorAll('[data-pane-body] [data-prd]').length === 2`);
await wait(`document.querySelectorAll('[data-pane-body] select[id$="-project-request-select"]').length === 2 && document.querySelectorAll('[data-pane-body] [data-prd]').length === 2`);
assert.deepEqual(await b.evaluate(state), { selected: ['request-2', 'request-1'], expanded: [true, false] });
});
@@ -338,3 +340,68 @@ test('each Agents pane keeps its own project choice for its organization', async
assert.deepEqual(await b.evaluate(`[${select('a')}.value, ${select('b')}.value]`), ['spring-launch', 'ops']);
assert.equal(await b.evaluate(`!${rows('b')}.includes('load-tester') && ${rows('a')}.includes('social-scheduler')`), true);
});
// Polish 1 (#1873): every id a pane draws is its own. With the same page in both panes no id
// appears twice, and every label, ARIA reference and in-page link resolves inside its own pane.
const idAudit = `(() => {
const count = new Map();
for (const el of document.querySelectorAll('[id]')) count.set(el.id, (count.get(el.id) || 0) + 1);
const duplicates = [...count].filter(([, n]) => n > 1).map(([id]) => id).sort();
const stray = [];
for (const pane of document.querySelectorAll('.panes > .pane:not([hidden])')) {
for (const attr of ['for', 'aria-labelledby', 'aria-describedby', 'aria-controls']) {
for (const el of pane.querySelectorAll('[' + attr + ']')) {
for (const id of el.getAttribute(attr).split(/\\s+/).filter(Boolean)) {
const target = document.getElementById(id);
if (!target || !pane.contains(target)) stray.push(pane.id + ' ' + attr + '=' + id);
}
}
}
for (const a of pane.querySelectorAll('a[href^="#"]:not([href^="#/"])')) {
const id = decodeURIComponent(a.getAttribute('href').slice(1));
const target = id && document.getElementById(id);
if (id && (!target || !pane.contains(target))) stray.push(pane.id + ' href=#' + id);
}
}
return { duplicates, stray: stray.sort() };
})()`;
test('the same project, dashboard and Agents page in both panes draw no duplicate ids, and every reference stays in its pane', async t => {
const { b, wait } = await open(t);
await wait(`${title('b')}.includes("Forge")`);
const { stub, P } = await import('./helpers/projects-review-fixture.mjs');
await b.evaluate(stub.replace('const path=String(url),method', `if(url==='/api/orgs'||url==='/api/themes')return real(url,init);const path=String(url).replace(/^\\/api\\/orgs\\/(?:ddigital|forge)\\//,'/api/'),method`));
const a = (name, level = 'org', definition = name) => ({ name, level, definition, model: null, scope: { kind: 'org' }, overrides: [], narrowed: [] });
const set = agents => ({ state: 'ok', agents, excluded: [] });
const levels = { root: { state: 'ok', assistant: null, agents: [] }, orgs: [
{ id: 'ddigital', state: 'ok', base: set([a('chief')]), defined: { [P]: [{ name: 'social-scheduler', form: 'define' }] },
projects: { [P]: set([a('chief'), a('social-scheduler', 'project', 'coder')]) } },
] };
await b.evaluate(`(() => { const real = window.fetch; window.fetch = (url, init) => String(url).endsWith('/api/agents/levels') ? Promise.resolve(new Response(${JSON.stringify(JSON.stringify(levels))}, { headers: { 'content-type': 'application/json' } })) : real(url, init); })()`);
await b.evaluate('window.MosaicPanes.refresh()');
await wait(`document.querySelector('#pane-a-pick option[value="ddigital/projects/${P}"]')`);
const both = route => `location.hash = '#/orgs/ddigital/side-by-side?a=ddigital${route}&b=ddigital${route}'`;
const pages = [
['/organization', `${title('a')}.startsWith('Dashboard') && ${title('b')}.startsWith('Dashboard') && document.querySelectorAll('[data-pane-body] [data-system-types-heading]').length === 2`],
['/agents', `${title('a')}.startsWith('Agents') && ${title('b')}.startsWith('Agents') && document.querySelectorAll('[data-pane-body] [data-agents-available] tr[data-agent]').length >= 2`],
[`/projects/${P}`, `${title('a')}.startsWith('Pomodoro Timer') && ${title('b')}.startsWith('Pomodoro Timer') && !${text('a')}.includes('Reading…') && !${text('b')}.includes('Reading…')`],
[`/projects/${P}/conversations`, `${title('a')}.includes('Conversations') && ${title('b')}.includes('Conversations')`],
[`/projects/${P}/tasks`, `${title('a')}.includes('Tasks') && ${title('b')}.includes('Tasks') && !${text('a')}.includes('Reading…') && !${text('b')}.includes('Reading…')`],
[`/projects/${P}/requirements`, `document.querySelectorAll('[data-pane-body] [data-prd]').length === 2 && document.querySelectorAll('[data-pane-body] select').length >= 2`],
[`/projects/${P}/files`, `${title('a')}.includes('Files') && ${title('b')}.includes('Files') && !${text('a')}.includes('Reading…') && !${text('b')}.includes('Reading…')`],
];
const found = {}, drawn = {};
for (const [route, ready] of pages) {
await b.evaluate(both(route));
await wait(ready);
await settled(b);
const page = route.replace(P, 'P');
found[page] = await b.evaluate(idAudit);
drawn[page] = await b.evaluate(`[...document.querySelectorAll('#pane-a [data-pane-body] [id]')].map(e => e.id)`);
}
assert.deepEqual(found, Object.fromEntries(pages.map(([route]) => [route.replace(P, 'P'), { duplicates: [], stray: [] }])));
// The audit is not vacuous: the dashboard draws its system types heading and the requirements page
// its request select, north star and PRD headings, each with the pane's prefix.
assert.deepEqual(drawn['/organization'], ['pane-a-ws-reload-why', 'pane-a-ws-types-h']);
assert.deepEqual(drawn['/projects/P/requirements'], ['pane-a-project-request-select', 'pane-a-project-north-star', 'pane-a-project-prd']);
});
@@ -128,11 +128,14 @@ test('org and project filters narrow the board', async t => {
});
test('the layout toggle is remembered across a reload', async t => {
const { b, wait } = await open(t);
const { b, wait, url } = await open(t);
await b.evaluate('document.querySelector("[data-tb-layout=list]").click()');
await wait('document.querySelector("[data-tasks-board]").dataset.layout === "list" && document.querySelector("[data-tb-list]")');
assert.equal(await b.evaluate('document.querySelector("[data-tb-layout=list]").getAttribute("aria-pressed")'), 'true');
await b.evaluate('location.reload()');
// Load the page again through navigate(), which waits for the load. A location.reload() from evaluate
// returns before the reload commits, so the next wait ran on the old page and the reload killed it
// (as instances-browser found). The same URL with only a hash would not load, so add a query.
await b.navigate(url.replace('/#', '/?reload=1#'));
await wait('document.querySelector("[data-tasks-board] [data-tb-tracker=home]")');
assert.equal(await b.evaluate('document.querySelector("[data-tasks-board]").dataset.layout'), 'list');
assert.equal(await b.evaluate('document.querySelectorAll("[data-tb-list] tr.tb-row").length'), 5);
@@ -221,6 +224,107 @@ test('an edit dialog left open while another writer changes the task saves again
assert.equal(await b.evaluate('document.querySelector("#tb-dialog").open'), false);
});
// Polish 1 (#1873): after a conflict the dialog reads the current task before it reopens. This holds
// that read: the first workspace read after the write's answer waits until window.__release().
const HOLD_CONFLICT_READ = `(() => {
const real = window.fetch;
window.__armed = false; window.__held = false;
window.fetch = (u, o) => {
const url = String(u);
if (url === '/api/workspace/tasks') return real(u, o).then(r => { window.__armed = true; return r; });
if (url.startsWith('/api/workspace?') && window.__armed && !window.__held) {
window.__held = true;
return new Promise(go => { window.__release = () => real(u, o).then(go); });
}
return real(u, o);
};
})()`;
const SAVE_AS = title => `(() => { const f = document.querySelector("#tb-dialog form"); f.title.value = ${JSON.stringify(title)}; f.requestSubmit(); })()`;
const STATUS = 'document.querySelector("#tb-dialog [data-tb-form-status]")?.textContent';
test('after a conflict the dialog shows the refusal only with the current task, and takes no save while it reads it', async t => {
const { b, wait, orgs } = await open(t);
await b.evaluate('document.querySelector("[data-tb-open=\'forge|task:42\']").click()');
await wait('document.querySelector("#tb-dialog[open] form[data-tb-form=edit]")');
const t42 = orgs.store.find(s => s.n === 42);
Object.assign(t42, { title: 'Invoice export (renamed elsewhere)', assignees: ['coder'], rev: t42.rev + 1 });
await b.evaluate(HOLD_CONFLICT_READ);
await b.evaluate(SAVE_AS('Invoice export, CSV'));
await wait('window.__held');
// The write was refused and the current task is still being read: the form still holds the old task,
// so it does not show the refusal and its Save is off.
assert.equal(await b.evaluate(STATUS), 'Reading the current task…');
assert.equal(await b.evaluate('document.querySelector("#tb-dialog form").title.value'), 'Invoice export, CSV');
assert.equal(await b.evaluate('document.querySelector("#tb-dialog [data-tb-submit]").disabled'), true);
// A save in this window, even one sent past the disabled button, goes nowhere.
await b.evaluate(SAVE_AS('Invoice export, CSV again'));
await b.evaluate('new Promise(r => setTimeout(r, 200))');
assert.deepEqual(orgs.writes, [['forge', 'tasks.edit', { task_ref: 'task:42', expect: D(2), title: 'Invoice export, CSV' }]]);
// Once the read answers, the refusal and the current task appear together.
await b.evaluate('window.__release()');
await wait('/changed after the board read it/.test(' + STATUS + ')');
assert.equal(await b.evaluate('document.querySelector("#tb-dialog form").title.value'), 'Invoice export (renamed elsewhere)');
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#tb-dialog input[name=assignees]:checked")].map(i => i.value)'), ['coder']);
assert.equal(await b.evaluate('document.querySelector("#tb-dialog [data-tb-submit]").disabled'), false);
await b.evaluate(SAVE_AS('Invoice export, CSV'));
await wait('/#42 saved in Forge/.test(document.querySelector("[data-tb-notice]").textContent)');
assert.deepEqual(orgs.writes.at(-1), ['forge', 'tasks.edit', { task_ref: 'task:42', expect: D(3), title: 'Invoice export, CSV' }]);
assert.equal(orgs.writes.length, 2);
});
test('after a conflict a task that can no longer be read leaves the dialog unable to save', async t => {
const { b, wait, orgs } = await open(t);
await b.evaluate('document.querySelector("[data-tb-open=\'forge|task:42\']").click()');
await wait('document.querySelector("#tb-dialog[open] form[data-tb-form=edit]")');
const t42 = orgs.store.find(s => s.n === 42);
t42.rev++;
await b.evaluate(HOLD_CONFLICT_READ);
await b.evaluate(SAVE_AS('Invoice export, CSV'));
await wait('window.__held');
// The task leaves the store before the dialog's read answers, so neither read finds it.
orgs.store.splice(orgs.store.indexOf(t42), 1);
await b.evaluate('window.__release()');
await wait('/could not be read/.test(' + STATUS + ')');
assert.match(await b.evaluate(STATUS), /^The task changed after the board read it\. Nothing was written, and the current task could not be read; close this form and open the task again\.$/);
assert.deepEqual(await b.evaluate('[...document.querySelectorAll("#tb-dialog button")].map(x => [x.textContent, x.disabled])'), [['Archive', true], ['Cancel', false], ['Save', true]]);
await b.evaluate(SAVE_AS('Invoice export, CSV again'));
await b.evaluate('new Promise(r => setTimeout(r, 200))');
assert.equal(orgs.writes.length, 1);
await b.evaluate('document.querySelector("#tb-dialog [data-tb-cancel]").click()');
await wait('!document.querySelector("#tb-dialog").open');
});
test('a dialog closed just before the conflict read answers stays closed', async t => {
const { b, wait, orgs } = await open(t);
await b.evaluate('document.querySelector("[data-tb-open=\'forge|task:42\']").click()');
await wait('document.querySelector("#tb-dialog[open] form[data-tb-form=edit]")');
orgs.store.find(s => s.n === 42).rev++;
// Hold the read's body, not the request, so the close and the read's answer land in one task:
// close() clears `open` at once, but the event that empties the dialog runs after the read resumes.
await b.evaluate(`(() => {
const real = window.fetch;
let armed = false, held = false;
window.fetch = (u, o) => {
if (String(u) === '/api/workspace/tasks') return real(u, o).then(r => { armed = true; return r; });
if (String(u).startsWith('/api/workspace?') && armed && !held) {
held = true;
return real(u, o).then(async r => {
const body = await r.json();
r.json = () => new Promise(go => { window.__releaseJson = () => go(structuredClone(body)); });
return r;
});
}
return real(u, o);
};
})()`);
await b.evaluate(SAVE_AS('Invoice export, CSV'));
await wait('typeof window.__releaseJson === "function"');
await b.evaluate('document.querySelector("#tb-dialog").close(); window.__releaseJson()');
await b.evaluate('new Promise(r => setTimeout(r, 250))');
assert.deepEqual(await b.evaluate('[document.querySelector("#tb-dialog").open, document.querySelector("#tb-dialog").innerHTML]'), [false, '']);
assert.equal(orgs.writes.length, 1);
});
test('a poll that answers after focus has moved leaves focus where the person put it', async t => {
const { b, wait } = await open(t);
await b.evaluate('document.querySelector("[data-tb-layout=list]").click()');
@@ -45,7 +45,7 @@ function call(target, { method = 'GET', path, headers = {}, body }) {
const AUTHORITY = { withinRole: ['task.update.assigned', 'message.send'], crossRole: [] };
async function bus(t, id, { taskTracker = null } = {}) {
const root = scratch(t, 'tb-');
const businesses = { [id]: { id, human: 'jason', arbiters: { technical: 'reviewer', delivery: 'coder' }, roles: { coder: { definition: 'coder', authority: AUTHORITY }, reviewer: { definition: 'reviewer', authority: AUTHORITY } } } };
const businesses = { [id]: { id, human: 'jason', arbiters: { technical: 'chief', delivery: 'chief' }, roles: {chief: { definition: 'chief-of-staff', authority: { withinRole: ['decision.resolve.technical', 'decision.resolve.delivery'], crossRole: [] } }, coder: { definition: 'coder', authority: AUTHORITY }, reviewer: { definition: 'reviewer', authority: AUTHORITY } } } };
const store = new Store(root);
const broker = new Broker({ store, businesses, ...(taskTracker ? { taskTracker } : {}) });
const adapter = await storeAdapter()({ broker, businesses });
+1 -1
View File
@@ -6,7 +6,7 @@
"tools": ["read", "grep", "find", "ls"],
"network": "api-only",
"authority": {
"withinRole": ["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire"],
"withinRole": ["message.send", "role.launch", "role.end", "organization.change", "record.write", "instance.name", "role.hire", "role.fire", "decision.resolve.technical", "decision.resolve.delivery"],
"crossRole": []
},
"credentials": [{"service": "gitea", "scopes": ["read:repository", "read:user"]}],
+1 -1
View File
@@ -7,7 +7,7 @@
"network": "api-only",
"skills": ["ms-unslop", "ms-goal", "ms-communications"],
"authority": {
"withinRole": ["task.create", "task.assign", "task.reassign", "task.schedule", "task.update.assigned", "task.close", "message.send", "record.write"],
"withinRole": ["task.create", "task.assign", "task.reassign", "task.schedule", "task.update.assigned", "task.close", "message.send", "record.write", "decision.resolve.delivery"],
"crossRole": ["task.priority.change", "task.scope.change"]
},
"credentials": [
+2
View File
@@ -90,6 +90,8 @@ order. You don't write the product's code and you don't review it.
## Authority
Resolve delivery decisions when the organization names you its delivery arbiter.
Within your grants, act on what you see without waiting to be asked, and
raise a decision or send a message for anything outside them.