@assuredloop/orca-bot-kit 0.31.0 → 0.33.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 +19 -14
- package/SETUP.md +10 -8
- package/package.json +1 -1
- package/skills/obk-grooming/SKILL.md +3 -2
- package/src/bot.js +1 -1
- package/src/clear.js +106 -37
- package/src/cli.js +19 -8
- package/src/conversations.js +1 -1
- package/src/health.js +72 -10
- package/src/init.js +2 -1
- package/src/launch.js +23 -0
- package/src/orca.js +92 -6
- package/src/pi-record.js +96 -0
- package/src/pi.js +55 -17
- package/src/rules.js +9 -1
- package/src/settings.js +109 -19
- package/src/skills.js +22 -9
- package/src/temp.js +69 -10
package/src/settings.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
//
|
|
3
3
|
// A session can run on settings other than the ones bot.yaml asks for: it
|
|
4
4
|
// started before a change, a default of the user's own leaked in, or a flag was
|
|
5
|
-
// dropped. Each harness writes down what it used (tech notes, sections 2 and
|
|
5
|
+
// dropped. Each harness writes down what it used (tech notes, sections 2, 3 and 6),
|
|
6
6
|
// so the two can be put side by side. Read only: these are the harness's files.
|
|
7
7
|
//
|
|
8
8
|
// Four answers, and only one of them is good news. A record that cannot be read,
|
|
@@ -10,7 +10,8 @@
|
|
|
10
10
|
// setting the session never asked for is not a disagreement either, whatever the
|
|
11
11
|
// harness chose: it is `not-asked-for`.
|
|
12
12
|
|
|
13
|
-
import {
|
|
13
|
+
import { defaultApproval } from './launch.js';
|
|
14
|
+
import { withoutThinking } from './pi.js';
|
|
14
15
|
import { lines } from './usage.js';
|
|
15
16
|
|
|
16
17
|
export const SETTINGS = ['model', 'effort', 'context', 'approval'];
|
|
@@ -22,47 +23,56 @@ export const SETTINGS = ['model', 'effort', 'context', 'approval'];
|
|
|
22
23
|
* every setting asked for is then unknown.
|
|
23
24
|
*/
|
|
24
25
|
export function settingsInUse(harness, session, file, since) {
|
|
25
|
-
const
|
|
26
|
-
|
|
26
|
+
const entries = file === undefined ? [] : lines(file);
|
|
27
|
+
const start = startOf(entries, since);
|
|
28
|
+
// Pi's reader also looks before the start, for the selection a reply follows.
|
|
29
|
+
const used = harness === 'pi' ? fromPi(entries, start)
|
|
30
|
+
: (harness === 'claude' ? fromClaude : fromCodex)(start === -1 ? [] : entries.slice(start));
|
|
31
|
+
return Object.fromEntries(SETTINGS.map((name) => [name, judged(harness, name, asked(harness, session, name), used[name])]));
|
|
27
32
|
}
|
|
28
33
|
|
|
29
34
|
/**
|
|
30
|
-
*
|
|
31
|
-
* the conversation it had, so the file still holds what the
|
|
32
|
-
* wrote, and that says what that process ran with, not this
|
|
35
|
+
* Where the record since the kit last started the session begins, or -1. A
|
|
36
|
+
* start resumes the conversation it had, so the file still holds what the
|
|
37
|
+
* process before it wrote, and that says what that process ran with, not this
|
|
38
|
+
* one. Everything
|
|
33
39
|
* from the first line written at or after the start counts, a line with no time
|
|
34
40
|
* of its own included. With no start to go by, no line can be shown to be this
|
|
35
41
|
* process's, so none counts.
|
|
36
42
|
*/
|
|
37
|
-
function
|
|
43
|
+
function startOf(entries, since) {
|
|
38
44
|
const from = Date.parse(since ?? '');
|
|
39
|
-
if (Number.isNaN(from)) return
|
|
40
|
-
|
|
41
|
-
return first === -1 ? [] : entries.slice(first);
|
|
45
|
+
if (Number.isNaN(from)) return -1;
|
|
46
|
+
return entries.findIndex((entry) => Date.parse(entry.timestamp ?? '') >= from);
|
|
42
47
|
}
|
|
43
48
|
|
|
44
49
|
/** What the session asks for, as bot.yaml has it, or undefined when it asks for nothing. */
|
|
45
|
-
function asked(session, name) {
|
|
50
|
+
function asked(harness, session, name) {
|
|
46
51
|
const value = session[name];
|
|
47
52
|
const set = value !== undefined && value !== null && String(value).trim() !== '';
|
|
48
53
|
// The kit puts an approval level on every launch line, so a session always
|
|
49
|
-
// asks for one:
|
|
50
|
-
|
|
54
|
+
// asks for one: its harness's default, when it names none (ADR 0015). Pi
|
|
55
|
+
// has one level (#547).
|
|
56
|
+
if (name === 'approval') return set ? String(value).trim() : defaultApproval(harness);
|
|
51
57
|
return set ? String(value).trim() : undefined;
|
|
52
58
|
}
|
|
53
59
|
|
|
54
60
|
function judged(harness, name, configured, observed) {
|
|
55
|
-
|
|
61
|
+
// A Pi reply can leave two values open (see fromPi): its own, which shows,
|
|
62
|
+
// and the selection before it. Agreeing with one of the two is not a match.
|
|
63
|
+
const choices = observed?.choices ?? [observed];
|
|
64
|
+
const shown = choices[0] === undefined ? {} : { observed: said(name, choices[0]) };
|
|
56
65
|
if (configured === undefined) return { state: 'not-asked-for', ...shown };
|
|
57
|
-
const
|
|
58
|
-
if (verdict ===
|
|
59
|
-
return { state:
|
|
66
|
+
const verdicts = choices.map((choice) => same(harness, name, configured, choice));
|
|
67
|
+
if (verdicts.every((verdict) => verdict === false)) return { state: 'mismatch', configured, ...shown };
|
|
68
|
+
if (verdicts.length === 1 && verdicts[0] === true) return { state: 'match', configured, ...shown };
|
|
69
|
+
return { state: 'unknown', configured, ...shown };
|
|
60
70
|
}
|
|
61
71
|
|
|
62
72
|
/** Whether the record says what was asked for; undefined when it cannot say. */
|
|
63
73
|
function same(harness, name, configured, observed) {
|
|
64
74
|
if (observed === undefined) return undefined;
|
|
65
|
-
if (name === 'model') return sameModel(configured, observed);
|
|
75
|
+
if (name === 'model') return harness === 'pi' ? samePiModel(configured, observed) : sameModel(configured, observed);
|
|
66
76
|
// Codex reports the window it was given at 95%, rounded down (tech notes,
|
|
67
77
|
// section 3): 200000 comes back as 190000.
|
|
68
78
|
if (name === 'context') return Number(observed) === Math.floor(Number(configured) * 0.95);
|
|
@@ -82,6 +92,27 @@ function sameModel(configured, observed) {
|
|
|
82
92
|
return model.includes('-') ? observed === model : observed.split('-').includes(model);
|
|
83
93
|
}
|
|
84
94
|
|
|
95
|
+
/**
|
|
96
|
+
* A Pi model as bot.yaml names it, `openai/gpt-6-luna` or `gpt-6-luna`, maybe
|
|
97
|
+
* with a trailing `:<thinking level>`, against the `<provider>/<model>` Pi
|
|
98
|
+
* wrote down: it matches either whole, in any case, as Pi does. Pi also takes
|
|
99
|
+
* a pattern that is part of a model's id, and which model it then chose
|
|
100
|
+
* depends on what else it lists, so such a pattern cannot be judged (Pi 1.3.0,
|
|
101
|
+
* resolveModelReference). Pi matches a display name too, which the record
|
|
102
|
+
* does not hold: such a pattern shows as a mismatch.
|
|
103
|
+
*/
|
|
104
|
+
function samePiModel(configured, observed) {
|
|
105
|
+
const model = withoutThinking(configured).toLowerCase();
|
|
106
|
+
const ran = observed.toLowerCase();
|
|
107
|
+
const provider = ran.slice(0, ran.indexOf('/'));
|
|
108
|
+
const id = ran.slice(ran.indexOf('/') + 1);
|
|
109
|
+
if (model === ran || model === id) return true;
|
|
110
|
+
// A provider before a slash is only a hint: the whole pattern may be part of an id.
|
|
111
|
+
const slash = model.indexOf('/');
|
|
112
|
+
const part = id.includes(model) || (slash !== -1 && model.slice(0, slash) === provider && id.includes(model.slice(slash + 1)));
|
|
113
|
+
return part ? undefined : false;
|
|
114
|
+
}
|
|
115
|
+
|
|
85
116
|
/** The permission mode Claude Code records for each level (tech notes, section 2). */
|
|
86
117
|
const CLAUDE_MODES = { auto: 'auto', ask: 'default', 'dangerously-skip': 'bypassPermissions' };
|
|
87
118
|
|
|
@@ -134,6 +165,65 @@ function fromClaude(entries) {
|
|
|
134
165
|
return used;
|
|
135
166
|
}
|
|
136
167
|
|
|
168
|
+
/** The api of a virtual model's catalog entry (Pi 1.3.0, virtual-models.js). */
|
|
169
|
+
const PI_VIRTUAL = 'pi-virtual';
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* What a Pi record says it runs with, from `start` on: the model, as
|
|
173
|
+
* `<provider>/<model>`, and the thinking level (tech notes, section 6). Pi
|
|
174
|
+
* writes nothing about approval.
|
|
175
|
+
*
|
|
176
|
+
* The latest line that names one decides. A `model_change` or
|
|
177
|
+
* `thinking_level_change` line decides alone. A reply names the model that
|
|
178
|
+
* answered, and the level it was asked at. On a resume with `--model` or
|
|
179
|
+
* `--thinking`, Pi writes no change line, so a change before a reply may be
|
|
180
|
+
* stale; but a change to a virtual model, such as `router/auto`, stays the
|
|
181
|
+
* selection while the router is registered, and its replies name the model
|
|
182
|
+
* it routed to (Pi 1.3.0, getBranchSelection, docs/virtual-models.md). The
|
|
183
|
+
* record cannot show what is registered now. So the reply stands when the
|
|
184
|
+
* change names the same model, or a model a reply shows is physical, and
|
|
185
|
+
* otherwise the two are left open. A router may change the level as well, so
|
|
186
|
+
* a reply's level stands alone only on a physical model. A reply a router
|
|
187
|
+
* could not route names the virtual model, and is passed over, as Pi does.
|
|
188
|
+
*/
|
|
189
|
+
function fromPi(entries, start) {
|
|
190
|
+
const named = (provider, model) => (typeof provider === 'string' && typeof model === 'string' ? `${provider}/${model}` : undefined);
|
|
191
|
+
const text = (value) => (typeof value === 'string' ? value : undefined);
|
|
192
|
+
const assistant = (entry) => (entry.type === 'message' && entry.message?.role === 'assistant' ? entry.message : undefined);
|
|
193
|
+
const reply = (entry) => (assistant(entry)?.api === PI_VIRTUAL ? undefined : assistant(entry));
|
|
194
|
+
const modelOf = (entry) => (entry.type === 'model_change' ? named(entry.provider, entry.modelId) : named(reply(entry)?.provider, reply(entry)?.model));
|
|
195
|
+
const levelOf = (entry) => text(entry.type === 'thinking_level_change' ? entry.thinkingLevel : reply(entry)?.thinkingLevel);
|
|
196
|
+
const changeBefore = (type, read, at) => {
|
|
197
|
+
for (let before = at - 1; before >= 0; before -= 1) if (entries[before].type === type) return read(entries[before]);
|
|
198
|
+
return undefined;
|
|
199
|
+
};
|
|
200
|
+
const physical = (model) => entries.some((entry) => entry.type !== 'model_change' && modelOf(entry) === model);
|
|
201
|
+
const latest = (read) => {
|
|
202
|
+
if (start === -1) return -1;
|
|
203
|
+
for (let at = entries.length - 1; at >= start; at -= 1) if (read(entries[at]) !== undefined) return at;
|
|
204
|
+
return -1;
|
|
205
|
+
};
|
|
206
|
+
/** The model the line at `at` says, and whether it is a physical one that a reply's level is sure of. */
|
|
207
|
+
const modelAt = (at) => {
|
|
208
|
+
const own = modelOf(entries[at]);
|
|
209
|
+
if (entries[at].type === 'model_change') return { model: own, sure: false };
|
|
210
|
+
const earlier = changeBefore('model_change', modelOf, at);
|
|
211
|
+
if (own === undefined) return { sure: false };
|
|
212
|
+
// The reply itself shows its own model is physical, so a change to that model is settled too.
|
|
213
|
+
if (earlier === undefined || physical(earlier)) return { model: own, sure: true };
|
|
214
|
+
return { model: { choices: [own, earlier] }, sure: false };
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
const model = latest(modelOf);
|
|
218
|
+
const level = latest(levelOf);
|
|
219
|
+
const own = level === -1 ? undefined : levelOf(entries[level]);
|
|
220
|
+
const earlier = level === -1 || entries[level].type === 'thinking_level_change' ? undefined : changeBefore('thinking_level_change', levelOf, level);
|
|
221
|
+
return {
|
|
222
|
+
model: model === -1 ? undefined : modelAt(model).model,
|
|
223
|
+
effort: earlier === undefined || earlier === own || modelAt(level).sure ? own : { choices: [own, earlier] },
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
137
227
|
/** What a Codex rollout says it runs with: its latest turn, and its latest count. */
|
|
138
228
|
function fromCodex(entries) {
|
|
139
229
|
const used = {};
|
package/src/skills.js
CHANGED
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
// away reaches it without a restart. Claude Code takes it through its own
|
|
9
9
|
// `/reload-skills`, which `obk skills build` types into the session's tab.
|
|
10
10
|
// Codex has no such command and takes it at the start of its next turn (tech
|
|
11
|
-
// notes, sections 2 and 3).
|
|
11
|
+
// notes, sections 2 and 3). Pi loads the Codex directory, which its launch
|
|
12
|
+
// line names, and takes a change at its own `/reload`, typed only while it is
|
|
13
|
+
// idle (section 6, #659). Outwards: the kit takes away only the links it put
|
|
12
14
|
// there, and never the skill one was pointing at.
|
|
13
15
|
//
|
|
14
16
|
// An entry names one of four shelves: the kit's own (`kit:<name>`), an online
|
|
@@ -25,7 +27,7 @@ import { parse, parseDocument, stringify } from 'yaml';
|
|
|
25
27
|
|
|
26
28
|
import { bookFile, readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
27
29
|
import { botDir, botNames, changesExactly, leadsOutside, readBot, refuseOutside, withBotTurn, writeWhole, YAML_OUT } from './bot.js';
|
|
28
|
-
import { harnessOf, ownCli, shellWord } from './launch.js';
|
|
30
|
+
import { harnessOf, ownCli, piSkillsDir, runsOnPi, shellWord } from './launch.js';
|
|
29
31
|
import { orcaTrouble, tabToTypeInto, typeIntoTab } from './orca.js';
|
|
30
32
|
import { listIn } from './rules.js';
|
|
31
33
|
import { cloneDir, isCloned, leavesClone, readSources, skillsIn, wrongClone } from './sources.js';
|
|
@@ -142,12 +144,18 @@ export function removeSkill(bots, bot, ref) {
|
|
|
142
144
|
/**
|
|
143
145
|
* Give one bot the skills its lists name. Returns what there is to report:
|
|
144
146
|
* `{ bot, skills, linked, removed }` for a bot whose links are in place, and
|
|
145
|
-
* `{ bot, skills, trouble }` for one whose list the kit could not follow.
|
|
147
|
+
* `{ bot, skills, trouble }` for one whose list the kit could not follow. A bot
|
|
148
|
+
* that runs on Pi has `pi` too: the directory its Pi sessions load (#659).
|
|
146
149
|
*
|
|
147
150
|
* A list with one entry the kit cannot follow links nothing at all: half a
|
|
148
151
|
* bot's skills is not a bot anybody asked for, and the user fixes one line.
|
|
149
152
|
*/
|
|
150
153
|
export function linkSkills(bots, home, bot) {
|
|
154
|
+
const entry = linkInto(bots, home, bot);
|
|
155
|
+
return runsOnPi(bot) ? { ...entry, pi: piSkillsDir(home) } : entry;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function linkInto(bots, home, bot) {
|
|
151
159
|
const record = readRecord(home);
|
|
152
160
|
const held = new Map();
|
|
153
161
|
|
|
@@ -440,7 +448,7 @@ const LOOK_MS = 2000;
|
|
|
440
448
|
* harness's own means (PRD 6.5: changes reach running sessions without a
|
|
441
449
|
* restart, and the sessions a change affects are told). Returns one entry per
|
|
442
450
|
* session, in the bot's order: `{ session, harness, state }`, where the state
|
|
443
|
-
* is `reloaded`, `next-turn` (with `read`), `not-up`, `blocked` (with
|
|
451
|
+
* is `reloaded`, `next-turn` (with `read`), `busy`, `not-up`, `blocked` (with
|
|
444
452
|
* `blocked`) or `unknown` (with `trouble`).
|
|
445
453
|
*
|
|
446
454
|
* Claude Code gets `/reload-skills` typed into its tab, through the same gate
|
|
@@ -449,7 +457,11 @@ const LOOK_MS = 2000;
|
|
|
449
457
|
* command and takes the change at the start of its next turn by itself, so
|
|
450
458
|
* nothing is typed, and the caller is given the SKILL.md of each skill linked
|
|
451
459
|
* in, for the session to read if the skill has not appeared (tech notes,
|
|
452
|
-
* sections 2 and 3).
|
|
460
|
+
* sections 2 and 3). Pi gets its own `/reload` through the same gate, and only
|
|
461
|
+
* while it is idle: a busy Pi refuses the command and keeps nothing for later,
|
|
462
|
+
* so a busy one is `busy`, and one with a draft in its input line is typed
|
|
463
|
+
* nothing either, since the command would join the draft (section 6, #659).
|
|
464
|
+
* Nothing is ever restarted.
|
|
453
465
|
*/
|
|
454
466
|
function tellSessions(home, bot, linked) {
|
|
455
467
|
let book;
|
|
@@ -478,9 +490,6 @@ function tellSessions(home, bot, linked) {
|
|
|
478
490
|
const told = { session: session.name, harness };
|
|
479
491
|
const tab = typeof book[session.name]?.tab === 'string' ? book[session.name].tab : undefined;
|
|
480
492
|
if (tab === undefined) return { ...told, state: 'not-up' };
|
|
481
|
-
// Skills reach a Pi session in a later slice (#547), and nothing is typed
|
|
482
|
-
// into its tab before then.
|
|
483
|
-
if (harness === 'pi') return { ...told, state: 'unknown', trouble: 'it runs on Pi, and the kit does not give a Pi session skills yet, so nothing was typed' };
|
|
484
493
|
if (down !== undefined) return { ...told, state: 'unknown', trouble: `the kit could not look at its tab, and nothing was typed: ${down}` };
|
|
485
494
|
let turn;
|
|
486
495
|
try {
|
|
@@ -505,7 +514,11 @@ function tellSessions(home, bot, linked) {
|
|
|
505
514
|
const read = linked.map((name) => path.join(home, SKILL_DIRS.codex, name, MANIFEST)).filter((file) => existsSync(file));
|
|
506
515
|
return { ...told, state: 'next-turn', read };
|
|
507
516
|
}
|
|
508
|
-
|
|
517
|
+
if (harness === 'pi') {
|
|
518
|
+
if (found.draft !== undefined) return { ...told, state: 'unknown', trouble: `its input line holds a draft, "${found.draft}", and /reload would join it, so nothing was typed` };
|
|
519
|
+
if (!found.idle) return { ...told, state: 'busy' };
|
|
520
|
+
}
|
|
521
|
+
typeIntoTab(found.handle, harness === 'pi' ? '/reload' : '/reload-skills');
|
|
509
522
|
return { ...told, state: 'reloaded' };
|
|
510
523
|
} catch (error) {
|
|
511
524
|
// The links are made whatever Orca says, so this is reported, not
|
package/src/temp.js
CHANGED
|
@@ -34,9 +34,10 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
34
34
|
import { readBook, takeLineTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook } from './book.js';
|
|
35
35
|
import { addSession, dropSession, NAME, readBot, tempRoles } from './bot.js';
|
|
36
36
|
import { untrustedKitHooks } from './hook-trust.js';
|
|
37
|
-
import { approvalRank, DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, refuseApprovalArgs, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
37
|
+
import { approvalRank, DEFAULT_APPROVAL, harnessOf, isShortPrompt, ownCli, piApprovalTrouble, refuseApprovalArgs, set, shellWord, startPrompt, workDirOf } from './launch.js';
|
|
38
38
|
import { findSession, sessionInTab } from './message.js';
|
|
39
39
|
import { orca, screenRows, tabs } from './orca.js';
|
|
40
|
+
import { piTrouble } from './pi.js';
|
|
40
41
|
import { retireSession } from './retire.js';
|
|
41
42
|
import { BOT_FATHER, bringUp, promptPath } from './up.js';
|
|
42
43
|
|
|
@@ -75,11 +76,23 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
75
76
|
|
|
76
77
|
const maker = bot.sessions.find((session) => session.name === caller.session);
|
|
77
78
|
const { settings, chosen } = settingsFor(bot, maker, role?.option ?? {}, given);
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
79
|
+
const onPi = chosen.harness.value === 'pi';
|
|
80
|
+
// A blank --approval names no level, so on Pi it is judged as left out (the review of PR #651).
|
|
81
|
+
const asked = onPi && !set(given.approval) ? undefined : given.approval;
|
|
82
|
+
if (onPi) {
|
|
83
|
+
// Pi has one level, whatever the maker's is (the owner, #547).
|
|
84
|
+
const trouble = piApprovalTrouble(asked, name);
|
|
85
|
+
if (trouble !== undefined) throw new Error(`${trouble} Nothing was made.`);
|
|
86
|
+
settings.approval = 'dangerously-skip';
|
|
87
|
+
if (asked === undefined) chosen.approval = { value: 'dangerously-skip', from: 'harness' };
|
|
88
|
+
}
|
|
89
|
+
refuseWiderApproval(bots, caller, bot, maker, asked ?? (onPi ? settings.approval : undefined), onPi && asked === undefined ? name : undefined);
|
|
82
90
|
refuseApprovalArgs(chosen.harness.value, given.extra_args);
|
|
91
|
+
// Asked before anything is written, so a Pi that cannot start one makes nothing (#578).
|
|
92
|
+
if (onPi) {
|
|
93
|
+
const trouble = piTrouble(set(settings.model) ? [settings.model] : []);
|
|
94
|
+
if (trouble !== undefined) throw new Error(`${trouble} Nothing was made.`);
|
|
95
|
+
}
|
|
83
96
|
settings.name = name;
|
|
84
97
|
if (given.extra_args !== undefined) settings.extra_args = given.extra_args;
|
|
85
98
|
if (given.prompt !== undefined) settings.prompt = given.prompt;
|
|
@@ -189,9 +202,10 @@ function settingsFor(bot, maker, option, given) {
|
|
|
189
202
|
/**
|
|
190
203
|
* Refuse an approval wider than the widest of the maker's own and the bot's
|
|
191
204
|
* `temp_approval`, which only the user's yes through `permission approval
|
|
192
|
-
* --temps` widens (ADR 0051).
|
|
205
|
+
* --temps` widens (ADR 0051). `onPi` names a Pi session that was given no
|
|
206
|
+
* level: Pi's one level is what is judged then (#648).
|
|
193
207
|
*/
|
|
194
|
-
function refuseWiderApproval(bots, caller, bot, maker, asked) {
|
|
208
|
+
function refuseWiderApproval(bots, caller, bot, maker, asked, onPi) {
|
|
195
209
|
if (asked === undefined) return;
|
|
196
210
|
// A blank level is the kit's default at launch (ADR 0015), so it is judged as that.
|
|
197
211
|
const effective = (given) => (given === undefined || given === null || String(given).trim() === '' ? DEFAULT_APPROVAL : given);
|
|
@@ -199,7 +213,8 @@ function refuseWiderApproval(bots, caller, bot, maker, asked) {
|
|
|
199
213
|
const own = effective(maker?.approval);
|
|
200
214
|
const widest = approvalRank(bot.temp_approval) > approvalRank(own) ? bot.temp_approval : own;
|
|
201
215
|
if (approvalRank(level) <= approvalRank(widest)) return;
|
|
202
|
-
|
|
216
|
+
const what = onPi === undefined ? `--approval ${level}` : `${onPi} would run on Pi, and every Pi session runs as ${level}, which`;
|
|
217
|
+
throw new Error(`${what} is wider than ${caller.session}'s own approval, ${own}${widest === own ? '' : `, and than ${caller.bot}'s temp_approval, ${widest}`}, and a session does not widen another's approval. The user allows it for ${caller.bot}'s temporary sessions with ${shellWord(ownCli())} permission approval --bots ${shellWord(bots)} --bot ${caller.bot} --temps --approval ${level}. Nothing was made.`);
|
|
203
218
|
}
|
|
204
219
|
|
|
205
220
|
/**
|
|
@@ -651,11 +666,54 @@ const wholeScreen = (rows) => rows.filter((row) => row.trim() !== '').map((row)
|
|
|
651
666
|
*/
|
|
652
667
|
export async function answerTemp(bots, { tab, name }) {
|
|
653
668
|
const caller = callerIn(bots, tab, 'answer', "answer a session's first-run screen");
|
|
654
|
-
ownTemp(caller, name, 'answer for', 'Nothing was typed.');
|
|
655
|
-
const
|
|
669
|
+
const { bot, session } = ownTemp(caller, name, 'answer for', 'Nothing was typed.');
|
|
670
|
+
const target = { run: `${caller.bot}/${name}`, home: caller.home, session: name };
|
|
671
|
+
if (harnessOf(session, bot.harness) === 'pi') sayPiScreen(target, 'temp');
|
|
672
|
+
const sent = await answerTeach(target, 'temp');
|
|
656
673
|
return { bot: caller.bot, session: name, maker: caller.session, sent };
|
|
657
674
|
}
|
|
658
675
|
|
|
676
|
+
/**
|
|
677
|
+
* The foot of an extension's question in Pi 1.3.0: `ctx.ui.select` and
|
|
678
|
+
* `confirm` draw the trust question's list foot, `ctx.ui.input` its own.
|
|
679
|
+
*/
|
|
680
|
+
const PI_ASK_FOOT = /(?:↑↓ navigate +enter select|enter submit) +escape\/ctrl\+c cancel/;
|
|
681
|
+
|
|
682
|
+
/** The foot of each step of Pi 1.3.0's first-time setup. */
|
|
683
|
+
const PI_SETUP_FOOT = /enter (?:continue|finish) +escape\/ctrl\+c skip setup/;
|
|
684
|
+
|
|
685
|
+
/**
|
|
686
|
+
* Refuse to answer a Pi session's screen, and say what it is (#648). The kit
|
|
687
|
+
* starts Pi with `--no-approve`, and then no screen of Pi's own waits for a
|
|
688
|
+
* key where Pi has its settings already (tech notes, section 6). What still
|
|
689
|
+
* can is named and left to the user: Pi's trust question, its first-time
|
|
690
|
+
* setup, its warning that waits for a key, and an extension's question.
|
|
691
|
+
* Nothing is typed, so no turn for a line is taken.
|
|
692
|
+
*/
|
|
693
|
+
function sayPiScreen({ run, home, session }, kind) {
|
|
694
|
+
const nothing = 'Nothing was typed.';
|
|
695
|
+
const tabId = readBook(home).sessions[session]?.tab;
|
|
696
|
+
const handle = tabId === undefined ? undefined : tabs(home).find((one) => one.tabId === tabId)?.handle;
|
|
697
|
+
if (handle === undefined) {
|
|
698
|
+
throw new Error(`${run} runs on Pi and has no tab open in Orca, so there is no screen of its to answer. ${nothing}`);
|
|
699
|
+
}
|
|
700
|
+
const seen = screenRows(handle);
|
|
701
|
+
if (seen.rows === undefined) {
|
|
702
|
+
throw new Error(`${run} runs on Pi, and its screen could not be read (${seen.unreadable}). ${nothing}`);
|
|
703
|
+
}
|
|
704
|
+
const has = (test) => seen.rows.some((row) => test(row.trim()));
|
|
705
|
+
const what = has((row) => row === 'Trust project folder?')
|
|
706
|
+
? `Pi's project-trust question, which Pi does not ask when the kit starts it with --no-approve. Trusting a folder is the user's choice`
|
|
707
|
+
: has((row) => PI_SETUP_FOOT.test(row))
|
|
708
|
+
? 'Pi\'s first-time setup, which asks for a theme and whether to share anonymous usage data (analytics) when Pi has no settings.json yet. Both are the user\'s choice'
|
|
709
|
+
: has((row) => row.endsWith('Press any key to continue...'))
|
|
710
|
+
? 'a warning of Pi\'s that waits for a key, such as its notice of an old hooks/ folder in ~/.pi/agent or the project. It is the user\'s to read and answer'
|
|
711
|
+
: has((row) => PI_ASK_FOOT.test(row))
|
|
712
|
+
? 'a question from an extension of Pi\'s, not from Pi or the kit. It is the user\'s to answer'
|
|
713
|
+
: `no first-run screen of Pi's: the kit starts Pi with --no-approve, and then Pi asks nothing before it works`;
|
|
714
|
+
throw new Error(`${run} runs on Pi, and its screen shows ${what}. ${kind} answer answers none of Pi's screens. ${nothing} It shows:\n${wholeScreen(seen.rows)}`);
|
|
715
|
+
}
|
|
716
|
+
|
|
659
717
|
/**
|
|
660
718
|
* Answer `target`'s Teach auto mode screen with Not now, for `temp answer` or
|
|
661
719
|
* `session answer` (`kind`). Returns what answered it.
|
|
@@ -714,6 +772,7 @@ export async function trustSessionHooks(bots, { tab, bot, session }) {
|
|
|
714
772
|
*/
|
|
715
773
|
export async function answerSession(bots, { tab, bot, session }) {
|
|
716
774
|
const target = longLivedTarget(bots, { tab, bot, session }, 'answer');
|
|
775
|
+
if (target.harness === 'pi') sayPiScreen(target, 'session');
|
|
717
776
|
const sent = await answerTeach(target, 'session');
|
|
718
777
|
return { bot: target.bot, session: target.session, sent };
|
|
719
778
|
}
|