@volter/supercode-orchestrator 0.5.228 → 0.5.230
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +18 -2
- package/activation.mjs +1 -1
- package/adapters/agent-mailbox.mjs +10 -0
- package/adapters/api-server.mjs +6 -0
- package/agents.mjs +149 -40
- package/apply/doors.mjs +3 -1
- package/bin/agent-channel.mjs +7 -2
- package/bin/connection-runner.mjs +60 -19
- package/bin/hermes-worker.mjs +2 -1
- package/bin/orchestrator.mjs +3 -3
- package/bin/runtime.mjs +3 -2
- package/board/agent-current.mjs +82 -0
- package/board/agent-declarations.mjs +10 -3
- package/board/args.mjs +137 -6
- package/board/call.mjs +16 -1
- package/board/caller-session.mjs +3 -1
- package/board/capacity.mjs +59 -45
- package/board/card-state.mjs +14 -5
- package/board/ci.mjs +521 -178
- package/board/cli.mjs +614 -209
- package/board/client.mjs +64 -21
- package/board/close-current.mjs +36 -0
- package/board/current-records.mjs +44 -0
- package/board/custody.mjs +2 -1
- package/board/dispatch.mjs +1973 -846
- package/board/doors.mjs +2 -2
- package/board/engine.mjs +623 -85
- package/board/event-stream.mjs +10 -5
- package/board/fence.mjs +2 -0
- package/board/files.mjs +93 -62
- package/board/generation-owner.mjs +188 -53
- package/board/git-document.mjs +3 -1
- package/board/github-rest.mjs +29 -7
- package/board/incidents.mjs +5 -17
- package/board/listing.mjs +14 -8
- package/board/local-admission.mjs +59 -0
- package/board/lock-holder.mjs +18 -6
- package/board/machine-fleet.mjs +66 -13
- package/board/owner-door-worker.mjs +3 -3
- package/board/owner-door.mjs +17 -7
- package/board/owner-health.mjs +12 -15
- package/board/owner-interface.mjs +6 -0
- package/board/publication-reader.mjs +50 -0
- package/board/published.mjs +40 -12
- package/board/queries.mjs +24 -4
- package/board/record-view.mjs +30 -3
- package/board/request-mail.mjs +37 -6
- package/board/resting-admission.mjs +318 -0
- package/board/resting-codec-qualifications.json +33 -0
- package/board/resting-protocol.mjs +71 -0
- package/board/resting-state-protocol.json +120 -0
- package/board/resting-state.mjs +113 -0
- package/board/runtime-door.mjs +2 -2
- package/board/session-continuity.mjs +127 -0
- package/board/session-work.mjs +25 -13
- package/board/standalone-declaration.mjs +24 -0
- package/board/storage-contract.mjs +129 -7
- package/board/store.mjs +73 -11
- package/board/tools.mjs +56 -13
- package/board/wake.mjs +18 -5
- package/board/workflow.mjs +284 -82
- package/board/workspace.mjs +86 -9
- package/board/ztrack.mjs +344 -91
- package/connection.mjs +72 -17
- package/gates.mjs +2 -1
- package/hand-off.mjs +4 -3
- package/hermes-cli.mjs +5 -3
- package/hermes-locks.mjs +5 -2
- package/hosted.mjs +2 -1
- package/index.d.ts +2 -0
- package/index.mjs +1 -1
- package/kanban-worker.mjs +63 -13
- package/loop.mjs +33 -0
- package/mcp/hermes-kanban-tools.json +109 -50
- package/mcp/server.mjs +7 -1
- package/ontology.d.ts +26 -0
- package/orchestration.mjs +15 -4
- package/package.json +19 -12
- package/session-recovery.mjs +2 -1
- package/worker-route.mjs +37 -19
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { registerWorkService, closeWorkLedger, stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
3
|
+
import { registerWorkOperations, measureWork } from "@volter/supercode-harness-sdk/work-ledger";
|
|
4
|
+
registerWorkOperations([{"kind":"pass","operation":"connection.alerts"},{"kind":"pass","operation":"connection.renew"}]);
|
|
2
5
|
// The home's installed-app runner owns only children it starts, and every connection keeps separate custody.
|
|
3
6
|
import { existsSync, openSync, closeSync, readFileSync, realpathSync, rmSync, unlinkSync, renameSync } from 'node:fs';
|
|
4
7
|
import { dirname, join } from 'node:path';
|
|
@@ -6,7 +9,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
6
9
|
import { spawn } from 'node:child_process';
|
|
7
10
|
import { parseArgs } from 'node:util';
|
|
8
11
|
import { readSourceLock, sourceRoot } from '@volter/supercode-teams/source-lock';
|
|
9
|
-
import { SERVICE_APPS, SERVICE_PROGRAM, connectionCall, connectionCanRun, connectionPath, generateModuleConfig, privateJson, runnerStopPath, stopConnectionRunner } from '../connection.mjs';
|
|
12
|
+
import { boardAppPrincipals, SERVICE_APPS, SERVICE_PROGRAM, connectionCall, connectionCanRun, connectionPath, generateModuleConfig, privateJson, runnerStopPath, stopConnectionRunner } from '../connection.mjs';
|
|
10
13
|
|
|
11
14
|
const {values}=parseArgs({options:{root:{type:'string'},connection:{type:'string'}}});
|
|
12
15
|
const path=connectionPath(values.root,values.connection),lock=join(dirname(path),'runner.lock'),stopFile=runnerStopPath(lock);
|
|
@@ -21,8 +24,10 @@ function serving(){
|
|
|
21
24
|
if(!declared)return true;
|
|
22
25
|
try{return realpathSync(declared.root)===tree;}catch{return false;}
|
|
23
26
|
}
|
|
24
|
-
|
|
27
|
+
// The record's text as this runner last read or wrote it: a save that would write the same text writes nothing.
|
|
28
|
+
let written=readFileSync(path,'utf8'),record=JSON.parse(written);
|
|
25
29
|
record.root=values.root;
|
|
30
|
+
registerWorkService({role:'connection-runner',scope:{kind:'install',id:`${values.root}\0${record.id}`}});
|
|
26
31
|
if(!connectionCanRun(record))process.exit(0);
|
|
27
32
|
if(existsSync(lock)){
|
|
28
33
|
const previous=JSON.parse(readFileSync(lock,'utf8'));
|
|
@@ -35,23 +40,38 @@ if(existsSync(lock)){
|
|
|
35
40
|
log('runner-handover',{predecessor:previous.pid,from:previous.tree??null,to:tree});
|
|
36
41
|
// Never ended alone: its modules would outlive it with nobody stopping them. The next gateway start asks again.
|
|
37
42
|
if(!await stopConnectionRunner(lock,previous)){log('runner-handover-refused',{predecessor:previous.pid,reason:'predecessor did not stop within 20s'});process.exit(1);}
|
|
38
|
-
|
|
43
|
+
written=readFileSync(path,'utf8');record=JSON.parse(written);record.root=values.root;
|
|
39
44
|
if(!connectionCanRun(record))process.exit(0);
|
|
40
45
|
}
|
|
41
46
|
if(existsSync(lock))renameSync(lock,`${lock}.stopped-${Date.now()}`);
|
|
42
47
|
}
|
|
43
48
|
const fd=openSync(lock,'wx',0o600);closeSync(fd);privateJson(lock,{pid:process.pid,connection:record.id,tree});
|
|
44
49
|
rmSync(stopFile,{force:true});
|
|
45
|
-
|
|
50
|
+
// The record keeps live alert state only (ADR 0032: conditions are raised and cleared as RH2 alerts). `raised` holds an
|
|
51
|
+
// alert this runner raised and has not yet cleared (its reason a string or an HTTP status). `health` holds a condition
|
|
52
|
+
// that is unavailable, from the event that issues it, with its reason and the time it was first issued for that reason,
|
|
53
|
+
// and a recovered one (`state:'current'`) only while its alert is still up, so any runner that walks `health` (as
|
|
54
|
+
// earlier ones do) clears it; once the clear is answered both leave (syncAlerts). A card's obligation is so kept exactly
|
|
55
|
+
// while its projection is pending or its alert is up. What an earlier runner kept beyond that (a recovered condition
|
|
56
|
+
// with no alert up, a cleared null alert) is dropped as read.
|
|
57
|
+
const children=new Map(),
|
|
58
|
+
raised=Object.fromEntries(Object.entries(record.raised??{}).filter(([,reason])=>reason!=null)),
|
|
59
|
+
health=Object.fromEntries(Object.entries(record.health??{}).filter(([name,state])=>state?.state==='unavailable'||state?.state==='current'&&Object.hasOwn(raised,name)));
|
|
46
60
|
let stopped=false;
|
|
47
|
-
const save=()=>{record.health=health;record.raised=raised;privateJson(path,record);};
|
|
61
|
+
const save=()=>{record.health=health;record.raised=raised;written=privateJson(path,record,written);};
|
|
48
62
|
const teamPath=`/api/v1/teams/${encodeURIComponent(record.teams.team)}`;
|
|
49
|
-
const issue=(name,reason)=>{health[name]={state:'unavailable',reason,at:new Date().toISOString()};save();};
|
|
50
|
-
const recover=name=>{
|
|
63
|
+
const issue=(name,reason)=>{if(health[name]?.reason===reason)return;health[name]={state:'unavailable',reason,at:new Date().toISOString()};save();};
|
|
64
|
+
const recover=name=>{
|
|
65
|
+
if(Object.hasOwn(raised,name)){if(health[name]?.state==='current')return;health[name]={state:'current',at:new Date().toISOString()};}
|
|
66
|
+
else if(Object.hasOwn(health,name))delete health[name];
|
|
67
|
+
else return;
|
|
68
|
+
save();
|
|
69
|
+
};
|
|
51
70
|
// The child a grant runs in: a module its own program; a Workplace service registration the one service, which the agents
|
|
52
71
|
// registration hosts (cards and machines start nothing of their own).
|
|
53
72
|
const childOf=grant=>SERVICE_APPS.includes(grant.name)?'agents':grant.name;
|
|
54
73
|
function start(grant){
|
|
74
|
+
if(grant.disabled)return;
|
|
55
75
|
if(SERVICE_APPS.includes(grant.name)&&grant.name!=='agents')return;
|
|
56
76
|
if(stopped||children.has(grant.name)||!existsSync(join(values.root,'apps',`${grant.name}.${record.id}.json`)))return;
|
|
57
77
|
const config=generateModuleConfig(values.root,record,grant);
|
|
@@ -79,21 +99,29 @@ function start(grant){
|
|
|
79
99
|
}
|
|
80
100
|
async function stopChild(name){
|
|
81
101
|
const child=children.get(name);if(!child)return;children.delete(name);
|
|
82
|
-
child.removeAllListeners('exit');child.kill('SIGTERM');
|
|
83
|
-
await new Promise(done=>{const timer=setTimeout(()=>{child.kill('SIGKILL');done();},5000);child.once('exit',()=>{clearTimeout(timer);done();});});
|
|
102
|
+
child.removeAllListeners('exit');stopWorkChild(child, () => child.kill('SIGTERM'), { signal: 'SIGTERM' });
|
|
103
|
+
await new Promise(done=>{const timer=setTimeout(()=>{stopWorkChild(child, () => child.kill('SIGKILL'), { signal: 'SIGKILL' });done();},5000);child.once('exit',()=>{clearTimeout(timer);done();});});
|
|
84
104
|
}
|
|
85
105
|
async function syncAlerts(){
|
|
106
|
+
return measureWork("pass", "connection.alerts", async () => {
|
|
86
107
|
const grant=record.grants.find(g=>g.manifest.actions.includes('alert.raise')&&existsSync(join(values.root,'apps',`${g.name}.${record.id}.json`)));
|
|
87
108
|
if(!grant)return;
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
109
|
+
// Every condition still unavailable and every alert still up: an alert whose condition recovered is cleared.
|
|
110
|
+
for(const name of new Set([...Object.keys(health),...Object.keys(raised)])){
|
|
111
|
+
const key=`connection.${record.id}.${name}`.slice(0,200),wanted=health[name]?.state==='unavailable'?health[name].reason:null;
|
|
112
|
+
if((raised[name]??null)===wanted)continue;
|
|
91
113
|
try{
|
|
92
114
|
await connectionCall(record,'rh2','POST',`/api/v3/organizations/${encodeURIComponent(record.organization)}/alerts${wanted?'':'/clear'}`,wanted?{key,severity:'warning',reason:wanted,targetRole:record.alertRole}:{key},grant.rh2.accessToken);
|
|
93
|
-
|
|
115
|
+
// A clear answered retires both; a raise whose condition recovered while it was asked keeps a current entry beside it.
|
|
116
|
+
if(wanted===null){delete raised[name];if(health[name]?.state==='current')delete health[name];}
|
|
117
|
+
else{raised[name]=wanted;health[name]??={state:'current',at:new Date().toISOString()};}
|
|
118
|
+
save();
|
|
94
119
|
}catch(error){log('connection-alert-pending',{module:grant.name,code:error.code??error.status??'unavailable'});}
|
|
95
120
|
}
|
|
96
|
-
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
});
|
|
124
|
+
}
|
|
97
125
|
async function renewTeams(value){
|
|
98
126
|
if(!value.pendingRenewal&&value.credential.expires_at-Date.now()>7*86400000)return false;
|
|
99
127
|
if(value.pendingRenewal&&value.renewalRetryAt>Date.now())return false;
|
|
@@ -113,16 +141,21 @@ async function renewTeams(value){
|
|
|
113
141
|
function randomHex(size){return crypto.getRandomValues(new Uint8Array(size)).reduce((out,n)=>out+n.toString(16).padStart(2,'0'),'');}
|
|
114
142
|
function randomBase64(){return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');}
|
|
115
143
|
async function renew(){
|
|
144
|
+
return measureWork("pass", "connection.renew", async () => {
|
|
116
145
|
const restart=new Set();
|
|
117
146
|
for(const grant of record.grants){
|
|
118
147
|
if(!existsSync(join(values.root,'apps',`${grant.name}.${record.id}.json`)))continue;
|
|
119
148
|
try{
|
|
120
149
|
let changed=false;
|
|
121
|
-
if(Date.parse(grant.rh2.expiresAt)-Date.now()<=7*86400000){
|
|
150
|
+
if(Date.parse(grant.rh2.expiresAt)-Date.now()<=7*86400000){
|
|
151
|
+
const renewed=(await connectionCall(record,'rh2','POST',`/api/v3/automation/installations/${grant.rh2.installationId}/renew`,{},grant.rh2.accessToken)).data;
|
|
152
|
+
if(renewed.installationId!==grant.rh2.installationId||renewed.principalId!==grant.rh2.principalId)throw new Error('renewal changed its consented installation or principal');
|
|
153
|
+
grant.rh2={...grant.rh2,...renewed};delete grant.disabled;changed=true;save();
|
|
154
|
+
}
|
|
122
155
|
if(grant.teams)changed=await renewTeams(grant.teams)||changed;
|
|
123
156
|
if(changed)restart.add(childOf(grant));
|
|
124
157
|
if(!grant.teams?.pendingRenewal)recover(`expiry.${grant.name}`);
|
|
125
|
-
}catch(error){issue(`expiry.${grant.name}`,error.code??'renewal-unavailable');}
|
|
158
|
+
}catch(error){if(error.status===401||error.code==='installation-unknown'){grant.disabled=true;await stopChild(childOf(grant));save();}issue(`expiry.${grant.name}`,error.code??'renewal-unavailable');}
|
|
126
159
|
}
|
|
127
160
|
// A renewed credential reaches its program by a restart with a regenerated config (the service's, for its registrations):
|
|
128
161
|
// once per pass, however many of the program's registrations renewed in it.
|
|
@@ -131,12 +164,15 @@ async function renew(){
|
|
|
131
164
|
// deployment's (connection.mjs adoptWorkplaceService).
|
|
132
165
|
if(!record.issuerCredential)return;
|
|
133
166
|
try{await renewTeams(record.issuerCredential);if(!record.issuerCredential.pendingRenewal)recover('issuer-renewal');}catch(error){issue('issuer-renewal',error.code??'renewal-unavailable');}
|
|
134
|
-
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
});
|
|
170
|
+
}
|
|
135
171
|
for(const signal of ['SIGTERM','SIGINT'])process.on(signal,()=>void stop());
|
|
136
172
|
// Windows delivers no SIGTERM to a handler: a successor or a disconnect asks this runner to stop through its stop file.
|
|
137
173
|
setInterval(()=>{try{if(JSON.parse(readFileSync(stopFile,'utf8')).pid===process.pid)void stop();}catch{}},500).unref();
|
|
138
174
|
for(const grant of record.grants)start(grant);
|
|
139
|
-
let lastRenew=0;
|
|
175
|
+
let lastRenew=0, boardAppsWritten=JSON.stringify(boardAppPrincipals(values.root,record));
|
|
140
176
|
// Teams verifies the Workplace's tokens against its own JWKS (ADR 0070): nothing is relayed for them here.
|
|
141
177
|
while(!stopped){
|
|
142
178
|
try{
|
|
@@ -145,8 +181,13 @@ while(!stopped){
|
|
|
145
181
|
if(!installed.length){await stop();break;}
|
|
146
182
|
const pendingRenewalDue=[record.issuerCredential,...installed.map(grant=>grant.teams)].some(value=>value?.pendingRenewal&&!(value.renewalRetryAt>Date.now()));
|
|
147
183
|
if(pendingRenewalDue||Date.now()-lastRenew>3600000){await renew();lastRenew=Date.now();}
|
|
184
|
+
const boardApps=JSON.stringify(boardAppPrincipals(values.root,record));
|
|
185
|
+
if(boardApps!==boardAppsWritten){
|
|
186
|
+
const agents=installed.find(grant=>grant.name==='agents');
|
|
187
|
+
if(agents){await stopChild(agents.name);start(agents);boardAppsWritten=boardApps;log('board-handover-config-changed');}
|
|
188
|
+
}
|
|
148
189
|
recover('issuer');await syncAlerts();
|
|
149
190
|
}catch(error){issue('issuer',error.code??error.status??'connection-unavailable');await syncAlerts();}
|
|
150
191
|
await new Promise(done=>setTimeout(done,2000));
|
|
151
192
|
}
|
|
152
|
-
async function stop(){if(stopped)return;stopped=true;await Promise.all([...children.keys()].map(stopChild));record.stoppedAt=new Date().toISOString();save();unlinkSync(lock);rmSync(stopFile,{force:true});process.exit(0);}
|
|
193
|
+
async function stop(){if(stopped)return;stopped=true;await Promise.all([...children.keys()].map(stopChild));record.stoppedAt=new Date().toISOString();save();unlinkSync(lock);rmSync(stopFile,{force:true});await closeWorkLedger();process.exit(0);}
|
package/bin/hermes-worker.mjs
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
// The command the board's dispatcher runs as a headless profile's worker (docs/plans/hermes-compat.md row 14):
|
|
3
3
|
// Hermes's own worker argv and environment; see `kanban-worker.mjs`.
|
|
4
4
|
import { runKanbanWorker } from '../kanban-worker.mjs';
|
|
5
|
+
import { currentRelease } from '@volter/teams/stable-release';
|
|
5
6
|
|
|
6
7
|
// Hermes watches this pid: it exits the moment the turn is over, whatever handle is still open.
|
|
7
|
-
process.exit(await runKanbanWorker(process.argv.slice(2), process.env).catch((error) => {
|
|
8
|
+
process.exit(await runKanbanWorker(process.argv.slice(2), process.env, currentRelease).catch((error) => {
|
|
8
9
|
process.stdout.write(`hermes-worker: ${error.stack ?? error.message}\n`);
|
|
9
10
|
return 1;
|
|
10
11
|
}));
|
package/bin/orchestrator.mjs
CHANGED
|
@@ -15,9 +15,9 @@ try { ({ lockedProgram } = await import('../source-lock.mjs')); }
|
|
|
15
15
|
catch (error) { process.stderr.write(`${error.message}\n`); process.exit(1); }
|
|
16
16
|
// Any command on a home whose record names another orchestrator is that orchestrator's (hand-off.mjs).
|
|
17
17
|
if (process.argv[2] !== 'workflow' && !(typeof process.send === 'function' && Number(process.env.SUPERCODE_BOARD_GENERATION_OWNER) === process.ppid)) {
|
|
18
|
-
const { handOff, homeOfArgs } = await import('../hand-off.mjs');
|
|
18
|
+
const { handOff, homeOfArgs } = await import('../hand-off.mjs'); const { enforceSourceLock } = await import('@volter/supercode-teams/source-lock');
|
|
19
19
|
let handed;
|
|
20
|
-
try { handed = handOff(homeOfArgs(process.argv.slice(2)), process.argv.slice(2)); }
|
|
20
|
+
try { handed = handOff(homeOfArgs(process.argv.slice(2)), process.argv.slice(2), enforceSourceLock); }
|
|
21
21
|
catch (error) { process.stderr.write(`supercode ${process.argv[2] ?? ''}: ${error.message}\n`); process.exit(1); }
|
|
22
22
|
if (handed !== null) process.exit(await handed);
|
|
23
23
|
}
|
|
@@ -33,7 +33,7 @@ if (process.argv[2] === 'workflow') {
|
|
|
33
33
|
const verb=process.argv.slice(3).find((arg,index,all)=>!arg.startsWith('-')&&!(index>0&&/^--(root|board|cwd|context|at)$/.test(all[index-1])));
|
|
34
34
|
if(verb!=='serve')timeCommand(`workflow ${verb??''}`.trim(),process.argv.slice(3));
|
|
35
35
|
let code=0;
|
|
36
|
-
try {code=await runWorkflow(process.argv.slice(3));}
|
|
36
|
+
try {code=await runWorkflow(process.argv.slice(3),{serveBoardOwner:async(root,args,firstReaderEpoch)=>(await import('../board/generation-owner.mjs')).serveBoardOwner(root,args,firstReaderEpoch)});}
|
|
37
37
|
// a store's own error (SQLite's, with its errcode) is no message written for a person: its stack says which statement
|
|
38
38
|
// met it, as a follower's end in the connector's log (board events) otherwise could not
|
|
39
39
|
catch(error) {noteCommandError(error);process.stderr.write(`supercode workflow: ${error.message??error}\n${error?.errcode!=null&&error.stack?`${error.stack}\n`:''}`);code=1;}
|
package/bin/runtime.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
2
3
|
// The orchestrator daemon entry (ORC-7).
|
|
3
4
|
//
|
|
4
5
|
// node bin/orchestrator.mjs --root <home> [--once]
|
|
@@ -492,7 +493,7 @@ async function appsMain(argv) {
|
|
|
492
493
|
process.exit(127);
|
|
493
494
|
}
|
|
494
495
|
const child = spawn(program, ['mcp'], { stdio: 'inherit', env: { ...process.env, ...rh2AgentEnv(record, home) } });
|
|
495
|
-
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
496
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => stopWorkChild(child, () => child.kill(signal), { signal: signal }));
|
|
496
497
|
child.on('error', (error) => { process.stderr.write(`apps mcp: ${error.message}\n`); process.exit(127); });
|
|
497
498
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
498
499
|
return;
|
|
@@ -707,7 +708,7 @@ async function main() {
|
|
|
707
708
|
// when its command line is a gateway's (hermes-locks.mjs). A launcher that did not name it so is re-run as it.
|
|
708
709
|
if (!args.once && basename(process.argv0) !== GATEWAY_KIND) {
|
|
709
710
|
const child = spawn(process.execPath, process.argv.slice(1), { argv0: GATEWAY_KIND, stdio: 'inherit', env: process.env });
|
|
710
|
-
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => child.kill(signal));
|
|
711
|
+
for (const signal of ['SIGTERM', 'SIGINT', 'SIGHUP']) process.on(signal, () => stopWorkChild(child, () => child.kill(signal), { signal: signal }));
|
|
711
712
|
child.on('exit', (code, signal) => process.exit(code ?? (signal ? 1 : 0)));
|
|
712
713
|
return;
|
|
713
714
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// Current delivery obligations, reconstructed from the board after restart. Successful ended
|
|
2
|
+
// records leave pending; changed board/agent identities put only their references back in it.
|
|
3
|
+
import { setImmediate as yieldToOwner } from 'node:timers/promises';
|
|
4
|
+
import { currentRecordChanges, currentRecordConnection } from './current-records.mjs';
|
|
5
|
+
import { agentRecordChanges, cardAgentAssignments } from '../agents.mjs';
|
|
6
|
+
import { json, RUN_AGENT_REF, runField, view } from './store.mjs';
|
|
7
|
+
|
|
8
|
+
const held = new WeakMap();
|
|
9
|
+
const FIELDS = ['session_id','address','machine','harness','surface','pane','pid','detached_at','agent_id','agent_org','mode','launch_key','launch_pending','launch_deferred'];
|
|
10
|
+
const sessionSql = () => `json_object(${FIELDS.flatMap(name => [`'${name}'`,runField(name)]).join(',')})`;
|
|
11
|
+
export function latestAgentRow(board, ref) {
|
|
12
|
+
return view(board, db => {
|
|
13
|
+
const row = db.prepare(`SELECT id,task_id,ended_at,${RUN_AGENT_REF} AS ref,${sessionSql()} AS session
|
|
14
|
+
FROM task_runs WHERE ${runField('agent_id')} IS NOT NULL AND ${RUN_AGENT_REF}=? ORDER BY id DESC LIMIT 1`).get(ref);
|
|
15
|
+
return row ? { ...row, session: json(row.session,{}) } : null;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const runtimeRef = row => row?.session.session_id ? `${row.session.harness}:${row.session.session_id}` : null;
|
|
19
|
+
function retain(state, ref, row) {
|
|
20
|
+
const old = state.rows.get(ref);
|
|
21
|
+
if (old) {
|
|
22
|
+
state.tasks.get(old.task_id)?.delete(ref);
|
|
23
|
+
state.runtime.get(runtimeRef(old))?.delete(ref);
|
|
24
|
+
}
|
|
25
|
+
state.rows.delete(ref); state.credentials.delete(ref);
|
|
26
|
+
if (!row) { state.pending.delete(ref); return; }
|
|
27
|
+
state.rows.set(ref,row);
|
|
28
|
+
const tasks = state.tasks.get(row.task_id) ?? new Set(); tasks.add(ref); state.tasks.set(row.task_id,tasks);
|
|
29
|
+
const runtime = state.runtime.get(runtimeRef(row)) ?? new Set(); runtime.add(ref); state.runtime.set(runtimeRef(row),runtime);
|
|
30
|
+
if (!row.ended_at || row.session.detached_at) state.credentials.add(ref);
|
|
31
|
+
if (!old || old.id !== row.id || old.task_id !== row.task_id || old.ended_at !== row.ended_at
|
|
32
|
+
|| JSON.stringify(old.session) !== JSON.stringify(row.session)) state.pending.add(ref);
|
|
33
|
+
}
|
|
34
|
+
/** Select/reconcile rows, not fleet/credential decisions; those are recomputed from live inputs. */
|
|
35
|
+
export async function currentAgentWork(board) {
|
|
36
|
+
let state = held.get(currentRecordConnection(board.db));
|
|
37
|
+
const boardChange = currentRecordChanges(board.db,state?.boardToken);
|
|
38
|
+
const agentChange = agentRecordChanges(board.root,state?.agentToken);
|
|
39
|
+
if (!state || boardChange.reset) {
|
|
40
|
+
state = { rows: new Map(), tasks: new Map(), runtime: new Map(), pending: new Set(), credentials: new Set(), assignments: new Set(), usageDue: new Map() };
|
|
41
|
+
held.set(currentRecordConnection(board.db),state);
|
|
42
|
+
let cursor = null;
|
|
43
|
+
for (;;) {
|
|
44
|
+
const refs = view(board, db => db.prepare(`SELECT ${RUN_AGENT_REF} AS ref FROM task_runs
|
|
45
|
+
WHERE ${runField('agent_id')} IS NOT NULL${cursor === null ? '' : ` AND ${RUN_AGENT_REF}>?`}
|
|
46
|
+
GROUP BY ${RUN_AGENT_REF} ORDER BY ${RUN_AGENT_REF} LIMIT 32`).all(...(cursor === null ? [] : [cursor])));
|
|
47
|
+
for (const {ref} of refs) retain(state,ref,latestAgentRow(board,ref));
|
|
48
|
+
if (refs.length < 32) break;
|
|
49
|
+
cursor = refs.at(-1).ref; await yieldToOwner();
|
|
50
|
+
}
|
|
51
|
+
for (const assignment of cardAgentAssignments(board.root)) state.assignments.add(assignment.target);
|
|
52
|
+
} else {
|
|
53
|
+
for (const target of boardChange.cards) {
|
|
54
|
+
state.assignments.add(target);
|
|
55
|
+
const affected = new Set(state.tasks.get(target));
|
|
56
|
+
let cursor = null;
|
|
57
|
+
for (;;) {
|
|
58
|
+
const refs = view(board,db => db.prepare(`SELECT DISTINCT ${RUN_AGENT_REF} AS ref FROM task_runs
|
|
59
|
+
WHERE task_id=? AND ${runField('agent_id')} IS NOT NULL${cursor === null ? '' : ` AND ${RUN_AGENT_REF}>?`}
|
|
60
|
+
ORDER BY ${RUN_AGENT_REF} LIMIT 32`).all(target,...(cursor === null ? [] : [cursor])));
|
|
61
|
+
for (const {ref} of refs) affected.add(ref);
|
|
62
|
+
if (refs.length < 32) break;
|
|
63
|
+
cursor = refs.at(-1).ref; await yieldToOwner();
|
|
64
|
+
}
|
|
65
|
+
for (const ref of affected) retain(state,ref,latestAgentRow(board,ref));
|
|
66
|
+
await yieldToOwner();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (agentChange.reset) {
|
|
70
|
+
for (const ref of state.rows.keys()) state.pending.add(ref);
|
|
71
|
+
for (const assignment of cardAgentAssignments(board.root)) state.assignments.add(assignment.target);
|
|
72
|
+
} else {
|
|
73
|
+
for (const ref of agentChange.sessions) for (const boardRef of state.runtime.get(ref) ?? []) state.pending.add(boardRef);
|
|
74
|
+
for (const target of agentChange.targets) state.assignments.add(target);
|
|
75
|
+
}
|
|
76
|
+
// Custody can retain credentials on an ended remote session even when its run predates the
|
|
77
|
+
// detached_at field. Only these retained addresses join the current credential candidates.
|
|
78
|
+
const custody = view(board,db => new Set(db.prepare('SELECT address FROM supercode_session_custody').all().map(row => row.address)));
|
|
79
|
+
for (const ref of custody) if (state.rows.has(ref)) state.credentials.add(ref);
|
|
80
|
+
state.boardToken = boardChange.token; state.agentToken = agentChange.token;
|
|
81
|
+
return state;
|
|
82
|
+
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Consume the card-creation intent after the board transaction; retries reuse the daemon-minted id. The card's agent is
|
|
2
2
|
// minted and recorded here, and its declaration handed to this machine's daemon, which declares it in Teams: whoever ran
|
|
3
3
|
// the effect (the creating command, or a delivery pass retrying it) makes no Teams call (D137).
|
|
4
|
-
import { agentScope, localCardAgent, renameCardAgent, finishAgentAssignment, cardAgentAssignments, cardAgentScopeTransition, pendingCardAgentChanges, acknowledgeCardAgentChanges, handOffAgents } from '../agents.mjs';
|
|
4
|
+
import { agentScope, localCardAgent, renameCardAgent, finishAgentAssignment, cardAgentAssignments, cardRoleAssignment, cardAgentScopeTransition, pendingCardAgentChanges, acknowledgeCardAgentChanges, handOffAgents } from '../agents.mjs';
|
|
5
5
|
import { cardHandleOf, readableIdOf, event, json, tx, view } from './store.mjs';
|
|
6
|
-
import { workflowFor } from './workflow.mjs';
|
|
6
|
+
import { requestSessionClose, workflowFor } from './workflow.mjs';
|
|
7
|
+
import { authorizeAgentHandoff } from './session-continuity.mjs';
|
|
7
8
|
import { cardAgentFacts, cardAgentReceiptRecorded, syncCardAgentScope } from './published.mjs';
|
|
8
9
|
|
|
9
10
|
function admitsScope(board,target,org){
|
|
@@ -29,6 +30,8 @@ export async function declareCardAgentEffect(board,effect) {
|
|
|
29
30
|
if(!admitsScope(board,effect.id,org))return true;
|
|
30
31
|
const options={root:board.root,org:scope.org,server:scope.server,profiles:entry.profiles,
|
|
31
32
|
profile,card:effect.id,handle:cardHandleOf(board,effect.id),readableId:readableIdOf(board,effect.id),role:effect.role,context,starterSession:effect.starter_session};
|
|
33
|
+
const previous=cardAgentAssignments(board.root,effect.id).find(item=>item.org===scope.org&&item.id===cardRoleAssignment(effect.id,effect.role));
|
|
34
|
+
if(previous)options.existingAgent=previous.agent_id;
|
|
32
35
|
const agent=await localCardAgent(options);
|
|
33
36
|
if(task.archived||['done','cancelled','archived'].includes(task.status))finishAgentAssignment(board.root,scope.org,agent.assignment,`card ${effect.id} ${task.status}`);
|
|
34
37
|
// A daemon not running keeps the declaration here; every delivery pass hands it on again.
|
|
@@ -73,7 +76,11 @@ export function recordCardAgentChanges(board,target){
|
|
|
73
76
|
const recorded=(db,change)=>cardAgentReceiptRecorded(db,target,change.receipt);
|
|
74
77
|
if(missingScope||view(board,db=>changes.some(change=>!recorded(db,change))))tx(board,db=>{
|
|
75
78
|
if(transition)syncCardAgentScope(db,target,transition);
|
|
76
|
-
for(const change of changes)if(!recorded(db,change)&&admitsScope(board,target,change.org))
|
|
79
|
+
for(const change of changes)if(!recorded(db,change)&&admitsScope(board,target,change.org)){
|
|
80
|
+
const source=event(db,target,change.kind??'agent_assignment_reassigned',change);
|
|
81
|
+
const assignment=cardAgentAssignments(board.root,target).find(item=>item.org===change.org&&item.id===change.assignment);
|
|
82
|
+
authorizeAgentHandoff(db,target,change,source,assignment,requestSessionClose);
|
|
83
|
+
}
|
|
77
84
|
});
|
|
78
85
|
acknowledgeCardAgentChanges(board.root,changes.map(change=>change.receipt));
|
|
79
86
|
}
|
package/board/args.mjs
CHANGED
|
@@ -3,11 +3,131 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
export const MULTI = new Set(['parent', 'skill', 'worker', 'child', 'ids', 'rm', 'tag', 'pr', 'pr-head', 'reviewed-sha', 'finding', 'source']);
|
|
5
5
|
export const FLAGS = new Set(['json', 'triage', 'no-notify', 'no-start', 'all', 'force', 'help', 'mine', 'archived', 'subtasks', 'reclaim', 'dry-run',
|
|
6
|
-
'once', 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose', 'side', 'plan']);
|
|
6
|
+
'once', 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose', 'side', 'plan', 'attest-release']);
|
|
7
7
|
// flags that take every following word up to the next flag (Hermes's nargs='+')
|
|
8
8
|
export const GREEDY = new Set(['ids', 'rm']);
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
// These are the options the native verb actually reads, not every option another verb happens to know. Keep the
|
|
11
|
+
// admission in the shared parser: both the caller and the serving generation must refuse before doing any work.
|
|
12
|
+
// Workflow-declared event verbs and `goto` carry open payloads; their fields are consumed, not native CLI options.
|
|
13
|
+
// `--break-glass "<reason>"` is every verb's: a card's session gives it to write outside its own card on purpose
|
|
14
|
+
// (cli.mjs authorize), and it is never part of a verb's payload.
|
|
15
|
+
const COMMON = new Set(['root', 'board', 'json', 'help', 'at', 'break-glass']);
|
|
16
|
+
const NATIVE = new Map();
|
|
17
|
+
const options = (verbs, names = '') => {
|
|
18
|
+
for (const verb of verbs.split(' ')) NATIVE.set(verb, new Set(names.split(' ').filter(Boolean)));
|
|
19
|
+
};
|
|
20
|
+
options('help declaration listeners context link unlink tag untag attachments attach-rm stats assignees capacity deliver compact repair resume start answer release close');
|
|
21
|
+
options('events', 'after once');
|
|
22
|
+
options('github-usage', 'since');
|
|
23
|
+
options('init', 'backing document commit manager dispatching archive-max-lines archive-period');
|
|
24
|
+
options('migrate', 'backing document status-map');
|
|
25
|
+
options('create', 'body body-file assignee machine reviewer readable-id workspace branch parent priority tenant triage idempotency-key max-runtime max-retries skill model provider created-by no-start adopt every subtask-of initial-status project goal no-goal goal-max-turns tag requested-by request notify no-notify');
|
|
26
|
+
options('swarm', 'readable-id worker verifier synthesizer tenant created-by priority idempotency-key');
|
|
27
|
+
options('list ls', 'assignee mine status machine archived all subtasks tenant session workflow-template-id step-key tag sort backlog');
|
|
28
|
+
options('show', 'state-type state-name all');
|
|
29
|
+
options('usage', 'agent');
|
|
30
|
+
options('brief', 'run');
|
|
31
|
+
options('runs', 'state-type state-name');
|
|
32
|
+
options('set', 'provider ids');
|
|
33
|
+
options('set-model', 'provider');
|
|
34
|
+
options('assign reassign', 'agent role');
|
|
35
|
+
options('set-goal', 'goal-max-turns off ids');
|
|
36
|
+
options('claim', 'ttl');
|
|
37
|
+
options('reclaim return-review cancel dispatching', 'reason');
|
|
38
|
+
options('pause', 'reason until');
|
|
39
|
+
options('backlog', 'reason pause-revision');
|
|
40
|
+
options('plan', 'reason');
|
|
41
|
+
options('heartbeat', 'note until');
|
|
42
|
+
options('comment', 'file max-len author pr pr-head source pr-disposition release-target reason released-artifact release-evidence attest-release');
|
|
43
|
+
options('attach', 'upload name content-type author');
|
|
44
|
+
options('attachment-read', 'offset length version');
|
|
45
|
+
options('complete', 'summary result metadata commit pr pr-head reviewed-sha');
|
|
46
|
+
options('person-done', 'person result-file result workplace-task');
|
|
47
|
+
options('edit', 'result summary metadata');
|
|
48
|
+
options('block', 'ids kind on waiting-on');
|
|
49
|
+
options('schedule', 'ids');
|
|
50
|
+
options('unblock', 'ids reason on waiting-on');
|
|
51
|
+
options('request-review', 'summary reviewer metadata commit branch pr pr-head head plan force supersedes');
|
|
52
|
+
options('request-changes', 'finding reviewer reason');
|
|
53
|
+
options('reopen reopen-review', 'ids reason');
|
|
54
|
+
options('promote', 'ids dry-run force');
|
|
55
|
+
options('archive', 'reason rm');
|
|
56
|
+
options('specify', 'all tenant readable-id title body body-file workspace branch machine author');
|
|
57
|
+
options('decompose', 'all tenant child author');
|
|
58
|
+
options('diagnostics diag', 'task severity');
|
|
59
|
+
options('log', 'tail');
|
|
60
|
+
options('tail watch', 'interval kinds assignee tenant');
|
|
61
|
+
options('notify-subscribe', 'for-card address platform chat-id thread-id role user-id user-id-alt chat-type notifier-profile delivery-mode');
|
|
62
|
+
options('notify-unsubscribe', 'for-card address platform chat-id thread-id role');
|
|
63
|
+
options('notify-list');
|
|
64
|
+
options('dispatch', 'failure-limit max dry-run');
|
|
65
|
+
options('daemon serve', 'interval max failure-limit pidfile verbose');
|
|
66
|
+
options('gc', 'event-retention-days log-retention-days');
|
|
67
|
+
options('run-session', 'harness task native-home');
|
|
68
|
+
options('detach', 'to');
|
|
69
|
+
options('adopt', 'side workspace assignee role');
|
|
70
|
+
options('handover', 'action transaction entry supercode candidate release');
|
|
71
|
+
NATIVE.set('goto', null);
|
|
72
|
+
const BOARDS = new Map();
|
|
73
|
+
const boardOptions = (actions, names = '') => {
|
|
74
|
+
for (const action of actions.split(' ')) BOARDS.set(action, new Set(names.split(' ').filter(Boolean)));
|
|
75
|
+
};
|
|
76
|
+
boardOptions('list ls', 'all');
|
|
77
|
+
boardOptions('create new', 'name description icon color default-workdir switch backing document');
|
|
78
|
+
boardOptions('rm remove delete', 'delete');
|
|
79
|
+
boardOptions('switch use show current set-default-workdir');
|
|
80
|
+
boardOptions('rename', 'name');
|
|
81
|
+
boardOptions('export', 'output no-attachments include-logs');
|
|
82
|
+
boardOptions('import', 'as switch');
|
|
83
|
+
|
|
84
|
+
function nativeOptions(args) {
|
|
85
|
+
const [verb, action = 'list'] = args._;
|
|
86
|
+
if (verb === 'boards') {
|
|
87
|
+
if (!BOARDS.has(action)) throw new Error(`boards ${action}: list | create | rm | switch | show | rename | set-default-workdir | export | import`);
|
|
88
|
+
return { label: `boards ${action}`, allowed: BOARDS.get(action) };
|
|
89
|
+
}
|
|
90
|
+
if (verb === 'handover') {
|
|
91
|
+
const action = args.action ?? 'replace';
|
|
92
|
+
const allowed = action === 'status' ? 'action'
|
|
93
|
+
: ['prepare', 'replace'].includes(action) ? 'action transaction entry supercode candidate release' : 'action transaction';
|
|
94
|
+
return { label: `handover --action ${action}`, allowed: new Set(allowed.split(' ')) };
|
|
95
|
+
}
|
|
96
|
+
return { label: verb ?? 'help', allowed: NATIVE.get(verb ?? 'help') };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Operator verbs act on the whole board and take no operand: a stray word (`dispatch status`) is retained as an
|
|
100
|
+
// incomplete request naming it (ADR 0252), never dropped and run as the bare verb.
|
|
101
|
+
const NO_OPERANDS = new Set(['dispatch', 'daemon', 'serve', 'gc', 'compact', 'repair']);
|
|
102
|
+
|
|
103
|
+
function assertNativeOptions(args) {
|
|
104
|
+
const { label, allowed } = nativeOptions(args);
|
|
105
|
+
if (NO_OPERANDS.has(args._[0]) && args._.length > 1) throw new Error(`${label} takes no operand; \`${args._.slice(1).join(' ')}\` is not one of its options. Nothing was done.`);
|
|
106
|
+
if (allowed === undefined || allowed === null) return;
|
|
107
|
+
for (const key of Object.keys(args)) {
|
|
108
|
+
if (key !== '_' && !COMMON.has(key) && !allowed.has(key)) throw new Error(`${label} does not support --${key}. Nothing was done.`);
|
|
109
|
+
}
|
|
110
|
+
// Options read only by one branch of a native verb must not disappear in another branch either.
|
|
111
|
+
const unused = args._[0] === 'adopt' && args.side ? ['workspace', 'assignee', 'role']
|
|
112
|
+
: ['assign', 'reassign'].includes(args._[0]) && !args.agent ? ['role']
|
|
113
|
+
: args._[0] === 'comment' && args['pr-disposition'] ? ['released-artifact', 'release-evidence', 'attest-release']
|
|
114
|
+
: ['notify-subscribe', 'notify-unsubscribe'].includes(args._[0]) && args['for-card'] ? ['user-id', 'user-id-alt', 'chat-type', 'notifier-profile', 'delivery-mode'] : [];
|
|
115
|
+
if (args._[0] === 'notify-unsubscribe' && !args['for-card']) unused.push('role');
|
|
116
|
+
for (const key of unused) if (Object.hasOwn(args, key)) throw new Error(`${label} does not use --${key} with these options. Nothing was done.`);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** The open payload door belongs only to events this home's workflow declares. Check before any board write. */
|
|
120
|
+
export function assertWorkflowVerb(args, workflow) {
|
|
121
|
+
const [verb] = args._;
|
|
122
|
+
if (!verb || verb === 'boards' || NATIVE.has(verb)) return;
|
|
123
|
+
const name = verb.replace(/-/g, '_');
|
|
124
|
+
if (!Object.values(workflow?.statuses ?? {}).some(status => status.on?.[name])
|
|
125
|
+
&& !workflow?.on?.[name] && !workflow?.verbs?.[name] && !workflow?.run?.on?.[name]) {
|
|
126
|
+
throw new Error(`no workflow verb ${verb}; \`supercode workflow help\` lists them`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export function parse(argv, { contextOnly = false } = {}) {
|
|
11
131
|
const out = { _: [] };
|
|
12
132
|
for (let i = 0; i < argv.length; i += 1) {
|
|
13
133
|
const a = argv[i];
|
|
@@ -16,23 +136,34 @@ export function parse(argv) {
|
|
|
16
136
|
// reached the board as one word " --pr … --pr-head …" that a comment stored as its text (and `--file` dropped),
|
|
17
137
|
// so its review was requested without two of its PRs.
|
|
18
138
|
if (/^\s+--[A-Za-z]/.test(a) || (a.startsWith('--') && /\s/.test(a.slice(2).split('=')[0]))) {
|
|
139
|
+
if (contextOnly) continue;
|
|
19
140
|
throw new Error(`One argument holds flags and their values together: ${JSON.stringify(a.length > 120 ? `${a.slice(0, 117)}...` : a)}. Pass each flag and each value as its own word (a shell variable holding several is split with \${=args} in zsh, or built as an array: "\${args[@]}" in bash). Nothing was done.`);
|
|
20
141
|
}
|
|
21
142
|
if (!a.startsWith('--') || a === '--') { out._.push(a); continue; }
|
|
22
143
|
const [key, inline] = a.slice(2).split(/=(.*)/s);
|
|
144
|
+
if (key === '_') { if (contextOnly) continue; throw new Error('--_ is reserved for positional arguments. Nothing was done.'); }
|
|
23
145
|
// init toggles document commits; completion/review names a git revision.
|
|
24
|
-
if (FLAGS.has(key) || (key === 'commit' && out._[0] === 'init')) {
|
|
146
|
+
if (FLAGS.has(key) || (key === 'commit' && out._[0] === 'init')) {
|
|
147
|
+
if (inline !== undefined && !contextOnly) throw new Error(`--${key} takes no value. Nothing was done.`);
|
|
148
|
+
out[key] = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
25
151
|
if (GREEDY.has(key)) {
|
|
26
152
|
const values = inline !== undefined ? [inline] : [];
|
|
27
153
|
while (i + 1 < argv.length && !argv[i + 1].startsWith('--')) values.push(argv[++i]);
|
|
28
154
|
(out[key] ??= []).push(...values);
|
|
29
155
|
continue;
|
|
30
156
|
}
|
|
31
|
-
const value = inline
|
|
32
|
-
if (value === undefined
|
|
157
|
+
const value = inline !== undefined ? inline : argv[i + 1];
|
|
158
|
+
if (value === undefined || (inline === undefined && value.startsWith('--'))) {
|
|
159
|
+
out[key] = null;
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
if (inline === undefined) i++;
|
|
33
163
|
if (MULTI.has(key)) (out[key] ??= []).push(value);
|
|
34
|
-
else out
|
|
164
|
+
else Object.defineProperty(out, key, { value, enumerable: true, writable: true, configurable: true });
|
|
35
165
|
}
|
|
166
|
+
if (!contextOnly) assertNativeOptions(out);
|
|
36
167
|
return out;
|
|
37
168
|
}
|
|
38
169
|
|
package/board/call.mjs
CHANGED
|
@@ -20,7 +20,7 @@ import { generationWork } from './generation.mjs';
|
|
|
20
20
|
|
|
21
21
|
const calls = new AsyncLocalStorage();
|
|
22
22
|
// what a call learns as it runs, and the workflow it read (pinned: a reload while it runs does not change its basis)
|
|
23
|
-
const fresh = () => ({ actor: null, principal: null, roles: () => ['manager'], recorded: null, workflows: new Map(), agentMains: new Map(), said: new Set() });
|
|
23
|
+
const fresh = () => ({ actor: null, principal: null, roles: () => ['manager'], recorded: null, workflows: new Map(), agentMains: new Map(), managerDeadline: null, managerUnread: null, said: new Set() });
|
|
24
24
|
// the process's own context pins no workflow (it lives as long as the process): its reads always take the latest
|
|
25
25
|
const unpinned = { get: () => undefined, set: () => {} };
|
|
26
26
|
const own = { kind: 'own', caller: null, facts: process.env, ...fresh(), workflows: unpinned, get cwd() { return process.cwd(); }, get stdin() { return readFileSync(0, 'utf8'); }, out: (text) => process.stdout.write(text), err: (text) => process.stderr.write(text) };
|
|
@@ -28,6 +28,16 @@ const own = { kind: 'own', caller: null, facts: process.env, ...fresh(), workflo
|
|
|
28
28
|
/** The call running now: a caller's verb in the owner, else the process's own. */
|
|
29
29
|
export const current = () => calls.getStore() ?? own;
|
|
30
30
|
|
|
31
|
+
/** The shared JSON answer for request receipts; each door selects the receipts its answer consumes. */
|
|
32
|
+
export function requestReceiptEnvelope({ requests, accepted, queued, unqueued }) {
|
|
33
|
+
if (!requests.length && !accepted.length && !queued.length && !unqueued.length) return null;
|
|
34
|
+
if (unqueued.length) return { ok: false, disposition: 'not_queued', queued: false, reviews: unqueued };
|
|
35
|
+
const kinds = [requests.length && 'routed', queued.length && 'queued', accepted.length && 'recorded'].filter(Boolean);
|
|
36
|
+
return { ok: true, disposition: kinds.length === 1 ? kinds[0] : 'recorded',
|
|
37
|
+
...(requests.length ? { requests } : {}), ...(queued.length ? { reviews: queued } : {}),
|
|
38
|
+
...(accepted.length ? { accepted } : {}) };
|
|
39
|
+
}
|
|
40
|
+
|
|
31
41
|
/** Run `fn` as one caller's call, its answer written through `out` and `err`. */
|
|
32
42
|
export function within({ caller = null, facts = {}, cwd, stdin = null, out, err, readOnly = false }, fn) {
|
|
33
43
|
return calls.run({ kind: 'caller', caller, facts, ...fresh(), cwd, stdin, out, err, readOnly }, fn);
|
|
@@ -45,5 +55,10 @@ export function callerOf(call = current()) {
|
|
|
45
55
|
return m ? { address: m[0], machine: m[1], harness: m[2], session: m[3] } : null;
|
|
46
56
|
}
|
|
47
57
|
|
|
58
|
+
/** An unread manager identity decides nothing (cli readAgentMains). A refusal for want of a role, asked by a session
|
|
59
|
+
* that might be the manager whose main could not be read, stops with that identity error: never refused, routed to
|
|
60
|
+
* the manager or granted by guess. A call that resolved to an allowed role never reaches this. */
|
|
61
|
+
export function refuseUnreadManager(call = current()) { if (call.managerUnread) throw call.managerUnread; }
|
|
62
|
+
|
|
48
63
|
/** Whether this is a caller's call in the owner rather than the process's own. */
|
|
49
64
|
export const isCallersCall = () => calls.getStore()?.kind === 'caller';
|
package/board/caller-session.mjs
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { measureSyncChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
2
|
+
const spawnSync = (...args) => measureSyncChild(() => workLedgerNativeSpawnSync(...args));
|
|
1
3
|
// The session this process runs in, as its own harness names it (Claude Code CLAUDE_CODE_SESSION_ID, Codex
|
|
2
4
|
// CODEX_THREAD_ID). A pane inherits every variable the program that started its terminal server had, so a Codex pane
|
|
3
5
|
// can carry a Claude session's id too: the nearest harness among the process's ancestors says which is its own.
|
|
4
|
-
import { spawnSync } from 'node:child_process';
|
|
6
|
+
import { spawnSync as workLedgerNativeSpawnSync } from 'node:child_process';
|
|
5
7
|
import { basename } from 'node:path';
|
|
6
8
|
|
|
7
9
|
export function processSession() {
|