@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.
Files changed (50) hide show
  1. package/README.md +31 -15
  2. package/SETUP.md +10 -4
  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 +108 -19
  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 +117 -33
  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 +38 -19
  39. package/src/orca-window.cjs +21 -0
  40. package/src/orca.js +191 -28
  41. package/src/pause.js +11 -6
  42. package/src/record.js +1 -1
  43. package/src/restart.js +6 -5
  44. package/src/retire.js +41 -6
  45. package/src/roster.js +1 -1
  46. package/src/rules.js +2 -2
  47. package/src/skills.js +113 -21
  48. package/src/sources.js +4 -3
  49. package/src/up.js +57 -23
  50. package/src/usage.js +2 -2
@@ -35,8 +35,12 @@ tests live and how they are named, what the tests next door already do, and
35
35
  which commands the project's own checks actually run. Never assume a default
36
36
  command because it is the usual one for the language.
37
37
 
38
- Use the focused command through the loop and the whole-suite command before you
39
- finish.
38
+ Use the focused command through the loop. Before you finish, run what the
39
+ project requires, plus the checks that cover the change and its credible effects
40
+ elsewhere. The whole suite earns its cost on a broad or shared change and at a
41
+ release gate; when you leave it out, name the coverage you ran instead and why it
42
+ is enough. A gate the project requires is never waived, and a run still going is
43
+ not a run that passed.
40
44
 
41
45
  ## Start from the requirement, not the code
42
46
 
@@ -306,8 +310,9 @@ A seam below that gives a green test and no protection. If no correct seam
306
310
  exists, that is itself the finding: report it, because the shape of the code is
307
311
  what is stopping the bug from being pinned down.
308
312
 
309
- Minimise the reproduction until every part of it is load-bearing, turn it into
310
- a failing test, watch it fail, fix the cause, watch it pass, then run the
313
+ Minimise the reproduction until reducing it further would not improve the
314
+ diagnosis or the regression check, and keep the evidence you stopped on. Turn it
315
+ into a failing test, watch it fail, fix the cause, watch it pass, then run the
311
316
  original unminimised scenario again.
312
317
 
313
318
  Two tests are often better than one here: one at the interface, saying what a
@@ -316,15 +321,20 @@ lives. Get both to pass. The first is what stops the bug coming back in a way
316
321
  anyone would notice; the second says where it was.
317
322
 
318
323
  A regression test written after the fix has proved nothing yet. Prove it:
319
- revert the fix, run it and watch it fail, restore the fix, run it again.
324
+ revert the fix, run it and watch it fail, restore the fix, run it again. A test
325
+ that already failed on a named pre-fix baseline and passes with the fix, unchanged
326
+ in between, has its proof: keep that receipt instead of reverting again. Where
327
+ the test changed since, or its sensitivity is unproven, prove it again on the bad
328
+ baseline in a disposable copy.
320
329
 
321
330
  When the bug is intermittent, make the test deterministic if you can, and say
322
331
  which signal you pinned down. When it is one of a class, land the test for the
323
332
  one in front of you first, then say what else has the same shape.
324
333
 
325
334
  When a failing test really is impractical, say so out loud, say why, and name
326
- the closest executable check you used instead. Quiet omission is the part that
327
- does damage: a reviewer treats a missing red receipt as a finding.
335
+ the closest executable check you used instead. Naming a check you will run later
336
+ is not having run one. Quiet omission is the part that does damage: a reviewer
337
+ treats a missing red receipt as a finding.
328
338
 
329
339
  ## Tests that catch nothing
330
340
 
@@ -406,11 +416,13 @@ The question is the whole point: if a real mistake were made in this code,
406
416
  would a test fail? There is no score to reach.
407
417
 
408
418
  **Everyday: your own hand check.** Once per piece of work, at the end, only
409
- where it earns its place. Five to eight deliberate breaks in the risky logic
410
- the work changed (money, permissions, eligibility, safety, data that could be
411
- lost, and whatever else the requirement cares most about), chosen before you
412
- look at the tests, each one run, each one expected to make a test fail, each
413
- one put back. Reported in three lines.
419
+ where it earns its place. A small representative set of distinct, plausible
420
+ mistakes in the risky logic the work changed (money, permissions, eligibility,
421
+ safety, data that could be lost, and whatever else the requirement cares most
422
+ about), chosen before you look at the tests, each one run, each one expected to
423
+ make a test fail, each one put back. Stop when another break would add no
424
+ evidence, and say why the set you ran was enough: two meaningful breaks beat five
425
+ padded ones. Reported in three lines.
414
426
 
415
427
  One break at a time, never two at once: note the original, apply the break, run
416
428
  the tests, record killed or survived, restore the code immediately, then the
@@ -429,8 +441,14 @@ removed so an absent value reaches further than it should; a call to a
429
441
  collaborator deleted.
430
442
 
431
443
  Skip it, and say that you skipped it, for work with no real logic in it: docs,
432
- config, wiring, renames, small fixes, prototypes, and code with no runnable
433
- tests.
444
+ wiring, renames, prototypes. Size and file type do not decide whether logic is
445
+ risky: a one-character permission fix is risky logic, and so is a configuration
446
+ that decides who gets what.
447
+
448
+ Code with no runnable tests is a different case: there is logic, and nothing to
449
+ run it against. Report the missing automated coverage, the reason, the stand-in
450
+ check you used and what is still uncertain. It is a finding when behaviour the
451
+ requirement cares about lacks adequate verification.
434
452
 
435
453
  **A survivor**, a break no test objected to, gets the equivalence question
436
454
  first: is there any input for which the changed code returns a different value,
@@ -503,14 +521,27 @@ the work is handed on.
503
521
 
504
522
  ## Away from code
505
523
 
506
- The same three moves work on documentation, configuration, prompts and data,
507
- where no tool will help: state the check before you make the change, see the
508
- check fail first, and at the end break the artefact on purpose to find out
509
- whether the check notices.
510
-
511
- They apply where there is something that can be wrong: a configuration that
512
- decides who gets what, a prompt whose branches behave differently, a
513
- transformation over data. The skip rule is the same as for code, and prose
514
- written for people earns no test and no mutant.
524
+ The same three moves work on configuration and data, where no tool will help:
525
+ state the check before you make the change, see the check fail first, and at
526
+ the end break the artefact on purpose to find out whether the check notices.
527
+
528
+ They apply where there is something that can be wrong and a check that can be
529
+ seen to fail: a configuration that decides who gets what, a transformation over
530
+ data. The skip rule is the same as for code.
531
+
532
+ Prose written for people is checked by review, not by a test: a test that matches
533
+ its text buys nothing. Text written for a model, a prompt included, is checked the
534
+ same way, with one addition: where the requirement is how the model behaves, a
535
+ representative use earns its place, and it is the whole check, without the three
536
+ moves above. A prompt that must hold an action until it is approved is exercised
537
+ once with the approval and once without. Neither needs a manufactured red, and no
538
+ wording edit needs a test of its own.
539
+
540
+ A decision that cannot be driven locally, such as a CI workflow or a setting in
541
+ someone else's service, is checked in two parts: its parsed structure and
542
+ whatever of it can be executed, then the real run when that is authorised.
543
+ Publishing, spending money or changing someone else's settings is never a
544
+ mandatory step of a check. A real run put off for later names who triggers it and
545
+ says what stays unverified until then.
515
546
 
516
547
  Sources and licences: [NOTICE.md](NOTICE.md).
@@ -48,14 +48,14 @@ thanks:
48
48
 
49
49
  Made generic, which is what the owner asked for. The original is built on top
50
50
  of two other skills it invokes by name, and a kit skill carries what it needs
51
- rather than depending on another being loaded (ADR 0009); here getting
51
+ rather than depending on another being loaded (ADR 0019); here getting
52
52
  oriented is part of the work, and where the reasons are the point that is
53
53
  named as its own kind of digging without requiring a particular skill. Its
54
54
  named image-generation tool is likewise described by what it produces rather
55
55
  than by the tool.
56
56
 
57
57
  One departure: the original keeps itself from being invoked on its own with
58
- `disable-model-invocation: true`, which is not portable frontmatter (ADR 0009
58
+ `disable-model-invocation: true`, which is not portable frontmatter (ADR 0019
59
59
  and PRD 7.2: these run on purpose, never automatically). The description
60
60
  carries that instead: the phrases that ask for it, and the note that answering
61
61
  a question in passing is not this.
@@ -60,7 +60,7 @@ confidence tiers and the null-result discipline in the skill itself, and leaves
60
60
  how the work is divided up to whoever is doing it.
61
61
 
62
62
  One departure: the original keeps itself from being invoked on its own with
63
- `disable-model-invocation: true`, which is not portable frontmatter (ADR 0009
63
+ `disable-model-invocation: true`, which is not portable frontmatter (ADR 0019
64
64
  and PRD 7.2: these run on purpose, never automatically). The description
65
65
  carries that instead: the phrases that ask for it, and the note that it is not
66
66
  something to start unasked before an ordinary change.
@@ -46,14 +46,15 @@ hangs off that anchor, and an investigation that never had one tends to answer
46
46
  a question nobody asked.
47
47
 
48
48
  Where the project keeps its history, start there. Who last touched these
49
- lines and in what change; the whole life of the file, including through
50
- renames; what the change said about itself; and whatever discussion is attached
51
- to it, which is usually where the reason actually is.
49
+ lines and in what change; as much of the file's life, through renames, as it
50
+ takes to reach the change that answers the question; what the change said about
51
+ itself; and whatever discussion is attached to it, which is usually where the
52
+ reason actually is.
52
53
 
53
54
  ## Then look where else this project keeps its reasons
54
55
 
55
56
  Different reasons live in different places, and which of these exist varies by
56
- project. Use the ones it has:
57
+ project. Use the ones it has, as far as the question needs:
57
58
 
58
59
  - **The history and its discussion**, where the project keeps them. Best for
59
60
  the reason given at the time by the person doing it, under review. For a
@@ -72,11 +73,15 @@ project. Use the ones it has:
72
73
  Best for defensive code: a guard, a retry, a limit, a special case usually
73
74
  exists because something happened.
74
75
 
75
- Look in them in parallel where you can, one pass per source with its own
76
- question. Where a source does not exist or you cannot reach it, that is a
77
- finding: write down that you could not look there. Skipping something because
78
- it is probably irrelevant is not the same as it being provably irrelevant, and
79
- the first one belongs in the write-up as a gap.
76
+ How many of them to open depends on what is still unanswered: once a change
77
+ and its discussion settle the reason, the rest adds nothing unless something
78
+ contradicts it, and they need neither a look nor a line in the write-up. While
79
+ the answer is open, look in the sources that could still change it, in parallel
80
+ where you can, one pass per source with its own question. Where one of those does
81
+ not exist or you cannot reach it, that is a finding: write down that you could
82
+ not look there. Skipping one of those because it is probably irrelevant is not
83
+ the same as it being provably irrelevant, and the first belongs in the write-up
84
+ as a gap.
80
85
 
81
86
  Watch for the newest change looking authoritative. The shape you are asking
82
87
  about is usually the accumulation of several decisions, and the most recent one
@@ -150,12 +155,18 @@ explanation of it.
150
155
 
151
156
  ## What you hand back
152
157
 
158
+ These are the questions the hand-back answers, not headings to print: a one-line
159
+ answer is one line, and it still carries its citation and any uncertainty or
160
+ search limit that would change how it is used.
161
+
153
162
  - **The question**, as you understood it.
154
163
  - **The code**, so the reader knows what is being explained.
155
164
  - **What was actually recorded**, cited.
156
165
  - **What follows from it**, marked as reasoning.
157
166
  - **Other explanations that fit**, where more than one does.
158
- - **What nobody wrote down**, with what you searched.
167
+ - **What nobody wrote down**, with what you searched: where the search
168
+ stopped, what settled it, and the places that could matter which you left
169
+ unchecked.
159
170
  - **Where you looked**, one line per source, including the ones that held
160
171
  nothing and the ones you could not reach, with the reason.
161
172
  - **How sure you are overall**, in a line.
package/src/book.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // The book: `sessions.yaml` in a bot's home. It is the authority for what the
2
- // kit knows about a bot's sessions (ADR 0002) — Orca forgets a tab's resume
2
+ // kit knows about a bot's sessions (ADR 0012) — Orca forgets a tab's resume
3
3
  // record the moment the tab is closed, so the kit keeps its own record.
4
4
  //
5
5
  // It is an ordinary file of the user's bots repo: committed, readable, and
@@ -254,7 +254,7 @@ const asRecord = (value) => (value !== null && typeof value === 'object' && !Arr
254
254
  /**
255
255
  * The session entry to keep once a harness has named the session it is running
256
256
  * as. The id the harness gave replaces the one the book held, and the one it
257
- * held goes into the history with the reason it was replaced (ADR 0002): old
257
+ * held goes into the history with the reason it was replaced (ADR 0012): old
258
258
  * ids are what recall, auditing and finops read later.
259
259
  *
260
260
  * The same id come back is a session resumed, not a new one, so nothing moves.
package/src/bot.js CHANGED
@@ -59,7 +59,7 @@ export function botNames(bots) {
59
59
  * link the user made takes it out of that folder; undefined when it stays in.
60
60
  *
61
61
  * The kit writes only in the bot folder, never in user-level settings (PRD 6.3,
62
- * ADR 0010), and a link would make the one the other without a word. So the
62
+ * ADR 0020), and a link would make the one the other without a word. So the
63
63
  * answer is the file system's, not the spelling's: see `whereItLeads`.
64
64
  */
65
65
  export function leadsOutside(home, target) {
package/src/cli.js CHANGED
@@ -9,15 +9,16 @@
9
9
 
10
10
  import { readFileSync, realpathSync } from 'node:fs';
11
11
  import path from 'node:path';
12
+ import { fileURLToPath } from 'node:url';
12
13
  import { parseArgs } from 'node:util';
13
14
 
14
- import { addSession, changeBot, changeSession, createBot, readBot, SESSION_FIELDS } from './bot.js';
15
+ import { addSession, changeBot, changeSession, createBot, leadsOutside, readBot, SESSION_FIELDS } from './bot.js';
15
16
  import { grooming } from './groom.js';
16
17
  import { checkHealth, orcaSettingFindings } from './health.js';
17
18
  import { initBots } from './init.js';
18
- import { APPROVALS, HARNESSES, shellWord } from './launch.js';
19
+ import { APPROVALS, HARNESSES, ownCli, shellWord, workDirOf } from './launch.js';
19
20
  import { checkMail, lookUp, sendMessage } from './message.js';
20
- import { orcaCli, orcaTrouble } from './orca.js';
21
+ import { orcaCli, orcaTrouble, RELOAD_LINE } from './orca.js';
21
22
  import { pauseSessions, unpauseSessions } from './pause.js';
22
23
  import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
23
24
  import { restartSessions } from './restart.js';
@@ -82,7 +83,8 @@ Usage:
82
83
  a bot: close its tabs, remove its Orca project and
83
84
  move its folder to retired/. It will not retire a
84
85
  bot whose Orca project holds a tab your book does
85
- not name.
86
+ not name, and moves the folder only once Orca no
87
+ longer lists the project.
86
88
  obk rules build --bots <path> [--bot <bot>]
87
89
  Build every bot's AGENTS.md from its charter and
88
90
  the rule units it carries, or just the one you
@@ -102,7 +104,10 @@ Usage:
102
104
  skills folder, or any path, and take away a link
103
105
  it made that no list names any more. What you put
104
106
  in a bot's skills directory yourself is left alone
105
- and shown as yours. It does not touch Orca.
107
+ and shown as yours. A bot whose links changed has
108
+ its running sessions told: Claude Code gets
109
+ /reload-skills typed in, and Codex takes the
110
+ change at its next turn.
106
111
  obk skills fetch --bots <path> [--source <name>]
107
112
  Clone the online sources skills.yaml lists, beside
108
113
  your bots folder and never inside it, each at the
@@ -380,6 +385,21 @@ function sameFleet(bots) {
380
385
  */
381
386
  const lookAt = (terminal) => `${shellWord(orcaCli())} terminal read --terminal ${terminal} --screen`;
382
387
 
388
+ /** The SETUP.md shipped with the running kit, whose section 5 has the answers. */
389
+ const SETUP = fileURLToPath(new URL('../SETUP.md', import.meta.url));
390
+
391
+ /**
392
+ * Who answers a tab left on a screen: the caller, not the user (PRD 6.5). The
393
+ * kit names where the answers are rather than carrying them, because which
394
+ * keys answer which screen is judgment that changes with every harness
395
+ * release, and that belongs in the skill (ADR 0016).
396
+ */
397
+ const ANSWER_IT = [
398
+ ` Answer what is on screen yourself, without asking the user: section 5 of ${SETUP}`,
399
+ ' and the obk-bot-building skill have the keys for the usual screens.',
400
+ ' Anything you do not recognise gets no keypress: take it to the user.',
401
+ ];
402
+
383
403
  /** The one command a harness runs rather than a person: the kit's hook. */
384
404
  const RECORD = 'session record';
385
405
 
@@ -389,7 +409,7 @@ const RECORD = 'session record';
389
409
  * A hook runs inside the user's own session, so this one stays out of the way:
390
410
  * it writes on standard output only what the harness is to read as JSON, and
391
411
  * whatever goes wrong, it goes wrong quietly. A book left stale is a thing the
392
- * health check finds later; a session disturbed is the user's work (ADR 0010).
412
+ * health check finds later; a session disturbed is the user's work (ADR 0020).
393
413
  */
394
414
  async function record(bots, bot) {
395
415
  try {
@@ -409,19 +429,19 @@ const commands = {
409
429
  const seeded = initBots(bots, values.harness);
410
430
  // The typed path was for making the folder and naming it in a refusal; from
411
431
  // here on it is the fleet, and the fleet is its real path (#164).
412
- const { tabs, rules, skills, paused } = await bringUp(sameFleet(seeded.bots), { bot: BOT_FATHER });
432
+ const { tabs, rules, skills, paused, projects } = await bringUp(sameFleet(seeded.bots), { bot: BOT_FATHER });
413
433
  // Setup is the other place the PRD asks for Orca's own launch arguments to
414
434
  // be looked at (6.5), and the one where the user is still standing in front
415
435
  // of the fleet they are making. Only that one check: a folder init has just
416
436
  // made has nothing else to say about itself.
417
- const answer = { bots: seeded.bots, created: seeded.created, completed: seeded.completed, rules, skills, tabs, paused, found: orcaSettingFindings() };
437
+ const answer = { bots: seeded.bots, created: seeded.created, completed: seeded.completed, rules, skills, tabs, paused, projects, found: orcaSettingFindings() };
418
438
  return { answer, lines: tabLines(answer, `Bot Father is up in Orca. Your bots folder: ${seeded.bots}`) };
419
439
  },
420
440
 
421
441
  async up(bots, values) {
422
442
  refuseWhenOrcaIsDown();
423
- const { tabs, rules, skills, paused } = await bringUp(bots, { bot: values.bot, session: values.session });
424
- const answer = { bots, created: [], completed: [], rules, skills, tabs, paused };
443
+ const { tabs, rules, skills, paused, projects } = await bringUp(bots, { bot: values.bot, session: values.session });
444
+ const answer = { bots, created: [], completed: [], rules, skills, tabs, paused, projects };
425
445
  const up = [...new Set(tabs.map((tab) => tab.bot))];
426
446
  const summary = up.length === 0
427
447
  ? `Nothing was brought up in Orca. Your bots folder: ${bots}`
@@ -448,8 +468,8 @@ const commands = {
448
468
 
449
469
  async restart(bots, values) {
450
470
  refuseWhenOrcaIsDown();
451
- const { closed, tabs, rules, skills, paused } = await restartSessions(bots, { bot: values.bot, session: values.session });
452
- const answer = { bots, created: [], completed: [], rules, skills, tabs, paused, closed };
471
+ const { closed, tabs, rules, skills, paused, projects } = await restartSessions(bots, { bot: values.bot, session: values.session });
472
+ const answer = { bots, created: [], completed: [], rules, skills, tabs, paused, projects, closed };
453
473
  const what = values.session === undefined ? values.bot : `${values.bot} ${values.session}`;
454
474
  return {
455
475
  answer,
@@ -470,7 +490,7 @@ const commands = {
470
490
  lines: [
471
491
  ...paused.closed.map((tab) => `closed ${tab.bot} ${tab.name} tab ${tab.tabId} terminal ${tab.terminal}`),
472
492
  `${paused.changed ? 'paused' : 'there'.padEnd(6)} ${what}${paused.changed ? '' : ' was paused already'}`,
473
- `obk up leaves it closed, and the book keeps its conversations. Bring it back: obk unpause --bots ${bots} --bot ${values.bot}${back}`,
493
+ `obk up leaves it closed, and the book keeps its conversations. Bring it back: ${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${values.bot}${back}`,
474
494
  ],
475
495
  };
476
496
  },
@@ -502,11 +522,25 @@ const commands = {
502
522
  }
503
523
 
504
524
  const retired = await retireBot(bots, { bot: values.bot });
525
+ if (retired.trouble !== undefined) {
526
+ return {
527
+ answer: { bots, ...retired },
528
+ lines: [
529
+ ...closedLines(retired.closed),
530
+ `${'trouble'.padEnd(9)} Orca project ${retired.project}`,
531
+ ` ${retired.trouble}`,
532
+ `Retire it again: ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${retired.bot}`,
533
+ ],
534
+ code: 1,
535
+ };
536
+ }
505
537
  return {
506
538
  answer: { bots, ...retired },
507
539
  lines: [
508
540
  ...closedLines(retired.closed),
509
- ...(retired.project === undefined ? [] : [`removed Orca project ${retired.project}`]),
541
+ // After every removal, whatever the window was told: Orca's window can
542
+ // keep a removed project's row even after it re-reads (stablyai/orca#20102).
543
+ ...(retired.project === undefined ? [] : [`removed Orca project ${retired.project}`, RELOAD_LINE]),
510
544
  `retired ${retired.bot}: moved to ${path.relative(bots, retired.moved)}, with its book, charter and memory`,
511
545
  ],
512
546
  };
@@ -532,8 +566,8 @@ const commands = {
532
566
  ...rulesLines(rules, bots),
533
567
  ...skillsLines(skills),
534
568
  trouble
535
- ? `${made.bot} is written, and its rules are not. Settle what the line above says, then: obk rules build --bots ${bots} --bot ${made.bot}`
536
- : `${made.bot} is written. Give it a session: obk session add --bots ${bots} --bot ${made.bot} --name <name>`,
569
+ ? `${made.bot} is written, and its rules are not. Settle what the line above says, then: ${shellWord(ownCli())} rules build --bots ${shellWord(bots)} --bot ${made.bot}`
570
+ : `${made.bot} is written. Give it a session: ${shellWord(ownCli())} session add --bots ${shellWord(bots)} --bot ${made.bot} --name <name>`,
537
571
  ],
538
572
  code: trouble ? 1 : 0,
539
573
  };
@@ -554,7 +588,7 @@ const commands = {
554
588
  `changed the charter in ${path.join('bots', changed.bot, 'bot.yaml')}`,
555
589
  ...rulesLines(rules, bots),
556
590
  trouble
557
- ? `${changed.bot}'s charter is written, and its rules are not. Settle what the line above says, then: obk rules build --bots ${bots} --bot ${changed.bot}`
591
+ ? `${changed.bot}'s charter is written, and its rules are not. Settle what the line above says, then: ${shellWord(ownCli())} rules build --bots ${shellWord(bots)} --bot ${changed.bot}`
558
592
  : `${changed.bot}'s charter is changed. A session that is running read the old one when it started; it reads this one when it next starts.`,
559
593
  ],
560
594
  code: trouble ? 1 : 0,
@@ -584,7 +618,7 @@ const commands = {
584
618
  added.state === 'added'
585
619
  ? `added ${added.skill} to ${path.join('bots', added.bot, 'bot.yaml')}`
586
620
  : `there ${added.skill} is on ${added.bot}'s list already, and nothing was written`,
587
- `Link it: obk skills build --bots ${bots} --bot ${added.bot}`,
621
+ `Link it: ${shellWord(ownCli())} skills build --bots ${shellWord(bots)} --bot ${added.bot}`,
588
622
  ],
589
623
  };
590
624
  },
@@ -597,7 +631,7 @@ const commands = {
597
631
  removed.state === 'removed'
598
632
  ? `removed ${removed.skill} from ${path.join('bots', removed.bot, 'bot.yaml')}`
599
633
  : `absent ${removed.skill} is not on ${removed.bot}'s list, and nothing was written`,
600
- `Unlink it: obk skills build --bots ${bots} --bot ${removed.bot}`,
634
+ `Unlink it: ${shellWord(ownCli())} skills build --bots ${shellWord(bots)} --bot ${removed.bot}`,
601
635
  ],
602
636
  };
603
637
  },
@@ -611,7 +645,7 @@ const commands = {
611
645
  ...Object.entries(source)
612
646
  .filter(([key]) => key !== 'name')
613
647
  .map(([key, value]) => ` ${key.padEnd(5)} ${value}`),
614
- `Fetch it: obk skills fetch --bots ${bots} --source ${source.name}`,
648
+ `Fetch it: ${shellWord(ownCli())} skills fetch --bots ${shellWord(bots)} --source ${source.name}`,
615
649
  ],
616
650
  };
617
651
  },
@@ -746,7 +780,8 @@ const commands = {
746
780
 
747
781
  'session add'(bots, values) {
748
782
  const added = addSession(bots, values.bot, settingsOf(values));
749
- const answer = { bots, bot: added.bot, home: added.home, session: added.session };
783
+ const found = workDirFound(values, added.bot, added.home, added.session.name);
784
+ const answer = { bots, bot: added.bot, home: added.home, session: added.session, found };
750
785
  return {
751
786
  answer,
752
787
  lines: [
@@ -754,7 +789,8 @@ const commands = {
754
789
  ...Object.entries(added.session)
755
790
  .filter(([key]) => key !== 'name')
756
791
  .map(([key, value]) => ` ${key} ${oneLine(value)}`),
757
- `Bring it up: obk up --bots ${bots} --bot ${added.bot}`,
792
+ ...foundLines(found),
793
+ `Bring it up: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${added.bot}`,
758
794
  ],
759
795
  };
760
796
  },
@@ -765,14 +801,16 @@ const commands = {
765
801
  }
766
802
  const { name, ...settings } = settingsOf(values);
767
803
  const changed = changeSession(bots, values.bot, values.session, settings);
768
- const restart = `obk restart --bots ${bots} --bot ${changed.bot} --session ${values.session}`;
804
+ const restart = `${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${changed.bot} --session ${values.session}`;
805
+ const found = workDirFound(values, changed.bot, changed.home, values.session);
769
806
  return {
770
- answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, restart },
807
+ answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, restart, found },
771
808
  lines: [
772
809
  `changed session ${values.session} in ${path.join('bots', changed.bot, 'bot.yaml')}`,
773
810
  ...Object.entries(changed.session)
774
811
  .filter(([key]) => key !== 'name')
775
812
  .map(([key, value]) => ` ${key} ${oneLine(value)}`),
813
+ ...foundLines(found),
776
814
  `A running session takes this when it next starts: ${restart}`,
777
815
  ],
778
816
  };
@@ -794,7 +832,7 @@ function toLines(answer, bots, where) {
794
832
  ...(answer.unnamed === true
795
833
  ? [` ${where} is a Claude session running under no name the kit gave it: it was started before the kit named sessions, and nothing renames a live harness. It gets one the next time it starts. Until then the mailbox is the road that reaches it.`]
796
834
  : []),
797
- ` Send it: obk message send --bots ${bots} --to ${where} --subject <text> --text <text>`,
835
+ ` Send it: ${shellWord(ownCli())} message send --bots ${shellWord(bots)} --to ${shellWord(where)} --subject <text> --text <text>`,
798
836
  ];
799
837
  }
800
838
 
@@ -814,6 +852,23 @@ function nudgeLine(answer, where) {
814
852
  return ` ${where} is not up, so nothing was typed anywhere: the message waits in its mailbox.`;
815
853
  }
816
854
 
855
+ /**
856
+ * What there is to say about the work dir this command was given: one finding
857
+ * when it leads out of the bot home, through `..` or a link or as a path
858
+ * elsewhere, and nothing otherwise (#222). A bot's work and its clones belong
859
+ * under its own `work/`. It is said, not refused: the user may ask for a folder
860
+ * anywhere (PRD 6.3), and it is written as they gave it.
861
+ */
862
+ function workDirFound(values, bot, home, session) {
863
+ const workDir = workDirOf({ work_dir: values['work-dir'] }, home);
864
+ if (workDir === undefined || leadsOutside(home, workDir) === undefined) return [];
865
+ return [{
866
+ kind: 'work-dir',
867
+ where: workDir,
868
+ says: `${workDir} is outside ${bot}'s folder, ${home}. A session's work and every clone it needs go under the bot's work/, as work/${session}, unless you asked for this place in plain words. It is written as given, and nothing was moved.`,
869
+ }];
870
+ }
871
+
817
872
  /** The settings a `session add` was given, as they go into bot.yaml. */
818
873
  function settingsOf(values) {
819
874
  const settings = { name: values.name };
@@ -938,15 +993,15 @@ function groomLines(groom, bots) {
938
993
  if (!groom.exists) {
939
994
  return [
940
995
  `${'groom'.padEnd(9)} there is no daily grooming yet`,
941
- `Make one: obk groom --bots ${bots} --at 04:00`,
996
+ `Make one: ${shellWord(ownCli())} groom --bots ${shellWord(bots)} --at 04:00`,
942
997
  ];
943
998
  }
944
999
 
945
1000
  return [
946
1001
  `${'groom'.padEnd(9)} daily at ${groom.at} ${groom.enabled ? 'on' : 'off'}`,
947
1002
  groom.enabled
948
- ? `It runs every day at ${groom.at} and spends tokens each time. Turn it off with: obk groom --bots ${bots} --off`
949
- : `It is not running yet. Try it by hand, read what it gives you, then: obk groom --bots ${bots} --on`,
1003
+ ? `It runs every day at ${groom.at} and spends tokens each time. Turn it off with: ${shellWord(ownCli())} groom --bots ${shellWord(bots)} --off`
1004
+ : `It is not running yet. Try it by hand, read what it gives you, then: ${shellWord(ownCli())} groom --bots ${shellWord(bots)} --on`,
950
1005
  ];
951
1006
  }
952
1007
 
@@ -980,7 +1035,7 @@ function rosterLines(roster, bots) {
980
1035
  }
981
1036
 
982
1037
  lines.push(roster.length === 0
983
- ? `No bots yet. Make one: obk bot create --bots ${bots} --name <name> --harness claude|codex`
1038
+ ? `No bots yet. Make one: ${shellWord(ownCli())} bot create --bots ${shellWord(bots)} --name <name> --harness claude|codex`
984
1039
  : `${roster.length} bot${roster.length === 1 ? '' : 's'}. Your bots folder: ${bots}`);
985
1040
  return lines;
986
1041
  }
@@ -1047,9 +1102,24 @@ function skillsLines(skills) {
1047
1102
  ...(entry.trouble === undefined ? [] : [` ${'trouble'.padEnd(9)} ${entry.trouble}`]),
1048
1103
  ...entry.skills.map((skill) => ` ${(skill.managed ? 'linked' : 'yours').padEnd(9)} ${skill.name.padEnd(24)} ${skill.from ?? 'no list names it; the kit leaves it alone'}`),
1049
1104
  ...(entry.removed ?? []).map((name) => ` ${'removed'.padEnd(9)} ${name.padEnd(24)} no list names it now`),
1105
+ ...(entry.sessions ?? []).map((one) => toldLine(entry.bot, one)),
1050
1106
  ]);
1051
1107
  }
1052
1108
 
1109
+ /** What `skills build` did about one running session of a bot whose skills changed. */
1110
+ function toldLine(bot, { session, harness, state, read, blocked, trouble }) {
1111
+ const who = `${bot}/${session}`;
1112
+ const after = harness === 'claude' ? 'Once that is settled, type /reload-skills in its tab.' : 'Its next turn takes the change by itself.';
1113
+ const [word, says] = {
1114
+ reloaded: ['reloaded', '/reload-skills was typed into its tab; a busy session runs it when its turn ends'],
1115
+ 'next-turn': ['next turn', `Codex takes the change at the start of its next turn. If a skill is not there then, a restart makes it appear${read?.length > 0 ? `; meanwhile it can read ${read.join(', ')}` : ''}`],
1116
+ 'not-up': ['not told', 'not up; it reads its skills when it starts'],
1117
+ blocked: ['not told', `its tab is waiting for an answer (${blocked}), so nothing was typed. ${after}`],
1118
+ unknown: ['not told', `${trouble}. ${after}`],
1119
+ }[state];
1120
+ return ` ${word.padEnd(9)} ${who.padEnd(24)} ${says}`;
1121
+ }
1122
+
1053
1123
  /**
1054
1124
  * What a check found, two lines each: what kind of trouble it is and the one
1055
1125
  * thing to go and look at, then the sentence about it. The same shape wherever
@@ -1058,7 +1128,7 @@ function skillsLines(skills) {
1058
1128
  const foundLines = (found) =>
1059
1129
  found.flatMap((one) => [`${one.kind.padEnd(9)} ${one.where}`, ` ${one.says}`]);
1060
1130
 
1061
- function tabLines({ bots, created, completed, rules, skills, tabs, paused = [], found = [] }, summary) {
1131
+ function tabLines({ bots, created, completed, rules, skills, tabs, paused = [], projects = [], found = [] }, summary) {
1062
1132
  const lines = [
1063
1133
  ...created.map((entry) => `created ${entry}`),
1064
1134
  ...completed.map((entry) => `completed ${entry}`),
@@ -1073,12 +1143,15 @@ function tabLines({ bots, created, completed, rules, skills, tabs, paused = [],
1073
1143
  for (const one of paused) {
1074
1144
  const what = one.session === undefined ? one.bot : `${one.bot} ${one.session}`;
1075
1145
  const back = one.session === undefined ? '' : ` --session ${one.session}`;
1076
- lines.push(`${'paused'.padEnd(9)} ${what} left closed. Bring it back: obk unpause --bots ${bots} --bot ${one.bot}${back}`);
1146
+ lines.push(`${'paused'.padEnd(9)} ${what} left closed. Bring it back: ${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${one.bot}${back}`);
1077
1147
  }
1078
1148
 
1079
1149
  // Last before the summary, because what a check found is about the setup the
1080
1150
  // run has just left behind rather than about any one thing it did.
1081
- lines.push(...foundLines(found), summary);
1151
+ lines.push(...foundLines(found));
1152
+ // Hedged, because nothing here can see whether the window was told (#224).
1153
+ if (projects.length > 0) lines.push(RELOAD_LINE);
1154
+ lines.push(summary);
1082
1155
  return lines;
1083
1156
  }
1084
1157
 
@@ -1103,15 +1176,26 @@ function harnessLines(tab, bots) {
1103
1176
  ...how,
1104
1177
  ' the harness was typed in, and no session came up in the tab.',
1105
1178
  ` Look at it: ${lookAt(tab.terminal)}`,
1179
+ ...ANSWER_IT,
1106
1180
  ];
1107
1181
  }
1108
1182
 
1183
+ // Orca naming no reason does not mean nothing is on screen: on 1.4.209 it
1184
+ // names none for Codex's trust and hooks screens, nor for Claude Code's trust
1185
+ // list (tech notes, section 1, #288). So the kit says what it cannot see.
1109
1186
  const lines = tab.blockedReason === undefined
1110
- ? [...how, ' the harness was typed in and came up.']
1187
+ ? [
1188
+ ...how,
1189
+ ' the harness was typed in and is running. The kit cannot see whether a screen',
1190
+ ' in it is waiting for an answer.',
1191
+ ` Look at it: ${lookAt(tab.terminal)}`,
1192
+ ...ANSWER_IT,
1193
+ ]
1111
1194
  : [
1112
1195
  ...how,
1113
1196
  ` the harness was typed in and came up, waiting on: ${tab.blockedReason}`,
1114
1197
  ` Look at it: ${lookAt(tab.terminal)}`,
1198
+ ...ANSWER_IT,
1115
1199
  ];
1116
1200
 
1117
1201
  if (tab.promptSent === true) lines.push(' the start prompt was typed in.');
package/src/groom.js CHANGED
@@ -22,6 +22,7 @@ import path from 'node:path';
22
22
 
23
23
  import { botDir, readBot, requireBotsFolder } from './bot.js';
24
24
  import { readBook, takeLock } from './book.js';
25
+ import { ownCli, shellWord } from './launch.js';
25
26
  import { orca } from './orca.js';
26
27
  import { BOT_FATHER } from './up.js';
27
28
 
@@ -35,10 +36,11 @@ const AT = /^([01]\d|2[0-3]):([0-5]\d)$/;
35
36
  const prompt = (bots) =>
36
37
  'Run the daily grooming for this fleet, with the obk-grooming skill. Fix this '
37
38
  + 'run\'s end now and read what has happened since the last run\'s end up to it, '
38
- + 'counting with obk usage --since <last end> --until <this end>, and write this '
39
+ + `counting with ${shellWord(ownCli())} usage --since <last end> --until <this end>, and write this `
39
40
  + 'end down for the next run. Work out what it cost with obk-finops, keep '
40
41
  + 'each bot\'s profile notes, and send one short report to Bot Father\'s '
41
- + `management session. The bots folder is ${bots}.`;
42
+ + `management session. The kit here is ${shellWord(ownCli())}: run it wherever a skill says obk. `
43
+ + `The bots folder is ${bots}.`;
42
44
 
43
45
  /**
44
46
  * Say what the grooming is, and make it so when asked.
@@ -66,7 +68,7 @@ export function grooming(bots, { at, on } = {}) {
66
68
  const bot = readBot(home, BOT_FATHER);
67
69
  const book = readBook(home);
68
70
  if (book.orca.project === undefined) {
69
- throw new Error(`${BOT_FATHER} has no Orca project yet, so there is nothing for the grooming to run in. Run obk up --bots ${bots} --bot ${BOT_FATHER} first.`);
71
+ throw new Error(`${BOT_FATHER} has no Orca project yet, so there is nothing for the grooming to run in. Run ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${BOT_FATHER} first.`);
70
72
  }
71
73
 
72
74
  // Looking and making are one turn. Orca will not stop two runs creating two