@assuredloop/orca-bot-kit 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -15
- package/SETUP.md +10 -4
- package/package.json +1 -1
- package/rules/finishing.md +3 -0
- package/rules/mail.md +3 -3
- package/rules/scope.md +4 -2
- package/rules/tests-first.md +6 -4
- package/skills/obk-arch/LICENSES.md +7 -0
- package/skills/obk-arch/NOTICE.md +113 -5
- package/skills/obk-arch/SKILL.md +221 -10
- package/skills/obk-bot-building/NOTICE.md +2 -2
- package/skills/obk-bot-building/SKILL.md +108 -19
- package/skills/obk-debugging/SKILL.md +30 -15
- package/skills/obk-decision-memo/NOTICE.md +1 -1
- package/skills/obk-finops/NOTICE.md +1 -1
- package/skills/obk-finops/SKILL.md +6 -4
- package/skills/obk-fleet-review/SKILL.md +42 -5
- package/skills/obk-grilling/NOTICE.md +11 -3
- package/skills/obk-grilling/SKILL.md +11 -7
- package/skills/obk-grooming/SKILL.md +5 -2
- package/skills/obk-handoff/NOTICE.md +1 -1
- package/skills/obk-recall/NOTICE.md +1 -1
- package/skills/obk-researching/NOTICE.md +1 -1
- package/skills/obk-researching/SKILL.md +19 -9
- package/skills/obk-reviewing/SKILL.md +4 -1
- package/skills/obk-tdd/SKILL.md +54 -23
- package/skills/obk-teach/NOTICE.md +2 -2
- package/skills/obk-why/NOTICE.md +1 -1
- package/skills/obk-why/SKILL.md +21 -10
- package/src/book.js +2 -2
- package/src/bot.js +1 -1
- package/src/cli.js +117 -33
- package/src/groom.js +5 -3
- package/src/health.js +2 -2
- package/src/hooks.js +73 -18
- package/src/init.js +3 -3
- package/src/launch.js +28 -5
- package/src/message.js +38 -19
- package/src/orca-window.cjs +21 -0
- package/src/orca.js +191 -28
- package/src/pause.js +11 -6
- package/src/record.js +1 -1
- package/src/restart.js +6 -5
- package/src/retire.js +41 -6
- package/src/roster.js +1 -1
- package/src/rules.js +2 -2
- package/src/skills.js +113 -21
- package/src/sources.js +4 -3
- package/src/up.js +57 -23
- package/src/usage.js +2 -2
package/src/health.js
CHANGED
|
@@ -266,8 +266,8 @@ function inOrca(home, bot, setups) {
|
|
|
266
266
|
/**
|
|
267
267
|
* The conversations a harness has on record in this bot's folder that the book
|
|
268
268
|
* does not name anywhere. The hook is how an id reaches the book, and when it
|
|
269
|
-
* fails it fails quietly (ADR
|
|
270
|
-
* harness's own record set beside it (ADR
|
|
269
|
+
* fails it fails quietly (ADR 0020), so this is where a stale book shows: the
|
|
270
|
+
* harness's own record set beside it (ADR 0012).
|
|
271
271
|
*/
|
|
272
272
|
function offTheBook(home, bot, book) {
|
|
273
273
|
const named = new Set();
|
package/src/hooks.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// The kit's own hook, in the bot's folder and nowhere else (ADR
|
|
1
|
+
// The kit's own hook, in the bot's folder and nowhere else (ADR 0020).
|
|
2
2
|
//
|
|
3
3
|
// Both harnesses read a hooks file out of the folder a session starts in, and
|
|
4
4
|
// every session starts at its bot home, so the bot folder is where the kit's
|
|
@@ -15,7 +15,7 @@ import path from 'node:path';
|
|
|
15
15
|
import { isDeepStrictEqual } from 'node:util';
|
|
16
16
|
|
|
17
17
|
import { leadsOutside } from './bot.js';
|
|
18
|
-
import { shellWord } from './launch.js';
|
|
18
|
+
import { ownCli, shellWord } from './launch.js';
|
|
19
19
|
|
|
20
20
|
/** Where each harness reads a project's hooks, inside the bot home. */
|
|
21
21
|
const HOOK_FILE = { claude: '.claude/settings.json', codex: '.codex/hooks.json' };
|
|
@@ -31,17 +31,16 @@ const EVENT = 'SessionStart';
|
|
|
31
31
|
/** How long the harness gives the hook before it goes on without it. */
|
|
32
32
|
const TIMEOUT = 10;
|
|
33
33
|
|
|
34
|
-
/** The kit's own hook among whatever else the user keeps in the file. */
|
|
35
|
-
const KIT_COMMAND = 'obk session record';
|
|
36
|
-
|
|
37
34
|
/**
|
|
38
|
-
* What the hook runs
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
35
|
+
* What the hook runs: the CLI that wrote it, by its own path, so the session
|
|
36
|
+
* reports to the kit that set it up and not to whatever `obk` PATH finds in the
|
|
37
|
+
* harness's shell (#220). `|| true` and a quiet stderr because a hook must never
|
|
38
|
+
* disturb the session it fires in (ADR 0020): a kit that is not there, or a book
|
|
39
|
+
* that cannot be written, leaves the session alone and the health check finds
|
|
40
|
+
* the stale book later.
|
|
42
41
|
*/
|
|
43
|
-
export const hookCommand = (bots, bot) =>
|
|
44
|
-
`${
|
|
42
|
+
export const hookCommand = (bots, bot, cli = ownCli()) =>
|
|
43
|
+
`${shellWord(cli)} session record --bots ${shellWord(bots)} --bot ${shellWord(bot)} 2>/dev/null || true`;
|
|
45
44
|
|
|
46
45
|
/**
|
|
47
46
|
* Make sure the bot at `home` has the kit's SessionStart hook for `harness`.
|
|
@@ -81,7 +80,7 @@ export function installHook(home, harness, { bots, bot }) {
|
|
|
81
80
|
*
|
|
82
81
|
* The same question `installHook` asks, without the writing. It matters more
|
|
83
82
|
* than it looks: the hook is the whole of how the book learns which
|
|
84
|
-
* conversation a session is running as (ADR
|
|
83
|
+
* conversation a session is running as (ADR 0012), and when it is not there
|
|
85
84
|
* nothing else says so — the book simply stops being true.
|
|
86
85
|
*/
|
|
87
86
|
export function hookTrouble(home, harness, { bots, bot }) {
|
|
@@ -95,7 +94,22 @@ export function hookTrouble(home, harness, { bots, bot }) {
|
|
|
95
94
|
let settings;
|
|
96
95
|
try {
|
|
97
96
|
settings = readSettings(file);
|
|
98
|
-
|
|
97
|
+
// The hook in the file is judged by what it runs, not by whether this is
|
|
98
|
+
// the install that wrote it. A bot made before the kit named itself by path
|
|
99
|
+
// runs the bare `obk`, and one another install wrote runs that install: both
|
|
100
|
+
// still reach a kit, and the next `obk up` writes this one's. So the line is
|
|
101
|
+
// checked against the program already in it, and only one that names a CLI
|
|
102
|
+
// that is not there any more is wrong — `|| true` would keep that quiet, and
|
|
103
|
+
// the book would stop being true with nothing to say why.
|
|
104
|
+
const running = kitProgramIn(settings);
|
|
105
|
+
if (running !== undefined && running !== BARE && !existsSync(running)) {
|
|
106
|
+
return {
|
|
107
|
+
where: file,
|
|
108
|
+
says: `${file} holds the kit's session hook, but it runs ${running}, which is not there any more. ${stale} obk up puts back one that runs the kit you have.`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
const program = running ?? ownCli();
|
|
112
|
+
wanted = { ...settings, hooks: withKitHook(settings.hooks, file, { type: 'command', command: hookCommand(bots, bot, program), timeout: TIMEOUT }) };
|
|
99
113
|
} catch (error) {
|
|
100
114
|
return { where: file, says: `${error.message} ${stale}` };
|
|
101
115
|
}
|
|
@@ -110,7 +124,7 @@ export function hookTrouble(home, harness, { bots, bot }) {
|
|
|
110
124
|
/**
|
|
111
125
|
* What to say about a hooks file that a link takes out of the bot folder, or
|
|
112
126
|
* undefined when it stays in. The kit writes its hook in the bot folder and
|
|
113
|
-
* nowhere else (ADR
|
|
127
|
+
* nowhere else (ADR 0020); through a link like that it would be writing the
|
|
114
128
|
* user's own settings, so it does not, and says why.
|
|
115
129
|
*/
|
|
116
130
|
function outside(home, file) {
|
|
@@ -169,14 +183,55 @@ function withKitHook(hooks, file, mine) {
|
|
|
169
183
|
/**
|
|
170
184
|
* The kit's own hook entry, wherever it sits and whatever sits beside it: a
|
|
171
185
|
* command of exactly the shape `hookCommand` writes, for any bots folder and
|
|
172
|
-
* bot,
|
|
173
|
-
*
|
|
186
|
+
* bot, run by a kit — the bare `obk` a bot made before #220 still has, this
|
|
187
|
+
* install, or another — so an entry written before the folder moved or the kit
|
|
188
|
+
* changed is still the kit's, and is rewritten in place rather than joined by a
|
|
189
|
+
* second. A line of the user's that only mentions the command, or runs it with
|
|
190
|
+
* a program of their own, is theirs (PRD 6.5).
|
|
174
191
|
*/
|
|
175
|
-
const isKitHook = (hook) =>
|
|
192
|
+
const isKitHook = (hook) => kitProgramOf(hook?.command) !== undefined;
|
|
176
193
|
|
|
177
194
|
/** One word as `shellWord` writes it: bare, or single-quoted with `'\''` inside. */
|
|
178
195
|
const WORD = String.raw`(?:[A-Za-z0-9,._+:@%/=-]+|'(?:[^']|'\\'')*')`;
|
|
179
|
-
const KIT_HOOK = new RegExp(String.raw
|
|
196
|
+
const KIT_HOOK = new RegExp(String.raw`^(${WORD}) session record --bots ${WORD} --bot ${WORD} 2>/dev/null \|\| true$`);
|
|
197
|
+
|
|
198
|
+
/** The program a bot made before the kit named itself by path runs. */
|
|
199
|
+
const BARE = 'obk';
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Whether a program is a kit: the bare name, an install's `obk` wherever it
|
|
203
|
+
* lives, or a checkout's `src/cli.js` — the only two ways `ownCli` is reached.
|
|
204
|
+
*/
|
|
205
|
+
const isKitCli = (program) => path.basename(program) === BARE || program.endsWith('/src/cli.js');
|
|
206
|
+
|
|
207
|
+
/** The program a hook command of the kit's runs, unquoted, or undefined for any other command. */
|
|
208
|
+
function kitProgramOf(command) {
|
|
209
|
+
const found = typeof command === 'string' ? KIT_HOOK.exec(command) : null;
|
|
210
|
+
if (found === null) return undefined;
|
|
211
|
+
const program = unquoted(found[1]);
|
|
212
|
+
return isKitCli(program) ? program : undefined;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* What the kit's hook in these settings runs, as a path or the bare name, or
|
|
217
|
+
* undefined when there is none. Read back out of its shell quoting, so it can be
|
|
218
|
+
* looked for on disk.
|
|
219
|
+
*/
|
|
220
|
+
function kitProgramIn(settings) {
|
|
221
|
+
for (const groups of Object.values(settings.hooks ?? {})) {
|
|
222
|
+
if (!Array.isArray(groups)) continue;
|
|
223
|
+
for (const group of groups) {
|
|
224
|
+
for (const hook of Array.isArray(group?.hooks) ? group.hooks : []) {
|
|
225
|
+
const program = kitProgramOf(hook?.command);
|
|
226
|
+
if (program !== undefined) return program;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/** A word as `shellWord` wrote it, read back to what it stands for. */
|
|
234
|
+
const unquoted = (word) => (word.startsWith("'") ? word.slice(1, -1).replaceAll(`'\\''`, "'") : word);
|
|
180
235
|
|
|
181
236
|
/**
|
|
182
237
|
* What the file says, or nothing when there is no file yet. A file the kit
|
package/src/init.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Creates the user's bots folder: their own git repo, seeded with the files the
|
|
2
2
|
// kit reads, and Bot Father. The kit puts none of its own code in here — kit
|
|
3
|
-
// skills are linked from the installed package (ADR
|
|
3
|
+
// skills are linked from the installed package (ADR 0014).
|
|
4
4
|
//
|
|
5
5
|
// Seeding only ever adds what is missing. The user edits these files, by hand
|
|
6
6
|
// or through Bot Father, so a re-run must never write over them.
|
|
@@ -15,7 +15,7 @@ import { RECORD, SKILL_DIRS } from './skills.js';
|
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* What the kit makes on this machine, and what the repo therefore does not
|
|
18
|
-
* carry (ADR
|
|
18
|
+
* carry (ADR 0014, PRD 6.3).
|
|
19
19
|
*
|
|
20
20
|
* A bot's skills are symlinks into wherever the kit and the user's skill
|
|
21
21
|
* sources are installed on the machine that made them, so a clone somewhere
|
|
@@ -126,7 +126,7 @@ const MANAGEMENT_SKILLS = ['obk-bot-building', 'obk-finops', 'obk-fleet-review',
|
|
|
126
126
|
const BOT_FATHER_YAML = 'bots/bot-father/bot.yaml';
|
|
127
127
|
|
|
128
128
|
/** The management session every Bot Father has: the tab you talk to it in. */
|
|
129
|
-
const DAILY_SESSION = 'daily';
|
|
129
|
+
export const DAILY_SESSION = 'daily';
|
|
130
130
|
|
|
131
131
|
const seeds = (harness) => [
|
|
132
132
|
['.gitignore', GITIGNORE],
|
package/src/launch.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// What a session is started with: the one line typed into its tab, which brings
|
|
2
2
|
// the harness up and carries the session's start prompt with it.
|
|
3
3
|
//
|
|
4
|
-
// The flag mapping is PRD 6.4 and ADR
|
|
4
|
+
// The flag mapping is PRD 6.4 and ADR 0015, re-checked against the installed
|
|
5
5
|
// CLIs (Claude Code 2.1.278, Codex 0.155.1). Every session is given its
|
|
6
6
|
// approval level explicitly, so a user's own global harness defaults cannot
|
|
7
7
|
// leak into a bot. The kit names no model: a session that sets none gets the
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { readFileSync } from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
|
+
import { fileURLToPath } from 'node:url';
|
|
15
16
|
|
|
16
17
|
/** The approval levels, and what each one is called on each harness. */
|
|
17
18
|
const APPROVAL = {
|
|
@@ -38,7 +39,7 @@ export const bypassFlags = (harness) => APPROVAL[harness]['dangerously-skip'];
|
|
|
38
39
|
|
|
39
40
|
export const HARNESSES = Object.keys(APPROVAL);
|
|
40
41
|
|
|
41
|
-
/** The level a session that names none runs at (ADR
|
|
42
|
+
/** The level a session that names none runs at (ADR 0015). */
|
|
42
43
|
export const DEFAULT_APPROVAL = 'auto';
|
|
43
44
|
|
|
44
45
|
/** A setting the user left out. An empty string is one too: it means the harness's own. */
|
|
@@ -49,7 +50,7 @@ export const harnessOf = (session, botHarness) => (set(session.harness) ? sessio
|
|
|
49
50
|
|
|
50
51
|
/**
|
|
51
52
|
* A session's name on Claude Code, which is also the address another Claude
|
|
52
|
-
* session writes to (ADR
|
|
53
|
+
* session writes to (ADR 0018). It goes on the launch line as `-n`, and it is
|
|
53
54
|
* re-applied every time the session is started: a resume keeps the name by
|
|
54
55
|
* itself (tech notes, section 2), and the kit does not depend on that.
|
|
55
56
|
*/
|
|
@@ -161,7 +162,7 @@ export function launchCommand(session, { harness, home, workDir, prompt, promptF
|
|
|
161
162
|
|
|
162
163
|
if (harness === 'claude') {
|
|
163
164
|
// The name is the address other Claude sessions write to, so it goes on
|
|
164
|
-
// every launch line, a resume included (ADR
|
|
165
|
+
// every launch line, a resume included (ADR 0018).
|
|
165
166
|
if (address !== undefined) words.push('-n', address);
|
|
166
167
|
// The context window rides on the model name: `sonnet[1m]`.
|
|
167
168
|
if (set(session.model)) words.push('--model', set(session.context) ? `${session.model}[${session.context}]` : session.model);
|
|
@@ -174,7 +175,7 @@ export function launchCommand(session, { harness, home, workDir, prompt, promptF
|
|
|
174
175
|
if (set(session.effort)) words.push('-c', `model_reasoning_effort=${session.effort}`);
|
|
175
176
|
if (set(session.context)) words.push('-c', `model_context_window=${session.context}`);
|
|
176
177
|
// In Codex's auto mode the sandbox lets it write in the folder it was
|
|
177
|
-
// started in, and nowhere else (ADR
|
|
178
|
+
// started in, and nowhere else (ADR 0015), so a work dir outside the bot
|
|
178
179
|
// home has to be named.
|
|
179
180
|
if (workDir !== undefined && !inside(home, workDir)) words.push('--add-dir', workDir);
|
|
180
181
|
}
|
|
@@ -184,6 +185,7 @@ export function launchCommand(session, { harness, home, workDir, prompt, promptF
|
|
|
184
185
|
// starts — the shell's child — from anything the session starts later inside
|
|
185
186
|
// the tab. `$$` is the shell's, and it is not quoted for that reason.
|
|
186
187
|
`${SHELL_ENV}=$$`,
|
|
188
|
+
`${CLI_ENV}=${quoted(ownCli())}`,
|
|
187
189
|
...words.map(quoted),
|
|
188
190
|
...extraWords(session.extra_args),
|
|
189
191
|
...resumeWords(harness, resume),
|
|
@@ -274,3 +276,24 @@ const quoted = (word) => (/^[A-Za-z0-9,._+:@%/=-]+$/.test(word) ? word : `'${wor
|
|
|
274
276
|
|
|
275
277
|
/** The same, for anything else of the kit's that has to build a shell line. */
|
|
276
278
|
export const shellWord = quoted;
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* The CLI that is running, by the path it was started by: the kit calls itself
|
|
282
|
+
* back with this, never with the bare name `obk`, because the bare name is
|
|
283
|
+
* whatever PATH says, and PATH is not the kit's to decide (#220). A user's
|
|
284
|
+
* install is started through npm's bin link, and the link is kept rather than
|
|
285
|
+
* resolved, so the path survives an upgrade of the package; a system test that
|
|
286
|
+
* runs a checkout's `src/cli.js` gets that checkout. What an environment
|
|
287
|
+
* variable claims is not asked: the kit says who it is from how it was started.
|
|
288
|
+
*/
|
|
289
|
+
export const ownCli = () => (process.argv[1] === undefined
|
|
290
|
+
// Nothing started as a script, as with `node -e`: the kit's own entry file.
|
|
291
|
+
? fileURLToPath(new URL('./cli.js', import.meta.url))
|
|
292
|
+
: path.resolve(process.argv[1]));
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* Where the launch line hands the session the CLI that started it, so a bot's
|
|
296
|
+
* own commands can call the same one. A plain variable, because a harness's
|
|
297
|
+
* shell keeps those where it rearranges PATH (#220).
|
|
298
|
+
*/
|
|
299
|
+
export const CLI_ENV = 'OBK_CLI';
|
package/src/message.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// `obk message`: one session writing to another (PRD 6.9, ADR
|
|
1
|
+
// `obk message`: one session writing to another (PRD 6.9, ADR 0018).
|
|
2
2
|
//
|
|
3
3
|
// Two roads, and the bot never picks. Claude to Claude in the same approval
|
|
4
4
|
// class is the harness's own messaging, which no command line can send for it —
|
|
@@ -20,8 +20,8 @@ import path from 'node:path';
|
|
|
20
20
|
|
|
21
21
|
import { readBook } from './book.js';
|
|
22
22
|
import { botDir, botNames, readBot } from './bot.js';
|
|
23
|
-
import { harnessOf, reachesMail } from './launch.js';
|
|
24
|
-
import { ackMailbox, postMessage, readMailbox, tabs,
|
|
23
|
+
import { harnessOf, ownCli, reachesMail, shellWord } from './launch.js';
|
|
24
|
+
import { ackMailbox, coordinatorOf, postMessage, readMailbox, tabs, tabToTypeInto, typeIntoTab, useMailbox } from './orca.js';
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* How much of a message travels as itself. Above this it is written to a file
|
|
@@ -147,7 +147,7 @@ export function sendMessage(bots, { to: target, from: sender, tab, subject, text
|
|
|
147
147
|
return {
|
|
148
148
|
...answer,
|
|
149
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:
|
|
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
151
|
};
|
|
152
152
|
}
|
|
153
153
|
|
|
@@ -173,7 +173,9 @@ export function sendMessage(bots, { to: target, from: sender, tab, subject, text
|
|
|
173
173
|
/**
|
|
174
174
|
* What is waiting for a session, and the reading of it.
|
|
175
175
|
*
|
|
176
|
-
* Orca fences a Run to one reader,
|
|
176
|
+
* Orca fences a Run to one reader, its coordinator. A session whose tab is live
|
|
177
|
+
* is bound to that tab and read as it; one whose tab is down is read as its
|
|
178
|
+
* Run's coordinator, and nothing is bound (issue #249). A read that
|
|
177
179
|
* is not a peek acknowledges the batch it read: that is what makes the next
|
|
178
180
|
* check bring the next one rather than the same again.
|
|
179
181
|
*/
|
|
@@ -192,10 +194,27 @@ export function checkMail(bots, { bot: botName, session: sessionName, tab, peek
|
|
|
192
194
|
};
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
// Read as the session's own tab, wherever this check was typed. Binding the
|
|
198
|
+
// tab that asked would hand it the session's mailbox, and Orca's notice for
|
|
199
|
+
// every message after (issue #228). A session with no live tab is read as
|
|
200
|
+
// whatever its mailbox is bound to, a closed tab included, and nothing is
|
|
201
|
+
// bound: the session's tab is bound again when it is back up (issue #249).
|
|
202
|
+
const live = who.tab === undefined ? undefined : tabs(who.home).find((tab) => tab.tabId === who.tab)?.handle;
|
|
203
|
+
if (live !== undefined) useMailbox(who.mailbox, live);
|
|
204
|
+
const handle = live ?? coordinatorOf(who.mailbox);
|
|
205
|
+
if (handle === undefined) {
|
|
206
|
+
return {
|
|
207
|
+
bots,
|
|
208
|
+
bot: who.bot,
|
|
209
|
+
session: who.session,
|
|
210
|
+
mailbox: who.mailbox,
|
|
211
|
+
messages: [],
|
|
212
|
+
trouble: `${who.bot}/${who.session} is not up, and its mailbox is bound to no tab, so there is nothing to read it as. Its mail waits until its tab is back: \`up\` brings it back, or \`unpause\` when it is paused.`,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
const found = readMailbox(who.mailbox, { peek, handle });
|
|
197
216
|
const messages = (found.messages ?? []).map((message) => asMessage(bots, message));
|
|
198
|
-
if (!peek && found.deliveryId !== undefined && messages.length > 0) ackMailbox(who.mailbox, found.deliveryId);
|
|
217
|
+
if (!peek && found.deliveryId !== undefined && messages.length > 0) ackMailbox(who.mailbox, found.deliveryId, handle);
|
|
199
218
|
|
|
200
219
|
return { bots, bot: who.bot, session: who.session, mailbox: who.mailbox, read: !peek, messages };
|
|
201
220
|
}
|
|
@@ -295,9 +314,10 @@ const stamp = () => new Date().toISOString().replaceAll(':', '-').replace('.', '
|
|
|
295
314
|
*
|
|
296
315
|
* Nothing in the mailbox reaches a running harness by itself, and a typed line
|
|
297
316
|
* is taken as the next turn by a busy session rather than cutting into the one
|
|
298
|
-
* it is having. A tab with no
|
|
317
|
+
* it is having. A tab with no harness in it is not typed into at all — there is
|
|
299
318
|
* nobody there to read it, and the message waits in the mailbox until the
|
|
300
|
-
* session is up.
|
|
319
|
+
* session is up. Nor is one the kit cannot tell about. A tab the book does not
|
|
320
|
+
* hold is never typed into on any road.
|
|
301
321
|
*
|
|
302
322
|
* Nor is a tab with something on screen waiting to be answered. A line typed
|
|
303
323
|
* into one of those is not a message: it is an answer to whatever question is
|
|
@@ -315,16 +335,15 @@ function nudge(to, from, subject) {
|
|
|
315
335
|
if (to.tab === undefined) return { nudged: false };
|
|
316
336
|
|
|
317
337
|
try {
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
if (
|
|
323
|
-
if (tui.blockedReason !== undefined) return { nudged: false, blocked: tui.blockedReason };
|
|
338
|
+
const found = tabToTypeInto(to.home, to.tab, LOOK_MS);
|
|
339
|
+
if (found.blocked !== undefined) return { nudged: false, blocked: found.blocked };
|
|
340
|
+
// A line that lands in a shell is run there, with the sender's subject in it.
|
|
341
|
+
if (found.unsure !== undefined) return { nudged: false, nudgeTrouble: found.unsure };
|
|
342
|
+
if (found.handle === undefined) return { nudged: false };
|
|
324
343
|
|
|
325
344
|
typeIntoTab(
|
|
326
|
-
|
|
327
|
-
`Fleet mail from ${from.bot}/${from.session}: ${subject}. Read it with
|
|
345
|
+
found.handle,
|
|
346
|
+
`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)}`,
|
|
328
347
|
);
|
|
329
348
|
return { nudged: true };
|
|
330
349
|
} catch (error) {
|
|
@@ -377,4 +396,4 @@ function whyNotReachable(bot, session, harness, held) {
|
|
|
377
396
|
}
|
|
378
397
|
|
|
379
398
|
const notUpYet = (who) =>
|
|
380
|
-
`${who.bot}/${who.session} has no mailbox yet: it has never been brought up. Start it and it gets one:
|
|
399
|
+
`${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)}`;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// Run by Orca's own binary as plain Node, the way Orca's `bin/orca` runs its
|
|
2
|
+
// CLI: `ELECTRON_RUN_AS_NODE=1 <Orca> orca-window.cjs <client> <projectId> <wait>`.
|
|
3
|
+
//
|
|
4
|
+
// It asks Orca's runtime, through Orca's own client, for `project.update` with
|
|
5
|
+
// no changes on one project. That is the call that makes Orca's window read its
|
|
6
|
+
// projects again, and Orca's CLI does not offer it (#224, ADR 0021). It exits 0
|
|
7
|
+
// only when the runtime took the call; anything else is a non-zero exit, and
|
|
8
|
+
// the kit reads every non-zero exit the same way.
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const [client, projectId, wait] = process.argv.slice(2);
|
|
13
|
+
|
|
14
|
+
process.exitCode = 1;
|
|
15
|
+
const { RuntimeClient } = require(client);
|
|
16
|
+
new RuntimeClient(undefined, Number(wait))
|
|
17
|
+
.call('project.update', { projectId, updates: {} })
|
|
18
|
+
.then((answer) => {
|
|
19
|
+
if (answer?.ok === true) process.exitCode = 0;
|
|
20
|
+
}, () => {})
|
|
21
|
+
.finally(() => process.exit());
|