@ouro.bot/cli 0.1.0-alpha.764 → 0.1.0-alpha.766

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 CHANGED
@@ -1,6 +1,21 @@
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.766",
6
+ "changes": [
7
+ "Keep Sanctuary status summaries grounded in successful current reads and stale-care-safe evidence.",
8
+ "Render Butler Telegram replies without literal bold markers while preserving commands and data.",
9
+ "Keep the packaged visibility contract clear of retired module markers."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.765",
14
+ "changes": [
15
+ "Require configured turn-local reads before accepting either tool-based or plain-text terminal responses, and fail closed at the bounded provider-turn cap when those reads never run.",
16
+ "Give Telegram concise phone-native plain-text response guidance instead of Teams Markdown guidance."
17
+ ]
18
+ },
4
19
  {
5
20
  "version": "0.1.0-alpha.764",
6
21
  "changes": [
@@ -1,5 +1,5 @@
1
1
  {
2
- "runtimeVersion": "0.1.0-alpha.764",
2
+ "runtimeVersion": "0.1.0-alpha.766",
3
3
  "bundleSchemaVersion": 3,
4
4
  "lastUpdated": "2026-08-30T00:00:00.000Z"
5
5
  }
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0"?>
2
2
  <Container version="2">
3
3
  <Name>Mendelow Cloud Butler</Name>
4
- <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.764</Repository>
4
+ <Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.766</Repository>
5
5
  <Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
6
6
  <Network>host</Network>
7
7
  <Shell>sh</Shell>
@@ -569,6 +569,11 @@ function toolResultIndicatesFailure(content) {
569
569
  || normalized.startsWith("blocked:")
570
570
  || normalized.startsWith("rejected:");
571
571
  }
572
+ function requiredToolResultSucceeded(name, content, validate) {
573
+ if (toolResultIndicatesFailure(content))
574
+ return false;
575
+ return validate?.(name, content) ?? true;
576
+ }
572
577
  function effectFingerprint(name, rawArguments) {
573
578
  let args;
574
579
  try {
@@ -1122,6 +1127,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1122
1127
  let providerIterations = 0;
1123
1128
  const requiredToolCallNames = [...new Set(options?.requiredToolCalls?.names ?? [])];
1124
1129
  const dispatchedRequiredToolCalls = new Set();
1130
+ const pendingRequiredToolCalls = () => {
1131
+ const missing = requiredToolCallNames.filter((name) => !dispatchedRequiredToolCalls.has(name));
1132
+ return missing.length > 0
1133
+ ? { missing, message: `${options.requiredToolCalls.retryMessage} Missing required tool calls: ${missing.join(", ")}.` }
1134
+ : null;
1135
+ };
1136
+ const queueRequiredCorrection = (message, limitContext) => {
1137
+ if (providerIterations >= exports.MAX_PROVIDER_ITERATIONS)
1138
+ throw new Error(`provider iteration limit exhausted at response ${exports.MAX_PROVIDER_ITERATIONS} ${limitContext}`);
1139
+ messages.push({ role: "user", content: message });
1140
+ providerRuntime.resetTurnState(messages);
1141
+ };
1125
1142
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
1126
1143
  const toolFrictionLedger = (0, tool_friction_1.createToolFrictionLedger)();
1127
1144
  const finishTerminalProviderError = (error, classification) => {
@@ -1520,6 +1537,28 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1520
1537
  });
1521
1538
  continue;
1522
1539
  }
1540
+ const requiredToolCallsGate = pendingRequiredToolCalls();
1541
+ if (requiredToolCallsGate) {
1542
+ streamCallbackBuffer?.discard();
1543
+ callbacks.onClearText?.();
1544
+ queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1545
+ (0, runtime_1.emitNervesEvent)({
1546
+ level: "warn",
1547
+ component: "engine",
1548
+ event: "engine.required_tool_calls_pending",
1549
+ message: "terminal response rejected until required tool handlers dispatch",
1550
+ meta: { missingToolNames: requiredToolCallsGate.missing },
1551
+ });
1552
+ continue;
1553
+ }
1554
+ const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(String(msg.content ?? ""));
1555
+ if (requiredAnswerRejection) {
1556
+ streamCallbackBuffer?.discard();
1557
+ callbacks.onClearText?.();
1558
+ queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1559
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported terminal answer rejected after required reads", meta: { answerLength: String(msg.content ?? "").length } });
1560
+ continue;
1561
+ }
1523
1562
  if (privateReturnTextAckRetryError) {
1524
1563
  streamCallbackBuffer?.discard();
1525
1564
  callbacks.onClearText?.();
@@ -1722,21 +1761,18 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1722
1761
  if (isSoleSettle) {
1723
1762
  const settleArgs = validatedCallArguments.get(result.toolCalls[0]);
1724
1763
  callbacks.onToolStart("settle", settleArgs);
1725
- const missingRequiredToolCalls = requiredToolCallNames.filter((name) => !dispatchedRequiredToolCalls.has(name));
1726
- if (missingRequiredToolCalls.length > 0) {
1764
+ const requiredToolCallsGate = pendingRequiredToolCalls();
1765
+ if (requiredToolCallsGate) {
1727
1766
  streamCallbackBuffer?.discard();
1728
1767
  callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1729
1768
  callbacks.onClearText?.();
1730
- pushGenerated(msg);
1731
- const gateMessage = `${options.requiredToolCalls.retryMessage} Missing required tool calls: ${missingRequiredToolCalls.join(", ")}.`;
1732
- pushGenerated({ role: "tool", tool_call_id: result.toolCalls[0].id, content: gateMessage });
1733
- providerRuntime.appendToolOutput(result.toolCalls[0].id, gateMessage);
1769
+ queueRequiredCorrection(requiredToolCallsGate.message, "before required tool calls completed");
1734
1770
  (0, runtime_1.emitNervesEvent)({
1735
1771
  level: "warn",
1736
1772
  component: "engine",
1737
1773
  event: "engine.required_tool_calls_pending",
1738
1774
  message: "settle rejected until required tool handlers dispatch",
1739
- meta: { missingToolNames: missingRequiredToolCalls },
1775
+ meta: { missingToolNames: requiredToolCallsGate.missing },
1740
1776
  });
1741
1777
  continue;
1742
1778
  }
@@ -1755,6 +1791,15 @@ async function runAgent(messages, callbacks, channel, signal, options) {
1755
1791
  // Extract answer from the tool call arguments.
1756
1792
  // Supports: {"answer":"text","intent":"..."} or "text" (JSON string).
1757
1793
  const { answer, intent } = parseSettlePayload(result.toolCalls[0].arguments);
1794
+ const requiredAnswerRejection = options?.requiredToolCalls?.validateTerminalAnswer?.(answer);
1795
+ if (requiredAnswerRejection) {
1796
+ streamCallbackBuffer?.discard();
1797
+ callbacks.onToolEnd("settle", (0, tools_1.summarizeArgs)("settle", settleArgs), false);
1798
+ callbacks.onClearText?.();
1799
+ queueRequiredCorrection(requiredAnswerRejection, "before required terminal answer validation completed");
1800
+ (0, runtime_1.emitNervesEvent)({ level: "warn", component: "engine", event: "engine.required_tool_answer_rejected", message: "unsupported settle answer rejected after required reads", meta: { answerLength: answer.length } });
1801
+ continue;
1802
+ }
1758
1803
  // Private-runtime settle: no CompletionMetadata, "(settled)" ack
1759
1804
  if (isPrivateRuntimeChannel) {
1760
1805
  streamCallbackBuffer?.discard();
@@ -2266,7 +2311,7 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2266
2311
  const execToolFn = options?.execTool ?? tools_1.execTool;
2267
2312
  const routineActionSelection = approvalCalls.find((entry) => entry.call.id === tc.id)?.routineActionSelection;
2268
2313
  const executionToolContext = routineActionSelection && augmentedToolContext ? { ...augmentedToolContext, routineActionSelection } : augmentedToolContext;
2269
- if (requiredToolCallNames.includes(tc.name))
2314
+ if (requiredToolCallNames.includes(tc.name) && !options?.requiredToolCalls?.requireSuccessfulResults)
2270
2315
  dispatchedRequiredToolCalls.add(tc.name);
2271
2316
  toolResult = await execToolFn(tc.name, args, executionToolContext);
2272
2317
  success = true;
@@ -2276,6 +2321,9 @@ async function runAgent(messages, callbacks, channel, signal, options) {
2276
2321
  success = false;
2277
2322
  augmentedToolContext?.habitSession?.recordError?.(toolResult);
2278
2323
  }
2324
+ if (success && requiredToolCallNames.includes(tc.name) && options?.requiredToolCalls?.requireSuccessfulResults
2325
+ && requiredToolResultSucceeded(tc.name, toolResult, options.requiredToolCalls.validateRequiredToolResult))
2326
+ dispatchedRequiredToolCalls.add(tc.name);
2279
2327
  if (success && currentEffectFingerprint && !toolResultIndicatesFailure(toolResult)) {
2280
2328
  unresolvedHistoricalEffects = unresolvedHistoricalEffects.filter((effect) => effect.fingerprint !== currentEffectFingerprint);
2281
2329
  }
@@ -410,6 +410,9 @@ function runtimeInfoSection(channel, options) {
410
410
  else if (channel === "mcp") {
411
411
  lines.push("this message arrived via a dev tool (e.g. claude code, codex) on behalf of a friend in a sense session. the user can see our conversation. if the dev-tool user asks a direct question, requests stop/pause/confirmation, or the work is complete, i answer with settle. during an active browser/tool/task flow, process comments are feedback to absorb; i keep using tools instead of settling for status. if friction appears, i first look for ad-hoc repairs with the tools i already have. if the friction reveals a harness gap, i create or revise a ponder packet and keep working. ponder does not create an outward deferral by itself.");
412
412
  }
413
+ else if (channel === "telegram") {
414
+ lines.push("i am responding in Telegram. i keep replies concise and phone-native. i do not use markdown. i do not introduce myself on boot.");
415
+ }
413
416
  else if (channel === "bluebubbles") {
414
417
  lines.push("i am responding in iMessage through BlueBubbles. i keep replies short and phone-native. i do not use markdown. i do not introduce myself on boot.");
415
418
  lines.push("during an active browser/tool/task flow, process comments from my friend are feedback to absorb, not a reason to settle. i speak once if useful, then keep working until complete, blocked, asked to stop/pause, or confirmation is required. timeout/recovery state is internal, not iMessage copy.");
@@ -45,6 +45,24 @@ const presence_1 = require("../arc/presence");
45
45
  const intentions_1 = require("../arc/intentions");
46
46
  const steward_policy_1 = require("../heart/steward-policy");
47
47
  const await_parser_1 = require("../heart/awaiting/await-parser");
48
+ function presentCare(care) {
49
+ const staleAt = care.nextCheckAt ? Date.parse(care.nextCheckAt) : Number.NaN;
50
+ if (care.kind !== "system" || !["active", "watching"].includes(care.status) || care.nextCheckAt === null)
51
+ return care;
52
+ if (Number.isFinite(staleAt) && staleAt >= Date.now())
53
+ return care;
54
+ return {
55
+ id: care.id,
56
+ kind: care.kind,
57
+ status: care.status,
58
+ salience: care.salience,
59
+ steward: care.steward,
60
+ evidenceStatus: "stale",
61
+ recheckRequired: true,
62
+ staleAt: care.nextCheckAt,
63
+ lastAssessedAt: care.updatedAt,
64
+ };
65
+ }
48
66
  exports.continuityToolDefinitions = [
49
67
  // ── Continuity tools ──────────────────────────────────────────────
50
68
  {
@@ -308,7 +326,7 @@ exports.continuityToolDefinitions = [
308
326
  },
309
327
  handler: (a) => {
310
328
  const agentRoot = (0, identity_1.getAgentRoot)();
311
- const cares = a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot);
329
+ const cares = (a.status === "all" ? (0, cares_1.readCares)(agentRoot) : (0, cares_1.readActiveCares)(agentRoot)).map(presentCare);
312
330
  (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.query_cares", message: `queried ${cares.length} cares`, meta: { count: cares.length } });
313
331
  return JSON.stringify(cares, null, 2);
314
332
  },
@@ -2,8 +2,55 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.sanctuaryFullVisibilityRequiredToolCalls = sanctuaryFullVisibilityRequiredToolCalls;
4
4
  const runtime_1 = require("../nerves/runtime");
5
- const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers"];
5
+ const REQUIRED_TOOL_NAMES = ["query_active_work", "query_cares", "unraid_get_system", "unraid_list_containers", "unraid_get_storage", "sanctuary_get_download_queue"];
6
6
  const WHOLE_STATUS_REQUESTS = new Set(["what are you working on", "what's going on with sanctuary"]);
7
+ function unsupportedCurrentClaim(answer) {
8
+ const unsupported = answer.replace(/docker\.img/giu, "docker image").split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((sentence) => {
9
+ if (!/docker image(?: disk)?/iu.test(sentence))
10
+ return false;
11
+ const uncertaintyOnly = /\b(?:cannot|can't|unable to) (?:currently )?(?:verify|measure)\b|\b(?:unknown|unverified)\b|\bneeds? (?:a )?(?:fresh |authoritative )*(?:check|measurement)\b/iu.test(sentence);
12
+ const stateClaim = /\b\d+(?:\.\d+)?\s*%|\bfull\b|\b(?:no|out of) space\b|\bwrites? (?:will |may )?fail\b|\b(?:healthy|unhealthy|running|stopped)\b/iu.test(sentence);
13
+ return stateClaim || !uncertaintyOnly;
14
+ });
15
+ if (unsupported)
16
+ return "No current Butler tool measures Docker image utilization. Do not report a Docker image percentage or full state as current; say that the stale care needs a fresh authoritative check.";
17
+ const unsupportedProviderClaim = answer.split(/[,;!?\n]|(?<!\d)\.(?!\d)/u).some((clause) => {
18
+ const explicitlyUnverified = /\b(?:cannot|can't|unable to) (?:currently )?(?:verify|confirm)|\b(?:unknown|unverified|stale|historical|previous|prior)\b|\bneeds? (?:a )?(?:fresh |authoritative )*(?:check|verification)\b/iu.test(clause);
19
+ if (explicitlyUnverified)
20
+ return false;
21
+ if (/\b(?:Astraweb|Usenet provider|download provider|(?:block|prepaid|download|provider) credit)\b/iu.test(clause))
22
+ return true;
23
+ return /\bSABnzbd\b/iu.test(clause) && /\b(?:auth(?:entication)?|authenticated|credentials?)\b/iu.test(clause);
24
+ });
25
+ return unsupportedProviderClaim ? "The current queue read does not prove provider credit or authentication status. Do not report Astraweb, block-credit, or authentication failure as current; say that the provider needs a fresh authoritative check." : undefined;
26
+ }
27
+ function successfulCurrentResult(name, result) {
28
+ if (Buffer.byteLength(result, "utf8") > 1_000_000)
29
+ return false;
30
+ if (name === "query_active_work")
31
+ return result.trimStart().startsWith("this is my current top-level live world-state.");
32
+ try {
33
+ const parsed = JSON.parse(result);
34
+ if (name === "query_cares")
35
+ return Array.isArray(parsed);
36
+ if (name === "sanctuary_get_download_queue") {
37
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
38
+ return false;
39
+ const queue = parsed;
40
+ const exactKeys = ["observedAt", "paused", "queuedJobs", "stateDigest", "status"];
41
+ return JSON.stringify(Object.keys(queue).sort()) === JSON.stringify(exactKeys)
42
+ && typeof queue.paused === "boolean"
43
+ && typeof queue.status === "string" && Buffer.byteLength(queue.status, "utf8") <= 64
44
+ && Number.isSafeInteger(queue.queuedJobs) && Number(queue.queuedJobs) >= 0 && Number(queue.queuedJobs) <= 1_000_000
45
+ && typeof queue.observedAt === "string" && Number.isFinite(Date.parse(queue.observedAt)) && new Date(queue.observedAt).toISOString() === queue.observedAt
46
+ && typeof queue.stateDigest === "string" && /^[a-f0-9]{64}$/u.test(queue.stateDigest);
47
+ }
48
+ return !!parsed && typeof parsed === "object" && !Array.isArray(parsed) && parsed.ok === true;
49
+ }
50
+ catch {
51
+ return false;
52
+ }
53
+ }
7
54
  function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames) {
8
55
  const normalized = request.normalize("NFKC").trim().toLocaleLowerCase("en-US").replaceAll("’", "'").replace(/[?!.\s]+$/gu, "");
9
56
  if (!WHOLE_STATUS_REQUESTS.has(normalized) || !REQUIRED_TOOL_NAMES.every((name) => advertisedToolNames.includes(name)))
@@ -11,6 +58,9 @@ function sanctuaryFullVisibilityRequiredToolCalls(request, advertisedToolNames)
11
58
  (0, runtime_1.emitNervesEvent)({ component: "senses", event: "senses.sanctuary_full_visibility_reads_required", message: "required current Sanctuary visibility reads", meta: { toolCount: REQUIRED_TOOL_NAMES.length } });
12
59
  return {
13
60
  names: REQUIRED_TOOL_NAMES,
14
- retryMessage: "Before answering, read current active work, cares, system health, and service state. Then give Ari one compact household summary; do not ask him to choose a status slice.",
61
+ retryMessage: "Before answering, read current active work, cares, system health, service state, storage, and the download queue. Current tool facts outrank care history; a stale care is a recheck item, not a present-tense fact. Then give Ari one compact household summary; do not ask him to choose a status slice.",
62
+ requireSuccessfulResults: true,
63
+ validateRequiredToolResult: successfulCurrentResult,
64
+ validateTerminalAnswer: unsupportedCurrentClaim,
15
65
  };
16
66
  }
@@ -76,6 +76,12 @@ function preparedTexts(effect) {
76
76
  return [requireText(effect.text, "Telegram text")];
77
77
  return [effect.text?.trim() || null];
78
78
  }
79
+ function telegramPlainText(text) {
80
+ return text.replace(/(^|[\s([{])\*\*([\p{L}\p{N}][^*\n]*?)\*\*(?=$|[\s.,;!?)\]}])/gmu, "$1$2");
81
+ }
82
+ function presentTelegramEffect(authorClass, effect) {
83
+ return authorClass === "butler" && effect.kind === "text" ? { ...effect, text: telegramPlainText(effect.text) } : effect;
84
+ }
79
85
  function assertEffectTarget(target, effect, idempotencyKey) {
80
86
  if (target.kind === "admission_gate") {
81
87
  if (effect.kind !== "admission_ack" || effect.text !== exports.FIXED_ADMISSION_ACKNOWLEDGEMENT || idempotencyKey !== `ack:${target.admissionId}`) {
@@ -425,11 +431,12 @@ function createTelegramAuthorizedEffectExecutor(options) {
425
431
  if (input.signal?.aborted)
426
432
  throw input.signal.reason;
427
433
  barrier();
428
- const authorization = await options.authorize({ phase: "prepare", ...input });
434
+ const presentedInput = { ...input, effect: presentTelegramEffect(input.authorClass, input.effect) };
435
+ const authorization = await options.authorize({ phase: "prepare", ...presentedInput });
429
436
  if (!authorization.allowed)
430
437
  throw new Error(`Telegram effect authorization denied: ${authorization.reason}`);
431
438
  const store = getStore();
432
- const prepared = prepareTelegramEffect(store, { ...input, authorization });
439
+ const prepared = prepareTelegramEffect(store, { ...presentedInput, authorization });
433
440
  try {
434
441
  const executed = await executeTelegramEffect(store, prepared.id, options.api, async (artifact) => {
435
442
  if (input.signal?.aborted)
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.764",
3
+ "version": "0.1.0-alpha.766",
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.764",
9
+ "version": "0.1.0-alpha.766",
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.764",
3
+ "version": "0.1.0-alpha.766",
4
4
  "engines": {
5
5
  "node": ">=22"
6
6
  },