@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.
Files changed (49) hide show
  1. package/README.md +15 -12
  2. package/SETUP.md +7 -2
  3. package/package.json +1 -1
  4. package/rules/finishing.md +3 -0
  5. package/rules/mail.md +3 -3
  6. package/rules/scope.md +4 -2
  7. package/rules/tests-first.md +6 -4
  8. package/skills/obk-arch/LICENSES.md +7 -0
  9. package/skills/obk-arch/NOTICE.md +113 -5
  10. package/skills/obk-arch/SKILL.md +221 -10
  11. package/skills/obk-bot-building/NOTICE.md +2 -2
  12. package/skills/obk-bot-building/SKILL.md +85 -8
  13. package/skills/obk-debugging/SKILL.md +30 -15
  14. package/skills/obk-decision-memo/NOTICE.md +1 -1
  15. package/skills/obk-finops/NOTICE.md +1 -1
  16. package/skills/obk-finops/SKILL.md +6 -4
  17. package/skills/obk-fleet-review/SKILL.md +42 -5
  18. package/skills/obk-grilling/NOTICE.md +11 -3
  19. package/skills/obk-grilling/SKILL.md +11 -7
  20. package/skills/obk-grooming/SKILL.md +5 -2
  21. package/skills/obk-handoff/NOTICE.md +1 -1
  22. package/skills/obk-recall/NOTICE.md +1 -1
  23. package/skills/obk-researching/NOTICE.md +1 -1
  24. package/skills/obk-researching/SKILL.md +19 -9
  25. package/skills/obk-reviewing/SKILL.md +4 -1
  26. package/skills/obk-tdd/SKILL.md +54 -23
  27. package/skills/obk-teach/NOTICE.md +2 -2
  28. package/skills/obk-why/NOTICE.md +1 -1
  29. package/skills/obk-why/SKILL.md +21 -10
  30. package/src/book.js +2 -2
  31. package/src/bot.js +1 -1
  32. package/src/cli.js +59 -20
  33. package/src/groom.js +5 -3
  34. package/src/health.js +2 -2
  35. package/src/hooks.js +73 -18
  36. package/src/init.js +3 -3
  37. package/src/launch.js +28 -5
  38. package/src/message.js +36 -14
  39. package/src/orca.js +95 -24
  40. package/src/pause.js +11 -6
  41. package/src/record.js +1 -1
  42. package/src/restart.js +6 -5
  43. package/src/retire.js +2 -2
  44. package/src/roster.js +1 -1
  45. package/src/rules.js +2 -2
  46. package/src/skills.js +7 -6
  47. package/src/sources.js +4 -3
  48. package/src/up.js +40 -15
  49. 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 0006). What is wrong with the setup is a different question, and
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 0003).
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 0003
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 0004, ADR 0009).
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 0009).
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 0010). A record that cannot be read is treated as an
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 0004).
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 obk skills fetch --bots ${bots} --source ${named}.`);
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 0004).
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 0004).
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 obk skills update --source ${source.name} to take it from there instead.`;
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 obk skills update --source ${source.name} to take what ${source.ref} names now, or point it at a version the repo still has.`
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, tuiInTab, typeIntoTab } from './orca.js';
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. A tab whose TUI is still there answers this at once; only one that
30
- * has fallen back to a shell waits it out.
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 0010) — and a restart closes a tab in
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 0010),
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. A tab that still has a TUI answers the second look at once,
325
- // whatever the agent in it is busy with.
326
- const tui = tuiInTab(made.handle, STARTUP_MS).running ? tuiInTab(made.handle, SECOND_LOOK_MS) : { running: false };
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 0002). But the book can be incomplete — on Codex a
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 0006).
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 0002). A session's conversations
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.