@assuredloop/orca-bot-kit 0.1.0 → 0.3.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 +548 -3
- package/SETUP.md +275 -0
- package/package.json +32 -13
- package/rules/evidence.md +15 -0
- package/rules/finishing.md +23 -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 +22 -0
- package/rules/simple.md +14 -0
- package/rules/talk.md +13 -0
- package/rules/tests-first.md +17 -0
- package/rules/the-ask.md +19 -0
- package/skills/obk-arch/LICENSES.md +52 -0
- package/skills/obk-arch/NOTICE.md +242 -0
- package/skills/obk-arch/SKILL.md +598 -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 +312 -0
- package/skills/obk-debugging/LICENSES.md +67 -0
- package/skills/obk-debugging/NOTICE.md +115 -0
- package/skills/obk-debugging/SKILL.md +434 -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 +148 -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 +256 -0
- package/skills/obk-grilling/LICENSES.md +42 -0
- package/skills/obk-grilling/NOTICE.md +111 -0
- package/skills/obk-grilling/SKILL.md +221 -0
- package/skills/obk-grooming/LICENSES.md +52 -0
- package/skills/obk-grooming/NOTICE.md +111 -0
- package/skills/obk-grooming/SKILL.md +222 -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 +269 -0
- package/skills/obk-reviewing/LICENSES.md +66 -0
- package/skills/obk-reviewing/NOTICE.md +58 -0
- package/skills/obk-reviewing/SKILL.md +411 -0
- package/skills/obk-tdd/LICENSES.md +68 -0
- package/skills/obk-tdd/NOTICE.md +54 -0
- package/skills/obk-tdd/SKILL.md +547 -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 +180 -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 +1169 -0
- package/src/conversations.js +150 -0
- package/src/groom.js +159 -0
- package/src/health.js +309 -0
- package/src/hooks.js +257 -0
- package/src/init.js +314 -0
- package/src/launch.js +299 -0
- package/src/message.js +402 -0
- package/src/orca.js +362 -0
- package/src/pause.js +63 -0
- package/src/record.js +209 -0
- package/src/restart.js +193 -0
- package/src/retire.js +92 -0
- package/src/roster.js +71 -0
- package/src/rules.js +408 -0
- package/src/skills.js +630 -0
- package/src/sources.js +371 -0
- package/src/up.js +500 -0
- package/src/usage.js +401 -0
- package/bin/okb.js +0 -4
- package/src/index.js +0 -11
package/src/message.js
ADDED
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
// `obk message`: one session writing to another (PRD 6.9, ADR 0018).
|
|
2
|
+
//
|
|
3
|
+
// Two roads, and the bot never picks. Claude to Claude in the same approval
|
|
4
|
+
// class is the harness's own messaging, which no command line can send for it —
|
|
5
|
+
// so the kit answers with the address and says to use it. Everything else goes
|
|
6
|
+
// through the Orca mailbox, which the kit does carry.
|
|
7
|
+
//
|
|
8
|
+
// What the mailbox is, and why it is not the tab: Orca calls a terminal a live
|
|
9
|
+
// terminal-only mailbox, warns that delivery does not outlive the tab, and
|
|
10
|
+
// refuses a send once the pane is gone. A Run survives the tab, the relaunch
|
|
11
|
+
// and the restart, so a session's address is its Run (tech notes, section 1).
|
|
12
|
+
//
|
|
13
|
+
// Nothing in a mailbox wakes the session it belongs to — a message addressed to
|
|
14
|
+
// a tab leaves the harness in it untouched, proved live — so a send also types
|
|
15
|
+
// one line into the receiver's tab. Both harnesses queue a typed line while
|
|
16
|
+
// they are busy, which is what PRD 6.9 means by queued and not interrupting.
|
|
17
|
+
|
|
18
|
+
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import path from 'node:path';
|
|
20
|
+
|
|
21
|
+
import { readBook } from './book.js';
|
|
22
|
+
import { botDir, botNames, readBot } from './bot.js';
|
|
23
|
+
import { harnessOf, ownCli, reachesMail, shellWord } from './launch.js';
|
|
24
|
+
import { ackMailbox, harnessInTab, postMessage, readMailbox, tabs, typeIntoTab, useMailbox } from './orca.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* How much of a message travels as itself. Above this it is written to a file
|
|
28
|
+
* beside the bots folder and the message names the file, which is PRD 6.9's one
|
|
29
|
+
* rule with no judgement in it.
|
|
30
|
+
*
|
|
31
|
+
* The number is the receiver's, not Orca's: 200 KB goes through the mailbox
|
|
32
|
+
* whole, but it lands in a session's context, and a command line dies near a
|
|
33
|
+
* megabyte anyway. Four kilobytes is a message; more than that is a document,
|
|
34
|
+
* and a document has a name.
|
|
35
|
+
*/
|
|
36
|
+
export const INLINE_LIMIT = 4 * 1024;
|
|
37
|
+
|
|
38
|
+
/** How long Orca is given to say whether a tab has a TUI in it to nudge. */
|
|
39
|
+
const LOOK_MS = 2000;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Everything the kit knows about one session as somebody to write to: which
|
|
43
|
+
* harness it runs on, which approval class it is in, its mailbox, its name on
|
|
44
|
+
* Claude Code, and the tab it is in. `trouble` is the one sentence that says
|
|
45
|
+
* why it cannot be written to, when it cannot.
|
|
46
|
+
*/
|
|
47
|
+
export function findSession(bots, target) {
|
|
48
|
+
const { bot: botName, session: sessionName } = splitTarget(target);
|
|
49
|
+
|
|
50
|
+
const names = botNames(bots);
|
|
51
|
+
if (!names.includes(botName)) {
|
|
52
|
+
throw new Error(`there is no bot called ${botName} in ${bots}. The bots there are: ${names.join(', ')}.`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Resolved, because Orca is asked about this folder by path and does not
|
|
56
|
+
// follow a link to it (tech notes, section 1).
|
|
57
|
+
const home = realpathSync(botDir(bots, botName));
|
|
58
|
+
const bot = readBot(home, botName);
|
|
59
|
+
const session = pickSession(bot, sessionName, target);
|
|
60
|
+
const harness = harnessOf(session, bot.harness);
|
|
61
|
+
const held = readBook(home).sessions[session.name] ?? {};
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
bots,
|
|
65
|
+
home,
|
|
66
|
+
bot: bot.name,
|
|
67
|
+
session: session.name,
|
|
68
|
+
harness,
|
|
69
|
+
approval: classOf(session),
|
|
70
|
+
address: typeof held.address === 'string' ? held.address : undefined,
|
|
71
|
+
mailbox: typeof held.mailbox === 'string' ? held.mailbox : undefined,
|
|
72
|
+
tab: typeof held.tab === 'string' ? held.tab : undefined,
|
|
73
|
+
trouble: whyNotReachable(bot.name, session, harness, held),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Which road a message between these two takes, and the address it goes to.
|
|
79
|
+
*
|
|
80
|
+
* Native is the harness's own cross-session messaging: Claude Code to Claude
|
|
81
|
+
* Code, and only where both sit in the same approval class, because a message
|
|
82
|
+
* across classes is held for somebody to approve and the kit chooses a road in
|
|
83
|
+
* advance rather than working around a hold (PRD 6.9).
|
|
84
|
+
*/
|
|
85
|
+
export function roadBetween(from, to) {
|
|
86
|
+
const pair = from.harness === 'claude' && to.harness === 'claude' && from.approval === to.approval;
|
|
87
|
+
if (pair && to.address !== undefined) return { transport: 'native', address: to.address };
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
transport: 'orca',
|
|
91
|
+
address: to.mailbox === undefined ? undefined : `run:${to.mailbox}`,
|
|
92
|
+
// A Claude pair that the native road cannot carry, because the receiver is
|
|
93
|
+
// running under no name the kit gave it: it was started before the kit
|
|
94
|
+
// named sessions, and nothing renames a live harness. The mailbox is the
|
|
95
|
+
// road that exists, and the caller is told why it is the one being used
|
|
96
|
+
// rather than left to wonder (review of PR #132, finding 1).
|
|
97
|
+
unnamed: pair ? true : undefined,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** What `obk message to` answers: the road, the address, and who is at each end. */
|
|
102
|
+
export function lookUp(bots, { to: target, from: sender, tab }) {
|
|
103
|
+
const from = whoIsWriting(bots, sender, tab);
|
|
104
|
+
const to = findSession(bots, target);
|
|
105
|
+
const road = roadBetween(from, to);
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
bots,
|
|
109
|
+
from: { bot: from.bot, session: from.session, harness: from.harness },
|
|
110
|
+
to: { bot: to.bot, session: to.session, harness: to.harness },
|
|
111
|
+
...road,
|
|
112
|
+
trouble: to.trouble ?? (road.address === undefined ? notUpYet(to) : undefined),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Send one message down the Orca road, and tell the receiver's tab to look.
|
|
118
|
+
*
|
|
119
|
+
* A native pair is not carried here: no command line can send a Claude session's
|
|
120
|
+
* own message. The kit says which address to write to instead, and sends
|
|
121
|
+
* nothing, which is the whole of "the bot never picks the transport".
|
|
122
|
+
*/
|
|
123
|
+
export function sendMessage(bots, { to: target, from: sender, tab, subject, text, textFile, thread }) {
|
|
124
|
+
const from = whoIsWriting(bots, sender, tab);
|
|
125
|
+
const to = findSession(bots, target);
|
|
126
|
+
const road = roadBetween(from, to);
|
|
127
|
+
|
|
128
|
+
const answer = {
|
|
129
|
+
bots,
|
|
130
|
+
from: { bot: from.bot, session: from.session },
|
|
131
|
+
to: { bot: to.bot, session: to.session },
|
|
132
|
+
subject,
|
|
133
|
+
...road,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
if (road.transport === 'native') {
|
|
137
|
+
return {
|
|
138
|
+
...answer,
|
|
139
|
+
sent: false,
|
|
140
|
+
trouble: `${to.bot}/${to.session} is a Claude session in your own approval class, so it is written to with your own harness's messaging, not through Orca. Its address is ${to.address}.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (to.trouble !== undefined) return { ...answer, sent: false, trouble: to.trouble };
|
|
145
|
+
if (road.address === undefined) return { ...answer, sent: false, trouble: notUpYet(to) };
|
|
146
|
+
if (from.mailbox === undefined) {
|
|
147
|
+
return {
|
|
148
|
+
...answer,
|
|
149
|
+
sent: false,
|
|
150
|
+
trouble: `${from.bot}/${from.session} has no mailbox of its own yet, so a reply would have nowhere to go. Bring it up first: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${shellWord(from.bot)}`,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const written = bodyOf(bots, { from, to, subject, text, textFile });
|
|
155
|
+
const message = postMessage({
|
|
156
|
+
to: road.address,
|
|
157
|
+
from: `run:${from.mailbox}`,
|
|
158
|
+
subject,
|
|
159
|
+
body: written.body,
|
|
160
|
+
thread,
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
...answer,
|
|
165
|
+
sent: true,
|
|
166
|
+
id: message.id,
|
|
167
|
+
thread: message.thread_id ?? thread,
|
|
168
|
+
file: written.file,
|
|
169
|
+
...nudge(to, from, subject),
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* What is waiting for a session, and the reading of it.
|
|
175
|
+
*
|
|
176
|
+
* Orca fences a Run to one reader, so this binds before it reads. A read that
|
|
177
|
+
* is not a peek acknowledges the batch it read: that is what makes the next
|
|
178
|
+
* check bring the next one rather than the same again.
|
|
179
|
+
*/
|
|
180
|
+
export function checkMail(bots, { bot: botName, session: sessionName, tab, peek = false }) {
|
|
181
|
+
const who = sessionName === undefined && botName === undefined
|
|
182
|
+
? whoIsWriting(bots, undefined, tab, '--bot <bot> [--session <name>]: whose mail to read')
|
|
183
|
+
: findSession(bots, sessionName === undefined ? botName : `${botName}/${sessionName}`);
|
|
184
|
+
|
|
185
|
+
if (who.mailbox === undefined) {
|
|
186
|
+
return {
|
|
187
|
+
bots,
|
|
188
|
+
bot: who.bot,
|
|
189
|
+
session: who.session,
|
|
190
|
+
messages: [],
|
|
191
|
+
trouble: who.trouble ?? notUpYet(who),
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// Read as the session's own tab, wherever this check was typed. Binding the
|
|
196
|
+
// tab that asked would hand it the session's mailbox, and Orca's notice for
|
|
197
|
+
// every message after (issue #228). A session with no live tab is read as
|
|
198
|
+
// this process's own terminal, as it always was.
|
|
199
|
+
const handle = who.tab === undefined ? undefined : tabs(who.home).find((tab) => tab.tabId === who.tab)?.handle;
|
|
200
|
+
useMailbox(who.mailbox, handle);
|
|
201
|
+
const found = readMailbox(who.mailbox, { peek, handle });
|
|
202
|
+
const messages = (found.messages ?? []).map((message) => asMessage(bots, message));
|
|
203
|
+
if (!peek && found.deliveryId !== undefined && messages.length > 0) ackMailbox(who.mailbox, found.deliveryId, handle);
|
|
204
|
+
|
|
205
|
+
return { bots, bot: who.bot, session: who.session, mailbox: who.mailbox, read: !peek, messages };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** One message, in the kit's words rather than Orca's. */
|
|
209
|
+
const asMessage = (bots, message) => ({
|
|
210
|
+
id: message.id,
|
|
211
|
+
// The mailbox it came from, said as the session it belongs to where the
|
|
212
|
+
// fleet knows it: a reply is written to a session, not to a run id.
|
|
213
|
+
from: whoOwns(bots, message.from_handle) ?? message.from_handle,
|
|
214
|
+
fromMailbox: message.from_handle,
|
|
215
|
+
subject: message.subject,
|
|
216
|
+
body: message.body,
|
|
217
|
+
thread: message.thread_id ?? undefined,
|
|
218
|
+
at: message.created_at,
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
/** Whose mailbox `run:<id>` is, as `<bot>/<session>`, when it is one of the fleet's. */
|
|
222
|
+
function whoOwns(bots, handle) {
|
|
223
|
+
if (typeof handle !== 'string' || !handle.startsWith('run:')) return undefined;
|
|
224
|
+
const mailbox = handle.slice('run:'.length);
|
|
225
|
+
|
|
226
|
+
for (const name of botNames(bots)) {
|
|
227
|
+
const book = readBook(botDir(bots, name));
|
|
228
|
+
for (const [session, entry] of Object.entries(book.sessions)) {
|
|
229
|
+
if (entry?.mailbox === mailbox) return `${name}/${session}`;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return undefined;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* The session that is writing: the one named, or the one whose Orca tab this
|
|
237
|
+
* command is running in. A caller that is in neither is asked to say, rather
|
|
238
|
+
* than being given somebody else's name.
|
|
239
|
+
*/
|
|
240
|
+
function whoIsWriting(bots, sender, tab, asked = '--from <bot>/<session>: which session is writing') {
|
|
241
|
+
if (sender !== undefined) return findSession(bots, sender);
|
|
242
|
+
|
|
243
|
+
const found = tab === undefined ? undefined : sessionInTab(bots, tab);
|
|
244
|
+
if (found === undefined) {
|
|
245
|
+
throw new Error(`${asked}. The kit reads it from the tab when it runs in one, and this is not one of the fleet's tabs.`);
|
|
246
|
+
}
|
|
247
|
+
return found;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** The session the book says is in this Orca tab, wherever in the fleet it is. */
|
|
251
|
+
function sessionInTab(bots, tab) {
|
|
252
|
+
for (const name of botNames(bots)) {
|
|
253
|
+
const book = readBook(botDir(bots, name));
|
|
254
|
+
for (const [session, entry] of Object.entries(book.sessions)) {
|
|
255
|
+
if (entry?.tab === tab) return findSession(bots, `${name}/${session}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
return undefined;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* The message as it will travel: the text itself when it is short enough, and
|
|
263
|
+
* otherwise a file beside the bots folder with a body that names it.
|
|
264
|
+
*
|
|
265
|
+
* The file is the kit's own, next to the bots repo the way start prompts are,
|
|
266
|
+
* so a long message is not written into the user's git status and does not
|
|
267
|
+
* depend on a file of theirs staying where it was when they sent it.
|
|
268
|
+
*/
|
|
269
|
+
function bodyOf(bots, { from, to, subject, text, textFile }) {
|
|
270
|
+
const said = textFile === undefined ? text : readText(textFile);
|
|
271
|
+
if (said === undefined || said.trim() === '') {
|
|
272
|
+
throw new Error('message send needs something to say: --text <text> or --text-file <path>, with something in it.');
|
|
273
|
+
}
|
|
274
|
+
if (Buffer.byteLength(said, 'utf8') <= INLINE_LIMIT) return { body: said };
|
|
275
|
+
|
|
276
|
+
const file = path.join(
|
|
277
|
+
`${bots}.messages`,
|
|
278
|
+
`${encodeURIComponent(`${from.bot}.${from.session}`)}.${encodeURIComponent(`${to.bot}.${to.session}`)}.${stamp()}.md`,
|
|
279
|
+
);
|
|
280
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
281
|
+
writeFileSync(file, said);
|
|
282
|
+
return {
|
|
283
|
+
body: `${subject} is longer than a message carries, so it is in this file, whole and as it was written:\n${file}`,
|
|
284
|
+
file,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function readText(file) {
|
|
289
|
+
try {
|
|
290
|
+
return readFileSync(file, 'utf8');
|
|
291
|
+
} catch (error) {
|
|
292
|
+
throw new Error(`--text-file names ${file}, and that file cannot be read (${error.code}). Write it, or name the one you meant.`);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const stamp = () => new Date().toISOString().replaceAll(':', '-').replace('.', '-');
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Tell the receiver's tab that mail is waiting: one line, typed in.
|
|
300
|
+
*
|
|
301
|
+
* Nothing in the mailbox reaches a running harness by itself, and a typed line
|
|
302
|
+
* is taken as the next turn by a busy session rather than cutting into the one
|
|
303
|
+
* it is having. A tab with no harness in it is not typed into at all — there is
|
|
304
|
+
* nobody there to read it, and the message waits in the mailbox until the
|
|
305
|
+
* session is up. Nor is one the kit cannot tell about. A tab the book does not
|
|
306
|
+
* hold is never typed into on any road.
|
|
307
|
+
*
|
|
308
|
+
* Nor is a tab with something on screen waiting to be answered. A line typed
|
|
309
|
+
* into one of those is not a message: it is an answer to whatever question is
|
|
310
|
+
* up. That is not a worry, it is a thing that happened — in slice 03 a second
|
|
311
|
+
* line went into a tab on Claude Code's folder-trust list, confirmed its
|
|
312
|
+
* default, `No, exit`, and the harness quit (tech notes, section 1). So where
|
|
313
|
+
* Orca says a tab is blocked, nothing is typed and the mail waits.
|
|
314
|
+
*
|
|
315
|
+
* Orca's `blockedReason` catches Codex's screens and not Claude Code's, which
|
|
316
|
+
* answer as idle with nothing said, so this narrows the case rather than
|
|
317
|
+
* closing it. What closes it is nobody sending to a session before its first
|
|
318
|
+
* screens are answered, which is the caller's work either way.
|
|
319
|
+
*/
|
|
320
|
+
function nudge(to, from, subject) {
|
|
321
|
+
if (to.tab === undefined) return { nudged: false };
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
const live = tabs(to.home).find((tab) => tab.tabId === to.tab);
|
|
325
|
+
if (live === undefined) return { nudged: false };
|
|
326
|
+
|
|
327
|
+
const seen = harnessInTab(live.handle, LOOK_MS);
|
|
328
|
+
if (seen.blockedReason !== undefined) return { nudged: false, blocked: seen.blockedReason };
|
|
329
|
+
// The shell in front is a tab with no harness, whatever a stale
|
|
330
|
+
// `agentIdentity` says.
|
|
331
|
+
if (seen.front === 'shell') return { nudged: false };
|
|
332
|
+
// Not knowing is not a reason to type: a line that lands in a shell is run
|
|
333
|
+
// there, with the sender's subject in it. The program in front has to be
|
|
334
|
+
// the agent Orca names. With no name it may be a harness a few seconds
|
|
335
|
+
// into its launch; under another name it may be a pager started after the
|
|
336
|
+
// harness quit, under an identity Orca has not let go of (tech notes,
|
|
337
|
+
// section 1). A harness run through a wrapper such as `node` lands here
|
|
338
|
+
// too, until #261.
|
|
339
|
+
if (seen.front === undefined || seen.agent === undefined || seen.command !== seen.agent) {
|
|
340
|
+
const why = seen.front === undefined
|
|
341
|
+
? seen.unreadable
|
|
342
|
+
: `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
|
|
343
|
+
return { nudged: false, nudgeTrouble: `the kit could not tell whether a harness is running in it (${why}), so nothing was typed` };
|
|
344
|
+
}
|
|
345
|
+
// A busy harness is typed into: it takes the line as its next turn.
|
|
346
|
+
|
|
347
|
+
typeIntoTab(
|
|
348
|
+
live.handle,
|
|
349
|
+
`Fleet mail from ${from.bot}/${from.session}: ${subject}. Read it with ${shellWord(ownCli())} message check --bots ${shellWord(to.bots)} --bot ${shellWord(to.bot)} --session ${shellWord(to.session)}`,
|
|
350
|
+
);
|
|
351
|
+
return { nudged: true };
|
|
352
|
+
} catch (error) {
|
|
353
|
+
// The message is already queued, and it is waiting whatever Orca says
|
|
354
|
+
// about the tab. So this is reported rather than thrown: a send that ends
|
|
355
|
+
// in an error the caller reads as "it did not go" would be a lie, and a
|
|
356
|
+
// silent `false` would read as "the session is not up", which is a
|
|
357
|
+
// different thing from "Orca would not say".
|
|
358
|
+
return { nudged: false, nudgeTrouble: error.message };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** `<bot>/<session>`, or a bot on its own when it has only the one session. */
|
|
363
|
+
function splitTarget(target) {
|
|
364
|
+
const parts = target.split('/');
|
|
365
|
+
if (parts.length > 2 || parts.some((part) => part.trim() === '')) {
|
|
366
|
+
throw new Error(`a session is written <bot>/<session>, and got: ${target}`);
|
|
367
|
+
}
|
|
368
|
+
return { bot: parts[0], session: parts[1] };
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
function pickSession(bot, sessionName, target) {
|
|
372
|
+
if (sessionName === undefined) {
|
|
373
|
+
if (bot.sessions.length === 1) return bot.sessions[0];
|
|
374
|
+
if (bot.sessions.length === 0) {
|
|
375
|
+
throw new Error(`${bot.name} has no sessions yet. Add one with obk session add --bots <path> --bot ${bot.name} --name <name>.`);
|
|
376
|
+
}
|
|
377
|
+
throw new Error(`${bot.name} has ${bot.sessions.length} sessions, so say which one: ${bot.sessions.map((session) => `${bot.name}/${session.name}`).join(', ')}.`);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const found = bot.sessions.find((session) => session.name === sessionName);
|
|
381
|
+
if (found === undefined) {
|
|
382
|
+
throw new Error(`${bot.name} has no session called ${sessionName}. It has: ${bot.sessions.map((session) => session.name).join(', ') || 'none'}.`);
|
|
383
|
+
}
|
|
384
|
+
return found;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Claude Code delivers between two sessions of the same class without asking,
|
|
389
|
+
* and holds a message that crosses classes for somebody to approve (tech notes,
|
|
390
|
+
* section 2). The kit's levels map onto the harness's two: the bypassing one is
|
|
391
|
+
* `dangerously-skip`, and everything else prompts.
|
|
392
|
+
*/
|
|
393
|
+
const classOf = (session) => (session.approval === 'dangerously-skip' ? 'bypassing' : 'prompting');
|
|
394
|
+
|
|
395
|
+
/** Why fleet mail cannot reach this session at all, when it cannot. */
|
|
396
|
+
function whyNotReachable(bot, session, harness, held) {
|
|
397
|
+
if (reachesMail(session, harness)) return undefined;
|
|
398
|
+
return `${bot}/${session.name} is a Codex session whose extra arguments turn sandbox_workspace_write.network_access off. Orca is out of reach from inside that sandbox, so it can neither be written to nor read its own mail. Take that argument out, or write to it another way.`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
const notUpYet = (who) =>
|
|
402
|
+
`${who.bot}/${who.session} has no mailbox yet: it has never been brought up. Start it and it gets one: ${shellWord(ownCli())} up --bots ${shellWord(who.bots)} --bot ${shellWord(who.bot)} --session ${shellWord(who.session)}`;
|