@ouro.bot/cli 0.1.0-alpha.911 → 0.1.0-alpha.913
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/changelog.json +12 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/heart/daemon/terminal-ui.js +10 -6
- package/dist/heart/daemon/up-progress.js +1 -1
- package/dist/senses/shepherd/cmux.js +21 -11
- package/dist/senses/shepherd/judge.js +21 -9
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.913",
|
|
6
|
+
"changes": [
|
|
7
|
+
"CLI screens read on light terminals: main text uses the terminal's own colour, and the accent colours hold about 4:1 contrast on both light and dark backgrounds."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"version": "0.1.0-alpha.912",
|
|
12
|
+
"changes": [
|
|
13
|
+
"Shepherd judges a hooked agent that sits mid-turn at a prompt without stopping (a permission or trust question) after 20 quiet seconds, leaves idle and freshly started sessions alone, and no longer stays silent when the judge writes its reply as a bare string or with code-style backticks; a refused reply is logged with what was wrong."
|
|
14
|
+
]
|
|
15
|
+
},
|
|
4
16
|
{
|
|
5
17
|
"version": "0.1.0-alpha.911",
|
|
6
18
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>ouro-butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.913</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -14,12 +14,15 @@ exports.renderTerminalOperation = renderTerminalOperation;
|
|
|
14
14
|
const runtime_1 = require("../../nerves/runtime");
|
|
15
15
|
const RESET = "\x1b[0m";
|
|
16
16
|
const BOLD = "\x1b[1m";
|
|
17
|
-
|
|
17
|
+
// The terminal's background is unknown, so every tone has to read on light and on dark: each
|
|
18
|
+
// holds about 4:1 contrast against both white and near-black. Main text takes no colour at all
|
|
19
|
+
// and stays in the terminal's own text colour.
|
|
20
|
+
const CANOPY = "\x1b[38;2;84;130;100m";
|
|
18
21
|
const SCALE = "\x1b[38;2;45;148;71m";
|
|
19
|
-
const GLOW = "\x1b[38;2;
|
|
20
|
-
const BONE = "
|
|
21
|
-
const MIST = "\x1b[38;2;
|
|
22
|
-
const ALERT = "\x1b[38;2;
|
|
22
|
+
const GLOW = "\x1b[38;2;34;160;80m";
|
|
23
|
+
const BONE = "";
|
|
24
|
+
const MIST = "\x1b[38;2;110;130;116m";
|
|
25
|
+
const ALERT = "\x1b[38;2;220;70;70m";
|
|
23
26
|
const ANSI_RE = /\x1b\[[0-9;]*m/g;
|
|
24
27
|
const MASTHEAD_WORD = "OUROBOROS";
|
|
25
28
|
const CLASSIC_WORDMARK_GLYPHS = {
|
|
@@ -32,7 +35,8 @@ const CLASSIC_WORDMARK_GLYPHS = {
|
|
|
32
35
|
function color(text, tone, bold = false) {
|
|
33
36
|
if (!text)
|
|
34
37
|
return text;
|
|
35
|
-
|
|
38
|
+
const style = `${tone}${bold ? BOLD : ""}`;
|
|
39
|
+
return style ? `${style}${text}${RESET}` : text;
|
|
36
40
|
}
|
|
37
41
|
function stripAnsi(text) {
|
|
38
42
|
return text.replace(ANSI_RE, "");
|
|
@@ -21,7 +21,7 @@ const RESET = "\x1b[0m";
|
|
|
21
21
|
const BOLD = "\x1b[1m";
|
|
22
22
|
const DIM = "\x1b[2m";
|
|
23
23
|
const GREEN = "\x1b[38;2;46;204;64m";
|
|
24
|
-
const RED = "\x1b[38;2;
|
|
24
|
+
const RED = "\x1b[38;2;220;70;70m";
|
|
25
25
|
const BASE_UP_PHASE_PLAN = [
|
|
26
26
|
"update check",
|
|
27
27
|
"system setup",
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.QUIET_MAX_MS = exports.QUIET_MS = exports.POLL_MS = exports.BACKGROUND_GRACE_MS = exports.GRACE_MS = exports.CMUX_STATUS_KEY = void 0;
|
|
36
|
+
exports.QUIET_MAX_MS = exports.HOOKED_QUIET_MS = exports.QUIET_MS = exports.POLL_MS = exports.BACKGROUND_GRACE_MS = exports.GRACE_MS = exports.CMUX_STATUS_KEY = void 0;
|
|
37
37
|
exports.realSchedule = realSchedule;
|
|
38
38
|
exports.readCmuxHookSession = readCmuxHookSession;
|
|
39
39
|
exports.createCmuxHost = createCmuxHost;
|
|
@@ -47,9 +47,11 @@ const runtime_1 = require("../../nerves/runtime");
|
|
|
47
47
|
* `agent.hook.Stop` event, which cmux publishes for every agent its hooks cover (Claude Code,
|
|
48
48
|
* Codex, Copilot and more). After a short grace period, a stop is dropped if the agent resumed. A
|
|
49
49
|
* stop that cmux's session store says left background work running waits longer, and is then
|
|
50
|
-
* passed on marked as such: the agent is still working, not handing back control.
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* passed on marked as such: the agent is still working, not handing back control. Every terminal
|
|
51
|
+
* whose agent is not idle is also polled: output that stops changing for a quiet window counts
|
|
52
|
+
* as a return. That covers an agent cmux does not hook, and a hooked agent sitting mid-turn at a
|
|
53
|
+
* prompt that fires no stop (a permission or trust question); a hooked agent gets a longer window,
|
|
54
|
+
* because its stop normally speaks first. cmux has no bell or output event, so polling is the fallback.
|
|
53
55
|
*/
|
|
54
56
|
exports.CMUX_STATUS_KEY = "ouro";
|
|
55
57
|
exports.GRACE_MS = 3_000;
|
|
@@ -57,11 +59,15 @@ exports.GRACE_MS = 3_000;
|
|
|
57
59
|
exports.BACKGROUND_GRACE_MS = 30_000;
|
|
58
60
|
exports.POLL_MS = 4_000;
|
|
59
61
|
exports.QUIET_MS = 8_000;
|
|
62
|
+
/** A hooked agent mid-turn: long enough that a stop hook or a spinner normally speaks before the quiet screen does. */
|
|
63
|
+
exports.HOOKED_QUIET_MS = 20_000;
|
|
60
64
|
/** A quiet terminal judged as still working or not an agent is checked less often, doubling up to this. */
|
|
61
65
|
exports.QUIET_MAX_MS = 300_000;
|
|
62
66
|
const POLL_LINES = 40;
|
|
63
67
|
const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
|
|
64
68
|
const STOP_HOOKS = new Set(["Stop", "StopFailure"]);
|
|
69
|
+
/** Hooks after which the agent waits for the human: a stop, or a session that has not been given work yet. */
|
|
70
|
+
const IDLE_HOOKS = new Set([...STOP_HOOKS, "SessionStart"]);
|
|
65
71
|
/** Hooks that say nothing about whether the main agent is working. */
|
|
66
72
|
const NEUTRAL_HOOKS = new Set(["SubagentStop", "Notification"]);
|
|
67
73
|
const NAMED_KEYS = new Set(["enter", "escape", "up", "down", "tab"]);
|
|
@@ -244,14 +250,17 @@ function createCmuxHost(client, options = {}) {
|
|
|
244
250
|
return;
|
|
245
251
|
const agent = str(payload._source) ?? str(frame.source) ?? "unknown";
|
|
246
252
|
const cwd = str(payload.cwd);
|
|
247
|
-
|
|
253
|
+
const neutral = NEUTRAL_HOOKS.has(hook);
|
|
254
|
+
// An idle agent's return came from its stop; the quiet-screen poll leaves it alone until it works again.
|
|
255
|
+
hooked.set(surface, { agent, cwd, sessionId, idle: IDLE_HOOKS.has(hook) || (neutral && hooked.get(surface)?.idle === true) });
|
|
248
256
|
if (STOP_HOOKS.has(hook))
|
|
249
257
|
return stopped(surface, `cmux:${String(frame.boot_id)}:${String(frame.seq)}`, agent, sessionId, cwd);
|
|
250
|
-
if (
|
|
258
|
+
if (neutral || hook === "SessionStart")
|
|
251
259
|
return;
|
|
252
|
-
// The agent is working again: no pending stop,
|
|
260
|
+
// The agent is working again: no pending stop, no stale status, and its quiet screen is checked at the normal pace.
|
|
253
261
|
cancelPending(surface);
|
|
254
262
|
clearStatus(surface);
|
|
263
|
+
quietMs.delete(surface);
|
|
255
264
|
// An agent that exited leaves a plain terminal, which the quiet-screen fallback watches again.
|
|
256
265
|
if (hook === "SessionEnd")
|
|
257
266
|
forget(surface);
|
|
@@ -285,10 +294,10 @@ function createCmuxHost(client, options = {}) {
|
|
|
285
294
|
},
|
|
286
295
|
});
|
|
287
296
|
};
|
|
288
|
-
/** Terminals
|
|
297
|
+
/** Terminals whose agent is not idle: output that changed and then held still for the quiet window is a return. */
|
|
289
298
|
const poll = async () => {
|
|
290
299
|
try {
|
|
291
|
-
const terminals = (await surfaces()).filter((surface) => surface.type === "terminal" &&
|
|
300
|
+
const terminals = (await surfaces()).filter((surface) => surface.type === "terminal" && hooked.get(surface.id)?.idle !== true);
|
|
292
301
|
const seen = new Set(terminals.map((surface) => surface.id));
|
|
293
302
|
for (const id of screens.keys())
|
|
294
303
|
if (!seen.has(id))
|
|
@@ -303,9 +312,10 @@ function createCmuxHost(client, options = {}) {
|
|
|
303
312
|
screens.set(surface.id, { hash, changedAt: at, active: true });
|
|
304
313
|
clearStatus(surface.id);
|
|
305
314
|
}
|
|
306
|
-
else if (last.active && at - last.changedAt >= (quietMs.get(surface.id) ?? exports.QUIET_MS)) {
|
|
315
|
+
else if (last.active && at - last.changedAt >= Math.max(quietMs.get(surface.id) ?? exports.QUIET_MS, hooked.has(surface.id) ? exports.HOOKED_QUIET_MS : 0)) {
|
|
307
316
|
last.active = false;
|
|
308
|
-
|
|
317
|
+
const hook = hooked.get(surface.id);
|
|
318
|
+
handlers.returned({ sessionId: surface.id, transitionId: `cmux:quiet:${surface.id}:${last.changedAt}`, agent: hook?.agent ?? null, cwd: hook?.cwd ?? null, lastBody: null, backgroundWork: null });
|
|
309
319
|
}
|
|
310
320
|
}
|
|
311
321
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.REPLY_MAX_CHARS = void 0;
|
|
4
|
+
exports.replyTextProblem = replyTextProblem;
|
|
4
5
|
exports.safeReplyText = safeReplyText;
|
|
5
6
|
exports.parseVerdict = parseVerdict;
|
|
6
7
|
exports.judgeMessages = judgeMessages;
|
|
@@ -16,14 +17,24 @@ const CALLBACKS = { onModelStart: NOOP, onModelStreamStart: NOOP, onTextChunk: N
|
|
|
16
17
|
* A reply that is safe to type: one line, short, and nothing an agent's input box treats as a
|
|
17
18
|
* command (a leading `/`, `!` or `#`, an `@` mention, or a backslash escape). Because the terminal
|
|
18
19
|
* may turn out to be a plain shell, nothing a shell treats as syntax either (`; $ \` | < > &`),
|
|
19
|
-
* and no invisible formatting or bidirectional-override characters.
|
|
20
|
-
*
|
|
20
|
+
* and no invisible formatting or bidirectional-override characters. Models write code-style
|
|
21
|
+
* backticks and line breaks out of habit, so those are dropped first: they are formatting, and
|
|
22
|
+
* without them nothing is left for a shell to substitute. Anything else is not sent; the return
|
|
23
|
+
* is let through and logged with what was wrong.
|
|
21
24
|
*/
|
|
25
|
+
function replyTextProblem(text) {
|
|
26
|
+
const line = text.replace(/`/g, "").replace(/\s+/g, " ").trim();
|
|
27
|
+
if (!line)
|
|
28
|
+
return { problem: "an empty reply" };
|
|
29
|
+
if (line.length > exports.REPLY_MAX_CHARS)
|
|
30
|
+
return { problem: `a reply of ${line.length} characters (the limit is ${exports.REPLY_MAX_CHARS})` };
|
|
31
|
+
if (/[\\@;$|<>&\p{Cc}\p{Cf}]/u.test(line) || /^[/!#]/.test(line))
|
|
32
|
+
return { problem: "a reply with characters a terminal could read as a command" };
|
|
33
|
+
return { line };
|
|
34
|
+
}
|
|
22
35
|
function safeReplyText(text) {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
return null;
|
|
26
|
-
return line;
|
|
36
|
+
const checked = replyTextProblem(text);
|
|
37
|
+
return "line" in checked ? checked.line : null;
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
29
40
|
* The JSON in a model's answer. Models differ: some wrap it in a code fence, some think aloud in
|
|
@@ -60,15 +71,16 @@ function parseVerdict(content) {
|
|
|
60
71
|
return { kind, reply: null, reason, news: true };
|
|
61
72
|
if (kind !== "premature")
|
|
62
73
|
return { kind, reply: null, reason };
|
|
63
|
-
|
|
74
|
+
// Some models write the reply as a bare string instead of {"text": ...} or {"key": ...}; a string that is exactly a key is one.
|
|
75
|
+
const reply = typeof value.reply === "string" ? (KEYS.has(value.reply) ? { key: value.reply } : { text: value.reply }) : value.reply && typeof value.reply === "object" ? value.reply : {};
|
|
64
76
|
if (typeof reply.key === "string" && KEYS.has(reply.key)) {
|
|
65
77
|
// A key can approve a prompt, and a key from JSON found inside prose may be one the screen planted.
|
|
66
78
|
if (parsed.inProse)
|
|
67
79
|
return { kind: "unclear", reply: null, reason: `premature, but the key came from JSON inside prose: ${reason}` };
|
|
68
80
|
return { kind, reply: { key: reply.key }, reason };
|
|
69
81
|
}
|
|
70
|
-
const
|
|
71
|
-
return
|
|
82
|
+
const checked = typeof reply.text === "string" ? replyTextProblem(reply.text) : { problem: "no reply" };
|
|
83
|
+
return "line" in checked ? { kind, reply: { text: checked.line }, reason } : { kind: "unclear", reply: null, reason: `premature, but the judge gave ${checked.problem}: ${reason}` };
|
|
72
84
|
}
|
|
73
85
|
function judgeMessages(packet) {
|
|
74
86
|
return [
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.913",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.913",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|