@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/skills/obk-tdd/SKILL.md
CHANGED
|
@@ -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
|
|
39
|
-
|
|
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
|
|
310
|
-
|
|
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.
|
|
327
|
-
|
|
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.
|
|
410
|
-
the work changed (money, permissions, eligibility,
|
|
411
|
-
lost, and whatever else the requirement cares most
|
|
412
|
-
look at the tests, each one run, each one expected to
|
|
413
|
-
one put back.
|
|
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
|
-
|
|
433
|
-
|
|
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
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
written for people
|
|
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
|
|
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
|
|
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.
|
package/skills/obk-why/NOTICE.md
CHANGED
|
@@ -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
|
|
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.
|
package/skills/obk-why/SKILL.md
CHANGED
|
@@ -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;
|
|
50
|
-
|
|
51
|
-
to it, which is usually where the
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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:
|
|
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
|
-
|
|
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:
|
|
536
|
-
: `${made.bot} is written. Give it a session:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
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
|
-
|
|
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 =
|
|
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:
|
|
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:
|
|
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:
|
|
949
|
-
: `It is not running yet. Try it by hand, read what it gives you, then:
|
|
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:
|
|
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:
|
|
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)
|
|
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
|
-
? [
|
|
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
|
-
+
|
|
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
|
|
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
|
|
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
|