@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/skills.js
CHANGED
|
@@ -1,26 +1,32 @@
|
|
|
1
1
|
// The skills a bot has: links in the bot's own folder, into both harnesses
|
|
2
|
-
// (PRD 6.7, ADR
|
|
2
|
+
// (PRD 6.7, ADR 0014, ADR 0019).
|
|
3
3
|
//
|
|
4
4
|
// Linked, never copied, in both directions. Inwards: what lands in the bot home
|
|
5
5
|
// is a symlink to the skill's own directory, so a kit skill is read where the
|
|
6
|
-
// package installed it and
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
6
|
+
// package installed it, and both harnesses follow the link. A running session
|
|
7
|
+
// reads an edited skill the next time it loads it. A skill linked in or taken
|
|
8
|
+
// away reaches it without a restart. Claude Code takes it through its own
|
|
9
|
+
// `/reload-skills`, which `obk skills build` types into the session's tab.
|
|
10
|
+
// Codex has no such command and takes it at the start of its next turn (tech
|
|
11
|
+
// notes, sections 2 and 3). Outwards: the kit takes away only the links it put
|
|
12
|
+
// there, and never the skill one was pointing at.
|
|
10
13
|
//
|
|
11
14
|
// An entry names one of four shelves: the kit's own (`kit:<name>`), an online
|
|
12
15
|
// source `skills.yaml` lists (`<source>:<name>`, read from its clone beside the
|
|
13
16
|
// bots folder), the user's common folder (a bare name), or a path to anywhere
|
|
14
17
|
// on disk.
|
|
15
18
|
|
|
16
|
-
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, readlinkSync, realpathSync, rmSync, statSync, symlinkSync, writeFileSync } from 'node:fs';
|
|
17
20
|
import os from 'node:os';
|
|
18
21
|
import path from 'node:path';
|
|
19
22
|
import { fileURLToPath } from 'node:url';
|
|
20
23
|
|
|
21
24
|
import { parse, parseDocument, stringify } from 'yaml';
|
|
22
25
|
|
|
26
|
+
import { bookFile, readBook } from './book.js';
|
|
23
27
|
import { botDir, botNames, changesExactly, leadsOutside, readBot, YAML_OUT } from './bot.js';
|
|
28
|
+
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
29
|
+
import { orcaCli, orcaTrouble, tabToTypeInto, typeIntoTab } from './orca.js';
|
|
24
30
|
import { listIn } from './rules.js';
|
|
25
31
|
import { cloneDir, isCloned, readSources, skillsIn, wrongClone } from './sources.js';
|
|
26
32
|
|
|
@@ -39,7 +45,7 @@ export const SKILL_DIRS = {
|
|
|
39
45
|
/**
|
|
40
46
|
* What a skill may be called: the Agent Skills name, which is also the name of
|
|
41
47
|
* the directory it lives in and of the link the kit makes (tech notes, section
|
|
42
|
-
* 4, and ADR
|
|
48
|
+
* 4, and ADR 0019).
|
|
43
49
|
*/
|
|
44
50
|
const NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
45
51
|
|
|
@@ -129,7 +135,7 @@ export function removeSkill(bots, bot, ref) {
|
|
|
129
135
|
|
|
130
136
|
/**
|
|
131
137
|
* Give one bot the skills its lists name. Returns what there is to report:
|
|
132
|
-
* `{ bot, skills, removed }` for a bot whose links are in place, and
|
|
138
|
+
* `{ bot, skills, linked, removed }` for a bot whose links are in place, and
|
|
133
139
|
* `{ bot, skills, trouble }` for one whose list the kit could not follow.
|
|
134
140
|
*
|
|
135
141
|
* A list with one entry the kit cannot follow links nothing at all: half a
|
|
@@ -209,6 +215,7 @@ export function linkSkills(bots, home, bot) {
|
|
|
209
215
|
// something to throw through `prepareBots` and stop the fleet. What was
|
|
210
216
|
// linked before it went wrong is still written down, so the next run knows it
|
|
211
217
|
// for the kit's own.
|
|
218
|
+
const linked = [];
|
|
212
219
|
const removed = [];
|
|
213
220
|
try {
|
|
214
221
|
for (const harness of Object.keys(SKILL_DIRS)) {
|
|
@@ -217,8 +224,8 @@ export function linkSkills(bots, home, bot) {
|
|
|
217
224
|
held.set(harness, heldIn(path.join(home, SKILL_DIRS[harness]), record[harness]));
|
|
218
225
|
continue;
|
|
219
226
|
}
|
|
220
|
-
|
|
221
|
-
|
|
227
|
+
// Filled as it goes, so what was changed before a failure is still known.
|
|
228
|
+
const done = link(path.join(home, SKILL_DIRS[harness]), wanted, record[harness], linked, removed);
|
|
222
229
|
held.set(harness, done.held);
|
|
223
230
|
}
|
|
224
231
|
} catch (error) {
|
|
@@ -231,6 +238,9 @@ export function linkSkills(bots, home, bot) {
|
|
|
231
238
|
return {
|
|
232
239
|
bot: bot.name,
|
|
233
240
|
skills: heldBy(home, [], held),
|
|
241
|
+
// What did change before it went wrong, so the sessions it reaches are told.
|
|
242
|
+
linked: [...new Set(linked)].sort(),
|
|
243
|
+
removed: [...new Set(removed)].sort(),
|
|
234
244
|
trouble: `${bot.name}'s skills could not be linked: ${error.message}. Nothing of yours was changed; put right what is named there, then build again.`,
|
|
235
245
|
};
|
|
236
246
|
}
|
|
@@ -242,6 +252,7 @@ export function linkSkills(bots, home, bot) {
|
|
|
242
252
|
return {
|
|
243
253
|
bot: bot.name,
|
|
244
254
|
skills,
|
|
255
|
+
linked: [...new Set(linked)].sort(),
|
|
245
256
|
removed: [...new Set(removed)].sort(),
|
|
246
257
|
trouble: [...away].map(([harness, real]) => `${path.join(home, SKILL_DIRS[harness])} leads outside the bot folder, to ${real}, through a link, and the kit links skills only inside the bot folder, so ${harness} does not get the skills the lists name. Replace the link with a directory of the bot's own, then build again.`).join(' '),
|
|
247
258
|
};
|
|
@@ -250,6 +261,7 @@ export function linkSkills(bots, home, bot) {
|
|
|
250
261
|
return {
|
|
251
262
|
bot: bot.name,
|
|
252
263
|
skills,
|
|
264
|
+
linked: [...new Set(linked)].sort(),
|
|
253
265
|
removed: [...new Set(removed)].sort(),
|
|
254
266
|
// A name the lists ask for that the bot has something else under. The user's
|
|
255
267
|
// own is never written over, so what they have to know is that the list did
|
|
@@ -280,7 +292,7 @@ export function unlinkSkills(home) {
|
|
|
280
292
|
* user made, or one of the kit's that they have since repointed, is theirs.
|
|
281
293
|
*
|
|
282
294
|
* It is the kit's own file and lives beside the hook files it already keeps in
|
|
283
|
-
* the bot folder (ADR
|
|
295
|
+
* the bot folder (ADR 0020). A record that cannot be read is treated as an
|
|
284
296
|
* empty one: the worst that follows is that the kit leaves alone something it
|
|
285
297
|
* would otherwise have tidied up, which is the safe way round.
|
|
286
298
|
*/
|
|
@@ -311,7 +323,7 @@ function writeRecord(home, record) {
|
|
|
311
323
|
*
|
|
312
324
|
* A link that leads nowhere is reported whoever made it. Whose it is changes
|
|
313
325
|
* what may be done about it, not the fact that a session cannot read the skill
|
|
314
|
-
* (ADR
|
|
326
|
+
* (ADR 0014).
|
|
315
327
|
*/
|
|
316
328
|
export function skillsTrouble(bots, home, bot) {
|
|
317
329
|
const record = readRecord(home);
|
|
@@ -390,7 +402,8 @@ function namesIn(dir) {
|
|
|
390
402
|
|
|
391
403
|
/**
|
|
392
404
|
* Give every bot in the folder its skills, or the one named, in name order.
|
|
393
|
-
* This is the whole of `obk skills build`: it writes links and
|
|
405
|
+
* This is the whole of `obk skills build`: it writes links, and a bot whose
|
|
406
|
+
* links changed has its running sessions told (`tellSessions`).
|
|
394
407
|
*/
|
|
395
408
|
export function buildSkills(bots, { bot: onlyBot } = {}) {
|
|
396
409
|
const names = botNames(bots);
|
|
@@ -400,11 +413,89 @@ export function buildSkills(bots, { bot: onlyBot } = {}) {
|
|
|
400
413
|
|
|
401
414
|
return (onlyBot === undefined ? names : [onlyBot]).map((name) => {
|
|
402
415
|
const home = botDir(bots, name);
|
|
416
|
+
let bot;
|
|
417
|
+
let entry;
|
|
403
418
|
try {
|
|
404
|
-
|
|
419
|
+
bot = readBot(home, name);
|
|
420
|
+
entry = linkSkills(bots, home, bot);
|
|
405
421
|
} catch (error) {
|
|
406
422
|
return { bot: name, skills: [], trouble: error.message };
|
|
407
423
|
}
|
|
424
|
+
const changed = (entry.linked?.length ?? 0) + (entry.removed?.length ?? 0) > 0;
|
|
425
|
+
return changed ? { ...entry, sessions: tellSessions(home, bot, entry.linked) } : entry;
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/** How long Orca is given to say what is in a session's tab. */
|
|
430
|
+
const LOOK_MS = 2000;
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Tell each session of a bot whose skill links just changed, through its
|
|
434
|
+
* harness's own means (PRD 6.5: changes reach running sessions without a
|
|
435
|
+
* restart, and the sessions a change affects are told). Returns one entry per
|
|
436
|
+
* session, in the bot's order: `{ session, harness, state }`, where the state
|
|
437
|
+
* is `reloaded`, `next-turn` (with `read`), `not-up`, `blocked` (with
|
|
438
|
+
* `blocked`) or `unknown` (with `trouble`).
|
|
439
|
+
*
|
|
440
|
+
* Claude Code gets `/reload-skills` typed into its tab, through the same gate
|
|
441
|
+
* as the mail nudge. A busy session queues it and runs it as a command when
|
|
442
|
+
* its turn ends, so it never reaches the model as text. Codex has no reload
|
|
443
|
+
* command and takes the change at the start of its next turn by itself, so
|
|
444
|
+
* nothing is typed, and the caller is given the SKILL.md of each skill linked
|
|
445
|
+
* in, for the session to read if the skill has not appeared (tech notes,
|
|
446
|
+
* sections 2 and 3). Nothing is ever restarted.
|
|
447
|
+
*/
|
|
448
|
+
function tellSessions(home, bot, linked) {
|
|
449
|
+
let book;
|
|
450
|
+
try {
|
|
451
|
+
book = readBook(home).sessions;
|
|
452
|
+
} catch (error) {
|
|
453
|
+
// The links are made; which tab is whose is what cannot be known.
|
|
454
|
+
const trouble = `${bookFile(home)} cannot be read (${error.message}), so the kit could not tell which tab is this session's, and nothing was typed`;
|
|
455
|
+
return bot.sessions.map((session) => ({ session: session.name, harness: harnessOf(session, bot.harness), state: 'unknown', trouble }));
|
|
456
|
+
}
|
|
457
|
+
// Resolved, because Orca is asked about this folder by path and does not
|
|
458
|
+
// follow a link to it (tech notes, section 1).
|
|
459
|
+
let real;
|
|
460
|
+
try {
|
|
461
|
+
real = realpathSync(home);
|
|
462
|
+
} catch {
|
|
463
|
+
real = home;
|
|
464
|
+
}
|
|
465
|
+
// An Orca whose runtime is not there still answers some commands from what
|
|
466
|
+
// it last knew, so it is asked whether it is up before it is asked anything
|
|
467
|
+
// about a tab.
|
|
468
|
+
const down = bot.sessions.some((session) => typeof book[session.name]?.tab === 'string') ? orcaTrouble() : undefined;
|
|
469
|
+
|
|
470
|
+
return bot.sessions.map((session) => {
|
|
471
|
+
const harness = harnessOf(session, bot.harness);
|
|
472
|
+
const told = { session: session.name, harness };
|
|
473
|
+
const tab = typeof book[session.name]?.tab === 'string' ? book[session.name].tab : undefined;
|
|
474
|
+
if (tab === undefined) return { ...told, state: 'not-up' };
|
|
475
|
+
if (down !== undefined) return { ...told, state: 'unknown', trouble: `Orca is not answering at ${orcaCli()}, so the kit could not look at its tab, and nothing was typed` };
|
|
476
|
+
try {
|
|
477
|
+
const found = tabToTypeInto(real, tab, LOOK_MS);
|
|
478
|
+
if (found.blocked !== undefined) return { ...told, state: 'blocked', blocked: found.blocked };
|
|
479
|
+
if (found.unsure !== undefined) return { ...told, state: 'unknown', trouble: found.unsure };
|
|
480
|
+
if (found.handle === undefined) return { ...told, state: 'not-up' };
|
|
481
|
+
// The book's tab, now running the other harness: its reload is not this one's.
|
|
482
|
+
if (found.agent !== harness) {
|
|
483
|
+
return { ...told, state: 'unknown', trouble: `Orca names ${found.agent} in its tab, not ${harness}, so nothing was typed` };
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
if (harness === 'codex') {
|
|
487
|
+
// Only what Codex can actually read: a link can fail for one harness
|
|
488
|
+
// alone, and a skills directory that is the user's gets none.
|
|
489
|
+
const read = linked.map((name) => path.join(home, SKILL_DIRS.codex, name, MANIFEST)).filter((file) => existsSync(file));
|
|
490
|
+
return { ...told, state: 'next-turn', read };
|
|
491
|
+
}
|
|
492
|
+
typeIntoTab(found.handle, '/reload-skills');
|
|
493
|
+
return { ...told, state: 'reloaded' };
|
|
494
|
+
} catch (error) {
|
|
495
|
+
// The links are made whatever Orca says, so this is reported, not
|
|
496
|
+
// thrown: the build did what it was asked, and the session was not told.
|
|
497
|
+
return { ...told, state: 'unknown', trouble: error.message };
|
|
498
|
+
}
|
|
408
499
|
});
|
|
409
500
|
}
|
|
410
501
|
|
|
@@ -460,7 +551,7 @@ function follow(bots, ref, sources) {
|
|
|
460
551
|
throw new Error(`${ref} names the source ${named}, and skills.yaml lists no source called that.`);
|
|
461
552
|
}
|
|
462
553
|
if (!isCloned(bots, named)) {
|
|
463
|
-
throw new Error(`${ref}: ${named} has not been fetched yet, so there is nothing to link. Run
|
|
554
|
+
throw new Error(`${ref}: ${named} has not been fetched yet, so there is nothing to link. Run ${shellWord(ownCli())} skills fetch --bots ${shellWord(bots)} --source ${named}.`);
|
|
464
555
|
}
|
|
465
556
|
const wrong = wrongClone(bots, source);
|
|
466
557
|
if (wrong !== undefined) throw new Error(`${ref}: ${wrong}`);
|
|
@@ -488,7 +579,9 @@ function follow(bots, ref, sources) {
|
|
|
488
579
|
|
|
489
580
|
/**
|
|
490
581
|
* Make one harness's skills directory hold what `wanted` says, as far as the
|
|
491
|
-
* kit is allowed to. Returns `{ removed, held }` — the names it
|
|
582
|
+
* kit is allowed to. Returns `{ linked, removed, held }` — the names it linked
|
|
583
|
+
* in or repointed, the names it took away (added to the lists given, as it
|
|
584
|
+
* goes, so a caller whose run throws still has them), and
|
|
492
585
|
* what is in the directory afterwards.
|
|
493
586
|
*
|
|
494
587
|
* The kit acts only where the entry on disk is still the link it wrote, which
|
|
@@ -496,12 +589,10 @@ function follow(bots, ref, sources) {
|
|
|
496
589
|
* link the very same skill by hand — so the kit remembers what it made rather
|
|
497
590
|
* than guessing from where a link points (PRD 6.7).
|
|
498
591
|
*/
|
|
499
|
-
function link(dir, wanted, record) {
|
|
592
|
+
function link(dir, wanted, record, linked = [], removed = []) {
|
|
500
593
|
// The directory is there: `linkSkills` makes both before it calls this.
|
|
501
594
|
|
|
502
595
|
const keep = new Map(wanted.map((skill) => [skill.name, skill]));
|
|
503
|
-
const removed = [];
|
|
504
|
-
|
|
505
596
|
// What the lists no longer name. The kit takes back only its own, and only
|
|
506
597
|
// while it is untouched; anything else it forgets about and leaves.
|
|
507
598
|
for (const name of Object.keys(record)) {
|
|
@@ -537,12 +628,13 @@ function link(dir, wanted, record) {
|
|
|
537
628
|
}
|
|
538
629
|
|
|
539
630
|
// Absolute: a kit skill lives in the installed package, outside the user's
|
|
540
|
-
// repo, and the link has to reach out of the repo to find it (ADR
|
|
631
|
+
// repo, and the link has to reach out of the repo to find it (ADR 0014).
|
|
541
632
|
symlinkSync(skill.dir, at);
|
|
542
633
|
record[skill.name] = skill.dir;
|
|
634
|
+
linked.push(skill.name);
|
|
543
635
|
}
|
|
544
636
|
|
|
545
|
-
return { removed, held: heldIn(dir, record) };
|
|
637
|
+
return { linked, removed, held: heldIn(dir, record) };
|
|
546
638
|
}
|
|
547
639
|
|
|
548
640
|
/**
|
package/src/sources.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Skills from an online repo, at the version the user pinned (PRD 6.7, ADR
|
|
1
|
+
// Skills from an online repo, at the version the user pinned (PRD 6.7, ADR 0014).
|
|
2
2
|
//
|
|
3
3
|
// A source is a git repository the user names in `<bots>/skills.yaml`. The kit
|
|
4
4
|
// clones it into `<bots>.skill-sources/<name>`, a sibling of the bots folder and
|
|
@@ -19,6 +19,7 @@ import path from 'node:path';
|
|
|
19
19
|
import { parseDocument } from 'yaml';
|
|
20
20
|
|
|
21
21
|
import { changesExactly, requireBotsFolder, YAML_OUT } from './bot.js';
|
|
22
|
+
import { ownCli, shellWord } from './launch.js';
|
|
22
23
|
|
|
23
24
|
/** The user's file that lists where skills come from. */
|
|
24
25
|
const SKILLS_YAML = 'skills.yaml';
|
|
@@ -201,7 +202,7 @@ function clone(bots, source, rev, recorded) {
|
|
|
201
202
|
export function wrongClone(bots, source) {
|
|
202
203
|
const from = originOf(cloneDir(bots, source.name));
|
|
203
204
|
if (from === source.repo) return undefined;
|
|
204
|
-
return `${source.name}: the clone beside your bots folder came from ${from}, and skills.yaml now names ${source.repo}. Run
|
|
205
|
+
return `${source.name}: the clone beside your bots folder came from ${from}, and skills.yaml now names ${source.repo}. Run ${shellWord(ownCli())} skills update --source ${source.name} to take it from there instead.`;
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
/** Where a clone came from, as git itself has it written down. */
|
|
@@ -226,7 +227,7 @@ function checkout(dir, source, rev, recorded) {
|
|
|
226
227
|
|
|
227
228
|
if (at === undefined) {
|
|
228
229
|
throw new Error(recorded
|
|
229
|
-
? `${source.name}: ${source.repo} no longer has the commit skills.yaml records for it (${rev.slice(0, 7)}), so the version you pinned cannot be put back. Run
|
|
230
|
+
? `${source.name}: ${source.repo} no longer has the commit skills.yaml records for it (${rev.slice(0, 7)}), so the version you pinned cannot be put back. Run ${shellWord(ownCli())} skills update --source ${source.name} to take what ${source.ref} names now, or point it at a version the repo still has.`
|
|
230
231
|
: `${source.name}: ${source.repo} has nothing called ${rev}`);
|
|
231
232
|
}
|
|
232
233
|
git(['-c', 'advice.detachedHead=false', 'checkout', '--quiet', at], dir, `${source.name}: could not go to ${rev}`);
|
package/src/up.js
CHANGED
|
@@ -12,7 +12,7 @@ import { botDir, botNames, displayName, readBot } from './bot.js';
|
|
|
12
12
|
import { conversationsIn } from './conversations.js';
|
|
13
13
|
import { installHook } from './hooks.js';
|
|
14
14
|
import { addressOf, harnessOf, isShortPrompt, launchCommand, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
15
|
-
import { asFolderProject, findProject, makeMailbox, makeProject, openTab, retitleTab, tabs,
|
|
15
|
+
import { asFolderProject, findProject, harnessInTab, makeMailbox, makeProject, openTab, retitleTab, tabs, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
16
16
|
import { buildAgents } from './rules.js';
|
|
17
17
|
import { linkSkills } from './skills.js';
|
|
18
18
|
|
|
@@ -26,8 +26,8 @@ const STARTUP_MS = 10000;
|
|
|
26
26
|
* And how long the second look takes. A harness can draw its first screen and
|
|
27
27
|
* then die on something it was handed — a context window it cannot read, an
|
|
28
28
|
* extra argument it does not know — and the first look would have called it
|
|
29
|
-
* started.
|
|
30
|
-
*
|
|
29
|
+
* started. An idle harness answers this at once; a busy one and a shell both
|
|
30
|
+
* wait it out, and it is the tab's foreground that tells them apart (#232).
|
|
31
31
|
*/
|
|
32
32
|
const SECOND_LOOK_MS = 2000;
|
|
33
33
|
|
|
@@ -35,6 +35,7 @@ const SECOND_LOOK_MS = 2000;
|
|
|
35
35
|
* Bring bots up in Orca. Returns `{ tabs, rules }`: one tab entry per tab it
|
|
36
36
|
* looked at — the sessions the book knows, and, for Bot Father, whatever else
|
|
37
37
|
* is open in its project — and one rules entry and one skills entry per bot.
|
|
38
|
+
* `projects` has one entry per Orca project this run made or renamed.
|
|
38
39
|
*
|
|
39
40
|
* With no name it is every bot in the folder, in name order. `bot` brings up
|
|
40
41
|
* one bot and `session` one of its sessions, for a caller that wants one thing
|
|
@@ -58,11 +59,14 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
58
59
|
const { running, rules, skills } = prepareBots(bots, names, onlySession);
|
|
59
60
|
|
|
60
61
|
const report = [];
|
|
62
|
+
const projects = [];
|
|
61
63
|
for (const { bot, home } of running) {
|
|
62
64
|
for (const session of sessionsOf(bot, onlySession)) {
|
|
63
65
|
if (session.paused === true) paused.push({ bot: bot.name, session: session.name });
|
|
64
66
|
}
|
|
65
|
-
|
|
67
|
+
const up = await bringUpBot(bots, home, bot, onlySession);
|
|
68
|
+
report.push(...up.tabs);
|
|
69
|
+
if (up.project !== undefined) projects.push(up.project);
|
|
66
70
|
}
|
|
67
71
|
|
|
68
72
|
// A bot whose rules would not build is reported here rather than in the
|
|
@@ -78,6 +82,7 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
78
82
|
})),
|
|
79
83
|
skills,
|
|
80
84
|
paused,
|
|
85
|
+
projects,
|
|
81
86
|
};
|
|
82
87
|
}
|
|
83
88
|
|
|
@@ -87,7 +92,7 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
87
92
|
*
|
|
88
93
|
* Separate from the tabs because the order matters twice over. A session reads
|
|
89
94
|
* its rules, its skills and its hooks as it comes up, so all three have to be
|
|
90
|
-
* there before the tab is (PRD 6.6, ADR
|
|
95
|
+
* there before the tab is (PRD 6.6, ADR 0020) — and a restart closes a tab in
|
|
91
96
|
* between, so everything that can refuse must have refused before that. It
|
|
92
97
|
* refuses by throwing, exactly as `up` always has; a bot it leaves out of
|
|
93
98
|
* `running` is one whose sessions must not be started.
|
|
@@ -133,7 +138,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
133
138
|
|
|
134
139
|
// And the kit's hook goes into every bot folder before Orca is asked for
|
|
135
140
|
// anything, for the same reason: a harness reads its hooks when it comes up,
|
|
136
|
-
// so one written later would miss the session it was written for (ADR
|
|
141
|
+
// so one written later would miss the session it was written for (ADR 0020),
|
|
137
142
|
// and a bot folder the kit cannot write it into stops the run with nothing
|
|
138
143
|
// opened anywhere. Only the harnesses a bot actually runs on; a bot with no
|
|
139
144
|
// sessions gets none.
|
|
@@ -197,8 +202,11 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
197
202
|
// Orca is asked first and the book is written after: nothing that takes time
|
|
198
203
|
// happens while the book is held, because a session's own hook may be writing
|
|
199
204
|
// its id into that same file at any moment.
|
|
200
|
-
const orca = orcaProject(home, title);
|
|
205
|
+
const { change, ...orca } = orcaProject(home, title);
|
|
201
206
|
await updateBook(home, (book) => { book.orca = orca; });
|
|
207
|
+
// Orca's window does not see a project made or renamed on the command line
|
|
208
|
+
// until it is told (#224).
|
|
209
|
+
if (change !== undefined) tellWindow(orca.project);
|
|
202
210
|
|
|
203
211
|
const live = new Map(tabs(home).map((tab) => [tab.tabId, tab]));
|
|
204
212
|
const report = [];
|
|
@@ -217,7 +225,7 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
217
225
|
: spare.map((tab) => entry(tab, { bot: name, name: null, created: false }))));
|
|
218
226
|
}
|
|
219
227
|
|
|
220
|
-
return report;
|
|
228
|
+
return { tabs: report, project: change === undefined ? undefined : { bot: name, project: orca.project, change } };
|
|
221
229
|
}
|
|
222
230
|
|
|
223
231
|
async function bringUpSession(bots, home, live, session, bot, title) {
|
|
@@ -234,7 +242,7 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
234
242
|
// live harness. Writing the name down here would advertise an address that
|
|
235
243
|
// answers to nobody (review of PR #132, finding 1). It gets one the next
|
|
236
244
|
// time it starts, which is the next time the kit types its launch line.
|
|
237
|
-
await ensureMailbox(home, bot, session, harness);
|
|
245
|
+
await ensureMailbox(home, bot, session, harness, known.handle);
|
|
238
246
|
|
|
239
247
|
// Whatever runs in the tab may have rewritten its title. The kit writes its
|
|
240
248
|
// own back, and reports that one rather than the name Orca last saw: the id
|
|
@@ -312,7 +320,7 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
312
320
|
// tab is on the books, so a mailbox Orca will not make leaves a tab the next
|
|
313
321
|
// run finds and finishes rather than a tab nobody owns (review of PR #132,
|
|
314
322
|
// finding 3).
|
|
315
|
-
await ensureMailbox(home, bot, session, harness, { named: harness === 'claude' });
|
|
323
|
+
await ensureMailbox(home, bot, session, harness, made.handle, { named: harness === 'claude', opened: true });
|
|
316
324
|
|
|
317
325
|
// And then asking whether a TUI came up, rather than assuming one did. The
|
|
318
326
|
// text goes into the tab's own shell, which may have been busy with a
|
|
@@ -321,9 +329,9 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
321
329
|
// Twice, because coming up and staying up are different things: a harness
|
|
322
330
|
// that refuses what it was handed draws a screen, prints its complaint and
|
|
323
331
|
// leaves a shell behind, and a run that looked once would report it as
|
|
324
|
-
// started.
|
|
325
|
-
|
|
326
|
-
const tui =
|
|
332
|
+
// started.
|
|
333
|
+
const first = lookFor(made.handle, STARTUP_MS);
|
|
334
|
+
const tui = first.running ? lookFor(made.handle, SECOND_LOOK_MS) : first;
|
|
327
335
|
|
|
328
336
|
// The start prompt went in with that line, as the harness's own prompt
|
|
329
337
|
// argument, so it is the harness that holds it until it is ready — through
|
|
@@ -344,6 +352,23 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
344
352
|
});
|
|
345
353
|
}
|
|
346
354
|
|
|
355
|
+
/**
|
|
356
|
+
* One look at a tab the kit just typed a launch line into: whether a harness
|
|
357
|
+
* is running there, and what it is waiting on if Orca says.
|
|
358
|
+
*
|
|
359
|
+
* The harness is up when something other than the shell holds the tab's
|
|
360
|
+
* terminal, busy or not, or when Orca sees a question on its screen. Orca's
|
|
361
|
+
* `agentIdentity` is not asked for: it can come seconds after the launch
|
|
362
|
+
* (tech notes, section 1). When the tab's foreground cannot be read, Orca's own
|
|
363
|
+
* answers stand in, for this report only: nothing more is typed here.
|
|
364
|
+
*/
|
|
365
|
+
function lookFor(handle, timeoutMs) {
|
|
366
|
+
const seen = harnessInTab(handle, timeoutMs);
|
|
367
|
+
const running = seen.blockedReason !== undefined
|
|
368
|
+
|| (seen.front === undefined ? seen.answered || seen.agent !== undefined : seen.front === 'program');
|
|
369
|
+
return { running, blockedReason: seen.blockedReason };
|
|
370
|
+
}
|
|
371
|
+
|
|
347
372
|
/**
|
|
348
373
|
* Make sure the book holds this session's mailbox, and — only where this run
|
|
349
374
|
* has just launched the session under it — the name it answers to.
|
|
@@ -361,15 +386,23 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
361
386
|
* a restart (tech notes, section 1). Orca offers no way to delete one, so this
|
|
362
387
|
* makes exactly one per session and never a second.
|
|
363
388
|
*
|
|
389
|
+
* It is bound to `handle`, the session's own tab, and never to the tab this run
|
|
390
|
+
* of `obk up` was typed in: Orca tells a Run's coordinator about its mail, and
|
|
391
|
+
* only the session the mail is for should be told (issue #228). A mailbox the
|
|
392
|
+
* session already had is bound again when this run `opened` a new tab for it —
|
|
393
|
+
* after a restart, or a closed tab brought back — or it would stay with the tab
|
|
394
|
+
* that is gone (review of PR #248, finding 1).
|
|
395
|
+
*
|
|
364
396
|
* A Codex session whose user turned the sandbox switch off gets none: it could
|
|
365
397
|
* not read a mailbox if it had one, and an address nobody can read is worse
|
|
366
398
|
* than none at all. `obk message` says so in those words.
|
|
367
399
|
*/
|
|
368
|
-
async function ensureMailbox(home, bot, session, harness, { named = false } = {}) {
|
|
400
|
+
async function ensureMailbox(home, bot, session, harness, handle, { named = false, opened = false } = {}) {
|
|
369
401
|
const held = readBook(home).sessions[session.name] ?? {};
|
|
402
|
+
if (opened && typeof held.mailbox === 'string') useMailbox(held.mailbox, handle);
|
|
370
403
|
|
|
371
404
|
const address = named ? addressOf(bot.name, session.name) : undefined;
|
|
372
|
-
const mailbox = mailboxFor(readBook(home), bot, session, harness);
|
|
405
|
+
const mailbox = mailboxFor(readBook(home), bot, session, harness, handle);
|
|
373
406
|
|
|
374
407
|
if (mailbox === undefined && (address === undefined || held.address === address)) return;
|
|
375
408
|
|
|
@@ -392,10 +425,10 @@ async function ensureMailbox(home, bot, session, harness, { named = false } = {}
|
|
|
392
425
|
* The Orca call is made outside the book's lock, which is held for one read and
|
|
393
426
|
* one write (book.js).
|
|
394
427
|
*/
|
|
395
|
-
function mailboxFor(book, bot, session, harness) {
|
|
428
|
+
function mailboxFor(book, bot, session, harness, handle) {
|
|
396
429
|
const held = book.sessions[session.name] ?? {};
|
|
397
430
|
if (typeof held.mailbox === 'string' || !reachesMail(session, harness)) return undefined;
|
|
398
|
-
return makeMailbox(`${bot.name}/${session.name}
|
|
431
|
+
return makeMailbox(`${bot.name}/${session.name}`, handle);
|
|
399
432
|
}
|
|
400
433
|
|
|
401
434
|
/**
|
|
@@ -403,7 +436,7 @@ function mailboxFor(book, bot, session, harness) {
|
|
|
403
436
|
* the one the book holds, one the harness itself still has on record, or none.
|
|
404
437
|
*
|
|
405
438
|
* One tab holds one session, and the book is the authority for which
|
|
406
|
-
* conversation that is (ADR
|
|
439
|
+
* conversation that is (ADR 0012). But the book can be incomplete — on Codex a
|
|
407
440
|
* hooks file must be trusted before any hook runs, and trusting it does not
|
|
408
441
|
* replay the event it missed — and "the book does not say" must never be read as
|
|
409
442
|
* "there was no conversation". So where the book is silent about a tab the kit
|
|
@@ -430,7 +463,8 @@ function whichConversation(book, home, bot, session, was, harness) {
|
|
|
430
463
|
}
|
|
431
464
|
|
|
432
465
|
/**
|
|
433
|
-
* A bot's Orca project, made if it is not there yet
|
|
466
|
+
* A bot's Orca project, made if it is not there yet, with `change` saying
|
|
467
|
+
* whether this run `made` it, `renamed` it or left it as it was (undefined).
|
|
434
468
|
*
|
|
435
469
|
* It has to be a folder workspace. Registering a folder that sits inside the
|
|
436
470
|
* bots repo gets it recorded as a git one, which Orca gives no workspace at
|
|
@@ -438,12 +472,12 @@ function whichConversation(book, home, bot, session, was, harness) {
|
|
|
438
472
|
*/
|
|
439
473
|
function orcaProject(home, title) {
|
|
440
474
|
const found = findProject(home);
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
: (found.kind === 'folder' ? found : asFolderProject(found.id, title));
|
|
444
|
-
return { project: setup.projectId, setup: setup.id };
|
|
475
|
+
if (found === undefined) return ids(makeProject(home, title), 'made');
|
|
476
|
+
return found.kind === 'folder' ? ids(found) : ids(asFolderProject(found.id, title), 'renamed');
|
|
445
477
|
}
|
|
446
478
|
|
|
479
|
+
const ids = (setup, change) => ({ project: setup.projectId, setup: setup.id, change });
|
|
480
|
+
|
|
447
481
|
/**
|
|
448
482
|
* Where the kit leaves a prompt for the launch line to pick up: a folder of its
|
|
449
483
|
* own beside the bots repo, the way skill-source clones sit beside it (PRD 6.3)
|
package/src/usage.js
CHANGED
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
// and efforts, between which two moments, and how many times it was compacted.
|
|
6
6
|
// What any of that costs is a skill's to work out: a price comes from a live
|
|
7
7
|
// lookup and a price that cannot be found has to be said to be unknown, which is
|
|
8
|
-
// judgement and not arithmetic (ADR
|
|
8
|
+
// judgement and not arithmetic (ADR 0016).
|
|
9
9
|
//
|
|
10
10
|
// Which conversation belongs to which session is the book's to say and is never
|
|
11
|
-
// guessed, as everywhere else in the kit (ADR
|
|
11
|
+
// guessed, as everywhere else in the kit (ADR 0012). A session's conversations
|
|
12
12
|
// are the one the book names for it now and the ones in its history. One in the
|
|
13
13
|
// bot's folder that no session claims is reported under the bot instead, with
|
|
14
14
|
// its figures, so that what it spent is visible rather than quietly dropped.
|