@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 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. The parked predecessor remains available
84
- for rollback after candidate loss. A request already sent to a lost generation returns an unknown outcome and
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`), the agent's channel joins its Room's conversation on its record,
521
- and orchestrator-launched sessions mount them as the `rh2-agent` MCP server, never in another agent's that shares its
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. Create one with
830
- `supercode workflow create "Voice calls" --readable-id voice-calls --root HOME`,
831
- or let the board derive it from the title. Names use lowercase kebab-case, start
832
- with a letter, have at most 32 characters and are unique among open cards.
833
- Automatic collisions gain a numeric suffix; explicit duplicates are refused.
834
-
835
- `workflow list` and `show` display the name. `workflow specify CARD --readable-id
836
- voice-calls --root HOME` changes it; the same `readable_id` property is editable
837
- in a ztrack card's metadata in `arcs.md`. A title edit keeps its existing id.
838
- Older boards fill missing names from leading title slugs before other title words.
839
-
840
- Generated branches use `vo-42-voice-calls`. Card agents become
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
- Existing worktrees, explicit branches and personal naming overrides keep their
845
- normal protections. Merge and release remain separate from this source behavior.
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 (RH2's `broughtIn` on the Room's members),
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, broughtIn), conversations (permissions, nextSequence)
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 is in the Room is RH2's own answer: brought in by the join command (`broughtIn`, runhuman-2
164
- // ADR 0063). Only then does it take the lines its channel participation asks for; elsewhere, only those naming it.
165
- const broughtIn = (view.members ?? []).some((m) => String(m.principalId) === this.principalId() && m.broughtIn === true);
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 { SERVICE_APPS, SERVICE_PROGRAM, connectionCall, connectionCanRun, connectionPath, generateModuleConfig, privateJson } from '../connection.mjs';
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
- try{process.kill(previous.pid,0);process.exit(0);}catch(error){if(error.code!=='ESRCH')throw error;}
17
- renameSync(lock,`${lock}.stopped-${Date.now()}`);
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
- child.stderr.on('data',chunk=>{process.stderr.write(chunk);if(!stopped)issue(grant.name,'module wrote a startup/runtime error; inspect its runner log');});
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
- // A renewed credential reaches its program by a restart with a regenerated config (the service's, for its registrations).
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);}
@@ -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/adoption path) once per state. */
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. */