@assuredloop/orca-bot-kit 0.2.0 → 0.4.0
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 +31 -15
- package/SETUP.md +10 -4
- package/package.json +1 -1
- package/rules/finishing.md +3 -0
- package/rules/mail.md +3 -3
- package/rules/scope.md +4 -2
- package/rules/tests-first.md +6 -4
- package/skills/obk-arch/LICENSES.md +7 -0
- package/skills/obk-arch/NOTICE.md +113 -5
- package/skills/obk-arch/SKILL.md +221 -10
- package/skills/obk-bot-building/NOTICE.md +2 -2
- package/skills/obk-bot-building/SKILL.md +108 -19
- package/skills/obk-debugging/SKILL.md +30 -15
- package/skills/obk-decision-memo/NOTICE.md +1 -1
- package/skills/obk-finops/NOTICE.md +1 -1
- package/skills/obk-finops/SKILL.md +6 -4
- package/skills/obk-fleet-review/SKILL.md +42 -5
- package/skills/obk-grilling/NOTICE.md +11 -3
- package/skills/obk-grilling/SKILL.md +11 -7
- package/skills/obk-grooming/SKILL.md +5 -2
- package/skills/obk-handoff/NOTICE.md +1 -1
- package/skills/obk-recall/NOTICE.md +1 -1
- package/skills/obk-researching/NOTICE.md +1 -1
- package/skills/obk-researching/SKILL.md +19 -9
- package/skills/obk-reviewing/SKILL.md +4 -1
- package/skills/obk-tdd/SKILL.md +54 -23
- package/skills/obk-teach/NOTICE.md +2 -2
- package/skills/obk-why/NOTICE.md +1 -1
- package/skills/obk-why/SKILL.md +21 -10
- package/src/book.js +2 -2
- package/src/bot.js +1 -1
- package/src/cli.js +117 -33
- package/src/groom.js +5 -3
- package/src/health.js +2 -2
- package/src/hooks.js +73 -18
- package/src/init.js +3 -3
- package/src/launch.js +28 -5
- package/src/message.js +38 -19
- package/src/orca-window.cjs +21 -0
- package/src/orca.js +191 -28
- package/src/pause.js +11 -6
- package/src/record.js +1 -1
- package/src/restart.js +6 -5
- package/src/retire.js +41 -6
- package/src/roster.js +1 -1
- package/src/rules.js +2 -2
- package/src/skills.js +113 -21
- package/src/sources.js +4 -3
- package/src/up.js +57 -23
- package/src/usage.js +2 -2
package/src/orca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Every call the kit makes to Orca goes through here (ADR
|
|
1
|
+
// Every call the kit makes to Orca goes through here (ADR 0021). Orca's CLI
|
|
2
2
|
// changes often, so the kit reads `--json` and never the human text, and keeps
|
|
3
3
|
// the parsing in one place.
|
|
4
4
|
//
|
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
// handed to Orca on the command line.
|
|
9
9
|
|
|
10
10
|
import { spawnSync } from 'node:child_process';
|
|
11
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
11
|
+
import { existsSync, readdirSync, readFileSync, realpathSync } from 'node:fs';
|
|
12
12
|
import { homedir } from 'node:os';
|
|
13
13
|
import path from 'node:path';
|
|
14
|
+
import { fileURLToPath } from 'node:url';
|
|
14
15
|
|
|
15
16
|
// The Orca that works for a normal user: `/usr/local/bin/orca` is a root-only
|
|
16
17
|
// symlink on this machine (tech notes, section 1). OBK_ORCA overrides it, for a
|
|
@@ -162,6 +163,49 @@ export function asFolderProject(setupId, title) {
|
|
|
162
163
|
*/
|
|
163
164
|
export const deleteProject = (setupId) => orca(['project', 'setup-delete', '--setup', setupId]);
|
|
164
165
|
|
|
166
|
+
/** Said after a run that made, renamed or removed a project, whatever `tellWindow` answered. */
|
|
167
|
+
export const RELOAD_LINE = "If Orca's sidebar does not show it, reload the window with Cmd+Shift+R.";
|
|
168
|
+
|
|
169
|
+
/** How long Orca's client waits for its runtime, and how long the kit waits for the client. */
|
|
170
|
+
const CLIENT_WAIT_MS = 2000;
|
|
171
|
+
const CLIENT_KILL_MS = 3000;
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Tell Orca's window that the project `projectId` changed, so it reads its
|
|
175
|
+
* projects again: true when Orca's runtime took the call, false otherwise.
|
|
176
|
+
*
|
|
177
|
+
* Orca's window re-reads only when its runtime says the projects changed, and
|
|
178
|
+
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
179
|
+
* changes does, and Orca's CLI does not offer it, so this goes through Orca's
|
|
180
|
+
* own runtime client out of the installed app, run by Orca's binary the way
|
|
181
|
+
* its `bin/orca` runs its CLI (ADR 0021). None of that is Orca's published
|
|
182
|
+
* interface, so anything that goes wrong is a quiet false: it never throws and
|
|
183
|
+
* is never tried twice, and the caller prints `RELOAD_LINE` either way.
|
|
184
|
+
*/
|
|
185
|
+
export function tellWindow(projectId) {
|
|
186
|
+
try {
|
|
187
|
+
// The CLI sits at <Orca.app>/Contents/Resources/bin/orca, often reached
|
|
188
|
+
// through a link.
|
|
189
|
+
const contents = path.resolve(realpathSync(orcaCli()), '..', '..', '..');
|
|
190
|
+
const client = path.join(contents, 'Resources', 'app.asar.unpacked', 'out', 'cli', 'runtime-client.js');
|
|
191
|
+
const env = { ...process.env, ELECTRON_RUN_AS_NODE: '1' };
|
|
192
|
+
// As Orca's `bin/orca` does: the kit's own Node options are not Orca's.
|
|
193
|
+
delete env.NODE_OPTIONS;
|
|
194
|
+
delete env.NODE_REPL_EXTERNAL_MODULE;
|
|
195
|
+
|
|
196
|
+
const asked = spawnSync(
|
|
197
|
+
path.join(contents, 'MacOS', 'Orca'),
|
|
198
|
+
[WINDOW_SCRIPT, client, projectId, String(CLIENT_WAIT_MS)],
|
|
199
|
+
{ env, stdio: 'ignore', timeout: CLIENT_KILL_MS },
|
|
200
|
+
);
|
|
201
|
+
return asked.error === undefined && asked.status === 0;
|
|
202
|
+
} catch {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const WINDOW_SCRIPT = fileURLToPath(new URL('./orca-window.cjs', import.meta.url));
|
|
208
|
+
|
|
165
209
|
/**
|
|
166
210
|
* The live tabs of the Orca project at `home`, each under its own tab id.
|
|
167
211
|
*
|
|
@@ -192,25 +236,125 @@ export const retitleTab = (handle, title) =>
|
|
|
192
236
|
orca(['terminal', 'rename', '--terminal', handle, '--title', title]).rename;
|
|
193
237
|
|
|
194
238
|
/**
|
|
195
|
-
*
|
|
239
|
+
* What can be learned about the harness in a tab: what Orca says, and who is
|
|
240
|
+
* in front of the tab's terminal.
|
|
196
241
|
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
-
*
|
|
200
|
-
*
|
|
201
|
-
*
|
|
202
|
-
*
|
|
242
|
+
* Orca's answers alone do not tell a harness from a shell (tech notes, section
|
|
243
|
+
* 1, #232). `tui-idle` refuses a busy harness with `timeout` just as it refuses
|
|
244
|
+
* a shell, and a shell Codex quit to answers `ok`. `agentIdentity` comes late
|
|
245
|
+
* and can outlast the harness by minutes. So the answer the caller acts on is
|
|
246
|
+
* `front`: `program` when something other than the tab's shell holds its
|
|
247
|
+
* terminal, with `command` its name, `shell` when the shell does, and
|
|
248
|
+
* undefined when that cannot be read, with `unreadable` saying why. `answered` (an `ok` from `tui-idle`) and
|
|
249
|
+
* `agent` are Orca's own hints, for a caller that has nothing better.
|
|
250
|
+
* `blockedReason` is Orca saying something on screen wants answering, which is
|
|
251
|
+
* for the caller to deal with, not the kit.
|
|
203
252
|
*/
|
|
204
|
-
export function
|
|
253
|
+
export function harnessInTab(handle, timeoutMs) {
|
|
205
254
|
const args = ['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)];
|
|
206
255
|
const answer = ask(args);
|
|
207
256
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
257
|
+
// Out of time means nothing went idle, busy or absent alike. That is an
|
|
258
|
+
// answer, not a breakdown.
|
|
259
|
+
if (answer.ok !== true && answer.error?.code !== 'timeout') {
|
|
211
260
|
throw new Error(`Orca refused ${args.join(' ')}: ${answer.error?.message ?? 'no reason given'}`);
|
|
212
261
|
}
|
|
213
|
-
|
|
262
|
+
|
|
263
|
+
const shown = orca(['terminal', 'show', '--terminal', handle]).terminal;
|
|
264
|
+
const agent = typeof shown?.agentIdentity === 'string' && shown.agentIdentity !== '' ? shown.agentIdentity : undefined;
|
|
265
|
+
return {
|
|
266
|
+
answered: answer.ok === true,
|
|
267
|
+
blockedReason: answer.result?.wait?.blockedReason,
|
|
268
|
+
agent,
|
|
269
|
+
...frontOf(shown?.ptyId),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Whether the kit may type a line into `tabId`, the tab the book holds for a
|
|
275
|
+
* session in the Orca project at `home`, and the handle to type into when it
|
|
276
|
+
* may: `{ handle, agent }`, with the agent Orca names there. Otherwise nothing
|
|
277
|
+
* is typed, and the answer says why:
|
|
278
|
+
* `{}` for a tab with no harness in it (none in the book, none Orca lists, or
|
|
279
|
+
* the shell in front), `{ blocked }` for one with something on screen waiting
|
|
280
|
+
* to be answered, and `{ unsure }`, a sentence, for one the kit cannot tell
|
|
281
|
+
* about. Orca refusing throws, for the caller to report.
|
|
282
|
+
*
|
|
283
|
+
* The one gate for every line the kit types into a running session: the mail
|
|
284
|
+
* nudge and the skills reload. A busy harness passes it, since both harnesses
|
|
285
|
+
* take a typed line as their next turn.
|
|
286
|
+
*/
|
|
287
|
+
export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
288
|
+
if (tabId === undefined) return {};
|
|
289
|
+
const live = tabs(home).find((tab) => tab.tabId === tabId);
|
|
290
|
+
if (live === undefined) return {};
|
|
291
|
+
|
|
292
|
+
const seen = harnessInTab(live.handle, timeoutMs);
|
|
293
|
+
// A line typed into a screen waiting for an answer is that answer: once, it
|
|
294
|
+
// confirmed Claude Code's folder-trust default, `No, exit` (tech notes,
|
|
295
|
+
// section 1).
|
|
296
|
+
if (seen.blockedReason !== undefined) return { blocked: seen.blockedReason };
|
|
297
|
+
// The shell in front is a tab with no harness, whatever a stale
|
|
298
|
+
// `agentIdentity` says.
|
|
299
|
+
if (seen.front === 'shell') return {};
|
|
300
|
+
// Not knowing is not a reason to type: a line that lands in a shell is run
|
|
301
|
+
// there. The program in front has to be the agent Orca names. With no name
|
|
302
|
+
// it may be a harness a few seconds into its launch; under another name it
|
|
303
|
+
// may be a pager started after the harness quit, under an identity Orca has
|
|
304
|
+
// not let go of (tech notes, section 1). A harness run through a wrapper such
|
|
305
|
+
// as `node` lands here too, until #261.
|
|
306
|
+
if (seen.front === undefined || seen.agent === undefined || seen.command !== seen.agent) {
|
|
307
|
+
const why = seen.front === undefined
|
|
308
|
+
? seen.unreadable
|
|
309
|
+
: `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
|
|
310
|
+
return { unsure: `the kit could not tell whether a harness is running in it (${why}), so nothing was typed` };
|
|
311
|
+
}
|
|
312
|
+
return { handle: live.handle, agent: seen.agent };
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** The `ps` this run reads. OBK_PS overrides it, as OBK_ORCA does Orca. */
|
|
316
|
+
const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Who holds the terminal of the pane `ptyId`: `{ front: 'shell' }`,
|
|
320
|
+
* `{ front: 'program', command }` with the name of the process leading the
|
|
321
|
+
* group in front, or `{ unreadable: <why> }`.
|
|
322
|
+
*
|
|
323
|
+
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
324
|
+
* gives that pid's terminal's foreground process group (ADR 0021).
|
|
325
|
+
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
326
|
+
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
327
|
+
* `diagnostics memory` is a diagnostics command and may change, so everything
|
|
328
|
+
* here ends in "cannot tell" rather than in an error or a guess.
|
|
329
|
+
*/
|
|
330
|
+
function frontOf(ptyId) {
|
|
331
|
+
let pane;
|
|
332
|
+
try {
|
|
333
|
+
pane = orca(['diagnostics', 'memory']).worktrees
|
|
334
|
+
.flatMap((worktree) => worktree.sessions ?? [])
|
|
335
|
+
.find((session) => session.sessionId === ptyId)?.pid;
|
|
336
|
+
} catch (error) {
|
|
337
|
+
return { unreadable: `Orca would not give the pane's pid: ${error.message}` };
|
|
338
|
+
}
|
|
339
|
+
if (!Number.isInteger(pane) || pane <= 0) return { unreadable: 'Orca gave no pid for its pane' };
|
|
340
|
+
|
|
341
|
+
const own = psLine(pane);
|
|
342
|
+
if (own === undefined) return { unreadable: `ps could not read its pane, pid ${pane}` };
|
|
343
|
+
if (own.tpgid === pane) return { front: 'shell' };
|
|
344
|
+
|
|
345
|
+
const front = psLine(own.tpgid);
|
|
346
|
+
if (front === undefined) return { unreadable: `ps could not read the process in front, pid ${own.tpgid}` };
|
|
347
|
+
const shell = front.ppid === pane && path.basename(own.comm) === 'login';
|
|
348
|
+
return shell ? { front: 'shell' } : { front: 'program', command: path.basename(front.comm) };
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/** One process as `ps` gives it, read only: `{ ppid, tpgid, comm }`, or undefined. */
|
|
352
|
+
function psLine(pid) {
|
|
353
|
+
const asked = spawnSync(psCli(), ['-o', 'pid=,ppid=,tpgid=,comm=', '-p', String(pid)], { encoding: 'utf8' });
|
|
354
|
+
if (asked.error || asked.status !== 0) return undefined;
|
|
355
|
+
const line = /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(.+?)\s*$/.exec(asked.stdout);
|
|
356
|
+
if (line === null) return undefined;
|
|
357
|
+
return { ppid: Number(line[2]), tpgid: Number(line[3]), comm: line[4] };
|
|
214
358
|
}
|
|
215
359
|
|
|
216
360
|
/**
|
|
@@ -257,19 +401,33 @@ const SUBMIT_WAIT_S = 5;
|
|
|
257
401
|
* A mailbox of one session's own: an Orca Run, which is a name and an inbox and
|
|
258
402
|
* nothing else — it schedules nothing and runs nobody. The objective is what a
|
|
259
403
|
* person sees in `orca orchestration run-list`, so it says whose it is.
|
|
404
|
+
*
|
|
405
|
+
* Bound to `handle`, the session's own tab, from the moment it is made. Orca
|
|
406
|
+
* tells a Run's coordinator terminal about its mail and nobody else, and binds
|
|
407
|
+
* the caller when no `--from` is given: the tab that ran `obk up` would be told
|
|
408
|
+
* about every session it brought up, and would lose its own Run's binding, since
|
|
409
|
+
* one terminal holds one Run (tech notes, section 1).
|
|
260
410
|
*/
|
|
261
|
-
export const makeMailbox = (objective) =>
|
|
262
|
-
orca(['orchestration', 'run-create', '--objective', `obk ${objective}
|
|
411
|
+
export const makeMailbox = (objective, handle) =>
|
|
412
|
+
orca(['orchestration', 'run-create', '--objective', `obk ${objective}`, '--from', handle]).run.id;
|
|
263
413
|
|
|
264
414
|
/**
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
268
|
-
*
|
|
269
|
-
*
|
|
270
|
-
* asked, not a lease anybody has to give back.
|
|
415
|
+
* Bind a mailbox to the terminal `handle`, which makes it the Run's coordinator
|
|
416
|
+
* and its one reader: Orca refuses a `check` from any other terminal with
|
|
417
|
+
* `consumer_fenced`. A read itself binds nothing, so this is for a session's own
|
|
418
|
+
* live tab, never for whoever is reading (tech notes, section 1). Without a
|
|
419
|
+
* handle it binds this process's own terminal.
|
|
271
420
|
*/
|
|
272
|
-
export const useMailbox = (id) =>
|
|
421
|
+
export const useMailbox = (id, handle) =>
|
|
422
|
+
orca(['orchestration', 'run-use', '--id', id, ...(handle === undefined ? [] : ['--from', handle])]).run;
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* The terminal a mailbox is bound to, or undefined when it has none. A closed
|
|
426
|
+
* tab keeps its binding, and a read as its handle still works: a read binds
|
|
427
|
+
* nothing, only `run-use` does (tech notes, section 1).
|
|
428
|
+
*/
|
|
429
|
+
export const coordinatorOf = (id) =>
|
|
430
|
+
orca(['orchestration', 'run-show', '--id', id]).run.coordinator_handle ?? undefined;
|
|
273
431
|
|
|
274
432
|
/** Queue one message. `to` and `from` are mailboxes, written `run:<id>`. */
|
|
275
433
|
export function postMessage({ to, from, subject, body, type = 'status', thread }) {
|
|
@@ -281,11 +439,16 @@ export function postMessage({ to, from, subject, body, type = 'status', thread }
|
|
|
281
439
|
|
|
282
440
|
/**
|
|
283
441
|
* What is waiting in a mailbox: the oldest batch that has not been
|
|
284
|
-
* acknowledged, or, peeking, whatever is unread without touching it.
|
|
442
|
+
* acknowledged, or, peeking, whatever is unread without touching it. Read as
|
|
443
|
+
* the terminal `handle` when one is given, which is how a read works from a tab
|
|
444
|
+
* bound to another Run; otherwise as this process's own terminal.
|
|
285
445
|
*/
|
|
286
|
-
export const readMailbox = (id, { peek = false } = {}) =>
|
|
287
|
-
orca(['orchestration', 'check', '--run', id, ...(peek ? ['--peek'] : [])]);
|
|
446
|
+
export const readMailbox = (id, { peek = false, handle } = {}) =>
|
|
447
|
+
orca(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])]);
|
|
288
448
|
|
|
289
449
|
/** Say a batch has been read, so the next check brings the one after it. */
|
|
290
|
-
export const ackMailbox = (id, delivery) =>
|
|
291
|
-
orca(['orchestration', 'check', '--run', id, '--ack', delivery]);
|
|
450
|
+
export const ackMailbox = (id, delivery, handle) =>
|
|
451
|
+
orca(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery]);
|
|
452
|
+
|
|
453
|
+
/** The reader of a `check`: the terminal named, or this process's own. */
|
|
454
|
+
const as = (handle) => (handle === undefined ? [] : ['--terminal', handle]);
|
package/src/pause.js
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
// A pause is a mark in the bot's `bot.yaml` and the bot's tabs closed. The
|
|
5
5
|
// mark is what `up` reads to leave it closed, and the book keeps the
|
|
6
6
|
// conversation each session was in, so an unpause is the mark taken off and
|
|
7
|
-
// the ordinary `up`, which resumes it (ADR
|
|
7
|
+
// the ordinary `up`, which resumes it (ADR 0012).
|
|
8
8
|
//
|
|
9
9
|
// The closing is restart's, with restart's rules: only the tabs the book names,
|
|
10
10
|
// one at a time by their own handles, and none at all when one of them holds a
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { realpathSync } from 'node:fs';
|
|
14
14
|
|
|
15
15
|
import { botDir, markPaused, readBot } from './bot.js';
|
|
16
|
+
import { DAILY_SESSION } from './init.js';
|
|
16
17
|
import { closeTabs, tabsToClose } from './restart.js';
|
|
17
18
|
import { BOT_FATHER, botsNamed, bringUp, sessionsOf } from './up.js';
|
|
18
19
|
|
|
@@ -22,7 +23,7 @@ import { BOT_FATHER, botsNamed, bringUp, sessionsOf } from './up.js';
|
|
|
22
23
|
* per tab it closed.
|
|
23
24
|
*/
|
|
24
25
|
export async function pauseSessions(bots, { bot, session }) {
|
|
25
|
-
const home = fleetMember(bots, bot, 'pause');
|
|
26
|
+
const home = fleetMember(bots, bot, 'pause', session);
|
|
26
27
|
const sessions = sessionsOf(readBot(home, bot), session);
|
|
27
28
|
|
|
28
29
|
// Judged before anything is written or closed, so a refusal leaves both as
|
|
@@ -38,7 +39,7 @@ export async function pauseSessions(bots, { bot, session }) {
|
|
|
38
39
|
* Returns `{ bot, session, changed, ...what up answered }`.
|
|
39
40
|
*/
|
|
40
41
|
export async function unpauseSessions(bots, { bot, session }) {
|
|
41
|
-
const home = fleetMember(bots, bot, 'unpause');
|
|
42
|
+
const home = fleetMember(bots, bot, 'unpause', session);
|
|
42
43
|
sessionsOf(readBot(home, bot), session);
|
|
43
44
|
|
|
44
45
|
const changed = markPaused(bots, bot, session, false);
|
|
@@ -47,12 +48,16 @@ export async function unpauseSessions(bots, { bot, session }) {
|
|
|
47
48
|
|
|
48
49
|
/**
|
|
49
50
|
* The bot's home, once it is certain the bot is there and is not Bot Father,
|
|
50
|
-
*
|
|
51
|
+
* or its management session: that is where the user asks to bring anything
|
|
52
|
+
* back. Bot Father's other sessions are like any bot's (#230).
|
|
51
53
|
*/
|
|
52
|
-
export function fleetMember(bots, bot, verb) {
|
|
53
|
-
if (bot === BOT_FATHER) {
|
|
54
|
+
export function fleetMember(bots, bot, verb, session) {
|
|
55
|
+
if (bot === BOT_FATHER && session === undefined) {
|
|
54
56
|
throw new Error(`${BOT_FATHER} runs the fleet, and the kit does not ${verb} it: it is where you ask for everything else.`);
|
|
55
57
|
}
|
|
58
|
+
if (bot === BOT_FATHER && session === DAILY_SESSION) {
|
|
59
|
+
throw new Error(`${DAILY_SESSION} is ${BOT_FATHER}'s management session, and the kit does not ${verb} it: it is where you ask for everything else.`);
|
|
60
|
+
}
|
|
56
61
|
botsNamed(bots, bot);
|
|
57
62
|
return realpathSync(botDir(bots, bot));
|
|
58
63
|
}
|
package/src/record.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// Both harnesses run the kit's hook (src/hooks.js) and hand it the session id
|
|
4
4
|
// they are running under. That is how the book stays the authority for session
|
|
5
|
-
// ids (ADR
|
|
5
|
+
// ids (ADR 0012) without the kit guessing from transcripts.
|
|
6
6
|
//
|
|
7
7
|
// Which session it is comes from the tab: Orca puts the tab id in the
|
|
8
8
|
// environment of everything running in it, and the book says which session owns
|
package/src/restart.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// avoided, not banned, and one happens when the user asks for it).
|
|
6
6
|
//
|
|
7
7
|
// When they do ask, the session has to come back as itself. The book holds the
|
|
8
|
-
// conversation each session is running (ADR
|
|
8
|
+
// conversation each session is running (ADR 0012), so a restart is: close the
|
|
9
9
|
// tab the book names, then let `up` open a new one and resume that
|
|
10
10
|
// conversation. Nothing else about it is new.
|
|
11
11
|
//
|
|
@@ -31,6 +31,7 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
31
31
|
|
|
32
32
|
import { bookFile, readBook } from './book.js';
|
|
33
33
|
import { botDir, readBot } from './bot.js';
|
|
34
|
+
import { ownCli, shellWord } from './launch.js';
|
|
34
35
|
import { closeTab, findProject, tabs } from './orca.js';
|
|
35
36
|
import { botsNamed, bringUp, prepareBots, sessionsOf } from './up.js';
|
|
36
37
|
|
|
@@ -49,10 +50,10 @@ export async function restartSessions(bots, { bot: name, session: onlySession }
|
|
|
49
50
|
// A paused bot or session has no tab to close and is not to be opened, so
|
|
50
51
|
// there is nothing here to restart.
|
|
51
52
|
if (known.paused === true) {
|
|
52
|
-
throw new Error(`${name} is paused, so there is nothing of it to restart and nothing was closed. Bring it back with
|
|
53
|
+
throw new Error(`${name} is paused, so there is nothing of it to restart and nothing was closed. Bring it back with ${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${name}.`);
|
|
53
54
|
}
|
|
54
55
|
if (onlySession !== undefined && sessions[0].paused === true) {
|
|
55
|
-
throw new Error(`${name} ${onlySession} is paused, so there is nothing of it to restart and nothing was closed. Bring it back with
|
|
56
|
+
throw new Error(`${name} ${onlySession} is paused, so there is nothing of it to restart and nothing was closed. Bring it back with ${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${name} --session ${onlySession}.`);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
// And the same preparations, made now rather than left to `up`, which makes
|
|
@@ -158,7 +159,7 @@ async function gone(home, closed, bots, bot) {
|
|
|
158
159
|
|
|
159
160
|
if (Date.now() >= until) {
|
|
160
161
|
const one = left.length === 1;
|
|
161
|
-
throw new Error(`Orca answered the close for ${one ? 'this tab' : 'these tabs'} and is still listing ${one ? 'it' : 'them'} ${SETTLED_MS / 1000} seconds later: ${left.join(', ')}. Nothing was opened in ${one ? 'its' : 'their'} place, because a tab that is on the way out is not a tab to start a harness in. The conversations are in the book:
|
|
162
|
+
throw new Error(`Orca answered the close for ${one ? 'this tab' : 'these tabs'} and is still listing ${one ? 'it' : 'them'} ${SETTLED_MS / 1000} seconds later: ${left.join(', ')}. Nothing was opened in ${one ? 'its' : 'their'} place, because a tab that is on the way out is not a tab to start a harness in. The conversations are in the book: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${bot} brings them back once Orca has caught up.`);
|
|
162
163
|
}
|
|
163
164
|
await pause(ASK_MS);
|
|
164
165
|
}
|
|
@@ -188,5 +189,5 @@ function alsoClosed(why, closed, bots, bot) {
|
|
|
188
189
|
if (closed.length === 0) return why;
|
|
189
190
|
|
|
190
191
|
const one = closed.length === 1;
|
|
191
|
-
return `${why} ${closed.map((tab) => tab.name).join(', ')} ${one ? 'was' : 'were'} closed before it and nothing was opened in ${one ? 'its' : 'their'} place:
|
|
192
|
+
return `${why} ${closed.map((tab) => tab.name).join(', ')} ${one ? 'was' : 'were'} closed before it and nothing was opened in ${one ? 'its' : 'their'} place: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${bot} brings ${one ? 'it' : 'them'} back with ${one ? 'its' : 'their'} conversation.`;
|
|
192
193
|
}
|
package/src/retire.js
CHANGED
|
@@ -14,18 +14,18 @@
|
|
|
14
14
|
//
|
|
15
15
|
// Closing a tab here ends the conversation in it on purpose, so unlike a
|
|
16
16
|
// restart or a pause it does not wait for the book to know which one it was.
|
|
17
|
-
// The mailbox Runs stay: Orca has no way to remove one (ADR
|
|
17
|
+
// The mailbox Runs stay: Orca has no way to remove one (ADR 0018).
|
|
18
18
|
|
|
19
|
-
import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
19
|
+
import { existsSync, mkdirSync, realpathSync, renameSync, rmSync } from 'node:fs';
|
|
20
20
|
import path from 'node:path';
|
|
21
21
|
|
|
22
22
|
import { readBook, tabIdsIn, updateBook } from './book.js';
|
|
23
23
|
import { botDir, dropSession, readBot } from './bot.js';
|
|
24
|
-
import { deleteProject, findProject, tabs } from './orca.js';
|
|
24
|
+
import { deleteProject, findProject, projects, tabs, tellWindow } from './orca.js';
|
|
25
25
|
import { fleetMember } from './pause.js';
|
|
26
26
|
import { closeTabs, tabsToClose } from './restart.js';
|
|
27
27
|
import { unlinkSkills } from './skills.js';
|
|
28
|
-
import { promptPath, sessionsOf } from './up.js';
|
|
28
|
+
import { BOT_FATHER, promptPath, sessionsOf } from './up.js';
|
|
29
29
|
|
|
30
30
|
/** Where retired bots go: beside `bots/`, where nothing the kit runs looks. */
|
|
31
31
|
export const retiredDir = (bots) => path.join(bots, 'retired');
|
|
@@ -34,7 +34,7 @@ export const retiredDir = (bots) => path.join(bots, 'retired');
|
|
|
34
34
|
* Retire the session `session` of the bot `bot`. Returns `{ bot, session, closed }`.
|
|
35
35
|
*/
|
|
36
36
|
export async function retireSession(bots, { bot, session }) {
|
|
37
|
-
const home = fleetMember(bots, bot, 'retire');
|
|
37
|
+
const home = fleetMember(bots, bot, 'retire', session);
|
|
38
38
|
const sessions = sessionsOf(readBot(home, bot), session);
|
|
39
39
|
|
|
40
40
|
const closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot);
|
|
@@ -55,6 +55,8 @@ export async function retireSession(bots, { bot, session }) {
|
|
|
55
55
|
/**
|
|
56
56
|
* Retire the bot `bot`. Returns `{ bot, closed, project, moved }`: the tabs it
|
|
57
57
|
* closed, the Orca project it took away (if it had one) and where the bot is now.
|
|
58
|
+
* When Orca does not confirm the project gone, it returns `{ bot, closed,
|
|
59
|
+
* project, trouble }` instead, and the bot is left where it was.
|
|
58
60
|
*/
|
|
59
61
|
export async function retireBot(bots, { bot }) {
|
|
60
62
|
const home = fleetMember(bots, bot, 'retire');
|
|
@@ -79,7 +81,24 @@ export async function retireBot(bots, { bot }) {
|
|
|
79
81
|
// they are gone.
|
|
80
82
|
const booked = Object.keys(readBook(home).sessions).map((name) => ({ name }));
|
|
81
83
|
const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot);
|
|
82
|
-
if (project !== undefined)
|
|
84
|
+
if (project !== undefined) {
|
|
85
|
+
deleteProject(project.id);
|
|
86
|
+
// Orca's answer to the delete is not the same as the project being gone
|
|
87
|
+
// (#282): its list afterwards is. Until that list is read and no longer
|
|
88
|
+
// has the project, the bot stays where it is, so that retiring it again
|
|
89
|
+
// takes the project away and then finishes. The list is read once, for
|
|
90
|
+
// this and for the window both.
|
|
91
|
+
let setups;
|
|
92
|
+
try {
|
|
93
|
+
setups = projects();
|
|
94
|
+
} catch (error) {
|
|
95
|
+
return { bot, closed, project: project.id, trouble: `Orca answered the delete of Orca project ${project.id}, and its project list could not be read afterwards, so the removal is not confirmed: ${error.message}. ${bot} was not moved. Once Orca is answering, retire ${bot} again with obk retire: it removes the project if it is still there, then finishes.` };
|
|
96
|
+
}
|
|
97
|
+
if (setups.some((setup) => setup.id === project.id || setup.path === home)) {
|
|
98
|
+
return { bot, closed, project: project.id, trouble: `Orca answered the delete of Orca project ${project.id}, and still lists it. ${bot} was not moved. Retire ${bot} again with obk retire; if Orca still lists the project after that, remove it in Orca yourself, then retire ${bot} again.` };
|
|
99
|
+
}
|
|
100
|
+
tellWindowOfRemoval(bots, setups);
|
|
101
|
+
}
|
|
83
102
|
|
|
84
103
|
for (const name of new Set([...known.sessions, ...booked].map((session) => session.name))) {
|
|
85
104
|
rmSync(promptPath(bots, bot, name), { force: true });
|
|
@@ -90,3 +109,19 @@ export async function retireBot(bots, { bot }) {
|
|
|
90
109
|
|
|
91
110
|
return { bot, closed, ...(project === undefined ? {} : { project: project.id }), moved };
|
|
92
111
|
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Tell Orca's window a project went (#224). The call names a project that is
|
|
115
|
+
* still there, and Bot Father's is the one that always is; when Orca has none
|
|
116
|
+
* for it in `setups`, Orca's list after the delete, there is nothing to call
|
|
117
|
+
* on. Finding it is part of the workaround, so it fails as quietly as the call.
|
|
118
|
+
*/
|
|
119
|
+
function tellWindowOfRemoval(bots, setups) {
|
|
120
|
+
try {
|
|
121
|
+
const home = botDir(bots, BOT_FATHER);
|
|
122
|
+
const father = existsSync(home) ? setups.find((setup) => setup.path === realpathSync(home)) : undefined;
|
|
123
|
+
if (father !== undefined) tellWindow(father.projectId);
|
|
124
|
+
} catch {
|
|
125
|
+
// Nothing: see above.
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/roster.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
// reads files and only files. It does not ask Orca, it changes nothing, and it
|
|
6
6
|
// judges nothing: whether a bot is the right bot, what its charter comes to in
|
|
7
7
|
// two lines, and which of its limits is the hard one are the reader's to say
|
|
8
|
-
// (ADR
|
|
8
|
+
// (ADR 0016). What is wrong with the setup is a different question, and
|
|
9
9
|
// `obk health` is the command that answers it.
|
|
10
10
|
//
|
|
11
11
|
// So a list here is the list as the user wrote it, unchecked. A skill named in
|
package/src/rules.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// A bot's `AGENTS.md`, built from its charter and the rule units it carries
|
|
2
|
-
// (PRD 6.6, ADR
|
|
2
|
+
// (PRD 6.6, ADR 0013).
|
|
3
3
|
//
|
|
4
4
|
// This is the one place the kit's own text lands inside the user's repo, so the
|
|
5
5
|
// promise around it is narrow: the build owns one marked block and nothing else
|
|
@@ -299,7 +299,7 @@ function writeBlock(file, body) {
|
|
|
299
299
|
* Whatever is already there is the user's and stays. What the build owes them
|
|
300
300
|
* is the fact they cannot see: Claude Code reads a `CLAUDE.md` *instead of*
|
|
301
301
|
* `AGENTS.md`, so one that is not this bot's `AGENTS.md` leaves the two
|
|
302
|
-
* harnesses starting from different rules, which is the one thing ADR
|
|
302
|
+
* harnesses starting from different rules, which is the one thing ADR 0013
|
|
303
303
|
* says must not happen.
|
|
304
304
|
*
|
|
305
305
|
* Returns `{ linked }` on the run that made the link, `{ trouble }` when what
|