@volter/supercode-orchestrator 0.5.226 → 0.5.227
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 +29 -18
- package/adapters/rh2.mjs +6 -5
- package/bin/connection-runner.mjs +46 -12
- package/bin/orchestrator.mjs +3 -3
- package/bin/runtime.mjs +6 -1
- package/board/agent-declarations.mjs +1 -1
- package/board/args.mjs +8 -1
- package/board/call.mjs +2 -2
- package/board/ci.mjs +158 -54
- package/board/cli.mjs +175 -53
- package/board/custody.mjs +5 -1
- package/board/dispatch.mjs +969 -256
- package/board/doors.mjs +7 -2
- package/board/engine.mjs +264 -48
- package/board/event-stream.mjs +3 -7
- package/board/files.mjs +4 -1
- package/board/fleet-listeners.mjs +3 -1
- package/board/fleet-observer.mjs +3 -2
- package/board/generation-owner.mjs +40 -10
- package/board/generation.mjs +17 -4
- package/board/machine-fleet.mjs +33 -9
- package/board/notices.mjs +10 -2
- package/board/published.mjs +107 -2
- package/board/readable-id.mjs +25 -2
- package/board/runtime-door.mjs +2 -2
- package/board/session-work.mjs +15 -1
- package/board/storage-contract.mjs +41 -3
- package/board/store.mjs +49 -7
- package/board/tools.mjs +1 -1
- package/board/workflow.mjs +197 -70
- package/board/workspace.mjs +99 -13
- package/board/ztrack.mjs +42 -14
- package/connection.mjs +252 -32
- package/index.d.ts +5 -1
- package/installed-apps.mjs +11 -5
- package/loop.mjs +6 -0
- package/mcp/hermes-kanban-tools.json +10 -1
- package/package.json +6 -6
- package/reducer.mjs +31 -2
package/README.md
CHANGED
|
@@ -80,8 +80,8 @@ Other SDK callers that persist runtime state set `preserve_workflow: true` on
|
|
|
80
80
|
`workflow serve` retains its public socket and kernel lock while dispatcher generations change.
|
|
81
81
|
`workflow handover --entry /absolute/path/to/bin/orchestrator.mjs --supercode /absolute/path/to/native --root HOME`
|
|
82
82
|
prepares a successor through the normal source checks, drains admitted work, and transfers its writer epoch.
|
|
83
|
-
It routes calls only after a clean round and a successful stats reply.
|
|
84
|
-
|
|
83
|
+
It routes calls only after a clean round and a successful stats reply. A candidate lost before the handover finishes
|
|
84
|
+
rolls back to a fresh generation started from the predecessor's selection; finishing ends the parked predecessor. A request already sent to a lost generation returns an unknown outcome and
|
|
85
85
|
is never replayed. This source mechanism still needs the complete machine release integration and recorded
|
|
86
86
|
qualification described in [ADR 0080](../../docs/adr/0080-serving-generation-handovers.md).
|
|
87
87
|
|
|
@@ -517,8 +517,9 @@ Workplace in the home: the Agents → Room agents integration (`sdk/teams/workpl
|
|
|
517
517
|
every agent it provides, the token of its principal minted at RH2's agent-token door on that registration's own
|
|
518
518
|
installation (`rh2-agent.<agent id>@<Workplace host>`, renewed before it lapses, revoked when Teams ends the agent); one
|
|
519
519
|
can also be kept by hand. Every session of the agent acts through them (the `rh2` CLI and MCP read the record for the
|
|
520
|
-
Workplace each call goes to, from `RH2_AGENT_HOMES`
|
|
521
|
-
|
|
520
|
+
Workplace each call goes to, from `RH2_AGENT_HOMES` or `SUPERCODE_AGENT_HOME`; a board pane of the agent on another
|
|
521
|
+
machine reads the copies the board keeps in the agent's home on that machine, `harness.v1.agent.credentials.keep`),
|
|
522
|
+
the agent's channel joins its Room's conversation on its record, and orchestrator-launched sessions mount them as the `rh2-agent` MCP server, never in another agent's that shares its
|
|
522
523
|
name or profile. Kept by hand:
|
|
523
524
|
|
|
524
525
|
```sh
|
|
@@ -826,20 +827,30 @@ return through adoption and released availability remain unqualified.
|
|
|
826
827
|
|
|
827
828
|
## Readable card ids
|
|
828
829
|
|
|
829
|
-
Every card has a `readable_id` beside its number and native id
|
|
830
|
-
`supercode workflow create "Voice calls" --readable-id voice-calls --root HOME
|
|
831
|
-
|
|
832
|
-
with a letter, have at most 32 characters and are unique
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
830
|
+
Every card has a `readable_id` beside its number and native id, given by its
|
|
831
|
+
creator: `supercode workflow create "Voice calls" --readable-id voice-calls --root HOME`.
|
|
832
|
+
A create without one is refused. Names are a few lowercase words joined by
|
|
833
|
+
hyphens, start with a letter, have at most 32 characters and are unique on the
|
|
834
|
+
board, among open and ended cards alike. A subtask, which no person names, takes
|
|
835
|
+
one from its title; so do rows an older board or a hand-written document left
|
|
836
|
+
without one.
|
|
837
|
+
|
|
838
|
+
`workflow list`, `show`, `create`, capacity, notices and mail subjects lead with
|
|
839
|
+
the name (`voice-calls (VO-42 t_4406c88a)`), and every verb takes it where it
|
|
840
|
+
takes a card id. `workflow specify CARD --readable-id voice-calls --root HOME`
|
|
841
|
+
changes it; the same `readable_id` property is editable in a ztrack card's
|
|
842
|
+
metadata in `arcs.md`. A title edit keeps its existing id.
|
|
843
|
+
|
|
844
|
+
A new worktree card's branch and worktree folder are its readable id
|
|
845
|
+
(`voice-calls`, under a project's `<slug>/` for the branch), refused when another
|
|
846
|
+
card's branch already has that name. Until the card's worktree is first made, its
|
|
847
|
+
machine refuses a branch of that name already in the checkout or on origin, or a
|
|
848
|
+
folder of that name, that the card did not make; `main`, `master`, `publish` and the
|
|
849
|
+
repository's default branch are never a card's. Fresh sessions are named `voice-calls-3f`
|
|
850
|
+
(a reviewer's `voice-calls-reviewer-3f`). Card agents become
|
|
841
851
|
`implementer-voice-calls` or `reviewer-voice-calls`, with the same immutable agent
|
|
842
852
|
id and sessions; the ordinary delivery pass sends existing agents' renames to
|
|
843
853
|
the machine. The Workplace updates its existing principal and spoken name.
|
|
844
|
-
|
|
845
|
-
|
|
854
|
+
A card's recorded branch and worktree folder never follow a later rename, and
|
|
855
|
+
existing sessions keep their names. Merge and release remain separate from this
|
|
856
|
+
source behavior.
|
package/adapters/rh2.mjs
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// content) / close(). The orchestrator holds an RH2 session (the bot
|
|
4
4
|
// principal's automation token), reads the Rooms it may read and answers
|
|
5
5
|
// every line its conversation participation asks for only in a Room it was
|
|
6
|
-
// brought into by the join command (
|
|
6
|
+
// brought into by the join command (a participant of the Room's base channel: an app holds no seat, runhuman-2 ADR 0063),
|
|
7
7
|
// elsewhere only a line naming it, the way it does in a Slack channel. RH2's own rule keeps a coding seat and this loop
|
|
8
8
|
// from answering each other: a seat is never woken by an agent's message, and
|
|
9
9
|
// this adapter turns only a person's line (kind `human`, or a trigger's
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
//
|
|
12
12
|
// Doors (RH2 `/api/v3`, bearer + `x-rh2-organization`):
|
|
13
13
|
// GET /rooms the Rooms to follow
|
|
14
|
-
// GET /rooms/:room/view members (names
|
|
14
|
+
// GET /rooms/:room/view members (names), conversations (permissions, participants, nextSequence)
|
|
15
15
|
// GET /rooms/:room/conversations/:c/messages?after=&limit=
|
|
16
16
|
// POST /rooms/:room/conversations/:c/messages {body, correlationId, kind: "agent", parentMessageId?}
|
|
17
17
|
// PATCH /rooms/:room/conversations/:c/messages/:id {body} — a streamed reply grows, a progress line updates, by edit
|
|
@@ -160,9 +160,10 @@ export class Rh2Adapter {
|
|
|
160
160
|
const room = view.room ?? {};
|
|
161
161
|
if (room.state && room.state !== 'active') continue;
|
|
162
162
|
const names = new Map((view.members ?? []).filter((m) => m.principalId).map((m) => [String(m.principalId), String(m.displayName ?? m.principalId)]));
|
|
163
|
-
// Whether this principal
|
|
164
|
-
//
|
|
165
|
-
|
|
163
|
+
// Whether this principal was brought into the Room is RH2's own answer: the join command makes an app a participant
|
|
164
|
+
// of the Room's base channel and gives it Room access, never a seat (runhuman-2 ADR 0063 decision 1). Only then does
|
|
165
|
+
// it take the lines its channel participation asks for; elsewhere, only those naming it.
|
|
166
|
+
const broughtIn = (view.conversations ?? []).some((c) => c.conversationId === room.defaultConversationId && (c.agentParticipants ?? []).some((p) => String(p.principalId) === this.principalId()));
|
|
166
167
|
// Where this principal's own seat carries a session, the Workplace delivers the Room's lines to that session itself
|
|
167
168
|
// (runhuman-2 ADR 0046 decision 4); this adapter then hands none of them on, so no line reaches it twice.
|
|
168
169
|
const seated = (view.members ?? []).some((m) => String(m.principalId) === this.principalId() && m.sessionBinding);
|
|
@@ -1,25 +1,49 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// The home's installed-app runner owns only children it starts, and every connection keeps separate custody.
|
|
3
|
-
import { existsSync, openSync, closeSync, readFileSync, unlinkSync, renameSync } from 'node:fs';
|
|
3
|
+
import { existsSync, openSync, closeSync, readFileSync, realpathSync, rmSync, unlinkSync, renameSync } from 'node:fs';
|
|
4
4
|
import { dirname, join } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
5
6
|
import { spawn } from 'node:child_process';
|
|
6
7
|
import { parseArgs } from 'node:util';
|
|
7
|
-
import {
|
|
8
|
+
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';
|
|
8
10
|
|
|
9
11
|
const {values}=parseArgs({options:{root:{type:'string'},connection:{type:'string'}}});
|
|
10
|
-
const path=connectionPath(values.root,values.connection),lock=join(dirname(path),'runner.lock');
|
|
12
|
+
const path=connectionPath(values.root,values.connection),lock=join(dirname(path),'runner.lock'),stopFile=runnerStopPath(lock);
|
|
13
|
+
const log=(event,data={})=>process.stdout.write(JSON.stringify({t:new Date().toISOString(),event,...data})+'\n');
|
|
14
|
+
const alive=pid=>{try{process.kill(pid,0);return true;}catch(error){if(error.code!=='ESRCH')throw error;return false;}};
|
|
15
|
+
// The tree this runner is loaded from, recorded in its lock: a source checkout's root, else its installed package.
|
|
16
|
+
const tree=sourceRoot(import.meta.url)??realpathSync(fileURLToPath(new URL('..',import.meta.url)));
|
|
17
|
+
// Serving is what the machine's source declaration names, as this runner's starter selected it (an activating kit's
|
|
18
|
+
// gateway carries its candidate); with none declared, the runner a gateway has just started is the selection.
|
|
19
|
+
function serving(){
|
|
20
|
+
let declared;try{declared=readSourceLock(process.env);}catch(error){log('runner-handover-unknown',{reason:String(error.message).slice(0,300)});return false;}
|
|
21
|
+
if(!declared)return true;
|
|
22
|
+
try{return realpathSync(declared.root)===tree;}catch{return false;}
|
|
23
|
+
}
|
|
11
24
|
let record=JSON.parse(readFileSync(path,'utf8'));
|
|
12
25
|
record.root=values.root;
|
|
13
26
|
if(!connectionCanRun(record))process.exit(0);
|
|
14
27
|
if(existsSync(lock)){
|
|
15
28
|
const previous=JSON.parse(readFileSync(lock,'utf8'));
|
|
16
|
-
|
|
17
|
-
|
|
29
|
+
// A source activation restarts the gateway on the new tree, which starts this home's runners again: a live runner of
|
|
30
|
+
// the same tree keeps the connection, and one of another tree (a lock without a tree predates this record) yields to a
|
|
31
|
+
// successor of the serving tree. It stops as `disconnect` stops it, its own children first, and the successor reads
|
|
32
|
+
// the record it last saved (renewed credentials, health) before starting any child, so no two runners hold custody.
|
|
33
|
+
if(alive(previous.pid)){
|
|
34
|
+
if(previous.tree===tree||!serving())process.exit(0);
|
|
35
|
+
log('runner-handover',{predecessor:previous.pid,from:previous.tree??null,to:tree});
|
|
36
|
+
// Never ended alone: its modules would outlive it with nobody stopping them. The next gateway start asks again.
|
|
37
|
+
if(!await stopConnectionRunner(lock,previous)){log('runner-handover-refused',{predecessor:previous.pid,reason:'predecessor did not stop within 20s'});process.exit(1);}
|
|
38
|
+
record=JSON.parse(readFileSync(path,'utf8'));record.root=values.root;
|
|
39
|
+
if(!connectionCanRun(record))process.exit(0);
|
|
40
|
+
}
|
|
41
|
+
if(existsSync(lock))renameSync(lock,`${lock}.stopped-${Date.now()}`);
|
|
18
42
|
}
|
|
19
|
-
const fd=openSync(lock,'wx',0o600);closeSync(fd);privateJson(lock,{pid:process.pid,connection:record.id});
|
|
43
|
+
const fd=openSync(lock,'wx',0o600);closeSync(fd);privateJson(lock,{pid:process.pid,connection:record.id,tree});
|
|
44
|
+
rmSync(stopFile,{force:true});
|
|
20
45
|
const children=new Map(),health=record.health??={},raised=record.raised??{};
|
|
21
46
|
let stopped=false;
|
|
22
|
-
const log=(event,data={})=>process.stdout.write(JSON.stringify({t:new Date().toISOString(),event,...data})+'\n');
|
|
23
47
|
const save=()=>{record.health=health;record.raised=raised;privateJson(path,record);};
|
|
24
48
|
const teamPath=`/api/v1/teams/${encodeURIComponent(record.teams.team)}`;
|
|
25
49
|
const issue=(name,reason)=>{health[name]={state:'unavailable',reason,at:new Date().toISOString()};save();};
|
|
@@ -31,7 +55,7 @@ function start(grant){
|
|
|
31
55
|
if(SERVICE_APPS.includes(grant.name)&&grant.name!=='agents')return;
|
|
32
56
|
if(stopped||children.has(grant.name)||!existsSync(join(values.root,'apps',`${grant.name}.${record.id}.json`)))return;
|
|
33
57
|
const config=generateModuleConfig(values.root,record,grant);
|
|
34
|
-
const program=SERVICE_APPS.includes(grant.name)?SERVICE_PROGRAM:join(record.moduleRoot,grant.name,'main.mjs');
|
|
58
|
+
const program=SERVICE_APPS.includes(grant.name)?SERVICE_PROGRAM:grant.program??join(record.moduleRoot,grant.name,'main.mjs');
|
|
35
59
|
const child=spawn(process.execPath,[program,config],{stdio:['ignore','pipe','pipe'],env:{...process.env,SUPERCODE_ORCHESTRATOR_HOME:values.root,HERMES_HOME:values.root}});
|
|
36
60
|
children.set(grant.name,child);let buffer='';
|
|
37
61
|
child.stdout.on('data',chunk=>{
|
|
@@ -44,7 +68,9 @@ function start(grant){
|
|
|
44
68
|
}
|
|
45
69
|
if(buffer.length>65536)buffer='';
|
|
46
70
|
});
|
|
47
|
-
|
|
71
|
+
// An exit names a module's trouble more exactly than its stderr; while it is unavailable a restart's stderr keeps the
|
|
72
|
+
// reason it has, so a crash loop is one raised alert, not one per restart.
|
|
73
|
+
child.stderr.on('data',chunk=>{process.stderr.write(chunk);if(!stopped&&health[grant.name]?.state!=='unavailable')issue(grant.name,'module wrote a startup/runtime error; inspect its runner log');});
|
|
48
74
|
child.on('error',error=>issue(grant.name,error.code??'start-failed'));
|
|
49
75
|
child.on('exit',(code,signal)=>{
|
|
50
76
|
children.delete(grant.name);if(!stopped){issue(grant.name,`module exited (${code??signal})`);setTimeout(()=>start(grant),5000);}
|
|
@@ -87,20 +113,28 @@ async function renewTeams(value){
|
|
|
87
113
|
function randomHex(size){return crypto.getRandomValues(new Uint8Array(size)).reduce((out,n)=>out+n.toString(16).padStart(2,'0'),'');}
|
|
88
114
|
function randomBase64(){return Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');}
|
|
89
115
|
async function renew(){
|
|
116
|
+
const restart=new Set();
|
|
90
117
|
for(const grant of record.grants){
|
|
91
118
|
if(!existsSync(join(values.root,'apps',`${grant.name}.${record.id}.json`)))continue;
|
|
92
119
|
try{
|
|
93
120
|
let changed=false;
|
|
94
121
|
if(Date.parse(grant.rh2.expiresAt)-Date.now()<=7*86400000){grant.rh2={...grant.rh2,...(await connectionCall(record,'rh2','POST',`/api/v3/automation/installations/${grant.rh2.installationId}/renew`,{},grant.rh2.accessToken)).data};changed=true;save();}
|
|
95
122
|
if(grant.teams)changed=await renewTeams(grant.teams)||changed;
|
|
96
|
-
|
|
97
|
-
if(changed){const host=record.grants.find(g=>g.name===childOf(grant));if(host&&existsSync(join(values.root,'apps',`${host.name}.${record.id}.json`))){await stopChild(host.name);start(host);}}
|
|
123
|
+
if(changed)restart.add(childOf(grant));
|
|
98
124
|
if(!grant.teams?.pendingRenewal)recover(`expiry.${grant.name}`);
|
|
99
125
|
}catch(error){issue(`expiry.${grant.name}`,error.code??'renewal-unavailable');}
|
|
100
126
|
}
|
|
127
|
+
// A renewed credential reaches its program by a restart with a regenerated config (the service's, for its registrations):
|
|
128
|
+
// once per pass, however many of the program's registrations renewed in it.
|
|
129
|
+
for(const name of restart){const host=record.grants.find(g=>g.name===name);if(host&&existsSync(join(values.root,'apps',`${host.name}.${record.id}.json`))){await stopChild(host.name);start(host);}}
|
|
130
|
+
// An adopted service's connection holds no issuer installation of its own: the Workplace's issuer registration is its
|
|
131
|
+
// deployment's (connection.mjs adoptWorkplaceService).
|
|
132
|
+
if(!record.issuerCredential)return;
|
|
101
133
|
try{await renewTeams(record.issuerCredential);if(!record.issuerCredential.pendingRenewal)recover('issuer-renewal');}catch(error){issue('issuer-renewal',error.code??'renewal-unavailable');}
|
|
102
134
|
}
|
|
103
135
|
for(const signal of ['SIGTERM','SIGINT'])process.on(signal,()=>void stop());
|
|
136
|
+
// Windows delivers no SIGTERM to a handler: a successor or a disconnect asks this runner to stop through its stop file.
|
|
137
|
+
setInterval(()=>{try{if(JSON.parse(readFileSync(stopFile,'utf8')).pid===process.pid)void stop();}catch{}},500).unref();
|
|
104
138
|
for(const grant of record.grants)start(grant);
|
|
105
139
|
let lastRenew=0;
|
|
106
140
|
// Teams verifies the Workplace's tokens against its own JWKS (ADR 0070): nothing is relayed for them here.
|
|
@@ -115,4 +149,4 @@ while(!stopped){
|
|
|
115
149
|
}catch(error){issue('issuer',error.code??error.status??'connection-unavailable');await syncAlerts();}
|
|
116
150
|
await new Promise(done=>setTimeout(done,2000));
|
|
117
151
|
}
|
|
118
|
-
async function stop(){if(stopped)return;stopped=true;await Promise.all([...children.keys()].map(stopChild));record.stoppedAt=new Date().toISOString();save();unlinkSync(lock);process.exit(0);}
|
|
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);}
|
package/bin/orchestrator.mjs
CHANGED
|
@@ -45,8 +45,8 @@ if (process.argv[2] === 'workflow') {
|
|
|
45
45
|
const {parseArgs}=await import('node:util');
|
|
46
46
|
const {connectWorkplace}=await import('../connection.mjs');
|
|
47
47
|
const {SERVICE_REGISTRATIONS}=await import('../connection.mjs');
|
|
48
|
-
const options=Object.fromEntries(['root','ticket-file','modules-root','source','context','teams-token-file','teams-url','team','alert-role','webhook-url','webhook-listen'].map(key=>[key,{type:'string'}]));
|
|
49
|
-
options['no-start']={type:'boolean'};options.registrations={type:'boolean'};
|
|
48
|
+
const options=Object.fromEntries(['root','ticket-file','modules-root','source','context','teams-token-file','teams-url','team','alert-role','webhook-url','webhook-listen','adopt'].map(key=>[key,{type:'string'}]));
|
|
49
|
+
options['no-start']={type:'boolean'};options.registrations={type:'boolean'};options['adopt-module']={type:'string',multiple:true};options['adopt-program']={type:'string',multiple:true};
|
|
50
50
|
try{
|
|
51
51
|
const {values}=parseArgs({args:process.argv.slice(3),options});
|
|
52
52
|
if(values.registrations)process.stdout.write(`${SERVICE_REGISTRATIONS}\n`);
|
|
@@ -56,7 +56,7 @@ if (process.argv[2] === 'workflow') {
|
|
|
56
56
|
} else if (process.argv[2] === 'disconnect') {
|
|
57
57
|
const {parseArgs}=await import('node:util');
|
|
58
58
|
const {disconnectWorkplace}=await import('../connection.mjs');
|
|
59
|
-
try{const {values}=parseArgs({args:process.argv.slice(3),options:{root:{type:'string'},connection:{type:'string'}}});process.stdout.write(JSON.stringify(await disconnectWorkplace(values),null,2)+'\n');}
|
|
59
|
+
try{const {values}=parseArgs({args:process.argv.slice(3),options:{root:{type:'string'},connection:{type:'string'},'uninstall-adopted':{type:'boolean'}}});process.stdout.write(JSON.stringify(await disconnectWorkplace(values),null,2)+'\n');}
|
|
60
60
|
catch(error){process.stderr.write(`disconnect: ${error.message}\n`);process.exitCode=1;}
|
|
61
61
|
} else if (process.argv[2] === 'mcp-server') {
|
|
62
62
|
// a session's orchestrator tools on stdio, bound by its environment (mcp/mount.mjs mounts this through the stable
|
package/bin/runtime.mjs
CHANGED
|
@@ -837,13 +837,18 @@ async function main() {
|
|
|
837
837
|
process.on('SIGINT', () => { void stop('SIGINT'); void finish(); });
|
|
838
838
|
try {
|
|
839
839
|
starting = (async () => {
|
|
840
|
+
// The control door answers for this incarnation's lease, never for its
|
|
841
|
+
// platforms: it binds before any adapter connects, so a platform's slow
|
|
842
|
+
// reads (rh2's sequential Room views take seconds) cannot outlast the
|
|
843
|
+
// caller's startup confirmation and get a live daemon killed. A stop it
|
|
844
|
+
// admits still joins `starting` before quiescing anything.
|
|
845
|
+
control = await startDaemonControl({ root, lease: daemonLease, stop, complete: () => { void finish(); } });
|
|
840
846
|
await orchestrator.start();
|
|
841
847
|
// A daemon that came BACK replays pending obligations, re-attaches or
|
|
842
848
|
// interrupts the bindings that held a lease, and clears the stale ones
|
|
843
849
|
// before it accepts anything new (§4.7). A clean stop removes its own lock,
|
|
844
850
|
// so an ordinary start finds none and replays nothing.
|
|
845
851
|
if (!stopped && crashed) await orchestrator.dispatch(events.restart(new Date().toISOString()));
|
|
846
|
-
if (!stopped) control = await startDaemonControl({ root, lease: daemonLease, stop, complete: () => { void finish(); } });
|
|
847
852
|
})();
|
|
848
853
|
await starting;
|
|
849
854
|
if (stopped) { await finish(); return; }
|
|
@@ -78,7 +78,7 @@ export function recordCardAgentChanges(board,target){
|
|
|
78
78
|
acknowledgeCardAgentChanges(board.root,changes.map(change=>change.receipt));
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
-
/** Mirror a retained assignment (including reviewer roles created on the launch
|
|
81
|
+
/** Mirror a retained assignment (including reviewer roles created on the launch path) once per state. */
|
|
82
82
|
export function recordCardAgentAssignment(board,assignment){
|
|
83
83
|
if(!assignment)return;
|
|
84
84
|
recordCardAgentChanges(board,assignment.target);
|
package/board/args.mjs
CHANGED
|
@@ -3,7 +3,7 @@ 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']);
|
|
6
|
+
'once', 'goal', 'no-goal', 'off', 'switch', 'delete', 'no-attachments', 'include-logs', 'verbose', 'side', 'plan']);
|
|
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
|
|
|
@@ -11,6 +11,13 @@ export function parse(argv) {
|
|
|
11
11
|
const out = { _: [] };
|
|
12
12
|
for (let i = 0; i < argv.length; i += 1) {
|
|
13
13
|
const a = argv[i];
|
|
14
|
+
// One word holding flags (a shell variable the shell did not split: zsh's `$args`, or a quoted "--pr URL --pr-head
|
|
15
|
+
// SHA") is refused, never read as text or as a flag named by the whole word: VO-3760's five `--pr` declarations
|
|
16
|
+
// reached the board as one word " --pr … --pr-head …" that a comment stored as its text (and `--file` dropped),
|
|
17
|
+
// so its review was requested without two of its PRs.
|
|
18
|
+
if (/^\s+--[A-Za-z]/.test(a) || (a.startsWith('--') && /\s/.test(a.slice(2).split('=')[0]))) {
|
|
19
|
+
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
|
+
}
|
|
14
21
|
if (!a.startsWith('--') || a === '--') { out._.push(a); continue; }
|
|
15
22
|
const [key, inline] = a.slice(2).split(/=(.*)/s);
|
|
16
23
|
// init toggles document commits; completion/review names a git revision.
|
package/board/call.mjs
CHANGED
|
@@ -35,8 +35,8 @@ export function within({ caller = null, facts = {}, cwd, stdin = null, out, err,
|
|
|
35
35
|
|
|
36
36
|
/** Run `fn` as a unit of the process's own work (a round, a delivery pass): the process's identity and output, with
|
|
37
37
|
* what it learns and the workflow it read kept to itself. */
|
|
38
|
-
export function ownCall(fn) {
|
|
39
|
-
return generationWork(() => withCredentialGuard(() => calls.run({ kind: 'own', caller: null, facts: process.env, ...fresh(), get cwd() { return process.cwd(); }, stdin: null, out: own.out, err: own.err }, fn)));
|
|
38
|
+
export function ownCall(fn, name = fn.name || 'own work') {
|
|
39
|
+
return generationWork(() => withCredentialGuard(() => calls.run({ kind: 'own', caller: null, facts: process.env, ...fresh(), get cwd() { return process.cwd(); }, stdin: null, out: own.out, err: own.err }, fn)), name);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
/** The caller as its address names it, or null. */
|