@assuredloop/orca-bot-kit 0.6.0 → 0.7.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 +5 -2
- package/SETUP.md +3 -2
- package/package.json +1 -1
- package/rules/commit.md +18 -0
- package/skills/obk-bot-building/SKILL.md +6 -2
- package/skills/obk-fleet-review/SKILL.md +2 -1
- package/skills/obk-grooming/SKILL.md +2 -1
- package/src/bot.js +1 -1
- package/src/cli.js +1 -1
- package/src/health.js +29 -7
- package/src/hooks.js +3 -3
- package/src/orca.js +16 -3
- package/src/record.js +25 -11
- package/src/skills.js +1 -1
- package/src/up.js +2 -2
package/README.md
CHANGED
|
@@ -340,7 +340,7 @@ with why that one ended and when
|
|
|
340
340
|
|
|
341
341
|
The book stays true to the harness through a hook `obk up` puts in the bot's own
|
|
342
342
|
folder — `.claude/settings.json` or `.codex/hooks.json`, never your user-level
|
|
343
|
-
settings ([ADR
|
|
343
|
+
settings ([ADR 0022](docs/adr/0022-kit-hooks-live-in-the-bot-folder.md)). Your own
|
|
344
344
|
settings in those files are kept. Codex asks you to trust a hooks file the first
|
|
345
345
|
time it sees one; answer its question in the tab.
|
|
346
346
|
|
|
@@ -545,6 +545,7 @@ failing quietly.
|
|
|
545
545
|
npm test # the whole suite, in a few minutes
|
|
546
546
|
npm run test:system # what the system tests would drive, and nothing else
|
|
547
547
|
npm run test:system -- --yes # drive them, on this machine, for real
|
|
548
|
+
npm run test:system -- --yes test/system/groom.test.js # only the ones you name
|
|
548
549
|
npm run mutate # the mutation audit, on what your branch changed
|
|
549
550
|
```
|
|
550
551
|
|
|
@@ -570,7 +571,9 @@ which Orca, which files, and what they will do to it — and then stops without
|
|
|
570
571
|
driving any of it, answering non-zero so that a run which did not run them
|
|
571
572
|
cannot be read as one that passed. `-- --yes` is how you say you meant it. When
|
|
572
573
|
Orca is not answering, the command says which Orca it asked and skips, rather
|
|
573
|
-
than report a kit that is not broken.
|
|
574
|
+
than report a kit that is not broken. Name files after it to run only those;
|
|
575
|
+
each must be one of the `*.test.js` files under `test/system/`, links followed,
|
|
576
|
+
and a name that is not is refused before anything runs.
|
|
574
577
|
|
|
575
578
|
`npm run mutate` runs [StrykerJS](https://stryker-mutator.io) over the
|
|
576
579
|
JavaScript this branch changed against `main` — committed, still in the working
|
package/SETUP.md
CHANGED
|
@@ -102,8 +102,9 @@ Ask both in one message, then wait.
|
|
|
102
102
|
|
|
103
103
|
**Where the bots folder goes.** An absolute path. It becomes a git repository of
|
|
104
104
|
theirs holding every bot's configuration: charters, rules, session settings, the
|
|
105
|
-
book of sessions. It holds none of the kit's code.
|
|
106
|
-
|
|
105
|
+
book of sessions. It holds none of the kit's code. The bots commit their own
|
|
106
|
+
changes to it; the kit's commands never commit, and pushing it somewhere is
|
|
107
|
+
theirs to decide.
|
|
107
108
|
|
|
108
109
|
**Which harness Bot Father runs on**, `claude` or `codex`. There is no default
|
|
109
110
|
and the kit will not guess. It is Bot Father's own harness only; each bot they
|
package/package.json
CHANGED
package/rules/commit.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: commit
|
|
3
|
+
title: Commit your changes
|
|
4
|
+
applies: all
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
Commit your own changes to the bots repo often, as each piece of work there is
|
|
8
|
+
done: what you changed, in whatever folder it is, and what the kit wrote in
|
|
9
|
+
your own bot's folder (its files there and your book of sessions). Stage each
|
|
10
|
+
file by name and name them in the commit too (`git commit -- <files>`), so
|
|
11
|
+
nothing another session staged goes in with yours. What no bot owns (init's
|
|
12
|
+
files, the defaults, the user's rules and skill lists) is Bot Father's to
|
|
13
|
+
commit. Commit yours even when the repo has no commit yet or holds files that
|
|
14
|
+
are not yours; leave those to their owner.
|
|
15
|
+
|
|
16
|
+
Committing is not pushing: pushing is the user's call. A clone in your work dir
|
|
17
|
+
follows its own repo's rules. When a commit fails, for example on another
|
|
18
|
+
session's index lock, try again, and say so if it still fails.
|
|
@@ -214,8 +214,12 @@ index load at every start. Codex's memories belong to the user and are shared
|
|
|
214
214
|
by every Codex session, so a Codex bot cannot have its own; say so before
|
|
215
215
|
turning them on for one. It is a hand edit, and say that you made it.
|
|
216
216
|
|
|
217
|
-
After each change, commit what it changed in the bots folder
|
|
218
|
-
|
|
217
|
+
After each change, commit what it changed in the bots folder, naming each
|
|
218
|
+
file when you stage it and again in the commit. Commit with it what no bot
|
|
219
|
+
owns and nobody has committed yet: `init`'s files, `defaults.yaml`,
|
|
220
|
+
`skills.yaml`, and the user's `rules/` and `skills/`. What it changed
|
|
221
|
+
includes the folder of a bot it made or changed. The kit never commits by
|
|
222
|
+
itself.
|
|
219
223
|
|
|
220
224
|
Do the reversible part and stop at the line. Making a bot, adding a session
|
|
221
225
|
and linking skills add things and are safe to run again; closing a tab,
|
|
@@ -251,6 +251,7 @@ identifiers, counts and paths that only show the work; where evidence matters,
|
|
|
251
251
|
say where it is.
|
|
252
252
|
|
|
253
253
|
Say what you did not look at. Where you changed anything in the bots folder,
|
|
254
|
-
commit it
|
|
254
|
+
commit it, naming each file when you stage it and again in the commit; the
|
|
255
|
+
kit never commits by itself.
|
|
255
256
|
|
|
256
257
|
Sources and licences: [NOTICE.md](NOTICE.md).
|
|
@@ -63,7 +63,8 @@ them says so and reads less, not everything.
|
|
|
63
63
|
The open findings are what stops the same thing being reported every day:
|
|
64
64
|
before filing an issue or sending feedback, look there, and where it is already
|
|
65
65
|
open, add to it rather than raising it again. Take a finding off when it is
|
|
66
|
-
fixed or no longer true. Commit these
|
|
66
|
+
fixed or no longer true. Commit these at the end of each run, naming each
|
|
67
|
+
file when you stage it and again in the commit.
|
|
67
68
|
|
|
68
69
|
## Keep the run bounded
|
|
69
70
|
|
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 0022), 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
|
@@ -414,7 +414,7 @@ const RECORD = 'session record';
|
|
|
414
414
|
* A hook runs inside the user's own session, so this one stays out of the way:
|
|
415
415
|
* it writes on standard output only what the harness is to read as JSON, and
|
|
416
416
|
* whatever goes wrong, it goes wrong quietly. A book left stale is a thing the
|
|
417
|
-
* health check finds later; a session disturbed is the user's work (ADR
|
|
417
|
+
* health check finds later; a session disturbed is the user's work (ADR 0022).
|
|
418
418
|
*/
|
|
419
419
|
async function record(bots, bot) {
|
|
420
420
|
try {
|
package/src/health.js
CHANGED
|
@@ -25,8 +25,9 @@ import { bookFile, readBook, sessionIdsIn, tabIdsIn } from './book.js';
|
|
|
25
25
|
import { botDir, botNames, botsDir, readBot, unknownKeys } from './bot.js';
|
|
26
26
|
import { transcriptsIn } from './conversations.js';
|
|
27
27
|
import { hookTrouble } from './hooks.js';
|
|
28
|
-
import { bypassFlags, harnessOf, HARNESSES, ownCli, sessionTrouble, shellWord } from './launch.js';
|
|
29
|
-
import { frontOfTab, orcaDefaultArgs, projects, tabs } from './orca.js';
|
|
28
|
+
import { bypassFlags, harnessOf, HARNESSES, ownCli, sessionTrouble, SHELL_ENV, shellWord } from './launch.js';
|
|
29
|
+
import { frontOfTab, orcaDefaultArgs, projects, tabs, wordsOfProcess } from './orca.js';
|
|
30
|
+
import { TAB_ENV } from './record.js';
|
|
30
31
|
import { agentsTrouble, rulesStamp } from './rules.js';
|
|
31
32
|
import { settingsInUse } from './settings.js';
|
|
32
33
|
import { skillsTrouble } from './skills.js';
|
|
@@ -215,8 +216,9 @@ const hooksOf = (bots, home, bot) => [...new Set(bot.sessions.map((session) => h
|
|
|
215
216
|
/**
|
|
216
217
|
* Each session of this bot running in a tab Orca has, with what it runs on set
|
|
217
218
|
* beside what the bot asks for now, added to `sessions`; a finding for each
|
|
218
|
-
* one that runs on something else (#271, #272)
|
|
219
|
-
*
|
|
219
|
+
* one that runs on something else (#271, #272), or whose harness the kit's
|
|
220
|
+
* launch line did not start (#318); and a finding for each one whose tab is
|
|
221
|
+
* open with its harness gone from it (#300).
|
|
220
222
|
*
|
|
221
223
|
* Two questions, each answered from where the answer is written. The settings
|
|
222
224
|
* from the harness's own record of the conversation the session is in now,
|
|
@@ -270,8 +272,15 @@ function runningOn(bots, home, bot, book, handles, sessions) {
|
|
|
270
272
|
if (running !== 'yes') continue;
|
|
271
273
|
|
|
272
274
|
const off = Object.entries(settings).filter(([, one]) => one.state === 'mismatch');
|
|
273
|
-
|
|
274
|
-
|
|
275
|
+
const parts = off.map(([name, one]) => `${name}: bot.yaml asks for ${one.configured}, and it runs on ${one.observed}`).join('; ');
|
|
276
|
+
if (startedByKit(front.pid, entry.tab) === false) {
|
|
277
|
+
// Orca brings its tabs back by itself after a restart or an update, with
|
|
278
|
+
// a bare resume and none of the kit's launch line (#318). What health can
|
|
279
|
+
// see may still match, by the harness's own defaults; the rest of what
|
|
280
|
+
// bot.yaml asks for is not on it either way.
|
|
281
|
+
const drift = off.length === 0 ? '' : ` ${parts}, as the harness's own record of its conversation ${conversation} says.`;
|
|
282
|
+
found.push(finding('session', entry.tab, `${bot.name}'s session ${session.name} was not started by the kit: the harness in its tab ${entry.tab} carries nothing of the kit's launch line, as when Orca brings its tabs back by itself after a restart or an update. So it runs on the harness's own defaults and whatever Orca added, not on what ${path.join(home, 'bot.yaml')} asks for.${drift} ${restart} starts it on bot.yaml.`, bot.name));
|
|
283
|
+
} else if (off.length > 0) {
|
|
275
284
|
found.push(finding('session', file, `${bot.name}'s session ${session.name} does not run on what ${path.join(home, 'bot.yaml')} asks for. ${parts}. That is what the harness's own record of its conversation ${conversation} says. A session takes these when it starts, so if bot.yaml changed after it started, ${restart} starts it on them; if not, something else set them, such as a default of the harness's own or a change made inside the session.`, bot.name));
|
|
276
285
|
}
|
|
277
286
|
|
|
@@ -289,6 +298,19 @@ function runningOn(bots, home, bot, book, handles, sessions) {
|
|
|
289
298
|
return found;
|
|
290
299
|
}
|
|
291
300
|
|
|
301
|
+
/**
|
|
302
|
+
* Whether the kit's launch line started the process `pid` in the tab `tab`:
|
|
303
|
+
* true when it carries the marker the line puts there, false when it carries
|
|
304
|
+
* that tab's id and no marker, and undefined when its environment cannot be
|
|
305
|
+
* read (tech notes, section 1). Only a whole word counts: an argument, such as
|
|
306
|
+
* a start prompt, can hold anything.
|
|
307
|
+
*/
|
|
308
|
+
function startedByKit(pid, tab) {
|
|
309
|
+
const words = pid === undefined ? undefined : wordsOfProcess(pid);
|
|
310
|
+
if (words === undefined || !words.includes(`${TAB_ENV}=${tab}`)) return undefined;
|
|
311
|
+
return words.some((word) => new RegExp(`^${SHELL_ENV}=[0-9]+$`).test(word));
|
|
312
|
+
}
|
|
313
|
+
|
|
292
314
|
/**
|
|
293
315
|
* What Orca has of this bot against what the book says it should have: a
|
|
294
316
|
* session whose tab is gone, a conversation nobody claims, and a tab in the
|
|
@@ -357,7 +379,7 @@ function inOrca(bots, home, bot, setups, sessions) {
|
|
|
357
379
|
/**
|
|
358
380
|
* The conversations a harness has on record in this bot's folder that the book
|
|
359
381
|
* does not name anywhere. The hook is how an id reaches the book, and when it
|
|
360
|
-
* fails it fails quietly (ADR
|
|
382
|
+
* fails it fails quietly (ADR 0022), so this is where a stale book shows: the
|
|
361
383
|
* harness's own record set beside it (ADR 0012).
|
|
362
384
|
*/
|
|
363
385
|
function offTheBook(home, bot, book, onRecord) {
|
package/src/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The kit's own hook, in the bot's folder and nowhere else (ADR
|
|
1
|
+
// The kit's own hook, in the bot's folder and nowhere else (ADR 0022).
|
|
2
2
|
//
|
|
3
3
|
// Both harnesses read a hooks file out of the folder a session starts in, and
|
|
4
4
|
// every session starts at its bot home, so the bot folder is where the kit's
|
|
@@ -35,7 +35,7 @@ const TIMEOUT = 10;
|
|
|
35
35
|
* What the hook runs: the CLI that wrote it, by its own path, so the session
|
|
36
36
|
* reports to the kit that set it up and not to whatever `obk` PATH finds in the
|
|
37
37
|
* harness's shell (#220). `|| true` and a quiet stderr because a hook must never
|
|
38
|
-
* disturb the session it fires in (ADR
|
|
38
|
+
* disturb the session it fires in (ADR 0022): a kit that is not there, or a book
|
|
39
39
|
* that cannot be written, leaves the session alone and the health check finds
|
|
40
40
|
* the stale book later.
|
|
41
41
|
*/
|
|
@@ -124,7 +124,7 @@ export function hookTrouble(home, harness, { bots, bot }) {
|
|
|
124
124
|
/**
|
|
125
125
|
* What to say about a hooks file that a link takes out of the bot folder, or
|
|
126
126
|
* undefined when it stays in. The kit writes its hook in the bot folder and
|
|
127
|
-
* nowhere else (ADR
|
|
127
|
+
* nowhere else (ADR 0022); through a link like that it would be writing the
|
|
128
128
|
* user's own settings, so it does not, and says why.
|
|
129
129
|
*/
|
|
130
130
|
function outside(home, file) {
|
package/src/orca.js
CHANGED
|
@@ -346,8 +346,8 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
346
346
|
|
|
347
347
|
/**
|
|
348
348
|
* Who holds the terminal of the pane `ptyId`: `{ front: 'shell' }`,
|
|
349
|
-
* `{ front: 'program', command }` with the name of the process
|
|
350
|
-
* group in front, or `{ unreadable: <why> }`.
|
|
349
|
+
* `{ front: 'program', command, pid }` with the name and pid of the process
|
|
350
|
+
* leading the group in front, or `{ unreadable: <why> }`.
|
|
351
351
|
*
|
|
352
352
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
353
353
|
* gives that pid's terminal's foreground process group (ADR 0021).
|
|
@@ -374,7 +374,20 @@ function frontOf(ptyId) {
|
|
|
374
374
|
const front = psLine(own.tpgid);
|
|
375
375
|
if (front === undefined) return { unreadable: `ps could not read the process in front, pid ${own.tpgid}` };
|
|
376
376
|
const shell = front.ppid === pane && path.basename(own.comm) === 'login';
|
|
377
|
-
return shell ? { front: 'shell' } : { front: 'program', command: path.basename(front.comm) };
|
|
377
|
+
return shell ? { front: 'shell' } : { front: 'program', command: path.basename(front.comm), pid: own.tpgid };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The process `pid` as `ps -E` gives it, word by word: its command, its
|
|
382
|
+
* arguments, then its environment as `NAME=value` words. Undefined when it
|
|
383
|
+
* cannot be read. macOS gives a same-user harness's environment this way (tech
|
|
384
|
+
* notes, section 1); nothing in it says where the arguments end, so a caller
|
|
385
|
+
* looks for one whole word it knows.
|
|
386
|
+
*/
|
|
387
|
+
export function wordsOfProcess(pid) {
|
|
388
|
+
const asked = spawnSync(psCli(), ['-E', '-ww', '-o', 'command=', '-p', String(pid)], { encoding: 'utf8' });
|
|
389
|
+
if (asked.error || asked.status !== 0) return undefined;
|
|
390
|
+
return asked.stdout.trim().split(/\s+/);
|
|
378
391
|
}
|
|
379
392
|
|
|
380
393
|
/** One process as `ps` gives it, read only: `{ ppid, tpgid, comm }`, or undefined. */
|
package/src/record.js
CHANGED
|
@@ -166,22 +166,26 @@ function unclaimedFor(book, home, bot, tabId, id) {
|
|
|
166
166
|
}
|
|
167
167
|
|
|
168
168
|
/**
|
|
169
|
-
* Whether the harness that ran this hook is the session's own — the one the
|
|
170
|
-
*
|
|
169
|
+
* Whether the harness that ran this hook is the session's own — the one the
|
|
170
|
+
* tab's shell started — rather than a harness started inside the session.
|
|
171
171
|
*
|
|
172
172
|
* Proved live on both harnesses (tech notes, sections 2 and 3): a hook's process
|
|
173
173
|
* is the harness's own child, or the child of the shell the harness ran the hook
|
|
174
174
|
* command through. Either way the harness itself is the process just above, and
|
|
175
|
-
* the session's harness is the child of the shell
|
|
176
|
-
*
|
|
177
|
-
*
|
|
175
|
+
* the session's harness is the child of the tab's shell. A harness started
|
|
176
|
+
* inside the session sits further down a longer chain, under whatever shell the
|
|
177
|
+
* agent ran it from, so it can never answer this.
|
|
178
|
+
*
|
|
179
|
+
* The launch line names the tab's shell. A tab Orca brought back by itself has
|
|
180
|
+
* no launch line (#318), and there the tab's shell is the one `login` started
|
|
181
|
+
* for the pane, which is where Orca's own resume puts the harness too (tech
|
|
182
|
+
* notes, section 1).
|
|
178
183
|
*
|
|
179
184
|
* Anything it cannot establish is a no: a report that cannot be shown to be the
|
|
180
185
|
* session's own is not written down.
|
|
181
186
|
*/
|
|
182
187
|
export function startedTheSession(shellPid, pid = process.pid) {
|
|
183
|
-
if (!/^[0-9]+$/.test(String(shellPid
|
|
184
|
-
const shell = Number(shellPid);
|
|
188
|
+
if (shellPid !== undefined && !/^[0-9]+$/.test(String(shellPid))) return false;
|
|
185
189
|
|
|
186
190
|
const tree = processTree();
|
|
187
191
|
if (tree === undefined) return false;
|
|
@@ -190,11 +194,21 @@ export function startedTheSession(shellPid, pid = process.pid) {
|
|
|
190
194
|
const parent = mine === undefined ? undefined : tree.get(mine.ppid);
|
|
191
195
|
if (parent === undefined) return false;
|
|
192
196
|
|
|
193
|
-
if (
|
|
194
|
-
|
|
197
|
+
if (shellPid !== undefined) {
|
|
198
|
+
const shell = Number(shellPid);
|
|
199
|
+
if (parent.ppid === shell) return true;
|
|
200
|
+
if (!isShell(parent.command)) return false;
|
|
201
|
+
|
|
202
|
+
const above = tree.get(parent.ppid);
|
|
203
|
+
return above !== undefined && above.ppid === shell;
|
|
204
|
+
}
|
|
195
205
|
|
|
196
|
-
|
|
197
|
-
|
|
206
|
+
// With no shell named, which process is the harness has to be settled first:
|
|
207
|
+
// the one above the shell the hook ran through, or the hook's own parent.
|
|
208
|
+
const harness = isShell(parent.command) ? tree.get(parent.ppid) : parent;
|
|
209
|
+
const shell = harness === undefined ? undefined : tree.get(harness.ppid);
|
|
210
|
+
const pane = shell === undefined ? undefined : tree.get(shell.ppid);
|
|
211
|
+
return pane !== undefined && path.basename(pane.command) === 'login';
|
|
198
212
|
}
|
|
199
213
|
|
|
200
214
|
/** Every process on the machine, by pid: who its parent is and what it runs. */
|
package/src/skills.js
CHANGED
|
@@ -292,7 +292,7 @@ export function unlinkSkills(home) {
|
|
|
292
292
|
* user made, or one of the kit's that they have since repointed, is theirs.
|
|
293
293
|
*
|
|
294
294
|
* It is the kit's own file and lives beside the hook files it already keeps in
|
|
295
|
-
* the bot folder (ADR
|
|
295
|
+
* the bot folder (ADR 0022). A record that cannot be read is treated as an
|
|
296
296
|
* empty one: the worst that follows is that the kit leaves alone something it
|
|
297
297
|
* would otherwise have tidied up, which is the safe way round.
|
|
298
298
|
*/
|
package/src/up.js
CHANGED
|
@@ -94,7 +94,7 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
94
94
|
*
|
|
95
95
|
* Separate from the tabs because the order matters twice over. A session reads
|
|
96
96
|
* its rules, its skills and its hooks as it comes up, so all three have to be
|
|
97
|
-
* there before the tab is (PRD 6.6, ADR
|
|
97
|
+
* there before the tab is (PRD 6.6, ADR 0022) — and a restart closes a tab in
|
|
98
98
|
* between, so everything that can refuse must have refused before that. It
|
|
99
99
|
* refuses by throwing, exactly as `up` always has; a bot it leaves out of
|
|
100
100
|
* `running` is one whose sessions must not be started.
|
|
@@ -140,7 +140,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
140
140
|
|
|
141
141
|
// And the kit's hook goes into every bot folder before Orca is asked for
|
|
142
142
|
// anything, for the same reason: a harness reads its hooks when it comes up,
|
|
143
|
-
// so one written later would miss the session it was written for (ADR
|
|
143
|
+
// so one written later would miss the session it was written for (ADR 0022),
|
|
144
144
|
// and a bot folder the kit cannot write it into stops the run with nothing
|
|
145
145
|
// opened anywhere. Only the harnesses a bot actually runs on; a bot with no
|
|
146
146
|
// sessions gets none.
|