honker: row 194 S2d bus: the chief's instance.hire and instance.fire through a resolved decision raised with the call's arguments, the hire checks against the role files (W-N2, M60), fire steps A, B, C with kill: true, W-N1 instance-firing at the verb, the take and role.launch (D2.1-D2.3, T8-T10, T9c, T18)

This commit is contained in:
jason.woltje committed 2026-10-20 15:53:08 -05:00
1 parent 03e5bcb3cb
commit 089b92ca34
5 files changed
+564 -8

No files matched your search

+49 -1
View File
@@ -22,7 +22,7 @@ brief commit). Slices that don't need row 191 build first. S6 waits for row 191'
| Slice | Scope | Tests | State |
|---|---|---|---|
| S1 | business: vocabulary (role.hire, role.fire hard waits, HIRE_ALLOWED), role.mjs `hireable`, business.mjs keep/noHire/retired, instance-settings delete and veto ops, chief-of-staff.json `hireable: []` | T6 (business), T7, T8c, T17, D2.2 writer | done, see below |
| S2 | bus: reloadBusiness and runtime callback, instance.launch/end/regenerate requests, instances.set delete, veto.set, instance.hire/fire, keep checks, D4 hook, tester checks | T8-T10, T9c, T12-T14, T18 | S2a (D3), S2b (veto, hook, keep, T13) and S2c (requests, terminate, delete) done, S2d next |
| S2 | bus: reloadBusiness and runtime callback, instance.launch/end/regenerate requests, instances.set delete, veto.set, instance.hire/fire, keep checks, D4 hook, tester checks | T8-T10, T9c, T12-T14, T18 | S2a (D3), S2b (veto, hook, keep, T13), S2c (requests, terminate, delete) and S2d (hire, fire, W-N1) done, S2e (PRD 1.7 routing, sage-1471) next |
| S3 | cli: launcher lifecyclePass, host ops, `business instance` verbs | T1-T6, T11 | |
| S4 | harness: launch tool schema, hire and fire tools | T8 (D3 item 5) | |
| S5 | webui: Agents section and routes | T16 | |
@@ -55,6 +55,9 @@ Listed here as the build changes them.
- `packages/bus/tests/instances-verb.test.mjs:78` keeps `restart: true`, now for a D3 item 3 reason:
its boot copy is a stub without the file's credential references. A comment there says so.
`reload.test.mjs` covers the reload from a file boot.
- `packages/bus/tests/hard-waits.test.mjs:38` (S2d): the every-hard-wait loop gives `role.hire` and
`role.fire` a target and `arguments`, which their raise now requires (D2.1). The class and route it
checks are unchanged.
## S1 notes
@@ -172,3 +175,48 @@ Listed here as the build changes them.
the take and the runtime delete end to end). Probe mutants on the expiry, starting (verb and
take), the take's expiry and tester checks, the retry gate, the tester's own-request read and
not-held all died. They are not the M-list run.
## S2d notes (the chief's hire and fire, W-N1)
- Still hard waits (PRD 1.6). Under PRD 1.7 they move to ROUTED in S2e, with the reason form.
- The raise: `decision.raise` takes `arguments`, only for `role.hire` (`{name, definition, vars?}`)
or `role.fire` (`{name, reason}`), and requires it for those. `target` must equal
`arguments.name`. The raise event records it, and the human's decision view shows it under
`authorization.arguments`. The shape checks are the call's (`#hireFireArgs`): identifiers, not
`human` or `tester`, vars only `model` and `limits.*`, reason 1-500 characters.
- The call: `instance.hire {name, definition, vars?, decision}` and `instance.fire {name, reason,
decision}`, from a chief session only. Order: tester-refused, read-only and agent-required
(`#agent`), keys, `role-not-chief` (launch.by), not-configured, decision-mismatch for a decision of
another business, `#checkAuthority`, the call's arguments against the raise's (canonical JSON,
decision-mismatch), decision-consumed, instance-kept. The writer gets the chief as actor
(`chief-of-staff.chief`) and the context `{instance, run, decision}`, which changes.jsonl records.
- Hire: `#hireable` loads the definition and the chief's own role file through the new `roleFile`
option (the runtime passes `loadRole(rolesDir, name)`). Version 1, any action off HIRE_ALLOWED
(`unhireableAction`), not on the chief's `hireable` list, on `noHire` or no file all refuse
definition-not-hireable before the writer. Then the writer's hire, then the consume with
`{transaction}`. Without `roleFile` a hire refuses not-configured.
- Fire, before step A: instance-kept, unknown-role, organization-definition-refused (END_PROTECTED),
organization-instance-referenced (launch.by), and invalid-request for the last listed name.
Step A, one transaction: revoke the claims with the decision and reason, cancel open requests,
list taken ones, write `lifecycle.end {via: 'chief', by, runs, reason, kill: true}` when runs exist,
and `fire.stopped {cites, request}`. Step B: the writer's fire (already delisted is done). Step C,
one transaction: the consume with `{reason, transaction}` and `fire.done {cites, transaction}`.
- The fire's own events cite the decision under `cites`, not `decision`, so `#spent` counts only the
consume. A failure at B or C leaves the decision unspent, so the repeat runs.
- Choice: a repeat after a failed B or C asks for the kill again for any run of I still bound. A
revoked run stays bound until the launcher reports it ended, and a second signal to a dead
process does nothing. The tradeoff is a duplicate `lifecycle.end` against a run that may outlive
the first one.
- W-N1 `#firing`: a `fire.stopped` with no `fire.done` for the same `cites`. It refuses
instance-firing at the human and tester verbs (`#launchable`), at the take of a launch or
regenerate (`#takeRefusal`), and at the chief's `role.launch` (`#authorityFor`).
- `#organizationFail` passes the writer's own definition-not-hireable and instance-kept through by
name for hire and fire. They come from the file as it stands, after the broker's checks passed.
- `lifecycleView` shows `kill: true` when the request carries it.
- Deferred: the kill escalation in the launcher (S3, S6).
- Tests: `packages/bus/tests/chief-of-staff.test.mjs`, 16 new (T8 x5, T8a, T8b x2, T9 x2, T9c x2,
T10, T18, the runtime end to end, W-N1 at the take). 39 in the file pass. Probe mutants on 21
lines (the arguments check, each W-N1 site, M60's version check, keep, the other-business check,
last-listed, kill, the chief's list, noHire, role-not-chief, the pre-check consume, both consumes'
events, the raise's argument rules, the revoke, the cancel, the pass-through, the view's kill) all
died. They are not the M-list run.
+145 -2
View File
@@ -4,6 +4,8 @@ import { NAME as HARNESS_NAME } from '../../plugins/src/kinds.mjs';
import { loadRequirements } from '../../tasks/src/requirements.mjs';
import { MERGE, LandingRefusal, readLanding } from './landing.mjs';
import { ID_PATTERN, THREAD_DEFINITION, END_PROTECTED } from '../../business/src/vocabulary.mjs';
import { unhireableAction } from '../../business/src/role.mjs';
import { canonicalJson } from '../../business/src/util.mjs';
// Kept closed while S1 lands; its vocabulary is the integration authority.
export const ACTIONS = Object.freeze([
@@ -119,11 +121,20 @@ const LIFECYCLE_STEPS = ['lifecycle.taken', 'lifecycle.done', 'lifecycle.cancell
// limit: the launcher checks each run's start time before it signals.
export const LIFECYCLE_REQUEST_MS = 60_000;
const LIFECYCLE_CODE = /^[a-z-]{1,64}$/;
// Row 194 (#1692, D2.1, D2.2): the chief's hire and fire arguments, allowed then required. A raise of
// role.hire or role.fire carries them, and the call must equal them field by field (N1).
const HIRE_FIRE_ARGS = Object.freeze({
'role.hire': [['name', 'definition', 'vars'], ['name', 'definition']],
'role.fire': [['name', 'reason'], ['name', 'reason']],
});
// N3: hire's vars hold only the model key and limits.* keys. The writer checks the values.
const HIRE_VAR = (key) => key === 'model' || key.startsWith('limits.');
// What instance.request and the host ops answer about a request.
const lifecycleView = (r) => ({
request: r.request, action: r.action, instance: r.instance, by: r.by, at: r.at, state: r.state,
...('revision' in r ? { revision: r.revision, retry: r.retry === true } : {}),
...('runs' in r ? { runs: r.runs } : {}),
...(r.kill === true ? { kill: true } : {}),
done: r.done ? { run: r.done.run ?? null, transaction: r.done.transaction ?? null, refusal: r.done.refusal ?? null } : null,
cancelled: r.cancelled ? { by: r.cancelled.by } : null,
});
@@ -257,11 +268,12 @@ export class Broker {
#limits;
#harnessOn;
#testerOn;
#roleFile;
// Row 125 (#1619, REQ-LIMIT-4): a run a limit stops, by business:run, until its next receive or state.
#stops = new Map();
#clock = 0;
// credentialStatus is the runtime's metadata-only Credentials.status(); the broker never gets use().
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, usage = null, limits = null, harnessOn = null, testerOn = null }) {
constructor({ store, businesses, secretCheck = () => {}, credentialStatus = () => [], organization = null, project = null, usage = null, limits = null, harnessOn = null, testerOn = null, roleFile = null }) {
if (!object(businesses) || !Object.keys(businesses).length) fail('invalid-business');
this.#businesses = structuredClone(businesses);
this.#store = store;
@@ -281,6 +293,10 @@ export class Broker {
// tester lifecycle call and at the take. Without it the broker's own copy decides.
if (testerOn !== null && typeof testerOn !== 'function') throw new TypeError('testerOn must be a function');
this.#testerOn = testerOn ?? ((business) => this.#business(business).vars?.['bus.tester'] === true);
// Row 194 (#1692, D2.3): roleFile(name) loads role definition `name` from the roles directory as it
// stands, for the hire checks. Without it a hire refuses not-configured.
if (roleFile !== null && typeof roleFile !== 'function') throw new TypeError('roleFile must be a function');
this.#roleFile = roleFile;
// Row 90 (#1584): the approved PRD's requirement ids. A bad requirements.json refuses here.
this.#requirements = loadRequirements();
for (const table of [
@@ -413,6 +429,15 @@ export class Broker {
if (!Object.hasOwn(b.roles, instance)) fail('unknown-role');
if (this.#threaded(s.business, instance)) fail(thread);
if (this.#store.get('SELECT state FROM launch_state WHERE business=?', s.business)?.state === 'revoked') fail('launch-revoked');
if (this.#firing(s.business, instance)) fail('instance-firing');
}
// W-N1 (webby-0014): a fire of I stopped its runs and hasn't delisted it and consumed its decision.
#firing(business, instance) {
return !!this.#store.get(
`SELECT 1 FROM events f WHERE f.business=? AND f.kind='action.allowed' AND f.subject=? AND json_extract(f.body,'$.action')='fire.stopped'
AND NOT EXISTS (SELECT 1 FROM events d WHERE d.business=f.business AND d.kind='action.allowed' AND json_extract(d.body,'$.action')='fire.done'
AND json_extract(d.body,'$.cites')=json_extract(f.body,'$.cites')) LIMIT 1`,
business, instance);
}
// D1.1 item 2: why the take closes a request instead of going, or null. A terminate's signal
// always goes. Its claims were revoked at the verb.
@@ -422,6 +447,7 @@ export class Broker {
if (r.action !== 'lifecycle.delete') {
if (this.#store.get('SELECT state FROM launch_state WHERE business=?', business)?.state === 'revoked') return 'launch-revoked';
if ((this.#business(business).retired ?? []).includes(r.instance)) return 'instance-retired';
if (this.#firing(business, r.instance)) return 'instance-firing';
}
if (r.by === 'tester' && this.#testerOn(business) !== true) return 'tester-off';
return null;
@@ -805,7 +831,8 @@ export class Broker {
options: JSON.parse(d.options),
blocking: !!d.blocking,
authorization: context
? { action: d.action, target: context.target, approvalChoice: context.approvalChoice }
? { action: d.action, target: context.target, approvalChoice: context.approvalChoice,
...(context.arguments ? { arguments: context.arguments } : {}) }
: null,
};
}
@@ -900,6 +927,7 @@ export class Broker {
this.#store.get('SELECT state FROM launch_state WHERE business=?', s.business)?.state === 'revoked'
)
fail('launch-revoked');
if (action === 'role.launch' && this.#firing(s.business, target)) fail('instance-firing');
const cls = this.#classification(s, action, target);
if (cls === 'within-role' && !decision) return { class: cls };
if (!decision) fail('decision-required');
@@ -1430,6 +1458,8 @@ export class Broker {
return this.#requestProject(s, { verb: request.verb, args: structuredClone(args) });
if (PROJECT_FILE_VERBS.includes(request.verb))
return this.#requestProjectFile(s, { verb: request.verb, args: structuredClone(args) });
if (request.verb === 'instance.hire' || request.verb === 'instance.fire')
return this.#chiefLifecycle(s, request.verb, structuredClone(args));
if (["limits", "limits.set", "organization.set", "organization.revision", "plugins", "plugins.set", "instances", "instances.set", "routes", "routes.set", "veto.set"].includes(request.verb)) {
// Row 123 (#1617, REQ-COS-1, REQ-LIMIT-1): organization.set is limits.set for a role whose
// file lists organization.change. It takes the same keys, runs the same write path and
@@ -1520,12 +1550,115 @@ export class Broker {
throw this.#refused(s, e, request);
}
}
// Row 194 (#1692, D2.1, D2.2): the chief's hire and fire, hard waits until PRD 1.7. Each cites a
// decision the human resolved, raised with these exact arguments. The writer commits outside the
// store transaction, so the decision is checked before it and consumed only after it, as
// organization.set's grant is.
#chiefLifecycle(s, verb, a) {
const hire = verb === 'instance.hire', action = hire ? 'role.hire' : 'role.fire';
this.#agent(s);
const [allowed, required] = HIRE_FIRE_ARGS[action];
keys(a, [...allowed, 'decision'], [...required, 'decision']);
identifier(a.decision);
const { decision, ...args } = a;
this.#hireFireArgs(action, args);
const b = this.#business(s.business);
if (b.launch?.by !== s.role) fail('role-not-chief');
if (!this.#organization) fail('not-configured');
if (!this.#store.get('SELECT 1 FROM decisions WHERE id=? AND business=?', decision, s.business) &&
this.#store.get('SELECT 1 FROM decisions WHERE id=?', decision)) fail('decision-mismatch');
const authority = { decision, target: args.name };
this.#checkAuthority(s, action, authority);
if (canonicalJson(this.#raised(s.business, decision)?.arguments ?? null) !== canonicalJson(args)) fail('decision-mismatch');
if (this.#spent(s.business, decision)) fail('decision-consumed');
// D2.4: a kept name is neither hired nor fired, whatever the decision.
if ((b.launch?.keep ?? []).includes(args.name)) fail('instance-kept');
const actor = b.roles[s.role].definition + '.' + s.role;
const context = { instance: s.role, run: s.run, decision };
const write = (op) => {
try {
return this.#organization(s.business, op, args, actor, context);
} catch (e) {
this.#organizationFail(e, verb);
}
};
if (hire) {
this.#hireable(s, args.definition);
const result = write('instance.hire');
this.#store.transaction(() => this.#consumeAuthority(s, action, authority, { transaction: result.transaction }));
return result;
}
if (!Object.hasOwn(b.roles, args.name)) fail('unknown-role');
if (END_PROTECTED.includes(b.roles[args.name].definition)) fail('organization-definition-refused');
if (b.launch?.by === args.name) fail('organization-instance-referenced');
// The last listed name refuses before step A stops anything, as the writer would at step B.
const listed = b.launch?.instances ?? [];
if (listed.length === 1 && listed[0] === args.name) fail('invalid-request');
// Step A (webby-0011 B2): the store side first, so a delisted agent never keeps a live process.
// V3's revoke, cancel and end, with kill: true for the launcher's escalation.
const stopped = this.#store.transaction(() => {
const held = this.#threaded(s.business, args.name)
? this.#threadHolders(s.business, args.name)
: [this.#holder(s.business, args.name)].filter((h) => h?.op === 'claim');
for (const h of held) this.#claimEnd(s, h, 'revoke', decision, args.reason);
const runs = held.map((h) => h.holder_run);
for (const [key, r] of this.#runs)
if (r.business === s.business && r.role === args.name && !this.#ended.has(key) && !runs.includes(r.run)) runs.push(r.run);
const open = this.#openRequests(s.business, args.name).filter((r) => r.action !== 'lifecycle.end');
const cancelled = open.filter((r) => r.state === 'open').map((r) => r.request);
const taken = open.filter((r) => r.state === 'taken').map((r) => r.request);
for (const request of cancelled) this.#lifecycleStep(s.business, 'lifecycle.cancelled', { request, by: s.role });
// The request the launcher's pass takes to signal the runs. The fire's own events cite the
// decision under `cites`, so #spent doesn't count them as its use.
const request = runs.length === 0 ? null : randomUUID();
if (request)
this.#event(s, 'action.allowed', { action: 'lifecycle.end', request, via: 'chief', by: s.role, runs, reason: args.reason, kill: true }, args.name);
this.#event(s, 'action.allowed', { action: 'fire.stopped', cites: decision, request }, args.name);
return { request, runs, cancelled, taken };
});
// Step B: the delist. An instance already delisted counts as done.
const result = write('instance.fire');
// Step C: consume the decision and record the fire done.
this.#store.transaction(() => {
this.#consumeAuthority(s, action, authority, { reason: args.reason, transaction: result.transaction ?? null });
this.#event(s, 'action.allowed', { action: 'fire.done', cites: decision, transaction: result.transaction ?? null }, args.name);
});
return { ...result, ...stopped };
}
// The shape of hire and fire arguments, at the raise and at the call.
#hireFireArgs(action, args) {
identifier(args.name);
if (args.name === 'human' || args.name === 'tester') fail('invalid-request');
if (action === 'role.fire') return string(args.reason, 500);
identifier(args.definition);
if ('vars' in args && (!object(args.vars) || !Object.keys(args.vars).every(HIRE_VAR))) fail('invalid-request');
}
// D2.3 at hire, against the role files as they stand: a version 2 definition whose every action is
// on HIRE_ALLOWED (W-N2), named in the chief role file's hireable list and not in launch.noHire.
#hireable(s, definition) {
if (!this.#roleFile) fail('not-configured');
const b = this.#business(s.business);
const load = (name) => {
try {
return this.#roleFile(name);
} catch (e) {
if (e.name === 'BusinessError') fail('definition-not-hireable');
throw e;
}
};
const def = load(definition);
if (def.roleVersion !== 2 || unhireableAction(def) !== null) fail('definition-not-hireable');
if (!(load(b.roles[s.role].definition).hireable ?? []).includes(definition)) fail('definition-not-hireable');
if ((b.launch?.noHire ?? []).includes(definition)) fail('definition-not-hireable');
}
// The bus code for an organization writer's refusal. Anything the writer didn't refuse by name is
// outcome-unknown: the write may have committed.
#organizationFail(e, verb) {
if (e.code === "revision-conflict") fail("revision-conflict");
if (e.code?.startsWith("organization-")) fail(e.code);
if (e.code === "invalid-plugins" || e.code === "unknown-plugin") fail(e.code);
// Row 194: the writer's own copy of the hire and fire checks, against the file as it stands.
if ((verb === "instance.hire" || verb === "instance.fire") && ["definition-not-hireable", "instance-kept"].includes(e.code)) fail(e.code);
// Row 65: the route checks every write of an instance's route vars runs.
if (["harness-off", "harness-refused", "thinking-unsupported"].includes(e.code)) fail(e.code);
// The routing table names a model the table lacks; the older verbs keep invalid-request.
@@ -1897,10 +2030,19 @@ export class Broker {
'approvalChoice',
'spec_path',
'spec_commit',
'arguments',
],
['action', 'question', 'options', 'recommendation', 'blocking'],
);
this.#agent(s);
// Row 194 (#1692, D2.1): a hire or fire raise carries the call's arguments, so the human
// resolves exactly what runs. The target is the instance name.
if (HIRE_FIRE_ARGS[a.action]) {
const [allowed, required] = HIRE_FIRE_ARGS[a.action];
keys(a.arguments, allowed, required);
if (a.target !== a.arguments.name) fail('invalid-request');
this.#hireFireArgs(a.action, a.arguments);
} else if ('arguments' in a) fail('invalid-request');
if ((a.spec_path !== undefined || a.spec_commit !== undefined) && !spec) fail('invalid-spec-citation');
const cls = this.#classification(s, a.action, a.target);
string(a.question);
@@ -1982,6 +2124,7 @@ export class Broker {
task_ref: a.task_ref ?? null,
target: a.target ?? null,
approvalChoice,
...(HIRE_FIRE_ARGS[a.action] ? { arguments: a.arguments } : {}),
holderRun:
a.action === 'role.revoke' ? (this.#holder(s.business, a.target)?.holder_run ?? null) : null,
},
+7 -1
View File
@@ -12,6 +12,7 @@ import { createLoader, hasModule } from '../../plugins/src/loader.mjs';
import { pluginSettings } from '../../business/src/plugin-settings.mjs';
import { instanceSettings } from '../../business/src/instance-settings.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { loadRole } from '../../business/src/role.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
import { canonicalJson } from '../../business/src/util.mjs';
import { busBusiness } from './business.mjs';
@@ -156,7 +157,7 @@ export async function startBroker({
},
// limits and limits.set (organization.set is limits.set) carry each token limit's count; a
// plugins.set reloads the business's limit checks in the background.
organization:settings?(id,verb,args,actor)=>{
organization:settings?(id,verb,args,actor,context)=>{
if(verb==='limits')return withTokens(id,settings.read(id));
if(verb==='plugins')return plugins.read(id);
if(verb==='instances')return instances.read(id);
@@ -171,12 +172,17 @@ export async function startBroker({
else if(verb==='plugins.set')result=plugins.set(id,args,actor);
// Row 194 (#1692, V6): the human's keep and noHire lists, through the instance writer.
else if(verb==='veto.set')result=instances.veto(id,args,actor);
// Row 194 (D2.1, D2.2): the chief's hire and fire, with the decision in the change context.
else if(verb==='instance.hire')result=instances.hire(id,args,{actor,context});
else if(verb==='instance.fire')result=instances.fire(id,{name:args.name},{actor,context});
else throw new BusError('invalid-request');
// Row 194 (D3 item 2): one reload, after every verb that committed, then the limit checks.
const live=reload(id);
limits.refresh(id).catch(e=>limitLog({event:'limit-refresh-failed',business:id,message:e.message}));
return verb==='instances.set'?{...result,restart:!live}:result;
}:null,
// Row 194 (D2.3): the role files as they stand, for the hire checks.
roleFile: organization ? (name) => loadRole(organization.rolesDir, name) : null,
secretCheck: (value) => credentials.assertClean(value),
credentialStatus: () => credentials.status(),
// Row 65: a new launch binds only on a harness its business switches on, read from the file
+357 -3
View File
@@ -7,9 +7,13 @@ import { createHash } from 'node:crypto';
import { DatabaseSync } from 'node:sqlite';
import { fileURLToPath, pathToFileURL } from 'node:url';
import { Store, schemaDigest } from '../src/store.mjs';
import { Broker } from '../src/broker.mjs';
import { Broker, ACTIONS } from '../src/broker.mjs';
import { HIRE_ALLOWED } from '../../business/src/vocabulary.mjs';
import { startBroker } from '../src/runtime.mjs';
import { businessDoc, rolesCopy, systemFor, writeJson } from '../../business/tests/helpers.mjs';
import { busBusiness } from '../src/business.mjs';
import { loadBusiness } from '../../business/src/business.mjs';
import { resolveInstance } from '../../business/src/resolve.mjs';
// Row 123 (#1617): the chief of staff ends runs without a decision and changes the organization
// file through the shared write path. REQ-ROLE-2 (one holder; revoke), REQ-ROLE-3 (the
@@ -44,7 +48,7 @@ const demo = (extra = {}) => ({
const PATCH = { revision: 'a'.repeat(64), limits: { total: 3 } };
function setup(t, extra = {}) {
function setup(t, extra = {}, { roleFile = null } = {}) {
const root = mkdtempSync(join(tmpdir(), 'bus-chief-'));
const store = new Store(root);
t.after(() => {
@@ -52,13 +56,16 @@ function setup(t, extra = {}) {
rmSync(root, { recursive: true, force: true });
});
const organization = [];
const writer = { error: null };
const writer = { error: null, during: null };
const b = new Broker({
store,
businesses: demo(extra),
roleFile,
organization: (...args) => {
organization.push(args);
if (writer.during) writer.during(args);
if (writer.error) throw writer.error;
if (args[1] === 'instance.hire' || args[1] === 'instance.fire') return { revision: 'b'.repeat(64), transaction: 't-' + organization.length };
// The human's limits read answers more than the revision; organization.revision must not.
return args[1] === 'limits'
? { revision: 'b'.repeat(64), limits: { total: 7 }, models: { m: {} }, tokens: { used: 1 } }
@@ -701,3 +708,350 @@ test('row 194 T18 (D2.4): from a chief session veto.set and instances.set refuse
call('human', 'veto.set', { revision: PATCH.revision, op: 'unkeep', name: 'coder' });
assert.deepEqual(organization.at(-1), ['demo', 'veto.set', { revision: PATCH.revision, op: 'unkeep', name: 'coder' }, 'jason']);
});
// Row 194 (#1692, D2.1-D2.3): the chief's hire and fire, hard waits under PRD 1.6. Each cites a
// decision the human resolved, raised with the call's exact arguments. The role files are stubs, so
// a definition's authority is the test's to set.
const LISTED = { launch: { by: 'chief', instances: ['pm', 'coder'], keep: ['pm'], noHire: ['banned'] } };
const roleDoc = (withinRole, extra = {}) => ({ roleVersion: 2, authority: { withinRole, crossRole: [] }, hireable: [], ...extra });
const ROLE_FILES = {
'chief-of-staff': roleDoc(chiefAuthority, { hireable: ['coder', 'researcher', 'wide', 'banned'] }),
coder: roleDoc(['message.send', 'record.write']),
// A version 1 file has empty authority lists, so only the version check refuses it (M60).
researcher: { roleVersion: 1, authority: { withinRole: [], crossRole: [] }, hireable: [] },
wide: roleDoc(['message.send', 'task.create']),
banned: roleDoc(['message.send']),
pm: roleDoc(['message.send']),
};
const roleFile = (files) => (name) => {
if (!Object.hasOwn(files, name)) throw Object.assign(new Error(`no role ${name}`), { name: 'BusinessError' });
return files[name];
};
function hireSetup(t, extra = LISTED, files = ROLE_FILES) {
return setup(t, extra, { roleFile: roleFile(files) });
}
const options = [{ key: 'yes', text: 'Yes' }, { key: 'no', text: 'No' }];
const raise = (call, action, args, who = 'chief') =>
call(who, 'decision.raise', { action, target: args.name, arguments: args, question: `${action} ${args.name}?`, options, recommendation: 'yes', blocking: false });
const approved = (call, action, args) => {
const d = raise(call, action, args);
call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
return d.id;
};
const HIRE = { name: 'helper', definition: 'coder' };
// The action's use, not the raise that names the same action.
const used = (allowed, action) => allowed(action).filter((e) => e.body.operation !== 'decision.raise');
const FIRE = { name: 'coder', reason: 'the work is done' };
test('row 194 T8: the chief hires through a resolved decision raised with the hire arguments, and the writer gets the decision as context', (t) => {
const { call, organization, allowed, store } = hireSetup(t);
const d = raise(call, 'role.hire', HIRE);
// A hard wait under PRD 1.6: the human decides, not the arbiter.
assert.deepEqual({ ...store.get('SELECT class, route_to FROM decisions WHERE id=?', d.id) }, { class: 'gated', route_to: 'human' });
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, decision: d.id }), /decision-not-approved/);
assert.throws(() => call('chief', 'instance.hire', HIRE), /invalid-request/);
call('human', 'decision.resolve', { id: d.id, choice: 'yes' });
assert.equal(organization.length, 0);
const result = call('chief', 'instance.hire', { ...HIRE, decision: d.id });
assert.equal(result.transaction, 't-1');
assert.deepEqual(organization, [['demo', 'instance.hire', HIRE, 'chief-of-staff.chief', { instance: 'chief', run: 'chief-run', decision: d.id }]]);
const [use, ...rest] = used(allowed, 'role.hire');
assert.equal(rest.length, 0);
assert.equal(use.subject, 'helper');
assert.equal(use.body.decision, d.id);
assert.equal(use.body.transaction, 't-1');
// Single use: a second call is refused and doesn't reach the writer.
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, decision: d.id }), /decision-consumed/);
assert.equal(organization.length, 1);
// The human's decision view shows the arguments it approves.
assert.deepEqual(call('human', 'decisions', { list: 'recent' }).find((x) => x.id === d.id).authorization.arguments, HIRE);
});
test('row 194 T8: a hire call that differs from the raised arguments, or another target, refuses decision-mismatch', (t) => {
const { call, organization } = hireSetup(t);
const id = approved(call, 'role.hire', { ...HIRE, vars: { model: 'm' } });
for (const args of [HIRE, { ...HIRE, vars: { model: 'n' } }, { ...HIRE, vars: { model: 'm', 'limits.x': 1 } }, { ...HIRE, definition: 'wide', vars: { model: 'm' } }])
assert.throws(() => call('chief', 'instance.hire', { ...args, decision: id }), /decision-mismatch/, JSON.stringify(args));
assert.throws(() => call('chief', 'instance.hire', { name: 'other', definition: 'coder', vars: { model: 'm' }, decision: id }), /decision-mismatch/);
// A fire decision doesn't hire, nor a hire decision fire.
const fire = approved(call, 'role.fire', FIRE);
assert.throws(() => call('chief', 'instance.hire', { name: 'coder', definition: 'coder', decision: fire }), /decision-mismatch/);
assert.throws(() => call('chief', 'instance.fire', { name: 'helper', reason: 'x', decision: id }), /decision-mismatch/);
assert.equal(organization.length, 0);
});
test('row 194 T8: the raise checks the arguments: required for hire and fire, refused on any other action, target equal to the name', (t) => {
const { call } = hireSetup(t);
const base = { question: 'q?', options, recommendation: 'yes', blocking: false };
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.hire', target: 'helper' }), /invalid-request/);
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.hire', target: 'other', arguments: HIRE }), /invalid-request/);
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.hire', target: 'helper', arguments: { ...HIRE, extra: 1 } }), /invalid-request/);
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.hire', target: 'helper', arguments: { ...HIRE, vars: { harness: 'pi' } } }), /invalid-request/);
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.fire', target: 'coder', arguments: { name: 'coder' } }), /invalid-request/);
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.revoke', target: 'coder', arguments: { name: 'coder' } }), /invalid-request/);
for (const name of ['human', 'tester'])
assert.throws(() => call('chief', 'decision.raise', { ...base, action: 'role.hire', target: name, arguments: { name, definition: 'coder' } }), /invalid-request/);
});
test('row 194 T8: only the chief hires or fires; the tester, the reader, the human and every other role refuse before the writer', (t) => {
const { call, organization } = hireSetup(t);
const id = approved(call, 'role.hire', HIRE);
for (const who of ['pm', 'coder', 'assistant']) {
assert.throws(() => call(who, 'instance.hire', { ...HIRE, decision: id }), /role-not-chief/, who);
assert.throws(() => call(who, 'instance.fire', { ...FIRE, decision: id }), /role-not-chief/, who);
}
assert.throws(() => call('tester', 'instance.hire', { ...HIRE, decision: id }), /tester-refused/);
assert.throws(() => call('reader', 'instance.hire', { ...HIRE, decision: id }), /read-only/);
assert.throws(() => call('human', 'instance.fire', { ...FIRE, decision: id }), /agent-required/);
assert.equal(organization.length, 0);
});
test('row 194 T8: a decision from another business refuses decision-mismatch, not decision-not-found', (t) => {
const { call, store } = hireSetup(t);
const id = approved(call, 'role.hire', HIRE);
const COLUMNS = 'at,project,raised_by_role,raised_by_run,class,action,route_to,question,options,recommendation,task_ref,requirement_ref,blocking,supersedes,spec_path,spec_commit';
store.run(`INSERT INTO decisions(id,business,${COLUMNS}) SELECT 'elsewhere','other',${COLUMNS} FROM decisions WHERE id=?`, id);
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, decision: 'elsewhere' }), /decision-mismatch/);
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, decision: 'no-such-decision' }), /decision-not-found/);
});
test('row 194 T8a: a re-hire of a fired name goes to the writer like a hire, and the writer\'s refusals pass through by name', (t) => {
const { call, organization, writer } = hireSetup(t);
const again = { name: 'coder', definition: 'coder' };
for (const code of ['organization-instance-exists', 'organization-instance-retired', 'definition-not-hireable', 'instance-kept']) {
writer.error = Object.assign(new Error(code), { code });
assert.throws(() => call('chief', 'instance.hire', { ...again, decision: approved(call, 'role.hire', again) }), new RegExp(code));
}
writer.error = null;
const id = approved(call, 'role.hire', again);
call('chief', 'instance.hire', { ...again, decision: id });
assert.deepEqual(organization.at(-1).slice(1, 3), ['instance.hire', again]);
});
test('row 194 T8b: a definition is hireable only when every action is on HIRE_ALLOWED, it is version 2, the chief lists it and noHire doesn\'t', (t) => {
assert.deepEqual([...HIRE_ALLOWED], ['message.send', 'record.write', 'review.request', 'task.update.assigned', 'task.scope.change', 'git.push.working']);
for (const action of ACTIONS) {
const files = { ...ROLE_FILES, wide: roleDoc(['message.send', action]) };
const { call, organization } = hireSetup(t, LISTED, files);
const args = { name: 'helper', definition: 'wide' };
const run = () => call('chief', 'instance.hire', { ...args, decision: approved(call, 'role.hire', args) });
if (HIRE_ALLOWED.includes(action)) {
run();
assert.equal(organization.length, 1, action);
} else {
assert.throws(run, /definition-not-hireable/, action);
assert.equal(organization.length, 0, action);
}
}
const { call, organization } = hireSetup(t);
const hire = (definition) => {
const args = { name: 'helper', definition };
return call('chief', 'instance.hire', { ...args, decision: approved(call, 'role.hire', args) });
};
// Version 1 (M60), noHire, not on the chief's list, and no role file.
for (const definition of ['researcher', 'banned', 'pm', 'nothing'])
assert.throws(() => hire(definition), /definition-not-hireable/, definition);
assert.equal(organization.length, 0);
hire('coder');
assert.equal(organization.length, 1);
});
test('row 194 T8b: a hire without the role files configured refuses not-configured', (t) => {
const { call, organization } = setup(t, LISTED);
const id = approved(call, 'role.hire', HIRE);
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, decision: id }), /not-configured/);
assert.equal(organization.length, 0);
assert.throws(() => new Broker({ store: null, businesses: demo(), roleFile: 'roles' }), /roleFile must be a function/);
});
test('row 194 T9: a fire revokes the claim, cancels open requests, asks the launcher to kill the runs, then delists and consumes', (t) => {
const { call, organization, allowed, lastClaim, writer } = hireSetup(t);
const id = approved(call, 'role.fire', FIRE);
// An open launch request of the coder, made by the human.
const open = call('human', 'instance.launch', { instance: 'coder' }).request;
let seen = null;
writer.during = ([, verb]) => { if (verb === 'instance.fire') seen = { stopped: allowed('fire.stopped').length, claim: lastClaim('coder').op, used: used(allowed, 'role.fire').length }; };
const result = call('chief', 'instance.fire', { ...FIRE, decision: id });
// Step A ran before the writer; step C after it.
assert.deepEqual(seen, { stopped: 1, claim: 'revoke', used: 0 });
assert.deepEqual(lastClaim('coder'), { op: 'revoke', decision: id, reason: 'the work is done', by: 'chief-run' });
assert.deepEqual(result.runs, ['coder-run']);
assert.deepEqual(result.cancelled, [open]);
assert.deepEqual(organization.at(-1), ['demo', 'instance.fire', FIRE, 'chief-of-staff.chief', { instance: 'chief', run: 'chief-run', decision: id }]);
const [end] = allowed('lifecycle.end');
assert.equal(end.subject, 'coder');
assert.deepEqual({ ...end.body, request: null }, { action: 'lifecycle.end', request: null, via: 'chief', by: 'chief', runs: ['coder-run'], reason: 'the work is done', kill: true });
assert.equal(allowed('lifecycle.cancelled').filter((e) => e.body.request === open).length, 1);
const [use] = used(allowed, 'role.fire');
assert.equal(use.body.decision, id);
assert.equal(use.body.reason, 'the work is done');
assert.equal(use.body.transaction, result.transaction);
assert.deepEqual(allowed('fire.done').map((e) => e.body.cites), [id]);
// The human's read of the request shows the kill.
assert.equal(call('human', 'instance.request', { request: end.body.request }).kill, true);
// A repeat after the consume stops nothing and doesn't reach the writer.
assert.throws(() => call('chief', 'instance.fire', { ...FIRE, decision: id }), /decision-consumed/);
assert.equal(organization.length, 1);
assert.equal(allowed('fire.stopped').length, 1);
});
test('row 194 T9: fire refusals come before step A and leave the claim, the requests and the file alone', (t) => {
const { call, organization, allowed, lastClaim } = hireSetup(t, { launch: { by: 'chief', instances: ['coder'], keep: ['pm'] } });
const fire = (args) => call('chief', 'instance.fire', { ...args, decision: approved(call, 'role.fire', args) });
assert.throws(() => fire({ name: 'pm', reason: 'r' }), /instance-kept/);
assert.throws(() => fire({ name: 'nobody', reason: 'r' }), /unknown-role/);
assert.throws(() => fire({ name: 'assistant', reason: 'r' }), /organization-definition-refused/);
assert.throws(() => fire({ name: 'chief', reason: 'r' }), /organization-instance-referenced/);
assert.throws(() => fire({ name: 'coder', reason: 'r' }), /invalid-request/);
assert.throws(() => fire({ name: 'coder', reason: '' }), /invalid-request/);
assert.equal(organization.length, 0);
assert.equal(allowed('fire.stopped').length + allowed('lifecycle.end').length + used(allowed, 'role.fire').length, 0);
assert.equal(lastClaim('coder').op, 'claim');
});
test('row 194 T9c: a writer failure at B leaves the instance firing, and the repeat delists and consumes once', (t) => {
const { call, b, caps, organization, allowed, writer } = hireSetup(t);
const id = approved(call, 'role.fire', FIRE);
writer.error = new Error('disk');
assert.throws(() => call('chief', 'instance.fire', { ...FIRE, decision: id }), /outcome-unknown/);
writer.error = null;
assert.equal(allowed('fire.stopped').length, 1);
assert.equal(used(allowed, 'role.fire').length, 0);
// W-N1: while it fires, no launch of the instance starts by any path.
assert.throws(() => call('human', 'instance.launch', { instance: 'coder' }), /instance-firing/);
assert.throws(() => b.authorize(caps.chief, 'role.launch', { target: 'coder' }), /instance-firing/);
// The repeat has no claim to revoke. The revoked run is still bound until the launcher reports it
// ended, so the repeat asks for its kill again, then delists and consumes.
const result = call('chief', 'instance.fire', { ...FIRE, decision: id });
assert.deepEqual(result.runs, ['coder-run']);
assert.equal(organization.filter(([, verb]) => verb === 'instance.fire').length, 2);
assert.equal(used(allowed, 'role.fire').length, 1);
assert.deepEqual(allowed('lifecycle.end').map((e) => e.body.kill), [true, true]);
assert.throws(() => call('chief', 'instance.fire', { ...FIRE, decision: id }), /decision-consumed/);
// Once done, the instance launches again.
assert.ok(call('human', 'instance.launch', { instance: 'coder' }).request);
});
test('row 194 T9c: a failure at C leaves the decision unspent and the instance firing, and the repeat consumes it', (t) => {
const { call, organization, allowed, store, writer } = hireSetup(t);
const id = approved(call, 'role.fire', FIRE);
// The delist commits, then the step C transaction aborts.
writer.during = () => store.run("CREATE TRIGGER fail_c BEFORE INSERT ON events WHEN json_extract(new.body,'$.action')='fire.done' BEGIN SELECT RAISE(ABORT, 'c failed'); END");
assert.throws(() => call('chief', 'instance.fire', { ...FIRE, decision: id }));
writer.during = null;
store.run('DROP TRIGGER fail_c');
assert.equal(used(allowed, 'role.fire').length, 0);
assert.equal(allowed('fire.done').length, 0);
assert.throws(() => call('human', 'instance.launch', { instance: 'coder' }), /instance-firing/);
call('chief', 'instance.fire', { ...FIRE, decision: id });
assert.equal(organization.filter(([, verb]) => verb === 'instance.fire').length, 2);
assert.equal(used(allowed, 'role.fire').length, 1);
assert.deepEqual(allowed('fire.done').map((e) => e.body.cites), [id]);
});
test('row 194 T10: a hire or fire of a kept name refuses instance-kept before the writer, whatever the decision', (t) => {
const { call, organization, lastClaim } = hireSetup(t);
for (const [action, verb, args] of [['role.hire', 'instance.hire', { name: 'pm', definition: 'coder' }], ['role.fire', 'instance.fire', { name: 'pm', reason: 'r' }]])
assert.throws(() => call('chief', verb, { ...args, decision: approved(call, action, args) }), /instance-kept/, verb);
assert.equal(organization.length, 0);
assert.equal(lastClaim('pm').op, 'claim');
});
test('row 194 T18: hire and fire take only their own keys, and a refused call writes nothing', (t) => {
const { call, organization, allowed } = hireSetup(t);
const hire = approved(call, 'role.hire', HIRE);
const fire = approved(call, 'role.fire', FIRE);
for (const extra of [{ keep: ['x'] }, { launch: {} }, { revision: 'a'.repeat(64) }, { force: true }]) {
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, ...extra, decision: hire }), /invalid-request/, JSON.stringify(extra));
assert.throws(() => call('chief', 'instance.fire', { ...FIRE, ...extra, decision: fire }), /invalid-request/, JSON.stringify(extra));
}
assert.throws(() => call('chief', 'instance.hire', { ...HIRE, vars: { harness: 'pi' }, decision: hire }), /invalid-request/);
assert.equal(organization.length, 0);
assert.equal(allowed('fire.stopped').length, 0);
});
test('row 194 T8, T9, T18: through the runtime the chief hires and fires with the real writer, the receipt names the decision, and a refusal leaves the file alone', async (t) => {
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');
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(readFileSync(chiefFile, 'utf8')), hireable: ['aide'] });
const file = join(root, 'businesses/demo.json');
writeJson(file, { ...businessDoc(root, 'demo'), plugins: { harness: ['pi'] } });
mkdirSync(join(root, 'data'), { mode: 0o700 });
// The boot copy is the file's own bus form, as cli bootConfig builds it, so a write reloads.
const busForm = (dir, roles) => {
const b = loadBusiness('demo', { dir, rolesDir: roles });
const resolved = {};
for (const instance of Object.keys(b.roles)) resolved[instance] = resolveInstance({ system: systemFor(dir), business: b, instance });
return busBusiness(b, resolved);
};
const bus = await startBroker({
dataRoot: join(root, 'data'),
businesses: { demo: busForm(root, rolesDir) },
organization: { dir: root, rolesDir, system: systemFor(root) },
});
t.after(async () => {
await bus.close();
rmSync(root, { recursive: true, force: true });
});
const { broker } = bus;
const human = broker.bindHuman({ business: 'demo', human: 'jason', via: 'cli', outsideAgent: true });
const chief = broker.bindLaunch({ business: 'demo', role: 'chief', run: 'chief-run', harness: 'pi' });
broker.request(chief, { verb: 'role.claim' });
const call = (cap, verb, args) => broker.request(cap, { verb, args });
const decide = (action, args) => {
const d = call(chief, 'decision.raise', { action, target: args.name, arguments: args, question: 'q?', options, recommendation: 'yes', blocking: false });
call(human, 'decision.resolve', { id: d.id, choice: 'yes' });
return d.id;
};
const receipts = () =>
readFileSync(join(root, 'businesses/.history/demo/changes.jsonl'), 'utf8').trim().split('\n').map(JSON.parse).filter((r) => r.state === 'committed');
const document = () => JSON.parse(readFileSync(file, 'utf8'));
// A refused hire: coder declares credentials and isn't on the chief's list. The file is unchanged.
const bytes = readFileSync(file, 'utf8');
const coder = { name: 'helper', definition: 'coder' };
assert.throws(() => call(chief, 'instance.hire', { ...coder, decision: decide('role.hire', coder) }), /definition-not-hireable/);
assert.equal(readFileSync(file, 'utf8'), bytes);
const hire = { name: 'helper', definition: 'aide' };
const hired = decide('role.hire', hire);
call(chief, 'instance.hire', { ...hire, decision: hired });
assert.deepEqual(document().roles.helper, { definition: 'aide' });
assert.ok(document().launch.instances.includes('helper'));
const [h] = receipts().slice(-1);
assert.equal(h.actor, 'chief-of-staff.chief');
assert.deepEqual(h.context, { instance: 'chief', run: 'chief-run', decision: hired });
const fire = { name: 'helper', reason: 'the work is done' };
const fired = decide('role.fire', fire);
const result = call(chief, 'instance.fire', { ...fire, decision: fired });
assert.equal(result.delisted, true);
assert.ok(!document().launch.instances.includes('helper'));
assert.deepEqual(receipts().slice(-1)[0].context, { instance: 'chief', run: 'chief-run', decision: fired });
// The reloaded business sees the delist: a repeat is spent, and the chief's launch of it is refused.
assert.throws(() => call(chief, 'instance.fire', { ...fire, decision: fired }), /decision-consumed/);
const after = readFileSync(file, 'utf8');
assert.throws(() => call(chief, 'instance.fire', { ...fire, extra: 1, decision: fired }), /invalid-request/);
assert.equal(readFileSync(file, 'utf8'), after);
});
test('row 194 W-N1: the launcher\'s take of a launch or regenerate of a firing instance closes it as instance-firing', (t) => {
const { b, call, store } = hireSetup(t);
const launch = call('human', 'instance.launch', { instance: 'coder' }).request;
const regenerate = call('human', 'instance.regenerate', { instance: 'pm' }).request;
// A fire's step A as the store holds it with no step C, for requests step A didn't see.
for (const instance of ['coder', 'pm'])
store.run("INSERT INTO events(id,at,business,kind,actor_role,actor_run,subject,body) VALUES(?,?,'demo','action.allowed','chief','chief-run',?,?)",
'stopped-' + instance, new Date().toISOString(), instance, JSON.stringify({ action: 'fire.stopped', cites: 'd-' + instance, request: null }));
for (const request of [launch, regenerate]) {
const { go, refusal } = b.lifecycleTake({ business: 'demo', request });
assert.deepEqual({ go, refusal }, { go: false, refusal: 'instance-firing' });
}
});
+6 -1
View File
@@ -37,9 +37,14 @@ test('REQ-WAIT-2: a business granting a hard wait at either class is refused; th
test('REQ-DEC-2: every hard wait goes to the human, cited or not, and the arbiter cannot resolve it', async t => {
const f = await fixture(t);
// Row 194 (#1692, D2.1): a hire or fire raise carries the call's arguments.
const named = {
'role.hire': { target: 'helper', arguments: { name: 'helper', definition: 'coder' } },
'role.fire': { target: 'coder', arguments: { name: 'coder', reason: 'done' } },
};
for (const action of WAITS) {
for (const args of [{}, cite(f), { ...cite(f), domain: 'technical' }]) {
const d = await f.raise({ action, ...args });
const d = await f.raise({ action, ...named[action], ...args });
assert.deepEqual(routed(f, d), { class: 'gated', route_to: 'human' }, `${action} ${JSON.stringify(args)}`);
}
}