@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/cli.js
ADDED
|
@@ -0,0 +1,1130 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// `obk` — the Orca Bot Kit command line. Bot Father's skills call it; a person
|
|
4
|
+
// can too. It writes files and reports what it did; it never commits.
|
|
5
|
+
//
|
|
6
|
+
// The caller is normally an LLM running a setup step, so every command says
|
|
7
|
+
// plainly what it made and what still wants looking at, and `--json` gives it
|
|
8
|
+
// the same facts to act on.
|
|
9
|
+
|
|
10
|
+
import { readFileSync, realpathSync } from 'node:fs';
|
|
11
|
+
import path from 'node:path';
|
|
12
|
+
import { parseArgs } from 'node:util';
|
|
13
|
+
|
|
14
|
+
import { addSession, changeBot, changeSession, createBot, readBot, SESSION_FIELDS } from './bot.js';
|
|
15
|
+
import { grooming } from './groom.js';
|
|
16
|
+
import { checkHealth, orcaSettingFindings } from './health.js';
|
|
17
|
+
import { initBots } from './init.js';
|
|
18
|
+
import { APPROVALS, HARNESSES, shellWord } from './launch.js';
|
|
19
|
+
import { checkMail, lookUp, sendMessage } from './message.js';
|
|
20
|
+
import { orcaCli, orcaTrouble } from './orca.js';
|
|
21
|
+
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
22
|
+
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
23
|
+
import { restartSessions } from './restart.js';
|
|
24
|
+
import { retireBot, retireSession } from './retire.js';
|
|
25
|
+
import { readRoster } from './roster.js';
|
|
26
|
+
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
27
|
+
import { addSkill, buildSkills, linkSkills, removeSkill } from './skills.js';
|
|
28
|
+
import { addSource, fetchSources } from './sources.js';
|
|
29
|
+
import { readUsage } from './usage.js';
|
|
30
|
+
import { BOT_FATHER, bringUp } from './up.js';
|
|
31
|
+
|
|
32
|
+
const USAGE = `obk — Orca Bot Kit.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
obk init --bots <path> --harness claude|codex
|
|
36
|
+
Create your bots folder: a git repo holding your
|
|
37
|
+
bots' configuration, with Bot Father in it, and
|
|
38
|
+
open Bot Father in Orca.
|
|
39
|
+
obk bot create --bots <path> --name <bot> --harness claude|codex
|
|
40
|
+
[--charter <text>]
|
|
41
|
+
Write a new bot in your bots folder. Nothing is
|
|
42
|
+
opened in Orca until you run obk up.
|
|
43
|
+
obk session add --bots <path> --bot <bot> --name <session>
|
|
44
|
+
[--harness claude|codex] [--model <m>] [--effort <e>]
|
|
45
|
+
[--context <c>] [--approval ${APPROVALS.join('|')}]
|
|
46
|
+
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
47
|
+
[--extra-arg=<arg>]
|
|
48
|
+
Add a session to a bot. Anything left out is the
|
|
49
|
+
harness's own default; approval is auto. A long
|
|
50
|
+
start prompt lives in a file in the bot home, and
|
|
51
|
+
--prompt-file names it.
|
|
52
|
+
A value of your own that starts with a dash is
|
|
53
|
+
given glued to its flag, so its dashes are not read
|
|
54
|
+
as ours: --prompt='- a bullet', and
|
|
55
|
+
--extra-arg=--search, once per extra argument.
|
|
56
|
+
obk bot change --bots <path> --bot <bot> --charter <text>
|
|
57
|
+
Give a bot a new charter and rebuild its AGENTS.md.
|
|
58
|
+
A running session reads it when it next starts.
|
|
59
|
+
obk session change --bots <path> --bot <bot> --session <session>
|
|
60
|
+
[--model <m>] [--effort <e>] [--context <c>]
|
|
61
|
+
[--approval ${APPROVALS.join('|')}]
|
|
62
|
+
[--prompt <text> | --prompt-file <path>] [--work-dir <path>]
|
|
63
|
+
[--extra-arg=<arg>]
|
|
64
|
+
Change a session's settings. What you leave out
|
|
65
|
+
stays as it is; a setting given empty, --model=,
|
|
66
|
+
goes back to the harness's own default. A running
|
|
67
|
+
session takes the change when it next starts. A
|
|
68
|
+
session keeps its harness: to move it, retire it
|
|
69
|
+
and add another.
|
|
70
|
+
obk pause --bots <path> --bot <bot> [--session <name>]
|
|
71
|
+
Stop a bot, or one of its sessions, for now: close
|
|
72
|
+
its tabs and have obk up leave it closed. The book
|
|
73
|
+
keeps its conversations. Like restart, it closes
|
|
74
|
+
only the tabs your book names, and none whose
|
|
75
|
+
conversation the book cannot name.
|
|
76
|
+
obk unpause --bots <path> --bot <bot> [--session <name>]
|
|
77
|
+
Take the pause off and bring it up again, each
|
|
78
|
+
session with the conversation it was having.
|
|
79
|
+
obk retire --bots <path> --bot <bot> [--session <name>]
|
|
80
|
+
End a session: close its tab and take it off the
|
|
81
|
+
bot, keeping its conversations in the book. Or end
|
|
82
|
+
a bot: close its tabs, remove its Orca project and
|
|
83
|
+
move its folder to retired/. It will not retire a
|
|
84
|
+
bot whose Orca project holds a tab your book does
|
|
85
|
+
not name.
|
|
86
|
+
obk rules build --bots <path> [--bot <bot>]
|
|
87
|
+
Build every bot's AGENTS.md from its charter and
|
|
88
|
+
the rule units it carries, or just the one you
|
|
89
|
+
name. Your own text outside the marked block is
|
|
90
|
+
kept; a block you edited by hand is reported and
|
|
91
|
+
never written over. It does not touch Orca.
|
|
92
|
+
obk skills add --bots <path> --bot <bot> --skill <ref>
|
|
93
|
+
Put one skill on a bot's list. It writes the list
|
|
94
|
+
and nothing else; obk skills build is what links
|
|
95
|
+
it. A skill already listed is left as it is.
|
|
96
|
+
obk skills remove --bots <path> --bot <bot> --skill <ref>
|
|
97
|
+
Take one skill off a bot's list. obk skills build
|
|
98
|
+
is what takes its link away.
|
|
99
|
+
obk skills build --bots <path> [--bot <bot>]
|
|
100
|
+
Link every bot's skills into both harnesses, or
|
|
101
|
+
just the one you name, from the kit, your own
|
|
102
|
+
skills folder, or any path, and take away a link
|
|
103
|
+
it made that no list names any more. What you put
|
|
104
|
+
in a bot's skills directory yourself is left alone
|
|
105
|
+
and shown as yours. It does not touch Orca.
|
|
106
|
+
obk skills fetch --bots <path> [--source <name>]
|
|
107
|
+
Clone the online sources skills.yaml lists, beside
|
|
108
|
+
your bots folder and never inside it, each at the
|
|
109
|
+
version you pinned, and write down the sha it got.
|
|
110
|
+
A source already there is left exactly as it is.
|
|
111
|
+
obk skills update --bots <path> [--source <name>]
|
|
112
|
+
Move a source on to what its ref names now, and
|
|
113
|
+
write down the new sha. This is the only thing that
|
|
114
|
+
moves one.
|
|
115
|
+
obk source add --bots <path> --name <name> --repo <url> --ref <ref>
|
|
116
|
+
[--path <subfolder>]
|
|
117
|
+
Write down where a shelf of skills online comes
|
|
118
|
+
from, and which version of it you want. It only
|
|
119
|
+
writes it down; obk skills fetch is what clones it.
|
|
120
|
+
obk message to --bots <path> --to <bot>[/<session>] [--from <bot>/<session>]
|
|
121
|
+
Say which road reaches that session and what its
|
|
122
|
+
address is. Claude to Claude in one approval class
|
|
123
|
+
is the harness's own messaging; everything else is
|
|
124
|
+
the Orca mailbox. Nothing is sent.
|
|
125
|
+
obk message send --bots <path> --to <bot>[/<session>] [--from <bot>/<session>]
|
|
126
|
+
--subject <text> [--text <text> | --text-file <path>]
|
|
127
|
+
[--thread <id>]
|
|
128
|
+
Put a message in that session's Orca mailbox and
|
|
129
|
+
tell its tab to look. A message too long to travel
|
|
130
|
+
as itself is written to a file beside your bots
|
|
131
|
+
folder and named in the message. A pair the
|
|
132
|
+
harness's own messaging reaches is not carried:
|
|
133
|
+
the address to write to is answered instead.
|
|
134
|
+
obk message check --bots <path> [--bot <bot>] [--session <name>] [--peek]
|
|
135
|
+
Read what is waiting for a session and mark it
|
|
136
|
+
read. --peek leaves it unread. Run in a session's
|
|
137
|
+
own tab, it is that session's mail.
|
|
138
|
+
obk up --bots <path> [--bot <bot>] [--session <name>]
|
|
139
|
+
Open whatever is missing in Orca, for every bot or
|
|
140
|
+
for the one you name. It only ever adds; it never
|
|
141
|
+
closes a tab. A session the book knows the harness
|
|
142
|
+
session of comes back with its conversation.
|
|
143
|
+
obk restart --bots <path> --bot <bot> [--session <name>]
|
|
144
|
+
Close a bot's session tabs and open them again,
|
|
145
|
+
each with the conversation it was having. It is the
|
|
146
|
+
one command that closes a tab, it closes only the
|
|
147
|
+
tabs your book names, and it will not close one
|
|
148
|
+
whose conversation the book cannot name.
|
|
149
|
+
obk health --bots <path> [--bot <bot>]
|
|
150
|
+
Say what is wrong with your setup: configuration
|
|
151
|
+
that will not work, a skill that is not where its
|
|
152
|
+
list says, a session Orca has lost, and what is
|
|
153
|
+
lying about that no bot owns. It reports and
|
|
154
|
+
changes nothing; what to do about each line is
|
|
155
|
+
yours to decide.
|
|
156
|
+
obk roster --bots <path> [--bot <bot>]
|
|
157
|
+
Say what your fleet is: every bot, its charter, the
|
|
158
|
+
rules and skills its lists name, and each session
|
|
159
|
+
with its settings, its tab and the conversation it
|
|
160
|
+
is in. It reads your files and reports them as they
|
|
161
|
+
stand; what to make of them is yours.
|
|
162
|
+
obk groom --bots <path> [--at <HH:MM>] [--on | --off]
|
|
163
|
+
Say whether the daily grooming exists, when it runs
|
|
164
|
+
and whether it is on, and set it up or change it
|
|
165
|
+
when you ask. --at makes one, in Bot Father's Orca
|
|
166
|
+
project. It is made off, because it spends tokens
|
|
167
|
+
every day: run it by hand once, read what it gives
|
|
168
|
+
you, then --on.
|
|
169
|
+
obk usage --bots <path> [--bot <bot>] [--session <name>] [--since <time>] [--until <time>]
|
|
170
|
+
Say what your sessions have used: the conversations
|
|
171
|
+
each one had, their calls and tokens, the models and
|
|
172
|
+
efforts they ran at, and how often they were
|
|
173
|
+
compacted. --since counts the calls made from that
|
|
174
|
+
moment on, and --until the calls made before that
|
|
175
|
+
one: a daily run asks from the last run's end up to
|
|
176
|
+
its own, and the next starts where it stopped, so no
|
|
177
|
+
call is counted twice. It counts tokens and never
|
|
178
|
+
money: what a token costs is looked up live by
|
|
179
|
+
whoever is asking.
|
|
180
|
+
obk session record --bots <path> --bot <bot>
|
|
181
|
+
For the kit's own hook, not for typing: it reads
|
|
182
|
+
what the harness says about a session starting on
|
|
183
|
+
standard input and writes it into the book.
|
|
184
|
+
obk --version Print the kit's version.
|
|
185
|
+
obk --help Print this text.
|
|
186
|
+
|
|
187
|
+
Every command is safe to run again. Most only add what is missing; the ones
|
|
188
|
+
that close a tab or take something away say so above.
|
|
189
|
+
Add --json to any of them for the same answer as JSON.
|
|
190
|
+
`;
|
|
191
|
+
|
|
192
|
+
/** The commands, and the flags each one cannot do without. */
|
|
193
|
+
const COMMANDS = {
|
|
194
|
+
init: ['bots', 'harness'],
|
|
195
|
+
up: ['bots'],
|
|
196
|
+
restart: ['bots', 'bot'],
|
|
197
|
+
pause: ['bots', 'bot'],
|
|
198
|
+
unpause: ['bots', 'bot'],
|
|
199
|
+
retire: ['bots', 'bot'],
|
|
200
|
+
health: ['bots'],
|
|
201
|
+
groom: ['bots'],
|
|
202
|
+
roster: ['bots'],
|
|
203
|
+
usage: ['bots'],
|
|
204
|
+
'bot create': ['bots', 'name', 'harness'],
|
|
205
|
+
'bot change': ['bots', 'bot'],
|
|
206
|
+
'rules build': ['bots'],
|
|
207
|
+
'skills add': ['bots', 'bot', 'skill'],
|
|
208
|
+
'skills remove': ['bots', 'bot', 'skill'],
|
|
209
|
+
'skills build': ['bots'],
|
|
210
|
+
'skills fetch': ['bots'],
|
|
211
|
+
'skills update': ['bots'],
|
|
212
|
+
'source add': ['bots', 'name', 'repo', 'ref'],
|
|
213
|
+
'session add': ['bots', 'bot', 'name'],
|
|
214
|
+
'session change': ['bots', 'bot', 'session'],
|
|
215
|
+
'message to': ['bots', 'to'],
|
|
216
|
+
'message send': ['bots', 'to', 'subject'],
|
|
217
|
+
'message check': ['bots'],
|
|
218
|
+
'session record': ['bots', 'bot'],
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
/** What each flag is for, in the sentence a caller reads when it is missing. */
|
|
222
|
+
const NEEDED = {
|
|
223
|
+
bots: '--bots <path>: where your bots folder is',
|
|
224
|
+
name: '--name <name>: what to call it',
|
|
225
|
+
bot: '--bot <bot>: which bot',
|
|
226
|
+
session: '--session <name>: which session',
|
|
227
|
+
source: '--source <name>: which source',
|
|
228
|
+
since: '--since <time>: the moment to count from',
|
|
229
|
+
until: '--until <time>: the moment to count up to, not including it',
|
|
230
|
+
at: '--at <HH:MM>: what time of day it runs',
|
|
231
|
+
skill: "--skill <ref>: which skill, as a bot's list names one",
|
|
232
|
+
repo: '--repo <url>: the repository to clone it from',
|
|
233
|
+
ref: '--ref <ref>: the branch, tag or commit to pin it at',
|
|
234
|
+
path: '--path <subfolder>: where the skills sit inside that repository',
|
|
235
|
+
harness: `--harness ${HARNESSES.join('|')}: which harness it runs on`,
|
|
236
|
+
to: '--to <bot>/<session>: which session to write to',
|
|
237
|
+
from: '--from <bot>/<session>: which session is writing',
|
|
238
|
+
subject: '--subject <text>: what the message is about',
|
|
239
|
+
};
|
|
240
|
+
|
|
241
|
+
/** The flags that name something. A name that is empty names nothing. */
|
|
242
|
+
const IDENTIFIERS = Object.keys(NEEDED);
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The session settings a flag can carry, as `[flag, field]`: every field a
|
|
246
|
+
* session has, spelled with dashes, except the name it is added under and the
|
|
247
|
+
* extra arguments, which come one flag at a time.
|
|
248
|
+
*/
|
|
249
|
+
const SETTINGS = SESSION_FIELDS
|
|
250
|
+
.filter((field) => field !== 'name' && field !== 'extra_args')
|
|
251
|
+
.map((field) => [field.replaceAll('_', '-'), field]);
|
|
252
|
+
|
|
253
|
+
function version() {
|
|
254
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
255
|
+
return pkg.version;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
async function run(argv) {
|
|
259
|
+
const { values, positionals } = parseArgs({
|
|
260
|
+
args: argv,
|
|
261
|
+
allowPositionals: true,
|
|
262
|
+
options: {
|
|
263
|
+
bots: { type: 'string' },
|
|
264
|
+
harness: { type: 'string' },
|
|
265
|
+
name: { type: 'string' },
|
|
266
|
+
bot: { type: 'string' },
|
|
267
|
+
session: { type: 'string' },
|
|
268
|
+
source: { type: 'string' },
|
|
269
|
+
since: { type: 'string' },
|
|
270
|
+
until: { type: 'string' },
|
|
271
|
+
at: { type: 'string' },
|
|
272
|
+
on: { type: 'boolean' },
|
|
273
|
+
off: { type: 'boolean' },
|
|
274
|
+
skill: { type: 'string' },
|
|
275
|
+
repo: { type: 'string' },
|
|
276
|
+
ref: { type: 'string' },
|
|
277
|
+
path: { type: 'string' },
|
|
278
|
+
to: { type: 'string' },
|
|
279
|
+
from: { type: 'string' },
|
|
280
|
+
subject: { type: 'string' },
|
|
281
|
+
text: { type: 'string' },
|
|
282
|
+
'text-file': { type: 'string' },
|
|
283
|
+
thread: { type: 'string' },
|
|
284
|
+
peek: { type: 'boolean' },
|
|
285
|
+
charter: { type: 'string' },
|
|
286
|
+
...Object.fromEntries(SETTINGS.map(([flag]) => [flag, { type: 'string' }])),
|
|
287
|
+
'extra-arg': { type: 'string', multiple: true },
|
|
288
|
+
json: { type: 'boolean' },
|
|
289
|
+
help: { type: 'boolean', short: 'h' },
|
|
290
|
+
version: { type: 'boolean' },
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
if (values.help) {
|
|
295
|
+
process.stdout.write(USAGE);
|
|
296
|
+
return 0;
|
|
297
|
+
}
|
|
298
|
+
if (values.version) {
|
|
299
|
+
process.stdout.write(`${version()}\n`);
|
|
300
|
+
return 0;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
if (positionals.length === 0) {
|
|
304
|
+
process.stderr.write(USAGE);
|
|
305
|
+
return 1;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// `bot`, `rules`, `skills`, `session`, `source` and `message` are commands of
|
|
309
|
+
// two words; the rest are one.
|
|
310
|
+
const words = ['bot', 'rules', 'skills', 'session', 'source', 'message'].includes(positionals[0]) ? 2 : 1;
|
|
311
|
+
const command = positionals.slice(0, words).join(' ');
|
|
312
|
+
const extra = positionals.slice(words);
|
|
313
|
+
|
|
314
|
+
if (!(command in COMMANDS)) {
|
|
315
|
+
throw new Error(`there is no "${command}" command. Run obk --help to see what there is.`);
|
|
316
|
+
}
|
|
317
|
+
if (extra.length > 0) {
|
|
318
|
+
throw new Error(`${command} takes no other arguments, and got: ${extra.join(' ')}`);
|
|
319
|
+
}
|
|
320
|
+
for (const flag of COMMANDS[command]) {
|
|
321
|
+
if (values[flag] === undefined) throw new Error(`${command} needs ${NEEDED[flag]}.`);
|
|
322
|
+
}
|
|
323
|
+
// A name with nothing in it is a name that was not given, whether the command
|
|
324
|
+
// needs it or not. The settings are not held to this: an empty model or
|
|
325
|
+
// effort is the harness's own, which is what leaving it out means too.
|
|
326
|
+
for (const flag of IDENTIFIERS) {
|
|
327
|
+
if (values[flag] !== undefined && values[flag].trim() === '') {
|
|
328
|
+
throw new Error(`${command} needs ${NEEDED[flag]}.`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (values.harness !== undefined && !HARNESSES.includes(values.harness)) {
|
|
332
|
+
throw new Error(`--harness is ${HARNESSES.join(' or ')}, and got: ${values.harness}`);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// One fleet, one identity, whatever spelling of its path was given (#164).
|
|
336
|
+
const bots = command === 'init' ? path.resolve(values.bots) : sameFleet(path.resolve(values.bots));
|
|
337
|
+
if (command === RECORD) return record(bots, values.bot);
|
|
338
|
+
|
|
339
|
+
const { answer, lines, code = 0 } = await commands[command](bots, values);
|
|
340
|
+
|
|
341
|
+
process.stdout.write(values.json ? `${JSON.stringify(answer, null, 2)}\n` : `${lines.join('\n')}\n`);
|
|
342
|
+
return code;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* The bots folder as the file system knows it. A folder reached through a
|
|
347
|
+
* symlink is the same fleet: the harnesses file their transcripts under the real
|
|
348
|
+
* path, Orca records an automation's workspace by it, and what the kit keeps
|
|
349
|
+
* beside the folder (the skill sources, the start prompts, the messages) is
|
|
350
|
+
* named after it. Asked by one spelling and then another, a fleet would
|
|
351
|
+
* otherwise have two of each, and a pin set through one is undone through the
|
|
352
|
+
* other.
|
|
353
|
+
*
|
|
354
|
+
* Every command but `init` from the start. It is given a path that may not exist
|
|
355
|
+
* yet, and when it refuses one it names what the user gave it rather than
|
|
356
|
+
* whatever the link pointed at, which is a file they never mentioned; once the
|
|
357
|
+
* folder is there, it too works on the real path.
|
|
358
|
+
*/
|
|
359
|
+
function sameFleet(bots) {
|
|
360
|
+
try {
|
|
361
|
+
return realpathSync(bots);
|
|
362
|
+
} catch {
|
|
363
|
+
return bots;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* The line that sends someone to look at a tab, naming the Orca the kit itself
|
|
369
|
+
* talks to rather than the word `orca`.
|
|
370
|
+
*
|
|
371
|
+
* A bare `orca` is not a command everywhere: on this machine it is a root-only
|
|
372
|
+
* symlink and answers that it cannot find the app (tech notes, section 1), and
|
|
373
|
+
* the notes already record a session following this very advice and failing. So
|
|
374
|
+
* what is printed is what the kit would run, which is the only path known to
|
|
375
|
+
* work here.
|
|
376
|
+
*
|
|
377
|
+
* Through the kit's own `shellWord`, because `OBK_ORCA` can point at a path with
|
|
378
|
+
* a space in it, and unquoted the shell reads that as two words and the line
|
|
379
|
+
* fails for a reason that looks nothing like its cause.
|
|
380
|
+
*/
|
|
381
|
+
const lookAt = (terminal) => `${shellWord(orcaCli())} terminal read --terminal ${terminal} --screen`;
|
|
382
|
+
|
|
383
|
+
/** The one command a harness runs rather than a person: the kit's hook. */
|
|
384
|
+
const RECORD = 'session record';
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* What the kit's hook does with what the harness told it, and what it answers.
|
|
388
|
+
*
|
|
389
|
+
* A hook runs inside the user's own session, so this one stays out of the way:
|
|
390
|
+
* it writes on standard output only what the harness is to read as JSON, and
|
|
391
|
+
* 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).
|
|
393
|
+
*/
|
|
394
|
+
async function record(bots, bot) {
|
|
395
|
+
try {
|
|
396
|
+
const answer = await recordSession(bots, bot, JSON.parse(readFileSync(0, 'utf8')), process.env[TAB_ENV], process.env[SHELL_ENV]);
|
|
397
|
+
if (answer !== undefined) process.stdout.write(`${JSON.stringify(answer)}\n`);
|
|
398
|
+
} catch {
|
|
399
|
+
// Nothing: see above.
|
|
400
|
+
}
|
|
401
|
+
return 0;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
const commands = {
|
|
405
|
+
async init(bots, values) {
|
|
406
|
+
// Asked before anything is written, so an Orca that is down leaves the disk
|
|
407
|
+
// exactly as it was and the caller can simply run the command again.
|
|
408
|
+
refuseWhenOrcaIsDown();
|
|
409
|
+
const seeded = initBots(bots, values.harness);
|
|
410
|
+
// The typed path was for making the folder and naming it in a refusal; from
|
|
411
|
+
// 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 });
|
|
413
|
+
// Setup is the other place the PRD asks for Orca's own launch arguments to
|
|
414
|
+
// be looked at (6.5), and the one where the user is still standing in front
|
|
415
|
+
// of the fleet they are making. Only that one check: a folder init has just
|
|
416
|
+
// 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() };
|
|
418
|
+
return { answer, lines: tabLines(answer, `Bot Father is up in Orca. Your bots folder: ${seeded.bots}`) };
|
|
419
|
+
},
|
|
420
|
+
|
|
421
|
+
async up(bots, values) {
|
|
422
|
+
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 };
|
|
425
|
+
const up = [...new Set(tabs.map((tab) => tab.bot))];
|
|
426
|
+
const summary = up.length === 0
|
|
427
|
+
? `Nothing was brought up in Orca. Your bots folder: ${bots}`
|
|
428
|
+
: `Up in Orca: ${up.join(', ')}. Your bots folder: ${bots}`;
|
|
429
|
+
return { answer, lines: tabLines(answer, summary) };
|
|
430
|
+
},
|
|
431
|
+
|
|
432
|
+
health(bots, values) {
|
|
433
|
+
refuseWhenOrcaIsDown();
|
|
434
|
+
const found = checkHealth(bots, { bot: values.bot });
|
|
435
|
+
return {
|
|
436
|
+
answer: { bots, found },
|
|
437
|
+
lines: [
|
|
438
|
+
...foundLines(found),
|
|
439
|
+
found.length === 0
|
|
440
|
+
? `Nothing to report: everything the kit keeps is where it should be. Your bots folder: ${bots}`
|
|
441
|
+
: `${found.length} thing${found.length === 1 ? '' : 's'} to look at above. What to do about each is yours to decide. Your bots folder: ${bots}`,
|
|
442
|
+
],
|
|
443
|
+
// Something to look at is not a command that failed, and the answer is
|
|
444
|
+
// printed either way; the code is there for whoever runs it in a script.
|
|
445
|
+
code: found.length === 0 ? 0 : 1,
|
|
446
|
+
};
|
|
447
|
+
},
|
|
448
|
+
|
|
449
|
+
async restart(bots, values) {
|
|
450
|
+
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 };
|
|
453
|
+
const what = values.session === undefined ? values.bot : `${values.bot} ${values.session}`;
|
|
454
|
+
return {
|
|
455
|
+
answer,
|
|
456
|
+
lines: [
|
|
457
|
+
...closed.map((tab) => `closed ${tab.bot} ${tab.name} tab ${tab.tabId} terminal ${tab.terminal}`),
|
|
458
|
+
...tabLines(answer, `Restarted in Orca: ${what}. Your bots folder: ${bots}`),
|
|
459
|
+
],
|
|
460
|
+
};
|
|
461
|
+
},
|
|
462
|
+
|
|
463
|
+
async pause(bots, values) {
|
|
464
|
+
refuseWhenOrcaIsDown();
|
|
465
|
+
const paused = await pauseSessions(bots, { bot: values.bot, session: values.session });
|
|
466
|
+
const what = values.session === undefined ? values.bot : `${values.bot} ${values.session}`;
|
|
467
|
+
const back = values.session === undefined ? '' : ` --session ${values.session}`;
|
|
468
|
+
return {
|
|
469
|
+
answer: { bots, ...paused },
|
|
470
|
+
lines: [
|
|
471
|
+
...paused.closed.map((tab) => `closed ${tab.bot} ${tab.name} tab ${tab.tabId} terminal ${tab.terminal}`),
|
|
472
|
+
`${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}`,
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
},
|
|
477
|
+
|
|
478
|
+
async unpause(bots, values) {
|
|
479
|
+
refuseWhenOrcaIsDown();
|
|
480
|
+
const { bot, session, changed, ...up } = await unpauseSessions(bots, { bot: values.bot, session: values.session });
|
|
481
|
+
const what = session === undefined ? bot : `${bot} ${session}`;
|
|
482
|
+
const answer = { bots, bot, session, changed, created: [], completed: [], ...up };
|
|
483
|
+
return {
|
|
484
|
+
answer,
|
|
485
|
+
lines: tabLines(answer, `${changed ? 'Unpaused' : 'Not paused, so brought up as it is'}: ${what}. Your bots folder: ${bots}`),
|
|
486
|
+
};
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
async retire(bots, values) {
|
|
490
|
+
refuseWhenOrcaIsDown();
|
|
491
|
+
const closedLines = (closed) => closed.map((tab) => `closed ${tab.bot} ${tab.name} tab ${tab.tabId} terminal ${tab.terminal}`);
|
|
492
|
+
|
|
493
|
+
if (values.session !== undefined) {
|
|
494
|
+
const retired = await retireSession(bots, { bot: values.bot, session: values.session });
|
|
495
|
+
return {
|
|
496
|
+
answer: { bots, ...retired },
|
|
497
|
+
lines: [
|
|
498
|
+
...closedLines(retired.closed),
|
|
499
|
+
`retired ${retired.bot} ${retired.session}: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
500
|
+
],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const retired = await retireBot(bots, { bot: values.bot });
|
|
505
|
+
return {
|
|
506
|
+
answer: { bots, ...retired },
|
|
507
|
+
lines: [
|
|
508
|
+
...closedLines(retired.closed),
|
|
509
|
+
...(retired.project === undefined ? [] : [`removed Orca project ${retired.project}`]),
|
|
510
|
+
`retired ${retired.bot}: moved to ${path.relative(bots, retired.moved)}, with its book, charter and memory`,
|
|
511
|
+
],
|
|
512
|
+
};
|
|
513
|
+
},
|
|
514
|
+
|
|
515
|
+
'bot create'(bots, values) {
|
|
516
|
+
const made = createBot(bots, { name: values.name, harness: values.harness, charter: values.charter });
|
|
517
|
+
// The bot's AGENTS.md is the rules build's, here as everywhere else, so
|
|
518
|
+
// that a new bot's file and a rebuilt one are written by the same code.
|
|
519
|
+
const rules = [buildAgents(bots, made.home, readBot(made.home))];
|
|
520
|
+
// A new bot is given what the lists already name, so it is whole before
|
|
521
|
+
// anybody opens a tab on it.
|
|
522
|
+
const skills = [linkSkills(bots, made.home, readBot(made.home))];
|
|
523
|
+
const answer = { bots, bot: made.bot, home: made.home, created: made.created, rules, skills };
|
|
524
|
+
// A bot whose rules would not build is made but not finished: it has no
|
|
525
|
+
// instructions, so `up` will not start it, and saying "give it a session"
|
|
526
|
+
// would send the caller past the thing that needs settling first.
|
|
527
|
+
const trouble = rules[0].trouble !== undefined;
|
|
528
|
+
return {
|
|
529
|
+
answer,
|
|
530
|
+
lines: [
|
|
531
|
+
...made.created.map((entry) => `created ${entry}`),
|
|
532
|
+
...rulesLines(rules, bots),
|
|
533
|
+
...skillsLines(skills),
|
|
534
|
+
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>`,
|
|
537
|
+
],
|
|
538
|
+
code: trouble ? 1 : 0,
|
|
539
|
+
};
|
|
540
|
+
},
|
|
541
|
+
|
|
542
|
+
'bot change'(bots, values) {
|
|
543
|
+
if (values.harness !== undefined) {
|
|
544
|
+
throw new Error(`bot change does not change a bot's harness: its sessions' conversations belong to the harness they ran on. To move to ${values.harness}, give it a session on ${values.harness} with obk session add, or retire the bot with obk retire and create a new one.`);
|
|
545
|
+
}
|
|
546
|
+
const changed = changeBot(bots, values.bot, { charter: values.charter });
|
|
547
|
+
// Rebuilt at once, as bot create builds it, so the charter a session reads
|
|
548
|
+
// is the one the bot now has.
|
|
549
|
+
const rules = [buildAgents(bots, changed.home, readBot(changed.home))];
|
|
550
|
+
const trouble = rules[0].trouble !== undefined;
|
|
551
|
+
return {
|
|
552
|
+
answer: { bots, bot: changed.bot, home: changed.home, charter: changed.charter, rules },
|
|
553
|
+
lines: [
|
|
554
|
+
`changed the charter in ${path.join('bots', changed.bot, 'bot.yaml')}`,
|
|
555
|
+
...rulesLines(rules, bots),
|
|
556
|
+
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}`
|
|
558
|
+
: `${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
|
+
],
|
|
560
|
+
code: trouble ? 1 : 0,
|
|
561
|
+
};
|
|
562
|
+
},
|
|
563
|
+
|
|
564
|
+
'skills build'(bots, values) {
|
|
565
|
+
const skills = buildSkills(bots, { bot: values.bot });
|
|
566
|
+
const trouble = skills.filter((entry) => entry.trouble !== undefined);
|
|
567
|
+
return {
|
|
568
|
+
answer: { bots, skills },
|
|
569
|
+
lines: [
|
|
570
|
+
...skillsLines(skills),
|
|
571
|
+
trouble.length === 0
|
|
572
|
+
? `Every bot has the skills its lists name. Your bots folder: ${bots}`
|
|
573
|
+
: `${trouble.map((entry) => entry.bot).join(', ')}: skills not linked. Settle what the lines above say, then link again.`,
|
|
574
|
+
],
|
|
575
|
+
code: trouble.length === 0 ? 0 : 1,
|
|
576
|
+
};
|
|
577
|
+
},
|
|
578
|
+
|
|
579
|
+
'skills add'(bots, values) {
|
|
580
|
+
const added = addSkill(bots, values.bot, values.skill);
|
|
581
|
+
return {
|
|
582
|
+
answer: { bots, bot: added.bot, home: added.home, skill: added.skill, state: added.state },
|
|
583
|
+
lines: [
|
|
584
|
+
added.state === 'added'
|
|
585
|
+
? `added ${added.skill} to ${path.join('bots', added.bot, 'bot.yaml')}`
|
|
586
|
+
: `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}`,
|
|
588
|
+
],
|
|
589
|
+
};
|
|
590
|
+
},
|
|
591
|
+
|
|
592
|
+
'skills remove'(bots, values) {
|
|
593
|
+
const removed = removeSkill(bots, values.bot, values.skill);
|
|
594
|
+
return {
|
|
595
|
+
answer: { bots, bot: removed.bot, home: removed.home, skill: removed.skill, state: removed.state },
|
|
596
|
+
lines: [
|
|
597
|
+
removed.state === 'removed'
|
|
598
|
+
? `removed ${removed.skill} from ${path.join('bots', removed.bot, 'bot.yaml')}`
|
|
599
|
+
: `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}`,
|
|
601
|
+
],
|
|
602
|
+
};
|
|
603
|
+
},
|
|
604
|
+
|
|
605
|
+
'source add'(bots, values) {
|
|
606
|
+
const source = addSource(bots, { name: values.name, repo: values.repo, ref: values.ref, path: values.path });
|
|
607
|
+
return {
|
|
608
|
+
answer: { bots, source },
|
|
609
|
+
lines: [
|
|
610
|
+
`added source ${source.name} to skills.yaml`,
|
|
611
|
+
...Object.entries(source)
|
|
612
|
+
.filter(([key]) => key !== 'name')
|
|
613
|
+
.map(([key, value]) => ` ${key.padEnd(5)} ${value}`),
|
|
614
|
+
`Fetch it: obk skills fetch --bots ${bots} --source ${source.name}`,
|
|
615
|
+
],
|
|
616
|
+
};
|
|
617
|
+
},
|
|
618
|
+
|
|
619
|
+
groom(bots, values) {
|
|
620
|
+
refuseWhenOrcaIsDown();
|
|
621
|
+
if (values.on === true && values.off === true) {
|
|
622
|
+
throw new Error('groom takes --on or --off, and got both. Say which one you want.');
|
|
623
|
+
}
|
|
624
|
+
const on = values.on === true ? true : (values.off === true ? false : undefined);
|
|
625
|
+
const groom = grooming(bots, { at: values.at, on });
|
|
626
|
+
return { answer: { bots, groom }, lines: groomLines(groom, bots) };
|
|
627
|
+
},
|
|
628
|
+
|
|
629
|
+
usage(bots, values) {
|
|
630
|
+
const usage = readUsage(bots, { bot: values.bot, session: values.session, since: values.since, until: values.until });
|
|
631
|
+
return { answer: { bots, usage }, lines: usageLines(usage, bots) };
|
|
632
|
+
},
|
|
633
|
+
|
|
634
|
+
roster(bots, values) {
|
|
635
|
+
const roster = readRoster(bots, { bot: values.bot });
|
|
636
|
+
return { answer: { bots, roster }, lines: rosterLines(roster, bots) };
|
|
637
|
+
},
|
|
638
|
+
|
|
639
|
+
'skills fetch'(bots, values) {
|
|
640
|
+
return fetched(bots, fetchSources(bots, { source: values.source }), 'Nothing to fetch');
|
|
641
|
+
},
|
|
642
|
+
|
|
643
|
+
'skills update'(bots, values) {
|
|
644
|
+
return fetched(bots, fetchSources(bots, { source: values.source, moving: true }), 'Nothing to update');
|
|
645
|
+
},
|
|
646
|
+
|
|
647
|
+
'rules build'(bots, values) {
|
|
648
|
+
const rules = buildRules(bots, { bot: values.bot });
|
|
649
|
+
const trouble = rules.filter((entry) => entry.trouble !== undefined);
|
|
650
|
+
return {
|
|
651
|
+
answer: { bots, rules },
|
|
652
|
+
lines: [
|
|
653
|
+
...rulesLines(rules, bots),
|
|
654
|
+
trouble.length === 0
|
|
655
|
+
? `Rules are built. Your bots folder: ${bots}`
|
|
656
|
+
// Not "not built": the file itself may be fine and the trouble be
|
|
657
|
+
// the CLAUDE.md beside it, which is a bot whose rules still do not
|
|
658
|
+
// reach both harnesses.
|
|
659
|
+
: `${trouble.map((entry) => entry.bot).join(', ')}: the rules are not in place. Settle what the lines above say, then build again.`,
|
|
660
|
+
],
|
|
661
|
+
// The build is the whole of this command, so a build it could not make is
|
|
662
|
+
// what the command ends in. `up` answers for its tabs and is not held to
|
|
663
|
+
// this: see bringUp.
|
|
664
|
+
code: trouble.length === 0 ? 0 : 1,
|
|
665
|
+
};
|
|
666
|
+
},
|
|
667
|
+
|
|
668
|
+
'message to'(bots, values) {
|
|
669
|
+
// A lookup reads the book and nothing else, so it answers with Orca down:
|
|
670
|
+
// knowing how to reach somebody is worth having when the app is not up.
|
|
671
|
+
const answer = lookUp(bots, { to: values.to, from: values.from, tab: process.env[TAB_ENV] });
|
|
672
|
+
const where = `${answer.to.bot}/${answer.to.session}`;
|
|
673
|
+
return {
|
|
674
|
+
answer,
|
|
675
|
+
lines: [
|
|
676
|
+
`${answer.transport.padEnd(9)} ${where.padEnd(24)} ${answer.address ?? '-'}`,
|
|
677
|
+
...toLines(answer, bots, where),
|
|
678
|
+
],
|
|
679
|
+
code: answer.trouble === undefined ? 0 : 1,
|
|
680
|
+
};
|
|
681
|
+
},
|
|
682
|
+
|
|
683
|
+
'message send'(bots, values) {
|
|
684
|
+
refuseWhenOrcaIsDown();
|
|
685
|
+
const answer = sendMessage(bots, {
|
|
686
|
+
to: values.to,
|
|
687
|
+
from: values.from,
|
|
688
|
+
tab: process.env[TAB_ENV],
|
|
689
|
+
subject: values.subject,
|
|
690
|
+
text: values.text,
|
|
691
|
+
textFile: values['text-file'],
|
|
692
|
+
thread: values.thread,
|
|
693
|
+
});
|
|
694
|
+
const where = `${answer.to.bot}/${answer.to.session}`;
|
|
695
|
+
|
|
696
|
+
if (!answer.sent) {
|
|
697
|
+
return {
|
|
698
|
+
answer,
|
|
699
|
+
lines: [`${'not sent'.padEnd(9)} ${where.padEnd(24)} ${answer.subject}`, ` ${answer.trouble}`],
|
|
700
|
+
code: 1,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return {
|
|
705
|
+
answer,
|
|
706
|
+
lines: [
|
|
707
|
+
`${'sent'.padEnd(9)} ${where.padEnd(24)} ${answer.subject}`,
|
|
708
|
+
...(answer.file === undefined
|
|
709
|
+
? []
|
|
710
|
+
: [` it was too long to travel as itself, so it went as a file: ${answer.file}`]),
|
|
711
|
+
...[nudgeLine(answer, where)],
|
|
712
|
+
],
|
|
713
|
+
};
|
|
714
|
+
},
|
|
715
|
+
|
|
716
|
+
'message check'(bots, values) {
|
|
717
|
+
refuseWhenOrcaIsDown();
|
|
718
|
+
const answer = checkMail(bots, {
|
|
719
|
+
bot: values.bot,
|
|
720
|
+
session: values.session,
|
|
721
|
+
tab: process.env[TAB_ENV],
|
|
722
|
+
peek: values.peek === true,
|
|
723
|
+
});
|
|
724
|
+
const where = `${answer.bot}/${answer.session}`;
|
|
725
|
+
|
|
726
|
+
if (answer.trouble !== undefined) {
|
|
727
|
+
return { answer, lines: [`${'trouble'.padEnd(9)} ${where}`, ` ${answer.trouble}`], code: 1 };
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
return {
|
|
731
|
+
answer,
|
|
732
|
+
lines: [
|
|
733
|
+
...answer.messages.flatMap((message) => [
|
|
734
|
+
`${'message'.padEnd(9)} ${message.from} ${message.at}`,
|
|
735
|
+
` ${message.subject}`,
|
|
736
|
+
...String(message.body ?? '').split('\n').map((line) => ` ${line}`),
|
|
737
|
+
]),
|
|
738
|
+
answer.messages.length === 0
|
|
739
|
+
? `Nothing is waiting for ${where}.`
|
|
740
|
+
// Said plainly, because a peek leaves the same mail there to be found
|
|
741
|
+
// again and a read does not.
|
|
742
|
+
: `${answer.messages.length} for ${where}${answer.read ? ', now read' : ', still unread: this was a peek'}.`,
|
|
743
|
+
],
|
|
744
|
+
};
|
|
745
|
+
},
|
|
746
|
+
|
|
747
|
+
'session add'(bots, values) {
|
|
748
|
+
const added = addSession(bots, values.bot, settingsOf(values));
|
|
749
|
+
const answer = { bots, bot: added.bot, home: added.home, session: added.session };
|
|
750
|
+
return {
|
|
751
|
+
answer,
|
|
752
|
+
lines: [
|
|
753
|
+
`added session ${added.session.name} to ${path.join('bots', added.bot, 'bot.yaml')}`,
|
|
754
|
+
...Object.entries(added.session)
|
|
755
|
+
.filter(([key]) => key !== 'name')
|
|
756
|
+
.map(([key, value]) => ` ${key} ${oneLine(value)}`),
|
|
757
|
+
`Bring it up: obk up --bots ${bots} --bot ${added.bot}`,
|
|
758
|
+
],
|
|
759
|
+
};
|
|
760
|
+
},
|
|
761
|
+
|
|
762
|
+
'session change'(bots, values) {
|
|
763
|
+
if (values.harness !== undefined) {
|
|
764
|
+
throw new Error(`session change does not change a session's harness: its conversations belong to the harness they ran on. To move it, retire it with obk retire and add one on ${values.harness} with obk session add.`);
|
|
765
|
+
}
|
|
766
|
+
const { name, ...settings } = settingsOf(values);
|
|
767
|
+
const changed = changeSession(bots, values.bot, values.session, settings);
|
|
768
|
+
const restart = `obk restart --bots ${bots} --bot ${changed.bot} --session ${values.session}`;
|
|
769
|
+
return {
|
|
770
|
+
answer: { bots, bot: changed.bot, home: changed.home, session: changed.session, restart },
|
|
771
|
+
lines: [
|
|
772
|
+
`changed session ${values.session} in ${path.join('bots', changed.bot, 'bot.yaml')}`,
|
|
773
|
+
...Object.entries(changed.session)
|
|
774
|
+
.filter(([key]) => key !== 'name')
|
|
775
|
+
.map(([key, value]) => ` ${key} ${oneLine(value)}`),
|
|
776
|
+
`A running session takes this when it next starts: ${restart}`,
|
|
777
|
+
],
|
|
778
|
+
};
|
|
779
|
+
},
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
/**
|
|
783
|
+
* What `obk message to` says under the road and the address: how to use it, or
|
|
784
|
+
* what is in the way. A Claude pair the native road cannot carry says so, since
|
|
785
|
+
* a caller told "use the mailbox" about two Claude sessions would otherwise
|
|
786
|
+
* think the kit had forgotten its own rule.
|
|
787
|
+
*/
|
|
788
|
+
function toLines(answer, bots, where) {
|
|
789
|
+
if (answer.trouble !== undefined) return [` ${answer.trouble}`];
|
|
790
|
+
if (answer.transport === 'native') {
|
|
791
|
+
return [` Write to ${answer.address} with your own harness's messaging. The kit does not carry that road.`];
|
|
792
|
+
}
|
|
793
|
+
return [
|
|
794
|
+
...(answer.unnamed === true
|
|
795
|
+
? [` ${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
|
+
: []),
|
|
797
|
+
` Send it: obk message send --bots ${bots} --to ${where} --subject <text> --text <text>`,
|
|
798
|
+
];
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* What became of the line that tells the receiver to look. The message is in
|
|
803
|
+
* its mailbox whatever this says, so each of these is about the tab and not
|
|
804
|
+
* about the message.
|
|
805
|
+
*/
|
|
806
|
+
function nudgeLine(answer, where) {
|
|
807
|
+
if (answer.nudged) return ' its tab was told to look; it will read it when it is done with what it is doing.';
|
|
808
|
+
if (answer.blocked !== undefined) {
|
|
809
|
+
return ` its tab has something waiting to be answered (${answer.blocked}), so nothing was typed into it. Settle that, and the mail is there.`;
|
|
810
|
+
}
|
|
811
|
+
if (answer.nudgeTrouble !== undefined) {
|
|
812
|
+
return ` it is queued, and its tab could not be told to look: ${answer.nudgeTrouble}`;
|
|
813
|
+
}
|
|
814
|
+
return ` ${where} is not up, so nothing was typed anywhere: the message waits in its mailbox.`;
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
/** The settings a `session add` was given, as they go into bot.yaml. */
|
|
818
|
+
function settingsOf(values) {
|
|
819
|
+
const settings = { name: values.name };
|
|
820
|
+
for (const [flag, key] of SETTINGS) {
|
|
821
|
+
if (values[flag] !== undefined) settings[key] = key === 'context' ? asNumberOrText(values[flag]) : values[flag];
|
|
822
|
+
}
|
|
823
|
+
if (values['extra-arg'] !== undefined) settings.extra_args = values['extra-arg'];
|
|
824
|
+
return settings;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/** A context window written as a plain number stays one in the file. */
|
|
828
|
+
const asNumberOrText = (value) => (/^\d+$/.test(value) ? Number(value) : value);
|
|
829
|
+
|
|
830
|
+
const oneLine = (value) => (Array.isArray(value) ? value.join(' ') : String(value)).replace(/\s+/g, ' ').trim();
|
|
831
|
+
|
|
832
|
+
function refuseWhenOrcaIsDown() {
|
|
833
|
+
const trouble = orcaTrouble();
|
|
834
|
+
if (trouble !== undefined) throw new Error(trouble);
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
/** The same facts as `--json`, as lines, for a person reading along. */
|
|
838
|
+
/** The one line that says which conversation this tab was given, and from where. */
|
|
839
|
+
const howLine = (tab) => (tab.resumed === true
|
|
840
|
+
? ' it was told to resume the session the book holds, with its conversation.'
|
|
841
|
+
: ' it was told to start a new session: the book holds none for this one yet.');
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* What the harness still has in this bot's folder that no session claims. The
|
|
845
|
+
* kit will not pick one — a bot's sessions and everything they start share that
|
|
846
|
+
* folder — so it says what is there and how to settle it.
|
|
847
|
+
*/
|
|
848
|
+
const unclaimedLines = (tab, bots) => {
|
|
849
|
+
const one = tab.unclaimed.length === 1;
|
|
850
|
+
return [
|
|
851
|
+
' the kit cannot say which conversation this session had.',
|
|
852
|
+
` ${one ? 'This one ran' : 'These ran'} in this bot's folder and no session claims ${one ? 'it' : 'them'}:`,
|
|
853
|
+
...tab.unclaimed.map((id) => ` ${id}`),
|
|
854
|
+
" A bot's sessions share that folder, and so does anything they start",
|
|
855
|
+
' inside themselves, so the kit does not guess. To give one back, write it',
|
|
856
|
+
` into ${path.join(bots, 'bots', tab.bot, 'sessions.yaml')}`,
|
|
857
|
+
` under ${tab.name} as session: <id> and run obk up again.`,
|
|
858
|
+
];
|
|
859
|
+
};
|
|
860
|
+
|
|
861
|
+
/**
|
|
862
|
+
* What `skills fetch` and `skills update` answer: a line per source saying what
|
|
863
|
+
* became of it, at which ref, and which commit that turned out to be — the
|
|
864
|
+
* version the user asked for beside the one they got.
|
|
865
|
+
*
|
|
866
|
+
* A source carrying scripts or hooks gets one line of its own, whatever is in
|
|
867
|
+
* it. The kit does not scan it and does not stand in the way; the risk is the
|
|
868
|
+
* user's to take and theirs to know about (PRD 6.7).
|
|
869
|
+
*/
|
|
870
|
+
function fetched(bots, sources, nothing) {
|
|
871
|
+
const trouble = sources.filter((source) => source.trouble !== undefined);
|
|
872
|
+
const lines = sources.flatMap((source) => (source.trouble !== undefined
|
|
873
|
+
? [`${'trouble'.padEnd(9)} ${source.name}`, ` ${source.trouble}`]
|
|
874
|
+
: [
|
|
875
|
+
`${source.state.padEnd(9)} ${source.name.padEnd(24)} ${source.ref} ${source.sha.slice(0, 7)}`,
|
|
876
|
+
...(source.runs ? [` ${source.name} carries scripts or hooks. The kit neither looks at them nor stops them; they are the source's, and the risk is yours.`] : []),
|
|
877
|
+
]));
|
|
878
|
+
|
|
879
|
+
return {
|
|
880
|
+
answer: { bots, sources },
|
|
881
|
+
lines: [
|
|
882
|
+
...lines,
|
|
883
|
+
// eslint-disable-next-line no-nested-ternary
|
|
884
|
+
sources.length === 0 ? `${nothing}: skills.yaml lists no sources. Your bots folder: ${bots}`
|
|
885
|
+
: trouble.length === 0 ? `Your sources are where they should be. Your bots folder: ${bots}`
|
|
886
|
+
: `${trouble.map((source) => source.name).join(', ')}: not fetched. Settle what the lines above say, then try again.`,
|
|
887
|
+
],
|
|
888
|
+
code: trouble.length === 0 ? 0 : 1,
|
|
889
|
+
};
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* What each bot's sessions have used, as lines: a block per session, a line per
|
|
894
|
+
* conversation, and the bot's unclaimed ones under it. Tokens and no money, the
|
|
895
|
+
* same as the answer, because the price is looked up by whoever is reading.
|
|
896
|
+
*/
|
|
897
|
+
function usageLines(usage, bots) {
|
|
898
|
+
const lines = [];
|
|
899
|
+
|
|
900
|
+
for (const entry of usage) {
|
|
901
|
+
lines.push(`${'bot'.padEnd(9)} ${entry.bot}`);
|
|
902
|
+
for (const session of entry.sessions) {
|
|
903
|
+
lines.push(`${'session'.padEnd(9)} ${session.name}`);
|
|
904
|
+
lines.push(...session.conversations.map(conversationLine));
|
|
905
|
+
if (session.conversations.length === 0) lines.push(' nothing on record');
|
|
906
|
+
}
|
|
907
|
+
if (entry.unclaimed.length > 0) {
|
|
908
|
+
lines.push(`${'unclaimed'.padEnd(9)} ${entry.bot}: no session of this bot claims these`);
|
|
909
|
+
lines.push(...entry.unclaimed.map(conversationLine));
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
lines.push(usage.length === 0
|
|
914
|
+
? `No bots yet. Your bots folder: ${bots}`
|
|
915
|
+
: `${usage.length} bot${usage.length === 1 ? '' : 's'}. What a token costs is yours to look up. Your bots folder: ${bots}`);
|
|
916
|
+
return lines;
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** One conversation: what it is, what it ran as, and what it used. */
|
|
920
|
+
function conversationLine(one) {
|
|
921
|
+
const ran = [...one.models, ...one.efforts].join(' ');
|
|
922
|
+
const used = Object.entries(one.tokens)
|
|
923
|
+
.filter(([, count]) => count > 0)
|
|
924
|
+
.map(([kind, count]) => `${kind} ${count}`)
|
|
925
|
+
.join(' ');
|
|
926
|
+
return ` ${one.id} ${one.calls} call${one.calls === 1 ? '' : 's'}`
|
|
927
|
+
+ `${ran === '' ? '' : ` ${ran}`}`
|
|
928
|
+
+ `${used === '' ? '' : ` ${used}`}`
|
|
929
|
+
+ `${one.compactions > 0 ? ` compacted ${one.compactions}` : ''}`;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* What there is to say about the daily grooming: whether it exists, when it
|
|
934
|
+
* runs, and whether it is on. A grooming that is off is not a fault, so this
|
|
935
|
+
* says what is there and what the next step would be rather than warning.
|
|
936
|
+
*/
|
|
937
|
+
function groomLines(groom, bots) {
|
|
938
|
+
if (!groom.exists) {
|
|
939
|
+
return [
|
|
940
|
+
`${'groom'.padEnd(9)} there is no daily grooming yet`,
|
|
941
|
+
`Make one: obk groom --bots ${bots} --at 04:00`,
|
|
942
|
+
];
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
return [
|
|
946
|
+
`${'groom'.padEnd(9)} daily at ${groom.at} ${groom.enabled ? 'on' : 'off'}`,
|
|
947
|
+
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`,
|
|
950
|
+
];
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** The settings a session carries, in the order a session is written down. */
|
|
954
|
+
const SHOWN = SESSION_FIELDS.filter((field) => field !== 'name' && field !== 'harness');
|
|
955
|
+
|
|
956
|
+
/**
|
|
957
|
+
* The fleet as lines: a block per bot, then a block per session under it. The
|
|
958
|
+
* facts and nothing else, in the order the answer carries them, so that what a
|
|
959
|
+
* person reads here and what a skill reads from `--json` are the same thing.
|
|
960
|
+
*
|
|
961
|
+
* The charter is printed whole. It is the one part of a bot that says what it
|
|
962
|
+
* is for, and a report that shortened it would be making the judgement this
|
|
963
|
+
* command does not make.
|
|
964
|
+
*/
|
|
965
|
+
function rosterLines(roster, bots) {
|
|
966
|
+
const lines = [];
|
|
967
|
+
|
|
968
|
+
for (const entry of roster) {
|
|
969
|
+
lines.push(`${'bot'.padEnd(9)} ${entry.bot}${entry.harness === undefined ? '' : ` ${entry.harness}`}${entry.paused === true ? ' paused' : ''}`);
|
|
970
|
+
if (entry.charter !== undefined) {
|
|
971
|
+
lines.push(...String(entry.charter).trimEnd().split('\n').map((line) => ` ${line}`.trimEnd()));
|
|
972
|
+
}
|
|
973
|
+
for (const [what, list] of [['rules', entry.rules], ['skills', entry.skills]]) {
|
|
974
|
+
if (list.length > 0) lines.push(` ${what.padEnd(6)} ${list.join(' ')}`);
|
|
975
|
+
}
|
|
976
|
+
if (entry.orca.project !== undefined) {
|
|
977
|
+
lines.push(` Orca project ${entry.orca.project}${entry.orca.setup === undefined ? '' : ` setup ${entry.orca.setup}`}`);
|
|
978
|
+
}
|
|
979
|
+
for (const session of entry.sessions) lines.push(...sessionLines(session));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
lines.push(roster.length === 0
|
|
983
|
+
? `No bots yet. Make one: obk bot create --bots ${bots} --name <name> --harness claude|codex`
|
|
984
|
+
: `${roster.length} bot${roster.length === 1 ? '' : 's'}. Your bots folder: ${bots}`);
|
|
985
|
+
return lines;
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
/**
|
|
989
|
+
* One session: what it is set to, then what the kit knows about it as a running
|
|
990
|
+
* thing. A setting it does not carry is left out rather than filled in, because
|
|
991
|
+
* left out is what it means — the harness's own default, whatever that is today.
|
|
992
|
+
*/
|
|
993
|
+
function sessionLines(session) {
|
|
994
|
+
const set = SHOWN
|
|
995
|
+
.filter((field) => session[field] !== undefined)
|
|
996
|
+
.map((field) => `${field} ${oneLine(session[field])}`);
|
|
997
|
+
const book = session.book;
|
|
998
|
+
|
|
999
|
+
return [
|
|
1000
|
+
`${'session'.padEnd(9)} ${session.name}${session.harness === undefined ? '' : ` ${session.harness}`}${set.length === 0 ? '' : ` ${set.join(' ')}`}${session.paused === true ? ' paused' : ''}`,
|
|
1001
|
+
...(book.tab === undefined
|
|
1002
|
+
? []
|
|
1003
|
+
: [` tab ${book.tab}${book.launched === undefined ? '' : ` launched ${book.launched}`}`]),
|
|
1004
|
+
...(book.session === undefined ? [] : [` conversation ${book.session}`]),
|
|
1005
|
+
...(book.history ?? [])
|
|
1006
|
+
.filter((was) => was?.session !== undefined)
|
|
1007
|
+
.map((was) => ` was ${was.session}${was.ended === undefined ? '' : ` ${was.ended}`}`),
|
|
1008
|
+
...(book.unclaimed ?? []).map((id) => ` unclaimed ${id}`),
|
|
1009
|
+
];
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
/**
|
|
1013
|
+
* What became of each bot's `AGENTS.md`, in the same column as the rest of the
|
|
1014
|
+
* report: what the build did, the file it did it to, and what it carries. A
|
|
1015
|
+
* build that would not go through says what is in the way instead, in the
|
|
1016
|
+
* words of whoever has to settle it.
|
|
1017
|
+
*/
|
|
1018
|
+
function rulesLines(rules, bots) {
|
|
1019
|
+
return rules.flatMap((entry) => {
|
|
1020
|
+
const what = entry.units === undefined
|
|
1021
|
+
? ''
|
|
1022
|
+
: ` ${entry.units.length} unit${entry.units.length === 1 ? '' : 's'}`;
|
|
1023
|
+
return [
|
|
1024
|
+
`${entry.state.padEnd(9)} ${path.relative(bots, entry.file)}${what}`,
|
|
1025
|
+
// Named on the run that made it: it is a file the command wrote.
|
|
1026
|
+
...(entry.linked === undefined
|
|
1027
|
+
? []
|
|
1028
|
+
: [`${'linked'.padEnd(9)} ${path.relative(bots, entry.linked)} -> AGENTS.md`]),
|
|
1029
|
+
...(entry.trouble === undefined ? [] : [` ${entry.trouble}`]),
|
|
1030
|
+
// Codex reads no more than this of an instructions file, and says nothing
|
|
1031
|
+
// when it stops reading (tech notes, section 3).
|
|
1032
|
+
...(entry.bytes > CODEX_CAP
|
|
1033
|
+
? [` it is over the ${CODEX_CAP / 1024} KiB Codex reads, so a Codex session will not see all of it.`]
|
|
1034
|
+
: []),
|
|
1035
|
+
];
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
/**
|
|
1040
|
+
* What each bot has in its skills directories: one line naming the bot, then
|
|
1041
|
+
* one per skill — what became of it, its name, and which shelf it came from.
|
|
1042
|
+
* A skill the user put there themselves is named too, and said to be theirs.
|
|
1043
|
+
*/
|
|
1044
|
+
function skillsLines(skills) {
|
|
1045
|
+
return skills.flatMap((entry) => [
|
|
1046
|
+
entry.bot,
|
|
1047
|
+
...(entry.trouble === undefined ? [] : [` ${'trouble'.padEnd(9)} ${entry.trouble}`]),
|
|
1048
|
+
...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
|
+
...(entry.removed ?? []).map((name) => ` ${'removed'.padEnd(9)} ${name.padEnd(24)} no list names it now`),
|
|
1050
|
+
]);
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
/**
|
|
1054
|
+
* What a check found, two lines each: what kind of trouble it is and the one
|
|
1055
|
+
* thing to go and look at, then the sentence about it. The same shape wherever
|
|
1056
|
+
* a command reports one, so a reader who has seen one has seen them all.
|
|
1057
|
+
*/
|
|
1058
|
+
const foundLines = (found) =>
|
|
1059
|
+
found.flatMap((one) => [`${one.kind.padEnd(9)} ${one.where}`, ` ${one.says}`]);
|
|
1060
|
+
|
|
1061
|
+
function tabLines({ bots, created, completed, rules, skills, tabs, paused = [], found = [] }, summary) {
|
|
1062
|
+
const lines = [
|
|
1063
|
+
...created.map((entry) => `created ${entry}`),
|
|
1064
|
+
...completed.map((entry) => `completed ${entry}`),
|
|
1065
|
+
...rulesLines(rules, bots),
|
|
1066
|
+
...skillsLines(skills),
|
|
1067
|
+
];
|
|
1068
|
+
|
|
1069
|
+
for (const tab of tabs) {
|
|
1070
|
+
lines.push(`${tab.created ? 'opened' : 'found '} ${tab.title} tab ${tab.tabId} terminal ${tab.terminal}`);
|
|
1071
|
+
lines.push(...harnessLines(tab, bots));
|
|
1072
|
+
}
|
|
1073
|
+
for (const one of paused) {
|
|
1074
|
+
const what = one.session === undefined ? one.bot : `${one.bot} ${one.session}`;
|
|
1075
|
+
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}`);
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
// Last before the summary, because what a check found is about the setup the
|
|
1080
|
+
// run has just left behind rather than about any one thing it did.
|
|
1081
|
+
lines.push(...foundLines(found), summary);
|
|
1082
|
+
return lines;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* What became of the harness in a tab this run opened. Nothing is typed into a
|
|
1087
|
+
* tab that was already there, and nothing into the plain tab beside the
|
|
1088
|
+
* sessions, so those have nothing to say.
|
|
1089
|
+
*
|
|
1090
|
+
* The caller decides what to do next from these lines, so they say both what
|
|
1091
|
+
* was typed and what was seen afterwards, and never one in place of the other.
|
|
1092
|
+
*/
|
|
1093
|
+
function harnessLines(tab, bots) {
|
|
1094
|
+
if (!tab.created || tab.name === null) return [];
|
|
1095
|
+
|
|
1096
|
+
// What the line that was typed in asked for, and where the kit got it: the
|
|
1097
|
+
// session the book holds, one the harness itself still had on record, or a new
|
|
1098
|
+
// one — and if a new one, whether anything was known about an older one.
|
|
1099
|
+
const how = [howLine(tab)];
|
|
1100
|
+
|
|
1101
|
+
if (!tab.harnessStarted) {
|
|
1102
|
+
return [
|
|
1103
|
+
...how,
|
|
1104
|
+
' the harness was typed in, and no session came up in the tab.',
|
|
1105
|
+
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1106
|
+
];
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const lines = tab.blockedReason === undefined
|
|
1110
|
+
? [...how, ' the harness was typed in and came up.']
|
|
1111
|
+
: [
|
|
1112
|
+
...how,
|
|
1113
|
+
` the harness was typed in and came up, waiting on: ${tab.blockedReason}`,
|
|
1114
|
+
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1115
|
+
];
|
|
1116
|
+
|
|
1117
|
+
if (tab.promptSent === true) lines.push(' the start prompt was typed in.');
|
|
1118
|
+
if (tab.promptSent === false) {
|
|
1119
|
+
lines.push(' the start prompt was not typed in: the tab was not ready for it.');
|
|
1120
|
+
}
|
|
1121
|
+
if (tab.unclaimed !== undefined) lines.push(...unclaimedLines(tab, bots));
|
|
1122
|
+
return lines;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
try {
|
|
1126
|
+
process.exitCode = await run(process.argv.slice(2));
|
|
1127
|
+
} catch (error) {
|
|
1128
|
+
process.stderr.write(`obk: ${error.message}\n`);
|
|
1129
|
+
process.exitCode = 1;
|
|
1130
|
+
}
|