@assuredloop/orca-bot-kit 0.2.0 → 0.3.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 +15 -12
- package/SETUP.md +7 -2
- 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 +85 -8
- 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 +59 -20
- 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 +36 -14
- package/src/orca.js +95 -24
- package/src/pause.js +11 -6
- package/src/record.js +1 -1
- package/src/restart.js +6 -5
- package/src/retire.js +2 -2
- package/src/roster.js +1 -1
- package/src/rules.js +2 -2
- package/src/skills.js +7 -6
- package/src/sources.js +4 -3
- package/src/up.js +40 -15
- package/src/usage.js +2 -2
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
|
package/src/skills.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
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
|
|
@@ -21,6 +21,7 @@ import { fileURLToPath } from 'node:url';
|
|
|
21
21
|
import { parse, parseDocument, stringify } from 'yaml';
|
|
22
22
|
|
|
23
23
|
import { botDir, botNames, changesExactly, leadsOutside, readBot, YAML_OUT } from './bot.js';
|
|
24
|
+
import { ownCli, shellWord } from './launch.js';
|
|
24
25
|
import { listIn } from './rules.js';
|
|
25
26
|
import { cloneDir, isCloned, readSources, skillsIn, wrongClone } from './sources.js';
|
|
26
27
|
|
|
@@ -39,7 +40,7 @@ export const SKILL_DIRS = {
|
|
|
39
40
|
/**
|
|
40
41
|
* What a skill may be called: the Agent Skills name, which is also the name of
|
|
41
42
|
* the directory it lives in and of the link the kit makes (tech notes, section
|
|
42
|
-
* 4, and ADR
|
|
43
|
+
* 4, and ADR 0019).
|
|
43
44
|
*/
|
|
44
45
|
const NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
45
46
|
|
|
@@ -280,7 +281,7 @@ export function unlinkSkills(home) {
|
|
|
280
281
|
* user made, or one of the kit's that they have since repointed, is theirs.
|
|
281
282
|
*
|
|
282
283
|
* It is the kit's own file and lives beside the hook files it already keeps in
|
|
283
|
-
* the bot folder (ADR
|
|
284
|
+
* the bot folder (ADR 0020). A record that cannot be read is treated as an
|
|
284
285
|
* empty one: the worst that follows is that the kit leaves alone something it
|
|
285
286
|
* would otherwise have tidied up, which is the safe way round.
|
|
286
287
|
*/
|
|
@@ -311,7 +312,7 @@ function writeRecord(home, record) {
|
|
|
311
312
|
*
|
|
312
313
|
* A link that leads nowhere is reported whoever made it. Whose it is changes
|
|
313
314
|
* what may be done about it, not the fact that a session cannot read the skill
|
|
314
|
-
* (ADR
|
|
315
|
+
* (ADR 0014).
|
|
315
316
|
*/
|
|
316
317
|
export function skillsTrouble(bots, home, bot) {
|
|
317
318
|
const record = readRecord(home);
|
|
@@ -460,7 +461,7 @@ function follow(bots, ref, sources) {
|
|
|
460
461
|
throw new Error(`${ref} names the source ${named}, and skills.yaml lists no source called that.`);
|
|
461
462
|
}
|
|
462
463
|
if (!isCloned(bots, named)) {
|
|
463
|
-
throw new Error(`${ref}: ${named} has not been fetched yet, so there is nothing to link. Run
|
|
464
|
+
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
465
|
}
|
|
465
466
|
const wrong = wrongClone(bots, source);
|
|
466
467
|
if (wrong !== undefined) throw new Error(`${ref}: ${wrong}`);
|
|
@@ -537,7 +538,7 @@ function link(dir, wanted, record) {
|
|
|
537
538
|
}
|
|
538
539
|
|
|
539
540
|
// 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
|
|
541
|
+
// repo, and the link has to reach out of the repo to find it (ADR 0014).
|
|
541
542
|
symlinkSync(skill.dir, at);
|
|
542
543
|
record[skill.name] = skill.dir;
|
|
543
544
|
}
|
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, 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
|
|
|
@@ -87,7 +87,7 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
87
87
|
*
|
|
88
88
|
* Separate from the tabs because the order matters twice over. A session reads
|
|
89
89
|
* 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
|
|
90
|
+
* there before the tab is (PRD 6.6, ADR 0020) — and a restart closes a tab in
|
|
91
91
|
* between, so everything that can refuse must have refused before that. It
|
|
92
92
|
* refuses by throwing, exactly as `up` always has; a bot it leaves out of
|
|
93
93
|
* `running` is one whose sessions must not be started.
|
|
@@ -133,7 +133,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
133
133
|
|
|
134
134
|
// And the kit's hook goes into every bot folder before Orca is asked for
|
|
135
135
|
// 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
|
|
136
|
+
// so one written later would miss the session it was written for (ADR 0020),
|
|
137
137
|
// and a bot folder the kit cannot write it into stops the run with nothing
|
|
138
138
|
// opened anywhere. Only the harnesses a bot actually runs on; a bot with no
|
|
139
139
|
// sessions gets none.
|
|
@@ -234,7 +234,7 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
234
234
|
// live harness. Writing the name down here would advertise an address that
|
|
235
235
|
// answers to nobody (review of PR #132, finding 1). It gets one the next
|
|
236
236
|
// time it starts, which is the next time the kit types its launch line.
|
|
237
|
-
await ensureMailbox(home, bot, session, harness);
|
|
237
|
+
await ensureMailbox(home, bot, session, harness, known.handle);
|
|
238
238
|
|
|
239
239
|
// Whatever runs in the tab may have rewritten its title. The kit writes its
|
|
240
240
|
// own back, and reports that one rather than the name Orca last saw: the id
|
|
@@ -312,7 +312,7 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
312
312
|
// tab is on the books, so a mailbox Orca will not make leaves a tab the next
|
|
313
313
|
// run finds and finishes rather than a tab nobody owns (review of PR #132,
|
|
314
314
|
// finding 3).
|
|
315
|
-
await ensureMailbox(home, bot, session, harness, { named: harness === 'claude' });
|
|
315
|
+
await ensureMailbox(home, bot, session, harness, made.handle, { named: harness === 'claude', opened: true });
|
|
316
316
|
|
|
317
317
|
// And then asking whether a TUI came up, rather than assuming one did. The
|
|
318
318
|
// text goes into the tab's own shell, which may have been busy with a
|
|
@@ -321,9 +321,9 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
321
321
|
// Twice, because coming up and staying up are different things: a harness
|
|
322
322
|
// that refuses what it was handed draws a screen, prints its complaint and
|
|
323
323
|
// leaves a shell behind, and a run that looked once would report it as
|
|
324
|
-
// started.
|
|
325
|
-
|
|
326
|
-
const tui =
|
|
324
|
+
// started.
|
|
325
|
+
const first = lookFor(made.handle, STARTUP_MS);
|
|
326
|
+
const tui = first.running ? lookFor(made.handle, SECOND_LOOK_MS) : first;
|
|
327
327
|
|
|
328
328
|
// The start prompt went in with that line, as the harness's own prompt
|
|
329
329
|
// argument, so it is the harness that holds it until it is ready — through
|
|
@@ -344,6 +344,23 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
344
344
|
});
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* One look at a tab the kit just typed a launch line into: whether a harness
|
|
349
|
+
* is running there, and what it is waiting on if Orca says.
|
|
350
|
+
*
|
|
351
|
+
* The harness is up when something other than the shell holds the tab's
|
|
352
|
+
* terminal, busy or not, or when Orca sees a question on its screen. Orca's
|
|
353
|
+
* `agentIdentity` is not asked for: it can come seconds after the launch
|
|
354
|
+
* (tech notes, section 1). When the tab's foreground cannot be read, Orca's own
|
|
355
|
+
* answers stand in, for this report only: nothing more is typed here.
|
|
356
|
+
*/
|
|
357
|
+
function lookFor(handle, timeoutMs) {
|
|
358
|
+
const seen = harnessInTab(handle, timeoutMs);
|
|
359
|
+
const running = seen.blockedReason !== undefined
|
|
360
|
+
|| (seen.front === undefined ? seen.answered || seen.agent !== undefined : seen.front === 'program');
|
|
361
|
+
return { running, blockedReason: seen.blockedReason };
|
|
362
|
+
}
|
|
363
|
+
|
|
347
364
|
/**
|
|
348
365
|
* Make sure the book holds this session's mailbox, and — only where this run
|
|
349
366
|
* has just launched the session under it — the name it answers to.
|
|
@@ -361,15 +378,23 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
361
378
|
* a restart (tech notes, section 1). Orca offers no way to delete one, so this
|
|
362
379
|
* makes exactly one per session and never a second.
|
|
363
380
|
*
|
|
381
|
+
* It is bound to `handle`, the session's own tab, and never to the tab this run
|
|
382
|
+
* of `obk up` was typed in: Orca tells a Run's coordinator about its mail, and
|
|
383
|
+
* only the session the mail is for should be told (issue #228). A mailbox the
|
|
384
|
+
* session already had is bound again when this run `opened` a new tab for it —
|
|
385
|
+
* after a restart, or a closed tab brought back — or it would stay with the tab
|
|
386
|
+
* that is gone (review of PR #248, finding 1).
|
|
387
|
+
*
|
|
364
388
|
* A Codex session whose user turned the sandbox switch off gets none: it could
|
|
365
389
|
* not read a mailbox if it had one, and an address nobody can read is worse
|
|
366
390
|
* than none at all. `obk message` says so in those words.
|
|
367
391
|
*/
|
|
368
|
-
async function ensureMailbox(home, bot, session, harness, { named = false } = {}) {
|
|
392
|
+
async function ensureMailbox(home, bot, session, harness, handle, { named = false, opened = false } = {}) {
|
|
369
393
|
const held = readBook(home).sessions[session.name] ?? {};
|
|
394
|
+
if (opened && typeof held.mailbox === 'string') useMailbox(held.mailbox, handle);
|
|
370
395
|
|
|
371
396
|
const address = named ? addressOf(bot.name, session.name) : undefined;
|
|
372
|
-
const mailbox = mailboxFor(readBook(home), bot, session, harness);
|
|
397
|
+
const mailbox = mailboxFor(readBook(home), bot, session, harness, handle);
|
|
373
398
|
|
|
374
399
|
if (mailbox === undefined && (address === undefined || held.address === address)) return;
|
|
375
400
|
|
|
@@ -392,10 +417,10 @@ async function ensureMailbox(home, bot, session, harness, { named = false } = {}
|
|
|
392
417
|
* The Orca call is made outside the book's lock, which is held for one read and
|
|
393
418
|
* one write (book.js).
|
|
394
419
|
*/
|
|
395
|
-
function mailboxFor(book, bot, session, harness) {
|
|
420
|
+
function mailboxFor(book, bot, session, harness, handle) {
|
|
396
421
|
const held = book.sessions[session.name] ?? {};
|
|
397
422
|
if (typeof held.mailbox === 'string' || !reachesMail(session, harness)) return undefined;
|
|
398
|
-
return makeMailbox(`${bot.name}/${session.name}
|
|
423
|
+
return makeMailbox(`${bot.name}/${session.name}`, handle);
|
|
399
424
|
}
|
|
400
425
|
|
|
401
426
|
/**
|
|
@@ -403,7 +428,7 @@ function mailboxFor(book, bot, session, harness) {
|
|
|
403
428
|
* the one the book holds, one the harness itself still has on record, or none.
|
|
404
429
|
*
|
|
405
430
|
* One tab holds one session, and the book is the authority for which
|
|
406
|
-
* conversation that is (ADR
|
|
431
|
+
* conversation that is (ADR 0012). But the book can be incomplete — on Codex a
|
|
407
432
|
* hooks file must be trusted before any hook runs, and trusting it does not
|
|
408
433
|
* replay the event it missed — and "the book does not say" must never be read as
|
|
409
434
|
* "there was no conversation". So where the book is silent about a tab the kit
|
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.
|