@assuredloop/orca-bot-kit 0.1.0 → 0.2.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/LICENSE +21 -0
- package/README.md +545 -3
- package/SETUP.md +270 -0
- package/package.json +32 -13
- package/rules/evidence.md +15 -0
- package/rules/finishing.md +20 -0
- package/rules/limits.md +18 -0
- package/rules/mail.md +17 -0
- package/rules/profiles.md +9 -0
- package/rules/review.md +14 -0
- package/rules/scope.md +20 -0
- package/rules/simple.md +14 -0
- package/rules/talk.md +13 -0
- package/rules/tests-first.md +15 -0
- package/rules/the-ask.md +19 -0
- package/skills/obk-arch/LICENSES.md +45 -0
- package/skills/obk-arch/NOTICE.md +134 -0
- package/skills/obk-arch/SKILL.md +387 -0
- package/skills/obk-bot-building/LICENSES.md +21 -0
- package/skills/obk-bot-building/NOTICE.md +113 -0
- package/skills/obk-bot-building/SKILL.md +235 -0
- package/skills/obk-debugging/LICENSES.md +67 -0
- package/skills/obk-debugging/NOTICE.md +115 -0
- package/skills/obk-debugging/SKILL.md +419 -0
- package/skills/obk-decision-memo/LICENSES.md +49 -0
- package/skills/obk-decision-memo/NOTICE.md +181 -0
- package/skills/obk-decision-memo/SKILL.md +241 -0
- package/skills/obk-finops/LICENSES.md +19 -0
- package/skills/obk-finops/NOTICE.md +77 -0
- package/skills/obk-finops/SKILL.md +146 -0
- package/skills/obk-fleet-review/LICENSES.md +52 -0
- package/skills/obk-fleet-review/NOTICE.md +104 -0
- package/skills/obk-fleet-review/SKILL.md +219 -0
- package/skills/obk-grilling/LICENSES.md +42 -0
- package/skills/obk-grilling/NOTICE.md +103 -0
- package/skills/obk-grilling/SKILL.md +217 -0
- package/skills/obk-grooming/LICENSES.md +52 -0
- package/skills/obk-grooming/NOTICE.md +111 -0
- package/skills/obk-grooming/SKILL.md +219 -0
- package/skills/obk-handoff/LICENSES.md +45 -0
- package/skills/obk-handoff/NOTICE.md +51 -0
- package/skills/obk-handoff/SKILL.md +119 -0
- package/skills/obk-personal-facilitation/LICENSES.md +53 -0
- package/skills/obk-personal-facilitation/NOTICE.md +62 -0
- package/skills/obk-personal-facilitation/SKILL.md +86 -0
- package/skills/obk-recall/LICENSES.md +42 -0
- package/skills/obk-recall/NOTICE.md +53 -0
- package/skills/obk-recall/SKILL.md +129 -0
- package/skills/obk-researching/LICENSES.md +67 -0
- package/skills/obk-researching/NOTICE.md +170 -0
- package/skills/obk-researching/SKILL.md +259 -0
- package/skills/obk-reviewing/LICENSES.md +66 -0
- package/skills/obk-reviewing/NOTICE.md +58 -0
- package/skills/obk-reviewing/SKILL.md +408 -0
- package/skills/obk-tdd/LICENSES.md +68 -0
- package/skills/obk-tdd/NOTICE.md +54 -0
- package/skills/obk-tdd/SKILL.md +516 -0
- package/skills/obk-tdd/mutation-tools.md +80 -0
- package/skills/obk-teach/LICENSES.md +42 -0
- package/skills/obk-teach/NOTICE.md +65 -0
- package/skills/obk-teach/SKILL.md +106 -0
- package/skills/obk-why/LICENSES.md +42 -0
- package/skills/obk-why/NOTICE.md +74 -0
- package/skills/obk-why/SKILL.md +169 -0
- package/skills/obk-writing/LICENSES.md +272 -0
- package/skills/obk-writing/NOTICE.md +161 -0
- package/skills/obk-writing/SKILL.md +254 -0
- package/src/book.js +268 -0
- package/src/bot.js +451 -0
- package/src/cli.js +1130 -0
- package/src/conversations.js +150 -0
- package/src/groom.js +157 -0
- package/src/health.js +309 -0
- package/src/hooks.js +202 -0
- package/src/init.js +314 -0
- package/src/launch.js +276 -0
- package/src/message.js +380 -0
- package/src/orca.js +291 -0
- package/src/pause.js +58 -0
- package/src/record.js +209 -0
- package/src/restart.js +192 -0
- package/src/retire.js +92 -0
- package/src/roster.js +71 -0
- package/src/rules.js +408 -0
- package/src/skills.js +629 -0
- package/src/sources.js +370 -0
- package/src/up.js +475 -0
- package/src/usage.js +401 -0
- package/bin/okb.js +0 -4
- package/src/index.js +0 -11
package/src/restart.js
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// `obk restart`: the one command in the kit that closes a tab.
|
|
2
|
+
//
|
|
3
|
+
// Everything else only adds. Closing a tab takes the user's screen away and
|
|
4
|
+
// Orca's resume record with it, so `up` never does it (PRD 6.5: restarts are
|
|
5
|
+
// avoided, not banned, and one happens when the user asks for it).
|
|
6
|
+
//
|
|
7
|
+
// When they do ask, the session has to come back as itself. The book holds the
|
|
8
|
+
// conversation each session is running (ADR 0002), so a restart is: close the
|
|
9
|
+
// tab the book names, then let `up` open a new one and resume that
|
|
10
|
+
// conversation. Nothing else about it is new.
|
|
11
|
+
//
|
|
12
|
+
// Safe is the whole of it, and it is three rules:
|
|
13
|
+
//
|
|
14
|
+
// 1. Only a tab the book itself names. Bot Father's ops tab, and anything
|
|
15
|
+
// else open in a bot's project, belongs to the user (PRD 6.2).
|
|
16
|
+
// 2. Never a tab whose conversation the kit could not name again. A live tab
|
|
17
|
+
// with no session id in the book is refused before anything is touched:
|
|
18
|
+
// the close would be the end of that conversation.
|
|
19
|
+
// 3. One tab at a time, by its own handle. Orca's `--worktree <sel> --all`
|
|
20
|
+
// ends every tab of a project and is never called from anywhere in the kit.
|
|
21
|
+
//
|
|
22
|
+
// And two things about timing, both of which cost a session when they are got
|
|
23
|
+
// wrong. Everything that can refuse has to refuse before the first close, which
|
|
24
|
+
// means making the preparations `up` makes rather than leaving them to it. And
|
|
25
|
+
// Orca answers a close before its own listing agrees, so the close has to be
|
|
26
|
+
// seen to have landed before `up` is handed the tabs — `up` finding a tab it
|
|
27
|
+
// was about to replace takes it for a session that is still running.
|
|
28
|
+
|
|
29
|
+
import { realpathSync } from 'node:fs';
|
|
30
|
+
import { setTimeout as pause } from 'node:timers/promises';
|
|
31
|
+
|
|
32
|
+
import { bookFile, readBook } from './book.js';
|
|
33
|
+
import { botDir, readBot } from './bot.js';
|
|
34
|
+
import { closeTab, findProject, tabs } from './orca.js';
|
|
35
|
+
import { botsNamed, bringUp, prepareBots, sessionsOf } from './up.js';
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Close the tabs of the bot's sessions, or of the one named, and bring them
|
|
39
|
+
* back. Returns `{ closed, ...what up answered }` — one entry per tab it
|
|
40
|
+
* closed, and then the whole of the run that opened them again.
|
|
41
|
+
*/
|
|
42
|
+
export async function restartSessions(bots, { bot: name, session: onlySession } = {}) {
|
|
43
|
+
// The same refusals `up` gives for a bot or a session that is not there, and
|
|
44
|
+
// given before Orca is asked for anything.
|
|
45
|
+
botsNamed(bots, name);
|
|
46
|
+
const home = realpathSync(botDir(bots, name));
|
|
47
|
+
const known = readBot(home, name);
|
|
48
|
+
const sessions = sessionsOf(known, onlySession);
|
|
49
|
+
// A paused bot or session has no tab to close and is not to be opened, so
|
|
50
|
+
// there is nothing here to restart.
|
|
51
|
+
if (known.paused === true) {
|
|
52
|
+
throw new Error(`${name} is paused, so there is nothing of it to restart and nothing was closed. Bring it back with obk unpause --bots ${bots} --bot ${name}.`);
|
|
53
|
+
}
|
|
54
|
+
if (onlySession !== undefined && sessions[0].paused === true) {
|
|
55
|
+
throw new Error(`${name} ${onlySession} is paused, so there is nothing of it to restart and nothing was closed. Bring it back with obk unpause --bots ${bots} --bot ${name} --session ${onlySession}.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// And the same preparations, made now rather than left to `up`, which makes
|
|
59
|
+
// them after the tabs would already be gone. A session the kit could not
|
|
60
|
+
// start again — a launch line it would refuse, rules that will not build, a
|
|
61
|
+
// hooks file it cannot write — is a session that keeps the tab it is in.
|
|
62
|
+
const prepared = prepareBots(bots, [name], onlySession);
|
|
63
|
+
if (prepared.running.length === 0) {
|
|
64
|
+
throw new Error(`${name} would not come up again as it is, so nothing was closed. ${prepared.rules[0].trouble}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const going = tabsToClose(bots, name, home, sessions.filter((session) => session.paused !== true));
|
|
68
|
+
const closed = await closeTabs(home, going, bots, name);
|
|
69
|
+
return { closed, ...(await bringUp(bots, { bot: name, session: onlySession })) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The live tabs of these sessions, as `{ name, tab }`, once every one of them
|
|
74
|
+
* has been judged safe to close: a live tab whose conversation the book cannot
|
|
75
|
+
* name refuses the lot, before anything is touched. With `keepless`, a tab is
|
|
76
|
+
* closed whatever the book knows, for a caller that is ending the session on
|
|
77
|
+
* purpose.
|
|
78
|
+
*/
|
|
79
|
+
export function tabsToClose(bots, name, home, sessions, { keepless = false } = {}) {
|
|
80
|
+
const book = readBook(home);
|
|
81
|
+
|
|
82
|
+
// Orca is asked what the project holds only when it has one: a folder Orca
|
|
83
|
+
// knows nothing about is refused with `selector_not_found` rather than
|
|
84
|
+
// answered with an empty list (tech notes, section 1). A bot that has never
|
|
85
|
+
// been up has no tabs, which is the answer anyway.
|
|
86
|
+
const live = findProject(home) === undefined
|
|
87
|
+
? new Map()
|
|
88
|
+
: new Map(tabs(home).map((tab) => [tab.tabId, tab]));
|
|
89
|
+
|
|
90
|
+
const going = [];
|
|
91
|
+
const refusals = [];
|
|
92
|
+
for (const session of sessions) {
|
|
93
|
+
const was = book.sessions[session.name];
|
|
94
|
+
const tab = typeof was?.tab === 'string' ? live.get(was.tab) : undefined;
|
|
95
|
+
// Nothing of this session's is open: there is nothing to close, and `up`
|
|
96
|
+
// brings it back the way it brings back a tab the user closed themselves.
|
|
97
|
+
if (tab === undefined) continue;
|
|
98
|
+
if (keepless || typeof was.session === 'string') going.push({ name: session.name, tab });
|
|
99
|
+
else refusals.push(cannotComeBack(bots, name, session.name, was));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Every session is judged before one tab is closed: a fleet of two where one
|
|
103
|
+
// cannot come back is not a fleet to close the other half of.
|
|
104
|
+
if (refusals.length > 0) throw new Error(refusals.join('\n'));
|
|
105
|
+
return going;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Close the tabs `tabsToClose` gave, one at a time by their own handles, and
|
|
110
|
+
* wait until Orca's listing agrees they are gone. Returns one entry per tab.
|
|
111
|
+
*/
|
|
112
|
+
export async function closeTabs(home, going, bots, name) {
|
|
113
|
+
const closed = [];
|
|
114
|
+
for (const { name: session, tab } of going) {
|
|
115
|
+
try {
|
|
116
|
+
closeTab(tab.handle);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// A tab that would not close still holds its session, so nothing is
|
|
119
|
+
// opened after this: two harnesses on one conversation is worse than a
|
|
120
|
+
// restart that did not happen.
|
|
121
|
+
throw new Error(alsoClosed(error.message, closed, bots, name));
|
|
122
|
+
}
|
|
123
|
+
closed.push({ bot: name, name: session, tabId: tab.tabId, terminal: tab.handle });
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
await gone(home, closed, bots, name);
|
|
127
|
+
return closed;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** How long Orca is given to stop listing a tab it has closed, and how often it is asked. */
|
|
131
|
+
const SETTLED_MS = 5000;
|
|
132
|
+
const ASK_MS = 100;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Wait until Orca's own listing has caught up with the closes.
|
|
136
|
+
*
|
|
137
|
+
* `terminal close` answers before `terminal list` stops reporting the tab —
|
|
138
|
+
* seen live on a busy machine, which is why both system tests poll for it. What
|
|
139
|
+
* that costs here is the whole command: `up` lists the tabs, finds the one just
|
|
140
|
+
* closed, takes it for a session that is still running and only retitles it, so
|
|
141
|
+
* the run ends having reported a restart and ended a session.
|
|
142
|
+
*
|
|
143
|
+
* A listing that never catches up is not something the kit can put right, so it
|
|
144
|
+
* stops and says which tab it is waiting on. The conversation is in the book,
|
|
145
|
+
* which is the way back to it.
|
|
146
|
+
*/
|
|
147
|
+
async function gone(home, closed, bots, bot) {
|
|
148
|
+
// Nothing closed is nothing to wait for, and a bot that has never been up has
|
|
149
|
+
// no Orca project to ask: Orca refuses the listing (tech notes, section 1).
|
|
150
|
+
if (closed.length === 0) return;
|
|
151
|
+
const until = Date.now() + SETTLED_MS;
|
|
152
|
+
const ids = closed.map((tab) => tab.tabId);
|
|
153
|
+
|
|
154
|
+
for (;;) {
|
|
155
|
+
const there = new Set(tabs(home).map((tab) => tab.tabId));
|
|
156
|
+
const left = ids.filter((id) => there.has(id));
|
|
157
|
+
if (left.length === 0) return;
|
|
158
|
+
|
|
159
|
+
if (Date.now() >= until) {
|
|
160
|
+
const one = left.length === 1;
|
|
161
|
+
throw new Error(`Orca answered the close for ${one ? 'this tab' : 'these tabs'} and is still listing ${one ? 'it' : 'them'} ${SETTLED_MS / 1000} seconds later: ${left.join(', ')}. Nothing was opened in ${one ? 'its' : 'their'} place, because a tab that is on the way out is not a tab to start a harness in. The conversations are in the book: obk up --bots ${bots} --bot ${bot} brings them back once Orca has caught up.`);
|
|
162
|
+
}
|
|
163
|
+
await pause(ASK_MS);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* A session the kit will not close a tab on: it is live, and the book does not
|
|
169
|
+
* say which conversation is in it — a Codex hooks file trusted too late is
|
|
170
|
+
* enough to leave the book like that (tech notes, section 3).
|
|
171
|
+
*
|
|
172
|
+
* The kit will not pick one either, so the way out is the user's: the ids it
|
|
173
|
+
* found and nobody claims are named, and the line to write is spelled out.
|
|
174
|
+
*/
|
|
175
|
+
function cannotComeBack(bots, bot, name, was) {
|
|
176
|
+
const unclaimed = Array.isArray(was.unclaimed) ? was.unclaimed : [];
|
|
177
|
+
return [
|
|
178
|
+
`${bot} ${name} is running in tab ${was.tab} and the book does not say which conversation that is, so closing the tab would be the end of it. Nothing was closed.`,
|
|
179
|
+
...(unclaimed.length === 0
|
|
180
|
+
? []
|
|
181
|
+
: [`These ran in this bot's folder and no session claims them: ${unclaimed.join(', ')}.`]),
|
|
182
|
+
`To settle it, write the id into ${bookFile(botDir(bots, bot))} under ${name} as session: <id> and run the command again.`,
|
|
183
|
+
].join(' ');
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/** What a failed close owes whoever is reading: the tabs that went before it. */
|
|
187
|
+
function alsoClosed(why, closed, bots, bot) {
|
|
188
|
+
if (closed.length === 0) return why;
|
|
189
|
+
|
|
190
|
+
const one = closed.length === 1;
|
|
191
|
+
return `${why} ${closed.map((tab) => tab.name).join(', ')} ${one ? 'was' : 'were'} closed before it and nothing was opened in ${one ? 'its' : 'their'} place: obk up --bots ${bots} --bot ${bot} brings ${one ? 'it' : 'them'} back with ${one ? 'its' : 'their'} conversation.`;
|
|
192
|
+
}
|
package/src/retire.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// `obk retire`: end a session, or a whole bot, on the user's word.
|
|
2
|
+
//
|
|
3
|
+
// Nothing is deleted that the user might want back. A retired session comes off
|
|
4
|
+
// its bot's `bot.yaml`, and what the book knew about it moves to the book's
|
|
5
|
+
// `retired` list, so the conversations it had are still accounted for (ADR
|
|
6
|
+
// 0002). A retired bot moves, folder, book, charter and memory, out of `bots/`
|
|
7
|
+
// into `retired/` beside it, where the kit no longer looks.
|
|
8
|
+
//
|
|
9
|
+
// What goes is what the kit made for it: its tabs, closed one at a time by
|
|
10
|
+
// their own handles; its Orca project, after the tabs and never before; its
|
|
11
|
+
// start-prompt files beside the bots folder; and, for a bot, the skill links
|
|
12
|
+
// the kit made in its folder. A tab the book does not name is the user's, so a
|
|
13
|
+
// bot whose project holds one is not retired until they have dealt with it.
|
|
14
|
+
//
|
|
15
|
+
// Closing a tab here ends the conversation in it on purpose, so unlike a
|
|
16
|
+
// restart or a pause it does not wait for the book to know which one it was.
|
|
17
|
+
// The mailbox Runs stay: Orca has no way to remove one (ADR 0008).
|
|
18
|
+
|
|
19
|
+
import { existsSync, mkdirSync, renameSync, rmSync } from 'node:fs';
|
|
20
|
+
import path from 'node:path';
|
|
21
|
+
|
|
22
|
+
import { readBook, tabIdsIn, updateBook } from './book.js';
|
|
23
|
+
import { botDir, dropSession, readBot } from './bot.js';
|
|
24
|
+
import { deleteProject, findProject, tabs } from './orca.js';
|
|
25
|
+
import { fleetMember } from './pause.js';
|
|
26
|
+
import { closeTabs, tabsToClose } from './restart.js';
|
|
27
|
+
import { unlinkSkills } from './skills.js';
|
|
28
|
+
import { promptPath, sessionsOf } from './up.js';
|
|
29
|
+
|
|
30
|
+
/** Where retired bots go: beside `bots/`, where nothing the kit runs looks. */
|
|
31
|
+
export const retiredDir = (bots) => path.join(bots, 'retired');
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Retire the session `session` of the bot `bot`. Returns `{ bot, session, closed }`.
|
|
35
|
+
*/
|
|
36
|
+
export async function retireSession(bots, { bot, session }) {
|
|
37
|
+
const home = fleetMember(bots, bot, 'retire');
|
|
38
|
+
const sessions = sessionsOf(readBot(home, bot), session);
|
|
39
|
+
|
|
40
|
+
const closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot);
|
|
41
|
+
dropSession(bots, bot, session);
|
|
42
|
+
|
|
43
|
+
const at = new Date().toISOString();
|
|
44
|
+
await updateBook(home, (book) => {
|
|
45
|
+
const entry = book.sessions[session];
|
|
46
|
+
if (entry === undefined) return;
|
|
47
|
+
delete book.sessions[session];
|
|
48
|
+
book.retired = [...(Array.isArray(book.retired) ? book.retired : []), { name: session, ...entry, retired: at }];
|
|
49
|
+
});
|
|
50
|
+
rmSync(promptPath(bots, bot, session), { force: true });
|
|
51
|
+
|
|
52
|
+
return { bot, session, closed };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Retire the bot `bot`. Returns `{ bot, closed, project, moved }`: the tabs it
|
|
57
|
+
* closed, the Orca project it took away (if it had one) and where the bot is now.
|
|
58
|
+
*/
|
|
59
|
+
export async function retireBot(bots, { bot }) {
|
|
60
|
+
const home = fleetMember(bots, bot, 'retire');
|
|
61
|
+
const moved = path.join(retiredDir(bots), bot);
|
|
62
|
+
if (existsSync(moved)) {
|
|
63
|
+
throw new Error(`there is already something at ${moved}, and retire never writes over it. Move it aside, then retire ${bot} again.`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const known = readBot(home, bot);
|
|
67
|
+
const project = findProject(home);
|
|
68
|
+
if (project !== undefined) {
|
|
69
|
+
const booked = tabIdsIn(readBook(home));
|
|
70
|
+
const theirs = tabs(home).filter((tab) => !booked.has(tab.tabId));
|
|
71
|
+
if (theirs.length > 0) {
|
|
72
|
+
throw new Error(`${bot}'s Orca project holds ${theirs.length === 1 ? 'a tab' : 'tabs'} the book does not name, so ${theirs.length === 1 ? 'it is' : 'they are'} not the kit's to close: ${theirs.map((tab) => tab.tabId).join(', ')}. Close ${theirs.length === 1 ? 'it' : 'them'} yourself, then retire ${bot} again. Nothing was done.`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Every session the book holds a tab for, whether or not bot.yaml still lists
|
|
77
|
+
// it: the same set the check above calls the kit's, so the project is not
|
|
78
|
+
// taken away with one of them still open. `closeTabs` waits until Orca agrees
|
|
79
|
+
// they are gone.
|
|
80
|
+
const booked = Object.keys(readBook(home).sessions).map((name) => ({ name }));
|
|
81
|
+
const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot);
|
|
82
|
+
if (project !== undefined) deleteProject(project.id);
|
|
83
|
+
|
|
84
|
+
for (const name of new Set([...known.sessions, ...booked].map((session) => session.name))) {
|
|
85
|
+
rmSync(promptPath(bots, bot, name), { force: true });
|
|
86
|
+
}
|
|
87
|
+
unlinkSkills(home);
|
|
88
|
+
mkdirSync(retiredDir(bots), { recursive: true });
|
|
89
|
+
renameSync(botDir(bots, bot), moved);
|
|
90
|
+
|
|
91
|
+
return { bot, closed, ...(project === undefined ? {} : { project: project.id }), moved };
|
|
92
|
+
}
|
package/src/roster.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// The fleet as facts (PRD 6.8): what each bot is, what each of its sessions is
|
|
2
|
+
// set to, and what the book records about them.
|
|
3
|
+
//
|
|
4
|
+
// This is what Bot Father reads before it says anything about the fleet, and it
|
|
5
|
+
// reads files and only files. It does not ask Orca, it changes nothing, and it
|
|
6
|
+
// judges nothing: whether a bot is the right bot, what its charter comes to in
|
|
7
|
+
// two lines, and which of its limits is the hard one are the reader's to say
|
|
8
|
+
// (ADR 0006). What is wrong with the setup is a different question, and
|
|
9
|
+
// `obk health` is the command that answers it.
|
|
10
|
+
//
|
|
11
|
+
// So a list here is the list as the user wrote it, unchecked. A skill named in
|
|
12
|
+
// it that is nowhere on disk is still reported, because the fact is that the
|
|
13
|
+
// bot's list names it.
|
|
14
|
+
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
|
|
17
|
+
import { botDir, botNames, readBot } from './bot.js';
|
|
18
|
+
import { readBook } from './book.js';
|
|
19
|
+
import { harnessOf } from './launch.js';
|
|
20
|
+
import { listIn } from './rules.js';
|
|
21
|
+
|
|
22
|
+
/** The bot's own file, which is where everything but the book comes from. */
|
|
23
|
+
const BOT_YAML = 'bot.yaml';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The fleet, or the one bot named, in the order `botNames` gives: one entry per
|
|
27
|
+
* bot, each holding what its `bot.yaml` says and what its book holds.
|
|
28
|
+
*/
|
|
29
|
+
export function readRoster(bots, { bot: only } = {}) {
|
|
30
|
+
const names = botNames(bots);
|
|
31
|
+
if (only !== undefined && !names.includes(only)) {
|
|
32
|
+
throw new Error(`there is no bot called ${only} in ${bots}. The bots there are: ${names.join(', ') || 'none'}.`);
|
|
33
|
+
}
|
|
34
|
+
return (only === undefined ? names : [only]).map((name) => card(bots, name));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One bot's entry. The settings come back as the session carries them, so a
|
|
39
|
+
* setting it does not carry is absent rather than guessed: the kit never
|
|
40
|
+
* invents a model or an effort, and not set means the harness's own.
|
|
41
|
+
*
|
|
42
|
+
* The one thing worked out rather than copied is the harness, because a session
|
|
43
|
+
* either names its own or runs on its bot's, and which of the two it is is not
|
|
44
|
+
* a fact about the fleet that anybody should have to derive twice.
|
|
45
|
+
*/
|
|
46
|
+
function card(bots, name) {
|
|
47
|
+
const home = botDir(bots, name);
|
|
48
|
+
const file = path.join(home, BOT_YAML);
|
|
49
|
+
const bot = readBot(home, name);
|
|
50
|
+
const book = readBook(home);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
bot: name,
|
|
54
|
+
home,
|
|
55
|
+
harness: bot.harness,
|
|
56
|
+
charter: bot.charter,
|
|
57
|
+
...(bot.paused === true ? { paused: true } : {}),
|
|
58
|
+
rules: listIn(file, 'rules'),
|
|
59
|
+
skills: listIn(file, 'skills'),
|
|
60
|
+
orca: book.orca,
|
|
61
|
+
sessions: bot.sessions.map((session) => ({
|
|
62
|
+
...session,
|
|
63
|
+
harness: harnessOf(session, bot.harness),
|
|
64
|
+
// What the kit knows about this session as a running thing, kept apart
|
|
65
|
+
// from what the user set: the tab it is in, the conversation it is
|
|
66
|
+
// having, the ones it had before, and anything in its folder that no
|
|
67
|
+
// session claims. Empty until it has been brought up.
|
|
68
|
+
book: book.sessions[session.name] ?? {},
|
|
69
|
+
})),
|
|
70
|
+
};
|
|
71
|
+
}
|