@volter/supercode-orchestrator 0.5.228 → 0.5.229
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
package/connection.mjs
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import { stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
2
|
+
import { registerWorkOperations, measureWork } from "@volter/supercode-harness-sdk/work-ledger";
|
|
3
|
+
registerWorkOperations([{"kind":"call","operation":"connection.http"}]);
|
|
4
|
+
import { measureSyncChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
5
|
+
const execFileSync = (...args) => measureSyncChild(() => workLedgerNativeExecFileSync(...args));
|
|
6
|
+
const spawnSync = (...args) => measureSyncChild(() => workLedgerNativeSpawnSync(...args));
|
|
1
7
|
// The kit's connection coordinator. Module manifests and programs belong to their application publisher.
|
|
2
8
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, openSync, closeSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
3
9
|
import { createHash, randomBytes } from 'node:crypto';
|
|
4
10
|
import { dirname, join, resolve } from 'node:path';
|
|
5
11
|
import { fileURLToPath } from 'node:url';
|
|
6
|
-
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
12
|
+
import { execFile, execFileSync as workLedgerNativeExecFileSync, spawn, spawnSync as workLedgerNativeSpawnSync } from 'node:child_process';
|
|
7
13
|
import { fromContext } from '@volter/teams/context';
|
|
8
14
|
import { supercodeHome } from '@volter/teams/home';
|
|
9
15
|
|
|
10
16
|
// RH2's modules, each its own program under --modules-root.
|
|
11
|
-
export const MODULE_APPS = ['orchestration-rh2'];
|
|
17
|
+
export const MODULE_APPS = ['orchestration-rh2', 'project-rooms'];
|
|
12
18
|
// supercode's Workplace service's registrations (supercode ADR 0041 decision 3; runhuman-2 ADR 0055 decision 3): one program,
|
|
13
19
|
// sdk/teams/workplace/main.mjs, hosts them all, each with its own credentials on both sides. Their manifests ship here, so an
|
|
14
20
|
// organization's file selects them by these names and `rh2 apply-org` reads them with --modules-root SERVICE_REGISTRATIONS.
|
|
@@ -17,12 +23,23 @@ const SERVICE_ROOT = new URL('./workplace/', import.meta.resolve('@volter/superc
|
|
|
17
23
|
export const SERVICE_REGISTRATIONS = fileURLToPath(new URL('registrations/', SERVICE_ROOT));
|
|
18
24
|
export const SERVICE_PROGRAM = fileURLToPath(new URL('main.mjs', SERVICE_ROOT));
|
|
19
25
|
const SERVICE_TOKENS = { agents: ['AGENTS_WORKPLACE_TOKEN','AGENTS_TEAMS_TOKEN'], cards: ['CARDS_WORKPLACE_TOKEN','CARDS_TEAMS_TOKEN'], machines: ['MACHINES_WORKPLACE_TOKEN','MACHINES_TEAMS_TOKEN'] };
|
|
20
|
-
|
|
26
|
+
// The connection's modules that project the board into the Workplace and place a card's run session there (Open Autonomy's
|
|
27
|
+
// Project Rooms seats it in its project's Room): a session a live card's run works in is handed to their principals
|
|
28
|
+
// (the service's rh2.boardApps; organization.mjs keepHandover, runhuman-2 ADR 0046 decision 6).
|
|
29
|
+
export const BOARD_APPS = ['project-rooms'];
|
|
30
|
+
/** Writes `value` whole through a private temporary file and a rename, and returns the text it holds. Given the text the
|
|
31
|
+
* caller last wrote there (`unchanged`), an identical value writes nothing: a record saved on every event costs a write
|
|
32
|
+
* only when its content changed. */
|
|
33
|
+
export function privateJson(path, value, unchanged) {
|
|
34
|
+
const text=JSON.stringify(value,null,2)+'\n';
|
|
35
|
+
if(text===unchanged)return text;
|
|
21
36
|
mkdirSync(dirname(path),{recursive:true,mode:0o700});
|
|
22
37
|
const pending=`${path}.${process.pid}.${randomBytes(6).toString('hex')}.tmp`;
|
|
23
|
-
writeFileSync(pending,
|
|
38
|
+
writeFileSync(pending,text,{mode:0o600});renameSync(pending,path);chmodSync(path,0o600);
|
|
39
|
+
return text;
|
|
24
40
|
}
|
|
25
41
|
export async function connectionCall(record, side, method, path, body, token) {
|
|
42
|
+
return measureWork("call", "connection.http", async () => {
|
|
26
43
|
const origin=side==='teams'?record.teams.server:record.origin;
|
|
27
44
|
// Registration, consent, exchange and renewal have receiver-staged random values in their bodies:
|
|
28
45
|
// their digest is one operation's key, retained on a retry.
|
|
@@ -38,20 +55,33 @@ export async function connectionCall(record, side, method, path, body, token) {
|
|
|
38
55
|
let payload;try{payload=text===''&&response.status===204?{}:JSON.parse(text);}catch{throw new Error(`${side} ${method} ${path}: invalid JSON (${response.status})`);}
|
|
39
56
|
if(!response.ok)throw Object.assign(new Error(`${side} ${method} ${path}: ${payload.error?.code??response.status}`),{code:payload.error?.code,status:response.status,detail:typeof payload.error?.message==='string'?payload.error.message:null});
|
|
40
57
|
return payload;
|
|
41
|
-
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
});
|
|
61
|
+
}
|
|
42
62
|
/** A registration that trusts a Workplace as its issuer trusts this connection's Workplace and organization, never the
|
|
43
63
|
* one its committed manifest names (agents.teams-app.json names production's): otherwise every connection's agents app
|
|
44
64
|
* trusted production's tokens, and its own Workplace's notices and views were decided as the issuer app instead. */
|
|
45
65
|
export function trustedIssuer(manifest,record){return manifest.issuer===undefined?{}:{issuer:record.issuer,issuer_organization:record.organization};}
|
|
46
66
|
export function connectionPath(root,id){return join(root,'apps','connections',id,'connection.json');}
|
|
67
|
+
export function boardAppPrincipals(root, record) {
|
|
68
|
+
return [...new Set(record.grants.filter(g=>BOARD_APPS.includes(g.name)&&g.rh2?.principalId&&!g.retiring&&!g.disabled
|
|
69
|
+
&&existsSync(join(root,'apps',`${g.name}.${record.id}.json`))).map(g=>g.rh2.principalId))].sort();
|
|
70
|
+
}
|
|
47
71
|
export function generateModuleConfig(root, record, grant) {
|
|
48
72
|
if(SERVICE_APPS.includes(grant.name))return generateServiceConfig(root,record,grant);
|
|
49
73
|
if(grant.adopted)return generateAdoptedConfig(root,record,grant);
|
|
50
74
|
const folder=dirname(connectionPath(root,record.id)), dir=join(folder,grant.name);
|
|
51
75
|
mkdirSync(dir,{recursive:true,mode:0o700});
|
|
52
76
|
const custody=join(dir,'credentials.env'), configPath=join(dir,'config.json');
|
|
53
|
-
writeFileSync(custody,`RH2_TOKEN=${grant.rh2.accessToken}\n`,{mode:0o600});chmodSync(custody,0o600);
|
|
54
|
-
const config=
|
|
77
|
+
writeFileSync(custody,`RH2_TOKEN=${grant.rh2.accessToken}\n${grant.teams?`TEAMS_TOKEN=${grant.teams.credential.token}\n`:''}`,{mode:0o600});chmodSync(custody,0o600);
|
|
78
|
+
const config=grant.name==='project-rooms'?{
|
|
79
|
+
teams:{server:record.teams.server,team:record.teams.team,source:record.source,tokenEnv:'TEAMS_TOKEN'},
|
|
80
|
+
rh2:{baseUrl:record.origin,issuer:record.issuer,organization:record.organization,tokenEnv:'RH2_TOKEN',principalId:grant.rh2.principalId,
|
|
81
|
+
agentsRegistration:record.grants.find(g=>g.name==='agents')?.clientId,cardsPrincipalId:record.grants.find(g=>g.name==='cards')?.rh2?.principalId},
|
|
82
|
+
...(record.scope?{source:{namespace:`cx${record.scope}`}}:{}),envFile:custody,stateFile:join(dir,'state.json'),
|
|
83
|
+
board:{root,...(process.env.SUPERCODE_BIN?{supercode:process.env.SUPERCODE_BIN}:{})}
|
|
84
|
+
}:{rh2:{baseUrl:record.origin,issuer:record.issuer,organization:record.organization,tokenEnv:'RH2_TOKEN'},envFile:custody,
|
|
55
85
|
source:{key:`connection-${record.scope}`,name:`${record.organization} / ${record.source}`,home:root,...(process.env.SUPERCODE_BIN?{program:process.env.SUPERCODE_BIN}:{}),harness:record.harness??'hermes'},
|
|
56
86
|
alerts:{role:record.alertRole??'admin'}};
|
|
57
87
|
privateJson(configPath,config);
|
|
@@ -66,7 +96,7 @@ function generateAdoptedConfig(root, record, grant) {
|
|
|
66
96
|
mkdirSync(dir,{recursive:true,mode:0o700});
|
|
67
97
|
const custody=join(dir,'credentials.env'), configPath=join(dir,'config.json');
|
|
68
98
|
writeFileSync(custody,`${grant.config.rh2.tokenEnv}=${grant.rh2.accessToken}\n${grant.teams?`${grant.config.teams.tokenEnv}=${grant.teams.credential.token}\n`:''}`,{mode:0o600});chmodSync(custody,0o600);
|
|
69
|
-
privateJson(configPath,{...grant.config,envFile:custody,...(grant.config.stateFile?{stateFile:join(dir,'state.json')}:{})});
|
|
99
|
+
privateJson(configPath,{...grant.config,...(grant.name==='project-rooms'?{rh2:{...grant.config.rh2,principalId:grant.rh2.principalId,agentsRegistration:record.grants.find(g=>g.name==='agents')?.clientId??grant.config.rh2.agentsRegistration,cardsPrincipalId:record.grants.find(g=>g.name==='cards')?.rh2?.principalId??grant.config.rh2.cardsPrincipalId}}:{}),envFile:custody,...(grant.config.stateFile?{stateFile:join(dir,'state.json')}:{})});
|
|
70
100
|
privateJson(join(root,'apps',`${grant.name}.${record.id}.json`),{app:grant.name,connection:record.id,configPath,program:grant.program,adopted:true,
|
|
71
101
|
origin:record.origin,organizationId:record.organization,principalId:grant.rh2.principalId,installationId:grant.rh2.installationId,expiresAt:grant.rh2.expiresAt});
|
|
72
102
|
return configPath;
|
|
@@ -82,11 +112,13 @@ export function generateServiceConfig(root, record, grant) {
|
|
|
82
112
|
const held=Object.fromEntries(record.grants.filter(g=>SERVICE_APPS.includes(g.name)&&g.rh2&&g.teams).map(g=>[g.name,g]));
|
|
83
113
|
writeFileSync(custody,Object.entries(held).map(([name,g])=>`${SERVICE_TOKENS[name][0]}=${g.rh2.accessToken}\n${SERVICE_TOKENS[name][1]}=${g.teams.credential.token}\n`).join(''),{mode:0o600});chmodSync(custody,0o600);
|
|
84
114
|
const agents=held.agents;
|
|
115
|
+
const boardApps=boardAppPrincipals(root,record);
|
|
85
116
|
if(agents){
|
|
86
117
|
const section=name=>({registration:held[name].clientId,principal:held[name].rh2.principalId,tokenEnv:SERVICE_TOKENS[name][0],teamsTokenEnv:SERVICE_TOKENS[name][1],stateFile:join(dir,`${name}.state.json`),...record.settings?.[name]});
|
|
87
118
|
const config={
|
|
88
119
|
teams:{server:record.teams.server,team:record.teams.team,installation:agents.teams.installation.id,source:record.source,tokenEnv:SERVICE_TOKENS.agents[1]},
|
|
89
|
-
rh2:{baseUrl:record.origin,organization:record.organization,registration:agents.clientId,...(record.issuer!==record.origin?{issuer:record.issuer}:{}),tokenEnv:SERVICE_TOKENS.agents[0],principal:agents.rh2.principalId
|
|
120
|
+
rh2:{baseUrl:record.origin,organization:record.organization,registration:agents.clientId,...(record.issuer!==record.origin?{issuer:record.issuer}:{}),tokenEnv:SERVICE_TOKENS.agents[0],principal:agents.rh2.principalId,
|
|
121
|
+
...(boardApps.length?{boardApps}:{})},
|
|
90
122
|
...(record.scope?{source:{namespace:`cx${record.scope}`}}:{}),envFile:custody,stateFile:join(dir,'agents.state.json'),
|
|
91
123
|
...record.settings?.service,
|
|
92
124
|
...(record.webhook?{webhook:record.webhook}:{}),
|
|
@@ -117,9 +149,12 @@ export const runnerStopPath=lock=>join(dirname(lock),'runner.stop');
|
|
|
117
149
|
* through that file, and an older one is ended with its whole process tree. */
|
|
118
150
|
export async function stopConnectionRunner(lock,running,wait=20000){
|
|
119
151
|
if(!alive(running.pid))return true;
|
|
120
|
-
if(process.platform!=='win32'){try{process.kill(running.pid,'SIGTERM');}catch(error){if(error.code!=='ESRCH')throw error;}}
|
|
152
|
+
if(process.platform!=='win32'){try{stopWorkChild(running.pid, () => process.kill(running.pid,'SIGTERM'), { signal: 'SIGTERM' });}catch(error){if(error.code!=='ESRCH')throw error;}}
|
|
121
153
|
else if(running.tree)privateJson(runnerStopPath(lock),{pid:running.pid,at:new Date().toISOString()});
|
|
122
|
-
else
|
|
154
|
+
else stopWorkChild(running.pid, () => {
|
|
155
|
+
const result = spawnSync('taskkill',['/PID',String(running.pid),'/T','/F'],{stdio:'ignore',windowsHide:true});
|
|
156
|
+
return !result.error && result.status === 0;
|
|
157
|
+
}, { exitCode: 1 });
|
|
123
158
|
for(let waited=0;waited<wait&&alive(running.pid);waited+=250)await new Promise(done=>setTimeout(done,250));
|
|
124
159
|
return !alive(running.pid);
|
|
125
160
|
}
|
|
@@ -157,6 +192,7 @@ export async function connectWorkplace(flags) {
|
|
|
157
192
|
if(!/^[0-9a-f]{24}$/.test(ticket.scope??''))throw new Error('connection ticket needs a source namespace; apply to a new private output path');
|
|
158
193
|
if(ticket.version!==1||!Array.isArray(ticket.grants)||!ticket.grants.length||ticket.grants.some(g=>!MODULE_APPS.includes(g.name)&&!SERVICE_APPS.includes(g.name))||new Set(ticket.grants.map(g=>g.name)).size!==ticket.grants.length)throw new Error(`unsupported or duplicate connection apps (connect takes ${[...MODULE_APPS,...SERVICE_APPS].join(', ')})`);
|
|
159
194
|
const names=ticket.grants.map(g=>g.name);
|
|
195
|
+
if(names.includes('project-rooms')&&(!names.includes('agents')||!names.includes('cards')))throw new Error('a fresh Project Rooms connection selects agents and cards too');
|
|
160
196
|
// The service is the agents registration's host: its other registrations run only inside it.
|
|
161
197
|
if(names.some(name=>name!=='agents'&&SERVICE_APPS.includes(name))&&!names.includes('agents'))throw new Error('cards and machines run in the Workplace service, which the agents registration hosts: select agents too');
|
|
162
198
|
if(names.some(name=>MODULE_APPS.includes(name))&&!flags['modules-root'])throw new Error(`--modules-root names where ${names.filter(name=>MODULE_APPS.includes(name)).join(' and ')} are`);
|
|
@@ -191,7 +227,7 @@ export async function connectWorkplace(flags) {
|
|
|
191
227
|
let grant=record.grants.find(g=>g.name===selected.name);
|
|
192
228
|
if(grant && JSON.stringify([grant.manifest,grant.teamsManifest])!==JSON.stringify([selected.manifest,selected.teamsManifest]))throw new Error('existing connection consent differs from the ticket');
|
|
193
229
|
if(!grant){grant={name:selected.name,clientId:selected.clientId,manifest:selected.manifest,teamsManifest:selected.teamsManifest};record.grants.push(grant);privateJson(path,record);}
|
|
194
|
-
if(SERVICE_APPS.includes(grant.name)&&!selected.teamsManifest)throw new Error(`${grant.name}: its Teams manifest is missing from the ticket (apply with --modules-root naming ${SERVICE_REGISTRATIONS})`);
|
|
230
|
+
if((SERVICE_APPS.includes(grant.name)||BOARD_APPS.includes(grant.name))&&!selected.teamsManifest)throw new Error(`${grant.name}: its Teams manifest is missing from the ticket (apply with --modules-root naming ${SERVICE_REGISTRATIONS})`);
|
|
195
231
|
if(!grant.rh2){
|
|
196
232
|
const recovering=typeof grant.redemption==='string';
|
|
197
233
|
grant.redemption??=randomBytes(32).toString('base64url');privateJson(path,record);
|
|
@@ -221,8 +257,8 @@ export async function connectWorkplace(flags) {
|
|
|
221
257
|
grant.teams=await connectionCall(record,'teams','POST','/api/v1/apps/token',{app_id:grant.registration.id,app_secret:grant.registration.secret,code:grant.consent.code,redirect_url:selected.teamsManifest.redirect_urls[0],credential_token:grant.stagedCredential});privateJson(path,record);
|
|
222
258
|
delete grant.consent;delete grant.registration.secret;privateJson(path,record);
|
|
223
259
|
}
|
|
224
|
-
if(!SERVICE_APPS.includes(grant.name))generateModuleConfig(root,record,grant);
|
|
225
260
|
}
|
|
261
|
+
for(const grant of record.grants.filter(g=>!SERVICE_APPS.includes(g.name)))generateModuleConfig(root,record,grant);
|
|
226
262
|
// The service's config names every one of its registrations, so it is written once all of them hold credentials.
|
|
227
263
|
for(const grant of record.grants.filter(g=>SERVICE_APPS.includes(g.name)))generateServiceConfig(root,record,grant);
|
|
228
264
|
if(!record.issuerRegistration){record.issuerStagedSecret??=`sca_${randomBytes(32).toString('base64url')}`;privateJson(path,record);record.issuerRegistration=(await connectionCall(record,'teams','POST',`/api/v1/teams/${encodeURIComponent(teams.team)}/apps`,{name:`Workplace issuer (${id.slice(0,8)})`,description:'The Workplace as a delegated issuer, verified against its JWKS',actions:['delegation.sessions','delegation.surfaces','delegation.notices','delegation.machines'],event_kinds:[],redirect_urls:['http://127.0.0.1/connected'],issuer:record.issuer,secret:record.issuerStagedSecret},owner)).data;privateJson(path,record);}
|
|
@@ -237,7 +273,7 @@ export async function connectWorkplace(flags) {
|
|
|
237
273
|
/** The service settings an adopted config carries as it ran (main.mjs names every key it accepts); everything else in its
|
|
238
274
|
* config is rebuilt from the connection's own custody, and any key that is neither is named in the answer as dropped. */
|
|
239
275
|
const ADOPTED_SETTINGS={service:['closedLanes','leadProfiles','launch','teamsPerMinute'],cards:['floorTagPrefix'],machines:[]};
|
|
240
|
-
const REBUILT={teams:['server','team','installation','source','tokenEnv'],rh2:['baseUrl','organization','registration','issuer','tokenEnv','principal','cardsPrincipalId'],
|
|
276
|
+
const REBUILT={teams:['server','team','installation','source','tokenEnv'],rh2:['baseUrl','organization','registration','issuer','tokenEnv','principal','cardsPrincipalId','boardApps'],
|
|
241
277
|
source:['namespace'],envFile:null,stateFile:null,webhook:['url','listen'],cards:['registration','principal','tokenEnv','teamsTokenEnv','stateFile'],machines:['registration','principal','tokenEnv','teamsTokenEnv','stateFile']};
|
|
242
278
|
function dropped(config){
|
|
243
279
|
const out=[];
|
|
@@ -402,7 +438,8 @@ export async function adoptWorkplaceService(flags) {
|
|
|
402
438
|
if(existsSync(a)){copyFileSync(a,`${b}.${process.pid}.tmp`);chmodSync(`${b}.${process.pid}.tmp`,0o600);renameSync(`${b}.${process.pid}.tmp`,b);}else if(existsSync(b))unlinkSync(b);
|
|
403
439
|
}
|
|
404
440
|
}
|
|
405
|
-
for(const grant of record.grants)if(SERVICE_APPS.includes(grant.name))
|
|
441
|
+
for(const grant of record.grants)if(!SERVICE_APPS.includes(grant.name))generateModuleConfig(root,record,grant);
|
|
442
|
+
for(const grant of record.grants)if(SERVICE_APPS.includes(grant.name))generateServiceConfig(root,record,grant);
|
|
406
443
|
stopped();
|
|
407
444
|
record.connectedAt??=new Date().toISOString();
|
|
408
445
|
record.preparing=false;record.paused=Boolean(flags['no-start']);privateJson(path,record);
|
|
@@ -427,21 +464,39 @@ export async function disconnectWorkplace(flags) {
|
|
|
427
464
|
record.paused=true;privateJson(path,record);
|
|
428
465
|
const lock=join(dirname(path),'runner.lock');
|
|
429
466
|
if(existsSync(lock)){
|
|
430
|
-
await stopConnectionRunner(lock,JSON.parse(readFileSync(lock,'utf8')));
|
|
467
|
+
if(!await stopConnectionRunner(lock,JSON.parse(readFileSync(lock,'utf8'))))throw new Error('the connection runner has not yielded its projection writers');
|
|
468
|
+
}
|
|
469
|
+
record.withdrawals??={};privateJson(path,record);
|
|
470
|
+
// Project Rooms removes its seats with its own credential, then Agents withdraws links and handovers with its own.
|
|
471
|
+
const owners=[...record.grants.filter(g=>BOARD_APPS.includes(g.name)),...record.grants.filter(g=>g.name==='agents')];
|
|
472
|
+
for(const grant of owners){
|
|
473
|
+
if(record.withdrawals[grant.name])continue;
|
|
474
|
+
const folder=dirname(path),service=SERVICE_APPS.includes(grant.name);
|
|
475
|
+
const config=join(folder,service?'service':grant.name,'config.json');
|
|
476
|
+
const program=service?SERVICE_PROGRAM:grant.program??join(record.moduleRoot,grant.name,'main.mjs');
|
|
477
|
+
try{
|
|
478
|
+
await new Promise((done,fail)=>execFile(process.execPath,[program,config,'--disconnect'],{windowsHide:true,maxBuffer:1024*1024},error=>error?fail(new Error(`${grant.name}: projection withdrawal remains incomplete; custody and state retained`)):done()));
|
|
479
|
+
record.withdrawals[grant.name]=new Date().toISOString();delete record.withdrawalError;privateJson(path,record);
|
|
480
|
+
}catch(error){record.withdrawalError={module:grant.name,at:new Date().toISOString()};privateJson(path,record);throw error;}
|
|
431
481
|
}
|
|
432
482
|
const team=`/api/v1/teams/${encodeURIComponent(record.teams.team)}`, left=[];
|
|
483
|
+
record.departures??={};
|
|
433
484
|
const leave=async(name,side,method,door,token)=>{
|
|
485
|
+
const key=`${name}:${side}`;
|
|
486
|
+
if(record.departures[key]){left.push({name,side,left:true,already:true});return;}
|
|
434
487
|
try{await connectionCall(record,side,method,door,side==='rh2'?{}:undefined,token);left.push({name,side,left:true});}
|
|
435
488
|
catch(error){if(error.status===404||error.code==='installation-unknown'){left.push({name,side,left:true,already:true});}else left.push({name,side,left:false,code:error.code??error.status??error.message});}
|
|
489
|
+
if(left.at(-1).left)record.departures[key]=true;
|
|
436
490
|
privateJson(path,record);
|
|
437
491
|
};
|
|
438
492
|
for(const grant of record.grants){
|
|
439
493
|
if(grant.teams?.installation?.id)await leave(grant.name,'teams','DELETE',`${team}/app-installations/${encodeURIComponent(grant.teams.installation.id)}`,grant.teams.credential.token);
|
|
440
494
|
if(grant.rh2?.installationId)await leave(grant.name,'rh2','POST',`/api/v3/automation/installations/${encodeURIComponent(grant.rh2.installationId)}/leave`,grant.rh2.accessToken);
|
|
441
|
-
const entry=join(root,'apps',`${grant.name}.${record.id}.json`);if(existsSync(entry))unlinkSync(entry);
|
|
495
|
+
const entry=join(root,'apps',`${grant.name}.${record.id}.json`);if((!grant.teams||record.departures[`${grant.name}:teams`])&&(!grant.rh2||record.departures[`${grant.name}:rh2`])&&existsSync(entry))unlinkSync(entry);
|
|
442
496
|
}
|
|
443
497
|
// The token exchange answers {installation, credential: {token, …}} (renewTeams reads the same shape).
|
|
444
498
|
if(record.issuerCredential?.installation?.id)await leave('issuer','teams','DELETE',`${team}/app-installations/${encodeURIComponent(record.issuerCredential.installation.id)}`,record.issuerCredential.credential?.token);
|
|
499
|
+
if(left.some(each=>!each.left)){record.left=left;privateJson(path,record);throw new Error('connection departure remains incomplete; custody and state retained for retry');}
|
|
445
500
|
record.disconnectedAt=new Date().toISOString();record.left=left;privateJson(path,record);
|
|
446
501
|
return {connection:record.id,root,left,registrations:[...record.grants.map(g=>g.registration?.id).filter(Boolean),record.issuerRegistration?.id].filter(Boolean)};
|
|
447
502
|
}
|
package/gates.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
1
2
|
// A fire's gate, the I/O half (docs/architecture/orchestrator.md §4.3 step 2a): the job's
|
|
2
3
|
// pre-run script and its monitor source, run by the loop off the event queue. Transcribed from
|
|
3
4
|
// Hermes 0.21.0: `cron/scheduler.py` `_run_job_script` / `_get_script_timeout`, and
|
|
@@ -107,7 +108,7 @@ export async function runJobScript(profile, scriptPath, workdir = null, { env =
|
|
|
107
108
|
// SUPERCODE_FIRE_ID names the fire it runs for, so what it files can be keyed to that fire (a retried run of the
|
|
108
109
|
// same fire files nothing new), not to the wall clock an interval schedule drifts against.
|
|
109
110
|
const child = spawn(argv[0], argv[1], { cwd, env: { ...scriptEnv(), HERMES_HOME: profile.dir, ...(automation ? { SUPERCODE_AUTOMATION: automation } : {}), ...(fire ? { SUPERCODE_FIRE_ID: String(fire) } : {}) }, detached: true, windowsHide: true, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
110
|
-
const kill = () => { try { process.kill(-child.pid, 'SIGKILL'); } catch { /* already gone */ } };
|
|
111
|
+
const kill = () => { try { stopWorkChild(child, () => process.kill(-child.pid, 'SIGKILL'), { signal: 'SIGKILL' }); } catch { /* already gone */ } };
|
|
111
112
|
const end = (result) => {
|
|
112
113
|
if (finished) return;
|
|
113
114
|
finished = true;
|
package/hand-off.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
1
2
|
// THE ONE PLACE A HOME'S ORCHESTRATOR IS DECIDED (D152 row 11). Every command on a home ends in an orchestrator's entry
|
|
2
3
|
// (bin/orchestrator.mjs), whichever way it came: a shell's or a pane's supercode, the native binary (its \`workflow\`,
|
|
3
4
|
// \`orchestrator start\`, \`apps\`, \`access\` and its cold write door all run \`node <entry> … --root <home>\` with the
|
|
@@ -12,7 +13,7 @@ import { spawn } from 'node:child_process';
|
|
|
12
13
|
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
13
14
|
import { join, resolve } from 'node:path';
|
|
14
15
|
import { fileURLToPath } from 'node:url';
|
|
15
|
-
|
|
16
|
+
// The pinned source's check (enforceSourceLock, the host's source lock) is handed in by handOff's caller, the entry.
|
|
16
17
|
|
|
17
18
|
/** The home a command (the whole of it, after the entry) names: --root, wherever it stands (the gateway's form,
|
|
18
19
|
* `node bin/orchestrator.mjs --root <home>`, has no verb before it); for `import`, which writes the home it names
|
|
@@ -27,7 +28,7 @@ export function homeOfArgs(argv, env = process.env) {
|
|
|
27
28
|
|
|
28
29
|
/** The command (\`argv\`, after node and the entry) handed to the orchestrator the home's record names: its exit code, or
|
|
29
30
|
* null when this orchestrator serves it (it is the one named, or the home has no record). */
|
|
30
|
-
export function handOff(root, argv) {
|
|
31
|
+
export function handOff(root, argv, enforceSourceLock) {
|
|
31
32
|
if (!root) return null;
|
|
32
33
|
let record;
|
|
33
34
|
try { record = JSON.parse(readFileSync(join(root, 'orchestrator.json'), 'utf8')); } catch { return null; }
|
|
@@ -43,7 +44,7 @@ export function handOff(root, argv) {
|
|
|
43
44
|
if (named === own) { process.env.SUPERCODE_BIN = supercode; return null; }
|
|
44
45
|
return new Promise((done) => {
|
|
45
46
|
const child = spawn(process.execPath, [named, ...argv], { stdio: 'inherit', env: { ...process.env, SUPERCODE_BIN: supercode } });
|
|
46
|
-
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => { try { child.kill(signal); } catch { /* gone */ } });
|
|
47
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => { try { stopWorkChild(child, () => child.kill(signal), { signal: signal }); } catch { /* gone */ } });
|
|
47
48
|
child.on('error', (error) => { process.stderr.write(`supercode: could not run orchestrator ${record.version ?? ''} (${named}): ${error.message}\n`); done(1); });
|
|
48
49
|
child.on('close', (code, signal) => done(signal ? 1 : code ?? 1));
|
|
49
50
|
});
|
package/hermes-cli.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
|
+
import { measureSyncChild, stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
2
|
+
const spawnSync = (...args) => measureSyncChild(() => workLedgerNativeSpawnSync(...args));
|
|
1
3
|
// Hermes's own tools beside a worker (docs/plans/hermes-compat.md row 7). A home is a Hermes home, and the
|
|
2
4
|
// skills it carries call Hermes's CLI (`hermes kanban …`, `hermes cron notepad …`, `hermes cron doctor`)
|
|
3
5
|
// against it, so a worker of any harness runs with `HERMES_HOME` set to its profile's folder and the
|
|
4
6
|
// pinned `hermes` on its `PATH`. Native cron incidents use the owning interchange codec, in the same executions store.
|
|
5
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
7
|
+
import { spawn, spawnSync as workLedgerNativeSpawnSync } from 'node:child_process';
|
|
6
8
|
import { accessSync, constants, existsSync, readFileSync, statSync } from 'node:fs';
|
|
7
9
|
import { delimiter, dirname, join } from 'node:path';
|
|
8
10
|
import { parseDotenv } from './orchestration.mjs';
|
|
9
11
|
import { locateHermes } from './apply/doors.mjs';
|
|
10
|
-
import { recordCronIncident } from '@volter/supercode-
|
|
12
|
+
import { recordCronIncident } from '@volter/supercode-teams/sessions-cron-incidents';
|
|
11
13
|
import { redactText } from '@volter/teams/redact';
|
|
12
14
|
|
|
13
15
|
/** Hermes's interpreter over `code`, `payload` on stdin, without blocking the daemon: `{ status, stdout, stderr }`. */
|
|
@@ -15,7 +17,7 @@ function python(install, code, payload, env, timeout) {
|
|
|
15
17
|
return new Promise((done) => {
|
|
16
18
|
const child = spawn(install.python, ['-c', code], { cwd: install.source, env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
17
19
|
let stdout = '', stderr = '';
|
|
18
|
-
const timer = setTimeout(() => child.kill('SIGKILL'), timeout);
|
|
20
|
+
const timer = setTimeout(() => stopWorkChild(child, () => child.kill('SIGKILL'), { signal: 'SIGKILL' }), timeout);
|
|
19
21
|
child.stdout.setEncoding('utf8').on('data', (c) => { stdout += c; });
|
|
20
22
|
child.stderr.setEncoding('utf8').on('data', (c) => { stderr = (stderr + c).slice(-4000); });
|
|
21
23
|
child.on('error', (error) => { clearTimeout(timer); done({ status: null, stdout, stderr: error.message }); });
|
package/hermes-locks.mjs
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
2
|
+
import { measureSyncChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
3
|
+
const spawnSync = (...args) => measureSyncChild(() => workLedgerNativeSpawnSync(...args));
|
|
1
4
|
// Hermes's two runtime locks, held the way Hermes holds them, so one home is served by one runtime and
|
|
2
5
|
// one bot token by one gateway (docs/plans/hermes-compat.md, row 1; pinned Hermes gateway/status.py).
|
|
3
6
|
//
|
|
@@ -9,7 +12,7 @@
|
|
|
9
12
|
// Hermes calls a holder live when its pid exists and its command line is a gateway's (`hermes-gateway`,
|
|
10
13
|
// `hermes gateway run`, `gateway/run.py`). The daemon runs as `hermes-gateway` (bin/orchestrator.mjs), so
|
|
11
14
|
// Hermes reads the orchestrator's locks as a gateway's, which they are.
|
|
12
|
-
import { spawn, spawnSync } from 'node:child_process';
|
|
15
|
+
import { spawn, spawnSync as workLedgerNativeSpawnSync } from 'node:child_process';
|
|
13
16
|
import { createHash } from 'node:crypto';
|
|
14
17
|
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, unlinkSync, writeSync } from 'node:fs';
|
|
15
18
|
import { homedir } from 'node:os';
|
|
@@ -163,7 +166,7 @@ function holdFileLock(path, home, { python, what, tail }) {
|
|
|
163
166
|
const line = out.split('\n')[0];
|
|
164
167
|
if (line === 'ok') {
|
|
165
168
|
done({ release: () => releasing ??= (async () => {
|
|
166
|
-
child.stdin.end();
|
|
169
|
+
stopWorkChild(child, () => child.stdin.end());
|
|
167
170
|
const result = await ended;
|
|
168
171
|
if (result.code !== 0) throw new Error(`lock holder for ${path} exited ${result.code ?? result.signal}`);
|
|
169
172
|
})() });
|
package/hosted.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { stopWorkChild } from '@volter/supercode-harness-sdk/work-ledger';
|
|
1
2
|
// Host integration: the orchestrator owns time and durable fires; a trusted
|
|
2
3
|
// application executes each task through its existing conversation/action door.
|
|
3
4
|
import { connect } from 'node:net';
|
|
@@ -71,7 +72,7 @@ export async function stopHostedDaemon({ root, executorUrl, timeoutMs = 30_000 }
|
|
|
71
72
|
if (!leaseIsHere(lease)) return { stopped: true, wasRunning: false };
|
|
72
73
|
const alive = () => { try { process.kill(lease.pid, 0); return true; } catch (error) { if (error.code === 'ESRCH') return false; throw error; } };
|
|
73
74
|
if (!alive()) return { stopped: true, wasRunning: false };
|
|
74
|
-
process.kill(lease.pid, 'SIGTERM');
|
|
75
|
+
stopWorkChild(lease.pid, () => process.kill(lease.pid, 'SIGTERM'), { signal: 'SIGTERM' });
|
|
75
76
|
const deadline = Date.now() + timeoutMs;
|
|
76
77
|
while (alive()) {
|
|
77
78
|
if (Date.now() >= deadline) throw new Error('The orchestrator has not finished stopping. Its lease and data were preserved.');
|
package/index.d.ts
CHANGED
|
@@ -599,3 +599,5 @@ export function leaseIsHere(lease: Pick<OrchestratorLease, 'host' | 'boot'> | nu
|
|
|
599
599
|
/** Whether the daemon a lease names is alive now: here, and its pid live. */
|
|
600
600
|
export function leaseIsLive(lease: OrchestratorLease | null | undefined): boolean;
|
|
601
601
|
export function readLease(root: string): OrchestratorLease | null;
|
|
602
|
+
|
|
603
|
+
export type { PrDispositionRecorded, PrReleaseReadback } from './ontology.js';
|
package/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import './source-lock.mjs';
|
|
1
|
+
import './source-lock.mjs'; // layer: the orchestrator package enforces its pinned source when it loads (the host's source lock); owed: the host hands the lock in
|
|
2
2
|
export {
|
|
3
3
|
LoadError, sessionReset, homeChannel, CHAT_TYPES, END_REASONS, FIRE_STATUSES, OBLIGATION_STATES,
|
|
4
4
|
renderSurfaceKey, parseSurfaceKey, surfaceKeyString, parseTarget, renderTarget, resolveRoute, routeSpecificity,
|
package/kanban-worker.mjs
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
// run), its memory is its harness's own, its requests are answered by supercode's unattended rule. A profile whose
|
|
9
9
|
// worker is Hermes runs Hermes itself, as its harness.
|
|
10
10
|
import { spawn } from 'node:child_process';
|
|
11
|
-
import { existsSync, statSync } from 'node:fs';
|
|
11
|
+
import { existsSync, statSync, writeFileSync } from 'node:fs';
|
|
12
12
|
import { basename, dirname, join, resolve } from 'node:path';
|
|
13
13
|
import { SupercodeHarnessClient } from '@volter/supercode-harness-sdk';
|
|
14
14
|
import { load } from './orchestration.mjs';
|
|
@@ -24,6 +24,7 @@ import { DEFAULT_BOARD, openBoard, view } from './board/store.mjs';
|
|
|
24
24
|
import { getTask } from './board/engine.mjs';
|
|
25
25
|
import { permissionResponse } from './reducer.mjs';
|
|
26
26
|
import { boardGuardArguments } from '@volter/teams/board-process-guard';
|
|
27
|
+
import { resolveNativeSessionHome } from '@volter/supercode-interchange';
|
|
27
28
|
|
|
28
29
|
/** Hermes's worker argv, as `_default_spawn` builds it. */
|
|
29
30
|
export function parseHermesArgv(argv) {
|
|
@@ -48,8 +49,14 @@ function rootOf(home) {
|
|
|
48
49
|
return basename(dirname(home)) === 'profiles' ? dirname(dirname(home)) : home;
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
|
|
52
|
+
// currentRelease: the machine's current release (the host's), which its caller hands in for the board's hook command.
|
|
53
|
+
export async function runKanbanWorker(argv, env = process.env, currentRelease) {
|
|
52
54
|
const say = (line) => process.stdout.write(`${line}\n`);
|
|
55
|
+
const refuse = (line) => {
|
|
56
|
+
say(line);
|
|
57
|
+
if (env.SUPERCODE_BOARD_FAILURE_FILE) writeFileSync(env.SUPERCODE_BOARD_FAILURE_FILE, `${line}\n`, { mode: 0o600 });
|
|
58
|
+
return 78;
|
|
59
|
+
};
|
|
53
60
|
const args = parseHermesArgv(argv);
|
|
54
61
|
if (!env.HERMES_HOME || !args.query) { say('hermes-worker: HERMES_HOME and -q are required (the dispatcher sets both)'); return 2; }
|
|
55
62
|
const home = resolve(env.HERMES_HOME);
|
|
@@ -57,22 +64,42 @@ export async function runKanbanWorker(argv, env = process.env) {
|
|
|
57
64
|
const { state, vault } = await load(root);
|
|
58
65
|
const profile = Object.values(state.profiles).find((p) => resolve(p.dir) === home) ?? state.profiles[args.profile ?? 'default'];
|
|
59
66
|
if (!profile?.worker?.harness) { say(`hermes-worker: no worker for profile ${args.profile ?? home}`); return 2; }
|
|
67
|
+
const task = Object.fromEntries(KANBAN_ENV.filter((k) => env[k]).map((k) => [k, env[k]]));
|
|
60
68
|
// The agent's home is already the effective profile. Hermes must not switch back to the shared template.
|
|
61
|
-
if (profile.worker.harness === 'hermes')
|
|
69
|
+
if (profile.worker.harness === 'hermes') {
|
|
70
|
+
if (env.SUPERCODE_BOARD_RESUME_SESSION) return refuse(`hermes-worker: Hermes headless continuation of ${env.SUPERCODE_BOARD_RESUME_SESSION} has no verified resume door; a deliberate disposition is required`);
|
|
71
|
+
try { await recordSession(root, task, 'hermes', null, await resolveNativeSessionHome('hermes', env, process.cwd())); }
|
|
72
|
+
catch (error) { return refuse(`hermes-worker: recording the native home on its run failed: ${error.message}`); }
|
|
73
|
+
return runHermes(env.SUPERCODE_AGENT_ID?withoutProfile(argv):argv, env);
|
|
74
|
+
}
|
|
62
75
|
const harness = profile.worker.harness;
|
|
63
|
-
const task = Object.fromEntries(KANBAN_ENV.filter((k) => env[k]).map((k) => [k, env[k]]));
|
|
64
76
|
const taskId = task.HERMES_KANBAN_TASK ?? 'none';
|
|
65
77
|
const workspace = env.HERMES_KANBAN_WORKSPACE;
|
|
66
78
|
const cwd = workspace && existsSync(workspace) && statSync(workspace).isDirectory() ? workspace : activationCwd(profile, profile.worker.cwd);
|
|
67
79
|
const worker = { ...profile.worker, ...(args.model ? { model: args.model } : {}) };
|
|
80
|
+
let retained = null;
|
|
81
|
+
if (env.SUPERCODE_BOARD_RESUME_SESSION) {
|
|
82
|
+
try {
|
|
83
|
+
retained = JSON.parse(env.SUPERCODE_BOARD_NATIVE_HOME ?? 'null');
|
|
84
|
+
const selected = retained?.env && await resolveNativeSessionHome(harness, retained.env, cwd);
|
|
85
|
+
if (!selected || JSON.stringify(selected.homes) !== JSON.stringify(retained.homes)) retained = null;
|
|
86
|
+
} catch { retained = null; }
|
|
87
|
+
if (!retained) return refuse('hermes-worker: the retained native home is unknown; nothing was resumed');
|
|
88
|
+
}
|
|
68
89
|
if (args.toolsets) say(`hermes-worker: --toolsets ${args.toolsets} is Hermes's agent's setting, carried and not performed for ${harness}`);
|
|
69
90
|
|
|
70
91
|
materializePersona(profile);
|
|
71
92
|
materializeSkills(profile);
|
|
72
|
-
const route = workerRoute(profile, worker, vault);
|
|
93
|
+
const route = workerRoute(profile, worker, vault, retained);
|
|
73
94
|
const surfaceKey = `kanban|task|${taskId}`;
|
|
74
95
|
const identityEnv=Object.fromEntries(Object.entries(env).filter(([key])=>key.startsWith('SUPERCODE_AGENT_')));
|
|
75
96
|
const runEnv = { ...activationEnv({ root, profile, surfaceKey, vault }), ...route.env, ...task, ...identityEnv,HERMES_HOME: home };
|
|
97
|
+
if (retained) {
|
|
98
|
+
for (const [name, value] of Object.entries(retained.env)) {
|
|
99
|
+
if (value === null) { delete runEnv[name]; delete process.env[name]; }
|
|
100
|
+
else runEnv[name] = value;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
76
103
|
const memory=env.SUPERCODE_AGENT_ID?await memorySnapshot(home):'';
|
|
77
104
|
// the persona, the task protocol and the pinned skills, supercode's own (board/worker-prompt.mjs)
|
|
78
105
|
// the board workflow's own words for this run (its status's prompt and doors), after the task protocol
|
|
@@ -80,14 +107,17 @@ export async function runKanbanWorker(argv, env = process.env) {
|
|
|
80
107
|
|
|
81
108
|
const client = new SupercodeHarnessClient(env.SUPERCODE_BIN ? { command: env.SUPERCODE_BIN } : {});
|
|
82
109
|
try {
|
|
83
|
-
return await runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say });
|
|
110
|
+
return await runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say, currentRelease, refuse, resumeSession: env.SUPERCODE_BOARD_RESUME_SESSION || null });
|
|
84
111
|
} finally {
|
|
85
112
|
await client.close().catch(() => {});
|
|
86
113
|
}
|
|
87
114
|
}
|
|
88
115
|
|
|
89
|
-
async function runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say }) {
|
|
116
|
+
async function runTurn({ client, profile, harness, worker, route, root, vault, surfaceKey, runEnv, instructions, task, cwd, args, say, currentRelease, refuse, resumeSession }) {
|
|
90
117
|
const descriptor = (await client.supportReport()).harnesses.find((h) => h.id === harness);
|
|
118
|
+
if (resumeSession && !descriptor?.runtime?.capabilities?.resume_session) {
|
|
119
|
+
return refuse(`hermes-worker: ${harness} cannot continue ${resumeSession}: its native runtime advertises no resume door; a deliberate disposition is required`);
|
|
120
|
+
}
|
|
91
121
|
// A worker's folder that is gone (a home moved or swept) fails its launch as the harness program missing; say which.
|
|
92
122
|
if (!existsSync(cwd) || !statSync(cwd).isDirectory()) { say(`hermes-worker: the worker's folder ${cwd} does not exist (the profile's worker.cwd)`); return 2; }
|
|
93
123
|
const base = descriptor?.runtime?.default_launch;
|
|
@@ -95,14 +125,31 @@ async function runTurn({ client, profile, harness, worker, route, root, vault, s
|
|
|
95
125
|
const reasoning = args.reasoning && harness === 'codex' ? ['-c', `model_reasoning_effort=${JSON.stringify(args.reasoning)}`] : [];
|
|
96
126
|
const launch = {
|
|
97
127
|
program: base.program,
|
|
98
|
-
arguments: boardGuardArguments(harness, [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...userHomeArguments(profile), ...reasoning, ...modeArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))], { enabled: ['claude-code','codex'].includes(harness), cwd }),
|
|
128
|
+
arguments: boardGuardArguments(harness, [...(base.arguments ?? []), ...modelArguments(worker), ...route.args, ...(instructions && INSTRUCTION_ARGUMENTS[harness] ? INSTRUCTION_ARGUMENTS[harness](instructions) : []), ...userHomeArguments(profile), ...reasoning, ...modeArguments(profile, harness), ...limitArguments(profile, worker, (level, line) => say(line))], { enabled: ['claude-code','codex'].includes(harness), cwd, currentRelease }),
|
|
99
129
|
env: { ...(base.env ?? {}), ...runEnv },
|
|
100
130
|
};
|
|
101
131
|
declareWorkerHome({ harness, env: launch.env, cwd, log: (_level, line) => say(`hermes-worker: ${line}`) });
|
|
102
132
|
const mount = mountOrchestratorMcp({ root, profile, harness, surfaceKey, launch, vault, task });
|
|
133
|
+
const nativeHome = await resolveNativeSessionHome(harness, { ...process.env, ...mount.launch.env }, cwd);
|
|
134
|
+
try { if (nativeHome) await recordSession(root, task, harness, null, nativeHome); }
|
|
135
|
+
catch (error) { return refuse(`hermes-worker: recording the native home on its run failed: ${error.message}`); }
|
|
103
136
|
const policy = approvalPolicy(profile, harness), reviewer = approvalsReviewer(profile, harness);
|
|
104
|
-
const
|
|
105
|
-
|
|
137
|
+
const params = { harness, cwd, launch: mount.launch, ...(mount.mcp_servers ? { mcp_servers: mount.mcp_servers } : {}), ...(policy ? { approval_policy: policy } : {}), ...(reviewer ? { approvals_reviewer: reviewer } : {}) };
|
|
138
|
+
let runtime;
|
|
139
|
+
try {
|
|
140
|
+
runtime = resumeSession ? await client.resumeManagedRuntime({ ...params, runtime_id: resumeSession }) : await client.startManagedRuntime(params);
|
|
141
|
+
} catch (error) {
|
|
142
|
+
const failure = `hermes-worker: ${resumeSession ? `native resume of ${resumeSession}` : 'native start'} failed: ${error.message}`;
|
|
143
|
+
if (resumeSession) return refuse(failure);
|
|
144
|
+
say(failure); return 1;
|
|
145
|
+
}
|
|
146
|
+
try {
|
|
147
|
+
if (resumeSession && runtime.handle.runtime_id !== resumeSession) throw new Error(`native resume returned ${runtime.handle.runtime_id}, expected ${resumeSession}`);
|
|
148
|
+
await recordSession(root, task, harness, runtime.handle.runtime_id);
|
|
149
|
+
} catch (error) {
|
|
150
|
+
await runtime.close().catch(() => {});
|
|
151
|
+
return refuse(`hermes-worker: recording the session on its run failed: ${error.message}`);
|
|
152
|
+
}
|
|
106
153
|
let text = '', last = '';
|
|
107
154
|
// `agent.max_turns` for a worker that takes no cap at launch, held as the daemon holds it (loop.mjs `holdTurnLimit`)
|
|
108
155
|
const counter = turnCounter(profile, harness);
|
|
@@ -187,15 +234,18 @@ function cardWords(root, task) {
|
|
|
187
234
|
* dispatcher recorded): the harness's own transcript, found and resumed by supercode like any session. Written by the
|
|
188
235
|
* board's owner, as every board write is (`workflow run-session`).
|
|
189
236
|
*/
|
|
190
|
-
async function recordSession(root, task, harness, sessionId) {
|
|
191
|
-
if (!task.HERMES_KANBAN_RUN_ID || !sessionId) return;
|
|
237
|
+
async function recordSession(root, task, harness, sessionId, nativeHome = null) {
|
|
238
|
+
if (!task.HERMES_KANBAN_RUN_ID || (!sessionId && !nativeHome)) return;
|
|
192
239
|
const answer = await askBoardOwner(root, {
|
|
193
|
-
argv: ['run-session', String(task.HERMES_KANBAN_RUN_ID), String(sessionId), '--harness', harness, '--task', String(task.HERMES_KANBAN_TASK ?? '')],
|
|
240
|
+
argv: ['run-session', String(task.HERMES_KANBAN_RUN_ID), ...(sessionId ? [String(sessionId)] : []), '--harness', harness, '--task', String(task.HERMES_KANBAN_TASK ?? ''), ...(nativeHome ? ['--native-home', JSON.stringify(nativeHome)] : [])],
|
|
194
241
|
board: task.HERMES_KANBAN_BOARD || null, caller: null, session: null, cwd: process.cwd(), stdin: null,
|
|
195
242
|
facts: { ...callerFacts(), SUPERCODE_CONTEXT: process.env.SUPERCODE_CONTEXT || 'local' },
|
|
196
243
|
});
|
|
197
244
|
if (answer.unanswered === true) throw new Error(unansweredText(answer, `the run's session record (run-session ${task.HERMES_KANBAN_RUN_ID})`).trim());
|
|
198
245
|
if (answer.code) throw new Error((answer.stderr || 'the board owner did not record it').trim());
|
|
246
|
+
let result;
|
|
247
|
+
try { result = JSON.parse(answer.stdout ?? '{}'); } catch { /* text output is allowed */ }
|
|
248
|
+
if (result?.error) throw new Error(String(result.error));
|
|
199
249
|
}
|
|
200
250
|
|
|
201
251
|
/** A profile whose worker is Hermes: the dispatcher's own command, run as Hermes would have run it. */
|