@assuredloop/orca-bot-kit 0.26.0 → 0.27.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 +30 -30
- package/package.json +1 -1
- package/rules/mail.md +5 -5
- package/skills/obk-bot-building/SKILL.md +5 -2
- package/src/book.js +55 -16
- package/src/bot.js +2 -2
- package/src/cli.js +53 -110
- package/src/commands.js +17 -9
- package/src/conversations.js +0 -37
- package/src/health.js +63 -6
- package/src/hooks.js +35 -63
- package/src/launch.js +21 -4
- package/src/message.js +42 -334
- package/src/name.js +1 -1
- package/src/orca.js +19 -35
- package/src/permissions.js +22 -6
- package/src/processes.js +392 -0
- package/src/retire.js +43 -40
- package/src/skills.js +1 -1
- package/src/temp.js +1 -1
- package/src/up.js +13 -2
- package/src/unread.js +0 -160
package/src/temp.js
CHANGED
|
@@ -178,7 +178,7 @@ function settingsFor(bot, maker, option, given) {
|
|
|
178
178
|
/**
|
|
179
179
|
* Refuse an approval wider than the widest of the maker's own and the bot's
|
|
180
180
|
* `temp_approval`, which only the user's yes through `permission approval
|
|
181
|
-
* --temps` widens (ADR
|
|
181
|
+
* --temps` widens (ADR 0041).
|
|
182
182
|
*/
|
|
183
183
|
function refuseWiderApproval(bots, caller, bot, maker, asked) {
|
|
184
184
|
if (asked === undefined) return;
|
package/src/up.js
CHANGED
|
@@ -13,7 +13,7 @@ import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
|
|
|
13
13
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
14
14
|
import { installHook } from './hooks.js';
|
|
15
15
|
import { writePermissions } from './permissions.js';
|
|
16
|
-
import { addressOf, harnessOf, isAddressOf, isShortPrompt, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
16
|
+
import { addressOf, harnessOf, isAddressOf, isShortPrompt, kitFolders, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
17
17
|
import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
18
18
|
import { TAB_ENV } from './record.js';
|
|
19
19
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
@@ -279,6 +279,9 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
279
279
|
// A work dir is a plain folder, made for the session before it is told about
|
|
280
280
|
// it (PRD 6.4). Nothing here is a git worktree.
|
|
281
281
|
if (workDir !== undefined) mkdirSync(workDir, { recursive: true });
|
|
282
|
+
// The kit's own folders for the bot, which a Codex launch line names for its
|
|
283
|
+
// sandbox to write, are there before the line is (#534).
|
|
284
|
+
if (harness === 'codex') for (const folder of Object.values(kitFolders(bots, bot.name))) mkdirSync(folder, { recursive: true });
|
|
282
285
|
writePrompt(launch);
|
|
283
286
|
|
|
284
287
|
const made = openTab(home, tabTitle);
|
|
@@ -550,6 +553,7 @@ function launchFor(bots, bot, session, harness, home, workDir, resume, held) {
|
|
|
550
553
|
promptFile,
|
|
551
554
|
resume,
|
|
552
555
|
address,
|
|
556
|
+
kit: kitFolders(bots, bot.name),
|
|
553
557
|
});
|
|
554
558
|
// The session's mailbox first, from inside its own tab (#317), and the
|
|
555
559
|
// harness whatever became of it.
|
|
@@ -870,9 +874,16 @@ const ids = (setup, change) => ({ project: setup.projectId, setup: setup.id, cha
|
|
|
870
874
|
*
|
|
871
875
|
* Beside *this* bots folder, and not in a shared temp directory: two bots
|
|
872
876
|
* folders may each hold an api-bot with a daily session, and one file for both
|
|
873
|
-
* of them is one bot's duty handed to another's session.
|
|
877
|
+
* of them is one bot's duty handed to another's session. In the bot's own
|
|
878
|
+
* folder there, which a Codex session of the bot may write and no other bot's
|
|
879
|
+
* may: a temporary session's maker writes its prompt from inside its sandbox
|
|
880
|
+
* (#534).
|
|
874
881
|
*/
|
|
875
882
|
export const promptPath = (bots, bot, session) =>
|
|
883
|
+
path.join(kitFolders(bots, bot).prompts, `${encodeURIComponent(session)}.txt`);
|
|
884
|
+
|
|
885
|
+
/** Where a session's start prompt was before #534, flat beside the others: removed with the session. */
|
|
886
|
+
export const oldPromptPath = (bots, bot, session) =>
|
|
876
887
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
877
888
|
|
|
878
889
|
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
package/src/unread.js
DELETED
|
@@ -1,160 +0,0 @@
|
|
|
1
|
-
// The kit's hint of the fleet mail it sent and that is not read yet (#509, ADR 0035).
|
|
2
|
-
//
|
|
3
|
-
// The mailbox is the record of a message; this is not. It is what the kit
|
|
4
|
-
// itself knows: a send writes one file for the message, `obk message check`
|
|
5
|
-
// takes out what it read, and a retire says what is left. A Claude session's
|
|
6
|
-
// turn-end hook reads it first, so a session with no mail asks Orca nothing,
|
|
7
|
-
// and asks Orca before it tells, so a stale entry tells nothing.
|
|
8
|
-
//
|
|
9
|
-
// It sits in the system temp folder because a sender inside Codex's sandbox
|
|
10
|
-
// may write there and nowhere outside its own bot (#350). So it is private:
|
|
11
|
-
// every folder the kit makes here is 0700, every file 0600, and a file holds
|
|
12
|
-
// who sent the message and its subject, never its body. A temp clean-up loses
|
|
13
|
-
// it, and loses no mail: the message still waits in the mailbox.
|
|
14
|
-
//
|
|
15
|
-
// One file for each message, `<session folder>/<message id>.json`, so senders
|
|
16
|
-
// writing at once never write the same file; a `<message id>.told` beside it
|
|
17
|
-
// once the session's hook told it, so it is told once; and a `<message
|
|
18
|
-
// id>.read` once `obk message check` read it, so a send whose answer came back
|
|
19
|
-
// after that read does not leave an entry for read mail (#509 review).
|
|
20
|
-
|
|
21
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
22
|
-
import os from 'node:os';
|
|
23
|
-
import path from 'node:path';
|
|
24
|
-
|
|
25
|
-
/** Where the hint lives. */
|
|
26
|
-
export const unreadRoot = () => path.join(os.tmpdir(), 'obk-unread');
|
|
27
|
-
|
|
28
|
-
/** One session's folder: its bot home, by its real path, and its name. */
|
|
29
|
-
function sessionDir(home, session) {
|
|
30
|
-
let real = home;
|
|
31
|
-
try {
|
|
32
|
-
real = realpathSync(home);
|
|
33
|
-
} catch {
|
|
34
|
-
// A home that is gone is named as it was given.
|
|
35
|
-
}
|
|
36
|
-
return path.join(unreadRoot(), encodeURIComponent(real), encodeURIComponent(session));
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** How long a read mark is kept for a send still waiting on Orca's answer. */
|
|
40
|
-
const READ_MARK_MS = 60 * 60 * 1000;
|
|
41
|
-
|
|
42
|
-
/** A message id as a file name: Orca's ids are plain, and anything else is made plain. */
|
|
43
|
-
const fileOf = (id) => encodeURIComponent(String(id));
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Note one message sent to `session` of the bot at `home`: `{ id, from, subject,
|
|
47
|
-
* at }`. Quiet whatever happens: the message is sent either way.
|
|
48
|
-
*/
|
|
49
|
-
export function noteUnread(home, session, { id, from, subject, at }) {
|
|
50
|
-
if (id === undefined) return;
|
|
51
|
-
try {
|
|
52
|
-
const dir = sessionDir(home, session);
|
|
53
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
54
|
-
const file = path.join(dir, `${fileOf(id)}.json`);
|
|
55
|
-
// Written aside, then put in place whole: a reader never takes half a file.
|
|
56
|
-
writeFileSync(`${file}.${process.pid}.writing`, `${JSON.stringify({ id, from, subject, at })}\n`, { mode: 0o600 });
|
|
57
|
-
renameSync(`${file}.${process.pid}.writing`, file);
|
|
58
|
-
// Read already, while the send waited for Orca's answer: the entry is
|
|
59
|
-
// written first and taken out here, and a check writes its mark before it
|
|
60
|
-
// takes an entry out, so in either order no entry is left.
|
|
61
|
-
const read = path.join(dir, `${fileOf(id)}.read`);
|
|
62
|
-
if (existsSync(read)) {
|
|
63
|
-
rmSync(file, { force: true });
|
|
64
|
-
rmSync(read, { force: true });
|
|
65
|
-
}
|
|
66
|
-
} catch {
|
|
67
|
-
// Nothing: the hint is lost, the mail is not.
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* What the hint holds for `session`: `[{ id, from, subject, at, told }]`,
|
|
73
|
-
* oldest first. An entry that cannot be read is left out. A folder that is not
|
|
74
|
-
* there is no mail; one that cannot be read throws, for the caller to stay
|
|
75
|
-
* quiet about.
|
|
76
|
-
*/
|
|
77
|
-
export function unreadOf(home, session) {
|
|
78
|
-
const dir = sessionDir(home, session);
|
|
79
|
-
let names;
|
|
80
|
-
try {
|
|
81
|
-
names = readdirSync(dir);
|
|
82
|
-
} catch (error) {
|
|
83
|
-
if (error.code === 'ENOENT') return [];
|
|
84
|
-
throw error;
|
|
85
|
-
}
|
|
86
|
-
const told = new Set(names.filter((name) => name.endsWith('.told')).map((name) => name.slice(0, -'.told'.length)));
|
|
87
|
-
return names
|
|
88
|
-
.filter((name) => name.endsWith('.json'))
|
|
89
|
-
.flatMap((name) => {
|
|
90
|
-
try {
|
|
91
|
-
const entry = JSON.parse(readFileSync(path.join(dir, name), 'utf8'));
|
|
92
|
-
if (entry === null || typeof entry !== 'object' || typeof entry.from !== 'string' || typeof entry.subject !== 'string') return [];
|
|
93
|
-
return [{ id: entry.id, from: entry.from, subject: entry.subject, at: entry.at, told: told.has(name.slice(0, -'.json'.length)) }];
|
|
94
|
-
} catch {
|
|
95
|
-
return [];
|
|
96
|
-
}
|
|
97
|
-
})
|
|
98
|
-
.sort((a, b) => String(a.at).localeCompare(String(b.at)));
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
/**
|
|
102
|
-
* Mark these messages told. Throws when a mark cannot be written: a hook that
|
|
103
|
-
* cannot remember it told must not tell, or it would tell again.
|
|
104
|
-
*/
|
|
105
|
-
export function markTold(home, session, ids) {
|
|
106
|
-
const dir = sessionDir(home, session);
|
|
107
|
-
for (const id of ids) writeFileSync(path.join(dir, `${fileOf(id)}.told`), '', { mode: 0o600 });
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Take these messages out: they were read. Each gets a read mark first, for a
|
|
112
|
-
* send still waiting on Orca's answer (see `noteUnread`); marks older than an
|
|
113
|
-
* hour are cleared here, since no send waits that long. Quiet whatever happens.
|
|
114
|
-
*/
|
|
115
|
-
export function forgetUnread(home, session, ids) {
|
|
116
|
-
const dir = sessionDir(home, session);
|
|
117
|
-
try {
|
|
118
|
-
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
119
|
-
for (const name of readdirSync(dir).filter((one) => one.endsWith('.read'))) {
|
|
120
|
-
if (Date.now() - statSync(path.join(dir, name)).mtimeMs > READ_MARK_MS) rmSync(path.join(dir, name), { force: true });
|
|
121
|
-
}
|
|
122
|
-
} catch {
|
|
123
|
-
// Nothing: a mark that stays is a few bytes in the temp folder.
|
|
124
|
-
}
|
|
125
|
-
for (const id of ids) {
|
|
126
|
-
try {
|
|
127
|
-
writeFileSync(path.join(dir, `${fileOf(id)}.read`), '', { mode: 0o600 });
|
|
128
|
-
} catch {
|
|
129
|
-
// Nothing: the entry is still taken out below.
|
|
130
|
-
}
|
|
131
|
-
for (const end of ['.json', '.told']) {
|
|
132
|
-
try {
|
|
133
|
-
rmSync(path.join(dir, `${fileOf(id)}${end}`), { force: true });
|
|
134
|
-
} catch {
|
|
135
|
-
// Nothing: a stale entry tells nothing, since the hook asks Orca first.
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
/**
|
|
142
|
-
* What the hint holds for a session that is going, as a retire says it:
|
|
143
|
-
* `{ count, from: [<bot>/<session>, …] }`, or undefined when nothing; and the
|
|
144
|
-
* session's folder is removed. Quiet whatever happens.
|
|
145
|
-
*/
|
|
146
|
-
export function takeUnread(home, session) {
|
|
147
|
-
let entries = [];
|
|
148
|
-
try {
|
|
149
|
-
entries = unreadOf(home, session);
|
|
150
|
-
} catch {
|
|
151
|
-
// Unreadable: nothing can be said of it.
|
|
152
|
-
}
|
|
153
|
-
try {
|
|
154
|
-
rmSync(sessionDir(home, session), { recursive: true, force: true });
|
|
155
|
-
} catch {
|
|
156
|
-
// Nothing: it is in the temp folder, and holds no body.
|
|
157
|
-
}
|
|
158
|
-
if (entries.length === 0) return undefined;
|
|
159
|
-
return { count: entries.length, from: [...new Set(entries.map((entry) => entry.from))] };
|
|
160
|
-
}
|