@ouro.bot/cli 0.1.0-alpha.893 → 0.1.0-alpha.895

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.
@@ -0,0 +1,407 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.findRepoRoot = findRepoRoot;
37
+ exports.resolveInsideRepo = resolveInsideRepo;
38
+ exports.tokenizeCommand = tokenizeCommand;
39
+ exports.gitConfigHazard = gitConfigHazard;
40
+ exports.evaluateFloor = evaluateFloor;
41
+ const fs = __importStar(require("node:fs"));
42
+ const path = __importStar(require("node:path"));
43
+ const runtime_1 = require("../../nerves/runtime");
44
+ const realFs = {
45
+ exists: (target) => fs.existsSync(target),
46
+ realpath: (target) => fs.realpathSync(target),
47
+ read: (target) => {
48
+ try {
49
+ return fs.readFileSync(target, "utf-8");
50
+ }
51
+ catch {
52
+ return null;
53
+ }
54
+ },
55
+ };
56
+ const MAX_COMMAND_CHARS = 2_000;
57
+ const EDIT_TOOLS = new Set(["Edit", "Write", "MultiEdit", "NotebookEdit"]);
58
+ const READ_TOOLS = new Set(["Read", "Glob", "Grep", "LS"]);
59
+ /** Characters that chain, pipe, redirect, substitute, expand or escape anywhere in the command, quoted or not. */
60
+ const SHELL_CONTROL = /[\n\r;&|`$<>(){}\\!#]/;
61
+ /** Characters the shell expands as a glob when they appear outside quotes. */
62
+ const GLOB = /[*?[\]]/;
63
+ const PROTECTED_SEGMENTS = new Set([".git", ".claude", ".github", ".husky", ".vscode", ".idea"]);
64
+ const PROTECTED_NAMES = new Set([
65
+ "package.json", ".npmrc", ".yarnrc", ".yarnrc.yml", ".mcp.json", ".gitmodules", ".gitattributes", ".gitconfig",
66
+ ".bashrc", ".bash_profile", ".bash_login", ".profile", ".zshrc", ".zshenv", ".zprofile", ".zlogin", ".envrc",
67
+ "claude.md", "agents.md", "copilot-instructions.md",
68
+ ]);
69
+ /** Protected paths that can hold credentials: even reading them escalates. */
70
+ const CREDENTIAL_TOUCHES = new Set(["touches an environment file", "touches .git", "touches .npmrc", "touches .yarnrc", "touches .yarnrc.yml", "touches .gitconfig", "touches .envrc", "touches .claude"]);
71
+ /** Programs never answered for the human, even with a precedent: they delete, escalate, reach the network, wrap or run other programs. */
72
+ const HARD_COMMANDS = new Set([
73
+ "rm", "rmdir", "unlink", "shred", "dd", "mkfs", "diskutil", "chmod", "chown", "chgrp", "chflags", "xattr", "mv", "cp", "ln", "install", "tee", "truncate",
74
+ "sudo", "su", "doas", "kill", "killall", "pkill", "launchctl", "shutdown", "reboot", "halt", "crontab", "defaults", "security", "osascript", "open",
75
+ "curl", "wget", "ssh", "scp", "sftp", "rsync", "nc", "ncat", "netcat", "telnet", "ftp", "socat", "gh", "az", "aws", "gcloud", "kubectl", "terraform", "docker", "fly", "vercel", "heroku",
76
+ "sh", "bash", "zsh", "fish", "dash", "ksh", "csh", "tcsh", "eval", "exec", "source", ".", "env", "xargs", "nohup", "time", "timeout", "nice", "caffeinate", "watch", "command", "builtin", "script", "expect", "at", "batch",
77
+ "npx", "pnpx", "bunx", "ouro", "cmux", "claude", "codex", "copilot", "base64", "openssl", "gpg", "date", "less", "more", "vi", "vim", "nano", "emacs",
78
+ ]);
79
+ /** Programs that run code they are handed: a precedent may answer them only with no options at all. */
80
+ const INTERPRETERS = new Set(["python", "python3", "node", "ruby", "perl", "php", "deno", "lua", "swift", "awk", "gawk", "sed", "jq", "make"]);
81
+ const PACKAGE_MANAGERS = new Set(["npm", "pnpm", "yarn", "bun", "pip", "pip3", "pipx", "brew", "gem", "cargo", "go", "uv", "poetry", "composer"]);
82
+ /** The repository's own checks, allowed only as these exact words. */
83
+ const TEST_COMMANDS = [
84
+ ["npm", "test"], ["npm", "run", "test"], ["npm", "run", "lint"], ["npm", "run", "typecheck"],
85
+ ["swift", "test"], ["swift", "build"], ["cargo", "test"], ["cargo", "check"], ["go", "test"], ["go", "vet"],
86
+ ];
87
+ const flags = (letters, values = {}) => ({ ...Object.fromEntries([...letters].map((letter) => [letter, "flag"])), ...values });
88
+ const longFlags = (names, values = {}) => ({ ...Object.fromEntries(names.map((name) => [name, "flag"])), ...values });
89
+ const GREP = {
90
+ short: flags("rRnilvwcEFHhosx", { e: "text", m: "num", A: "num", B: "num", C: "num" }),
91
+ long: longFlags(["--line-number", "--ignore-case", "--recursive", "--count", "--files-with-matches", "--fixed-strings", "--word-regexp"], { "--include": "text", "--exclude": "text", "--exclude-dir": "text" }),
92
+ };
93
+ const COMMANDS = {
94
+ ls: { short: flags("1aAlhRtrSdF"), long: longFlags(["--all", "--human-readable", "--recursive"]) },
95
+ pwd: { noPositional: true },
96
+ cat: { short: flags("n") },
97
+ head: { short: { n: "num", c: "num" }, digits: true },
98
+ tail: { short: { n: "num", c: "num" }, digits: true },
99
+ wc: { short: flags("lwcm") },
100
+ grep: GREP, egrep: GREP, fgrep: GREP,
101
+ rg: {
102
+ short: flags("inlwcFSuvHo", { g: "text", t: "text", e: "text", m: "num", A: "num", B: "num", C: "num" }),
103
+ long: longFlags(["--hidden", "--files", "--fixed-strings", "--ignore-case", "--line-number", "--count", "--files-with-matches", "--no-ignore"], { "--glob": "text", "--type": "text" }),
104
+ },
105
+ find: { long: longFlags(["-print", "-empty"], { "-name": "text", "-iname": "text", "-type": "text", "-maxdepth": "num", "-mindepth": "num" }) },
106
+ tree: { short: flags("adf", { L: "num" }) },
107
+ file: { short: flags("b") },
108
+ stat: {},
109
+ diff: { short: flags("uqrN") },
110
+ which: { short: flags("a") },
111
+ echo: { short: flags("n") },
112
+ du: { short: flags("sh", { d: "num" }) },
113
+ true: { noPositional: true },
114
+ };
115
+ /** Read-only git subcommands and their options. Any other git subcommand escalates. */
116
+ const GIT = {
117
+ status: { short: flags("sb"), long: longFlags(["--short", "--branch", "--porcelain"]) },
118
+ log: { short: { n: "num" }, digits: true, long: longFlags(["--oneline", "--decorate", "--graph"], { "--max-count": "num" }) },
119
+ diff: { long: longFlags(["--name-only", "--name-status", "--cached", "--staged", "--no-ext-diff", "--no-textconv"]) },
120
+ show: { long: longFlags(["--name-only", "--name-status", "--oneline", "--no-ext-diff", "--no-textconv"]) },
121
+ "rev-parse": { long: longFlags(["--show-toplevel", "--abbrev-ref", "--short", "--is-inside-work-tree"]) },
122
+ "ls-files": { long: longFlags(["--others", "--exclude-standard", "--cached", "--modified"]) },
123
+ branch: { short: flags("arv"), long: longFlags(["--all", "--remotes", "--verbose", "--show-current"]), noPositional: true },
124
+ "merge-base": {},
125
+ "rev-list": { long: longFlags(["--count"]) },
126
+ describe: { long: longFlags(["--tags", "--always"]) },
127
+ };
128
+ /** Repository git config keys and sections that make read-only git commands run a program. */
129
+ const GIT_CONFIG_HAZARD = /^\s*(?:fsmonitor|external|textconv|pager|sshcommand|hookspath|askpass|editor|clean|smudge|process|command|cmd|helper|program|tool)\s*=|^\s*\[\s*(?:include|includeif|filter|alias)\b/im;
130
+ function hard(reason) {
131
+ return { verdict: "hard", reason };
132
+ }
133
+ function parseObject(text) {
134
+ if (text === null)
135
+ return null;
136
+ try {
137
+ const value = JSON.parse(text);
138
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
139
+ }
140
+ catch {
141
+ return null;
142
+ }
143
+ }
144
+ /** The nearest ancestor of `cwd` (after resolving symlinks) that holds a `.git` entry. */
145
+ function findRepoRoot(cwd, fsx = realFs) {
146
+ if (!path.isAbsolute(cwd) || !fsx.exists(cwd))
147
+ return null;
148
+ let dir = fsx.realpath(cwd);
149
+ for (;;) {
150
+ if (fsx.exists(path.join(dir, ".git")))
151
+ return dir;
152
+ const parent = path.dirname(dir);
153
+ if (parent === dir)
154
+ return null;
155
+ dir = parent;
156
+ }
157
+ }
158
+ /**
159
+ * The repo-relative path of `target` after resolving `..` and every symlink on the part of the path
160
+ * that exists, or null when it lands outside the repository. A `~` path is never resolved.
161
+ */
162
+ function resolveInsideRepo(repoRoot, cwd, target, fsx = realFs) {
163
+ if (target.startsWith("~"))
164
+ return null;
165
+ let existing = path.resolve(cwd, target);
166
+ const rest = [];
167
+ while (!fsx.exists(existing)) {
168
+ rest.unshift(path.basename(existing));
169
+ existing = path.dirname(existing);
170
+ }
171
+ const resolved = path.join(fsx.realpath(existing), ...rest);
172
+ if (resolved === repoRoot)
173
+ return ".";
174
+ if (!resolved.startsWith(`${repoRoot}${path.sep}`))
175
+ return null;
176
+ return path.relative(repoRoot, resolved);
177
+ }
178
+ function protectedPath(relative) {
179
+ const segments = relative.split(path.sep).map((segment) => segment.toLowerCase());
180
+ const protectedSegment = segments.find((segment) => PROTECTED_SEGMENTS.has(segment));
181
+ if (protectedSegment)
182
+ return `touches ${protectedSegment}`;
183
+ const name = segments[segments.length - 1];
184
+ if (PROTECTED_NAMES.has(name))
185
+ return `touches ${name}`;
186
+ if (name.startsWith(".env"))
187
+ return "touches an environment file";
188
+ return null;
189
+ }
190
+ /**
191
+ * Shell words for a single simple command, or null when quoting is unbalanced. `globbed` is true when
192
+ * a glob character appears outside quotes. Escapes and expansions are refused before this runs.
193
+ */
194
+ function tokenizeCommand(command) {
195
+ const tokens = [];
196
+ let current = "";
197
+ let started = false;
198
+ let quote = null;
199
+ let globbed = false;
200
+ for (const char of command) {
201
+ if (quote) {
202
+ if (char === quote)
203
+ quote = null;
204
+ else
205
+ current += char;
206
+ continue;
207
+ }
208
+ if (char === "'" || char === "\"") {
209
+ quote = char;
210
+ started = true;
211
+ continue;
212
+ }
213
+ if (/\s/.test(char)) {
214
+ if (started)
215
+ tokens.push(current);
216
+ current = "";
217
+ started = false;
218
+ continue;
219
+ }
220
+ if (GLOB.test(char))
221
+ globbed = true;
222
+ current += char;
223
+ started = true;
224
+ }
225
+ if (quote)
226
+ return null;
227
+ if (started)
228
+ tokens.push(current);
229
+ return { tokens, globbed };
230
+ }
231
+ /** Whether repository git config could make a read-only git command run a program. Unreadable config counts. */
232
+ function gitConfigHazard(repoRoot, fsx = realFs) {
233
+ const pointer = fsx.read(path.join(repoRoot, ".git"));
234
+ const gitDir = pointer === null ? path.join(repoRoot, ".git") : path.resolve(repoRoot, pointer.replace(/^gitdir:\s*/, "").trim());
235
+ const common = fsx.read(path.join(gitDir, "commondir"));
236
+ const configs = [path.join(gitDir, "config"), path.join(gitDir, "config.worktree"), ...(common ? [path.join(path.resolve(gitDir, common.trim()), "config")] : [])];
237
+ const texts = configs.map((file) => fsx.read(file));
238
+ if (texts[0] === null && (common === null || texts[2] === null))
239
+ return true;
240
+ return texts.some((text) => text !== null && GIT_CONFIG_HAZARD.test(text));
241
+ }
242
+ function checkWord(repoRoot, cwd, word, fsx) {
243
+ const relative = resolveInsideRepo(repoRoot, cwd, word, fsx);
244
+ return relative === null ? { outside: true, touches: null } : { outside: false, touches: protectedPath(relative) };
245
+ }
246
+ /** Null when every option is on the command's list and every value fits its kind; otherwise the reason. */
247
+ function checkOptions(spec, args, positional) {
248
+ const value = (kind, text, name) => {
249
+ if (text === undefined)
250
+ return `${name} is missing its value`;
251
+ if (kind === "num" ? !/^\d+$/.test(text) : text.includes("/") || text.startsWith("~"))
252
+ return `${name} has a value the floor does not accept`;
253
+ return null;
254
+ };
255
+ let endOfOptions = false;
256
+ for (let index = 0; index < args.length; index += 1) {
257
+ const arg = args[index];
258
+ if (endOfOptions || !arg.startsWith("-") || arg === "-") {
259
+ const reason = spec.noPositional ? "takes no arguments here" : positional(arg);
260
+ if (reason)
261
+ return reason;
262
+ continue;
263
+ }
264
+ if (arg === "--") {
265
+ endOfOptions = true;
266
+ continue;
267
+ }
268
+ const equals = arg.indexOf("=");
269
+ const name = equals > 0 ? arg.slice(0, equals) : arg;
270
+ const longKind = spec.long?.[name];
271
+ if (longKind) {
272
+ if (longKind === "flag") {
273
+ if (equals > 0)
274
+ return `${name} takes no value`;
275
+ continue;
276
+ }
277
+ const reason = value(longKind, equals > 0 ? arg.slice(equals + 1) : args[++index], name);
278
+ if (reason)
279
+ return reason;
280
+ continue;
281
+ }
282
+ if (arg.startsWith("--"))
283
+ return `${name} is not on the allowlist`;
284
+ if (spec.digits && /^-\d+$/.test(arg))
285
+ continue;
286
+ for (let at = 1; at < arg.length; at += 1) {
287
+ const letter = arg[at];
288
+ const kind = spec.short?.[letter];
289
+ if (!kind)
290
+ return `-${letter} is not on the allowlist`;
291
+ if (kind === "flag")
292
+ continue;
293
+ const attached = arg.slice(at + 1);
294
+ const reason = value(kind, attached || args[++index], `-${letter}`);
295
+ if (reason)
296
+ return reason;
297
+ break;
298
+ }
299
+ }
300
+ return null;
301
+ }
302
+ function bashVerdict(command, repoRoot, cwd, fsx) {
303
+ if (typeof command !== "string" || !command.trim())
304
+ return hard("no command to judge");
305
+ if (command.length > MAX_COMMAND_CHARS)
306
+ return hard("command is too long to judge");
307
+ if (SHELL_CONTROL.test(command))
308
+ return hard("command chains, pipes, redirects, substitutes or expands");
309
+ const parsed = tokenizeCommand(command);
310
+ if (!parsed || parsed.tokens.length === 0)
311
+ return hard("command quoting does not parse");
312
+ if (parsed.globbed)
313
+ return hard("command has an unquoted glob");
314
+ const tokens = parsed.tokens;
315
+ const [program, ...args] = tokens;
316
+ const name = program.toLowerCase();
317
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(program))
318
+ return hard("command sets environment variables");
319
+ if (HARD_COMMANDS.has(name))
320
+ return hard(`${name} is never answered for the human`);
321
+ // Every word, and every value glued to an option, must stay inside the repository; credential paths escalate.
322
+ let touches = null;
323
+ for (const arg of args) {
324
+ const values = arg.startsWith("-") ? [arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : arg.slice(2)] : [arg];
325
+ for (const word of values.filter((entry) => entry.includes("/") || entry.startsWith("~") || !arg.startsWith("-"))) {
326
+ const check = checkWord(repoRoot, cwd, word, fsx);
327
+ if (check.outside)
328
+ return hard("command reaches outside the repository");
329
+ if (!arg.startsWith("-"))
330
+ touches ??= check.touches;
331
+ }
332
+ }
333
+ if (touches && CREDENTIAL_TOUCHES.has(touches))
334
+ return hard(`command ${touches}`);
335
+ const shape = { repoRoot, tool: "Bash", tokens };
336
+ if (TEST_COMMANDS.some((entry) => entry.length === tokens.length && entry.every((token, index) => token === tokens[index])))
337
+ return { verdict: "allow", reason: "runs the repository's own checks", shape };
338
+ if (name === "git") {
339
+ const subcommand = args[0] ?? "";
340
+ const spec = GIT[subcommand];
341
+ if (!spec)
342
+ return hard(`git ${subcommand || "with global options"} is never answered for the human`);
343
+ if (gitConfigHazard(repoRoot, fsx))
344
+ return hard("the repository's git config can run programs");
345
+ const reason = checkOptions(spec, args.slice(1), () => null);
346
+ return reason ? hard(`git ${subcommand}: ${reason}`) : { verdict: "allow", reason: `git ${subcommand} only reads`, shape };
347
+ }
348
+ const spec = COMMANDS[name];
349
+ if (spec && !program.includes("/")) {
350
+ const reason = checkOptions(spec, args, () => null);
351
+ return reason ? hard(`${name}: ${reason}`) : { verdict: "allow", reason: `${name} only reads`, shape };
352
+ }
353
+ if (PACKAGE_MANAGERS.has(name)) {
354
+ const script = (args[0] === "run" && args.length === 2) || (args[0] === "test" && args.length === 1);
355
+ if (!script)
356
+ return hard(`${name} ${args[0] ?? ""} is never answered for the human`.trim());
357
+ }
358
+ if ((INTERPRETERS.has(name) || /^python\d/.test(name)) && args.some((arg) => arg.startsWith("-")))
359
+ return hard(`${name} with options can run inline code`);
360
+ if (touches)
361
+ return hard(`command ${touches}`);
362
+ return { verdict: "soft", reason: program.includes("/") ? "runs a program by path" : `${name} is not on the allowlist`, shape };
363
+ }
364
+ function pathVerdict(tool, input, repoRoot, cwd, fsx) {
365
+ const raw = input.file_path ?? input.notebook_path ?? input.path ?? (READ_TOOLS.has(tool) ? cwd : undefined);
366
+ if (typeof raw !== "string" || !raw.trim())
367
+ return hard(`${tool} names no path`);
368
+ const relative = resolveInsideRepo(repoRoot, cwd, raw, fsx);
369
+ if (relative === null)
370
+ return hard(`${tool} reaches outside the repository`);
371
+ const blocked = protectedPath(relative);
372
+ if (blocked && (EDIT_TOOLS.has(tool) || CREDENTIAL_TOUCHES.has(blocked)))
373
+ return hard(`${tool} ${blocked}`);
374
+ return { verdict: "allow", reason: EDIT_TOOLS.has(tool) ? `${tool} inside the repository` : `${tool} only reads inside the repository`, shape: { repoRoot, tool, tokens: [relative] } };
375
+ }
376
+ function evaluateFloor(item, fsx = realFs) {
377
+ const verdict = judge(item, fsx);
378
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_floor_judged", message: "judged a cmux Feed item against the floor", meta: { tool: item.toolName, verdict: verdict.verdict, reason: verdict.reason } });
379
+ return verdict;
380
+ }
381
+ function judge(item, fsx) {
382
+ if (item.kind !== "permissionRequest")
383
+ return hard("questions and plan approvals always go to the human");
384
+ if (item.source !== "claude")
385
+ return hard(`${item.source} sessions are observe-only`);
386
+ if (item.toolInputTruncated)
387
+ return hard("the request was truncated");
388
+ const tool = item.toolName;
389
+ if (!tool)
390
+ return hard("the request names no tool");
391
+ if (tool.startsWith("mcp__"))
392
+ return hard("MCP tools always go to the human");
393
+ if (!item.cwd)
394
+ return hard("the request has no working directory");
395
+ const repoRoot = findRepoRoot(item.cwd, fsx);
396
+ if (!repoRoot)
397
+ return hard("the working directory is not inside a repository");
398
+ const cwd = fsx.realpath(item.cwd);
399
+ const input = parseObject(item.toolInput);
400
+ if (!input)
401
+ return hard("the request input is not readable");
402
+ if (tool === "Bash")
403
+ return bashVerdict(input.command, repoRoot, cwd, fsx);
404
+ if (EDIT_TOOLS.has(tool) || READ_TOOLS.has(tool))
405
+ return pathVerdict(tool, input, repoRoot, cwd, fsx);
406
+ return hard(`${tool} always goes to the human`);
407
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.SEED_CMUX_PRINCIPLES = exports.CMUX_PRINCIPLES_FILE = void 0;
37
+ exports.readCmuxPrinciples = readCmuxPrinciples;
38
+ const fs = __importStar(require("node:fs"));
39
+ const path = __importStar(require("node:path"));
40
+ const runtime_1 = require("../../nerves/runtime");
41
+ /**
42
+ * Judgment guidance for coding-agent prompts, written by the human. It is guidance only: it never
43
+ * authorizes a reply. Live replies come from the code floor, the human's exact precedents and a
44
+ * standing owner grant. The agent reads these when it judges an escalated prompt.
45
+ *
46
+ * The bundle may hold its own `cmux-principles.md` (synced, human-authored); otherwise the seed
47
+ * below applies. The seed is drawn from the operator's standing desk instructions.
48
+ */
49
+ exports.CMUX_PRINCIPLES_FILE = "cmux-principles.md";
50
+ exports.SEED_CMUX_PRINCIPLES = [
51
+ "Keep routine work moving: a reversible step inside the session's own repository is the kind of thing the human wants answered without being woken.",
52
+ "Never act in the human's voice: anything that sends, posts, comments, merges or messages as them goes to them.",
53
+ "Never cause loss that cannot be undone: deleting files, force-pushing, rewriting shared history or wiping data goes to the human.",
54
+ "Credentials, sign-in, two-factor codes, payments and accepting terms always belong to the human.",
55
+ "Stay in scope: actions outside the session's repository, or beyond the task the session was given, go to the human.",
56
+ "Never widen permissions: no 'always', 'all' or bypass answers, and no edits to agent settings, hooks or CI.",
57
+ "Production, publishing and shared infrastructure go to the human unless they granted that exact action.",
58
+ "When unsure, ask: a blocked routine prompt costs the human a minute; a wrong approval can cost far more.",
59
+ ];
60
+ function readCmuxPrinciples(agentRoot) {
61
+ try {
62
+ const text = fs.readFileSync(path.join(agentRoot, exports.CMUX_PRINCIPLES_FILE), "utf-8");
63
+ const principles = text.split("\n").map((line) => line.replace(/^\s*(?:[-*]|\d+[.)])\s+/, "").trim()).filter((line) => line && !line.startsWith("#"));
64
+ if (principles.length > 0)
65
+ return { source: "bundle", principles };
66
+ }
67
+ catch {
68
+ // No bundle file: use the seed.
69
+ }
70
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_principles_seeded", message: "using the seed cmux principles", meta: { agentRoot } });
71
+ return { source: "seed", principles: [...exports.SEED_CMUX_PRINCIPLES] };
72
+ }
@@ -47,6 +47,7 @@ const runtime_credentials_1 = require("../../heart/runtime-credentials");
47
47
  const pending_1 = require("../../mind/pending");
48
48
  const runtime_1 = require("../../nerves/runtime");
49
49
  const attention_1 = require("./attention");
50
+ const answer_1 = require("./answer");
50
51
  const client_1 = require("./client");
51
52
  const RECONNECT_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000];
52
53
  const FEED_POLL_MS = 30_000;
@@ -111,6 +112,7 @@ async function startCmuxSenseApp(options) {
111
112
  return () => clearTimeout(timer);
112
113
  });
113
114
  const statePath = cmuxStatePath(agent);
115
+ const answers = { agentRoot: (0, identity_1.getAgentRoot)(agent), stateDir: path.dirname(statePath), client, now: nowMs, cmuxVersion: () => state.cmuxVersion };
114
116
  const state = (0, attention_1.readCmuxState)(statePath, now());
115
117
  state.connected = false;
116
118
  let stopped = false;
@@ -142,22 +144,59 @@ async function startCmuxSenseApp(options) {
142
144
  const saveSoon = () => {
143
145
  cancelSave ??= schedule(save, SAVE_DELAY_MS);
144
146
  };
147
+ /**
148
+ * One pending item: answered once when the code allows it, otherwise escalated with the reason.
149
+ * A failure while judging or answering escalates the item with the error; a failure to record the
150
+ * escalation leaves it unremembered, so the next check retries it. One item never blocks another.
151
+ */
152
+ async function handleItem(item) {
153
+ let reason;
154
+ let judgment = null;
155
+ try {
156
+ judgment = (0, answer_1.judgeFeedItem)(answers, item);
157
+ if (judgment.reply) {
158
+ const outcome = await (0, answer_1.answerOnce)(answers, item, judgment);
159
+ if (outcome === "replied_once" || outcome === "race") {
160
+ (0, attention_1.rememberEscalation)(state, item.requestId);
161
+ return null;
162
+ }
163
+ reason = outcome === "unconfirmed" ? "the sense's reply may or may not have reached cmux, and cmux does not show the request resolved by it" : "the sense's own reply did not go out";
164
+ }
165
+ else {
166
+ reason = judgment.reason;
167
+ }
168
+ }
169
+ catch (error) {
170
+ reason = `the sense could not judge or answer it: ${error.message}`;
171
+ }
172
+ const escalation = (0, attention_1.escalationMessage)(agent, item, (0, attention_1.surfaceForSession)(state, item.workstreamId), reason);
173
+ try {
174
+ await escalate(escalation);
175
+ }
176
+ catch (error) {
177
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_escalation_error", message: "could not record a cmux Feed request; the next check retries", meta: { agent, error: error.message } });
178
+ return null;
179
+ }
180
+ (0, attention_1.rememberEscalation)(state, item.requestId);
181
+ (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_escalated", message: "brought a cmux Feed request to the agent", meta: { agent, kind: item.kind, source: item.source } });
182
+ // The escalation is queued either way; a decision log that cannot take the line is noted, not retried.
183
+ try {
184
+ if (judgment)
185
+ (0, answer_1.recordDecision)(answers, item, judgment, "escalated", reason);
186
+ }
187
+ catch (error) {
188
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_decision_log_error", message: "could not log a cmux escalation", meta: { agent, error: error.message } });
189
+ }
190
+ return escalation.requestId;
191
+ }
145
192
  async function checkFeed() {
146
193
  try {
147
194
  const items = (0, attention_1.pendingFeedItems)(await client.call("feed.list", { pending_only: true }));
148
195
  const queued = [];
149
196
  for (const item of items.filter((entry) => !state.escalated.includes(entry.requestId))) {
150
- // One failing item never blocks the others; it stays unremembered, so the next check retries it.
151
- try {
152
- const escalation = (0, attention_1.escalationMessage)(agent, item, (0, attention_1.surfaceForSession)(state, item.workstreamId));
153
- await escalate(escalation);
154
- queued.push(escalation.requestId);
155
- (0, attention_1.rememberEscalation)(state, item.requestId);
156
- (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_escalated", message: "brought a cmux Feed request to the agent", meta: { agent, kind: item.kind, source: item.source } });
157
- }
158
- catch (error) {
159
- (0, runtime_1.emitNervesEvent)({ level: "warn", component: "senses", event: "senses.cmux_escalation_error", message: "could not record a cmux Feed request; the next check retries", meta: { agent, error: error.message } });
160
- }
197
+ const id = await handleItem(item);
198
+ if (id !== null)
199
+ queued.push(id);
161
200
  }
162
201
  // One wake per check pass: the private turn reads every queued escalation at once.
163
202
  if (queued.length > 0)
@@ -205,8 +244,8 @@ async function startCmuxSenseApp(options) {
205
244
  state.connected = true;
206
245
  state.lastError = null;
207
246
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.cmux_stream_connected", message: "cmux event stream connected", meta: { agent, reset } });
208
- void identify();
209
- requestFeedCheck();
247
+ // The version gates answering, so the first Feed check on a connection waits for it.
248
+ void identify().then(requestFeedCheck);
210
249
  return;
211
250
  }
212
251
  if (frame.type !== "event")
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.893",
3
+ "version": "0.1.0-alpha.895",
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.893",
9
+ "version": "0.1.0-alpha.895",
10
10
  "dependencies": {
11
11
  "@anthropic-ai/sdk": "^0.78.0",
12
12
  "@azure/identity": "^4.13.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.893",
3
+ "version": "0.1.0-alpha.895",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },