@ouro.bot/cli 0.1.0-alpha.638 → 0.1.0-alpha.640

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,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.640",
6
+ "changes": [
7
+ "Reject text-only private-return queued acknowledgements unless ponder created packet-backed return work."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.639",
12
+ "changes": [
13
+ "Rollback safety guard: ouro rollback now validates the target installed @ouro.bot/cli package payload before flipping CurrentVersion, and refuses cached runtimes whose shipped assets contain blocked removed user-facing failure text so old local versions cannot resurrect the removed BlueBubbles live-turn timeout notice."
14
+ ]
15
+ },
4
16
  {
5
17
  "version": "0.1.0-alpha.638",
6
18
  "changes": [
@@ -298,12 +298,18 @@ function messageContentText(content) {
298
298
  .filter(Boolean)
299
299
  .join("\n");
300
300
  }
301
+ function isHarnessCorrectiveUserText(text) {
302
+ return text.startsWith("no tool was called this turn. you must end every turn")
303
+ || text.startsWith("private-return acknowledgement claimed work was queued, but no ponder packet was created this turn.");
304
+ }
301
305
  function latestUserMessageText(messages) {
302
306
  for (let i = messages.length - 1; i >= 0; i -= 1) {
303
307
  const message = messages[i];
304
308
  if (message?.role !== "user")
305
309
  continue;
306
310
  const text = messageContentText(message.content).trim();
311
+ if (isHarnessCorrectiveUserText(text))
312
+ continue;
307
313
  if (text.length > 0)
308
314
  return text;
309
315
  }
@@ -675,12 +681,11 @@ async function runAgent(messages, callbacks, channel, signal, options) {
675
681
  // which doesn't update mid-turn. The agent only needs to be told once;
676
682
  // after that, repeated rest attempts mean they've acknowledged.
677
683
  let freshWorkGateFired = false;
678
- // Counter for "no tool call returned despite tool_choice=required" violations.
679
- // MiniMax reasoning models occasionally emit only a <think>...</think>
680
- // block and stop, without any tool call even when tool_choice is set to
681
- // "required". This is a provider-level violation; the harness retries with
682
- // a corrective nudge up to a small cap rather than silently accepting an
683
- // empty turn.
684
+ // Counter for no-tool-call violations. MiniMax reasoning models occasionally
685
+ // emit only a <think>...</think> block and stop, without any tool call — even
686
+ // when tool_choice is set to "required". Private-return requests also need
687
+ // a hard no-tool guard: a text-only "queued" acknowledgement is false unless
688
+ // a ponder packet created the return obligation in this turn.
684
689
  let noToolCallRetries = 0;
685
690
  const NO_TOOL_CALL_MAX_RETRIES = 2;
686
691
  const toolLoopState = (0, tool_loop_1.createToolLoopState)();
@@ -938,7 +943,60 @@ async function runAgent(messages, callbacks, channel, signal, options) {
938
943
  && typeof result.content === "string"
939
944
  && stripThinkBlocksForViolationCheck(result.content).length === 0
940
945
  && result.content.length > 0;
946
+ const privateReturnTextAckRetryError = !result.toolCalls.length
947
+ ? privateReturnMissingPonderError({
948
+ latestUserRequest: latestUserMessageText(messages),
949
+ answer: stripThinkBlocksForViolationCheck(result.content),
950
+ sawPonder,
951
+ })
952
+ : null;
941
953
  if (!result.toolCalls.length) {
954
+ if (privateReturnTextAckRetryError) {
955
+ callbacks.onClearText?.();
956
+ if (noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
957
+ noToolCallRetries++;
958
+ (0, runtime_1.emitNervesEvent)({
959
+ level: "warn",
960
+ component: "engine",
961
+ event: "engine.no_tool_call_retry",
962
+ message: "model returned a text-only private-return acknowledgement without ponder; retrying with corrective nudge",
963
+ meta: {
964
+ attempt: noToolCallRetries,
965
+ cap: NO_TOOL_CALL_MAX_RETRIES,
966
+ provider: providerRuntime.id,
967
+ model: providerRuntime.model,
968
+ reason: "private_return_missing_ponder",
969
+ contentLength: result.content.length,
970
+ },
971
+ });
972
+ messages.push(msg);
973
+ messages.push({
974
+ role: "user",
975
+ content: `${privateReturnTextAckRetryError} Emit the ponder(action=create, ...) tool call now, or ask a blocking clarification without saying the private work is queued.`,
976
+ });
977
+ continue;
978
+ }
979
+ const blockedAnswer = "I could not start the private pass. No private-attention packet was created, so no return work was queued.";
980
+ (0, runtime_1.emitNervesEvent)({
981
+ level: "error",
982
+ component: "engine",
983
+ event: "engine.private_return_missing_ponder_blocked",
984
+ message: "private-return text acknowledgement skipped ponder through the retry cap; failing closed",
985
+ meta: {
986
+ cap: NO_TOOL_CALL_MAX_RETRIES,
987
+ provider: providerRuntime.id,
988
+ model: providerRuntime.model,
989
+ contentLength: result.content.length,
990
+ },
991
+ });
992
+ msg.content = blockedAnswer;
993
+ messages.push(msg);
994
+ callbacks.onTextChunk(blockedAnswer);
995
+ completion = { answer: blockedAnswer, intent: "blocked" };
996
+ outcome = "blocked";
997
+ done = true;
998
+ continue;
999
+ }
942
1000
  if (onlyThinkContent && toolChoiceRequired && noToolCallRetries < NO_TOOL_CALL_MAX_RETRIES) {
943
1001
  // Provider-level violation: tool_choice was required, model emitted
944
1002
  // only a <think>...</think> block (or empty content) with no tool
@@ -719,6 +719,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
719
719
  });
720
720
  },
721
721
  installCliVersion: async (version) => { (0, ouro_version_manager_1.installVersion)(version, {}); },
722
+ validateCliVersionForActivation: (version) => (0, ouro_version_manager_1.validateInstalledVersionForActivation)(version, {}),
722
723
  activateCliVersion: (version) => {
723
724
  (0, ouro_version_manager_1.activateVersion)(version, {});
724
725
  // Same self-prune as ensureCurrentVersionInstalled — fires from the
@@ -6112,6 +6112,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6112
6112
  return message;
6113
6113
  }
6114
6114
  }
6115
+ const validation = deps.validateCliVersionForActivation?.(command.version);
6116
+ if (validation && !validation.ok) {
6117
+ const message = `refusing to roll back to ${command.version}: ${validation.message}`;
6118
+ return returnCliFailure(deps, message);
6119
+ }
6115
6120
  deps.activateCliVersion(command.version);
6116
6121
  }
6117
6122
  else {
@@ -6122,6 +6127,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6122
6127
  deps.writeStdout(message);
6123
6128
  return message;
6124
6129
  }
6130
+ const validation = deps.validateCliVersionForActivation?.(previousVersion);
6131
+ if (validation && !validation.ok) {
6132
+ const message = `refusing to roll back to ${previousVersion}: ${validation.message}`;
6133
+ return returnCliFailure(deps, message);
6134
+ }
6125
6135
  deps.activateCliVersion(previousVersion);
6126
6136
  command = { ...command, version: previousVersion };
6127
6137
  }
@@ -39,6 +39,7 @@ exports.getCurrentVersion = getCurrentVersion;
39
39
  exports.getPreviousVersion = getPreviousVersion;
40
40
  exports.buildChangelogCommand = buildChangelogCommand;
41
41
  exports.listInstalledVersions = listInstalledVersions;
42
+ exports.validateInstalledVersionForActivation = validateInstalledVersionForActivation;
42
43
  exports.installVersion = installVersion;
43
44
  exports.activateVersion = activateVersion;
44
45
  exports.compareCliVersions = compareCliVersions;
@@ -51,6 +52,31 @@ const path = __importStar(require("path"));
51
52
  const runtime_1 = require("../../nerves/runtime");
52
53
  /** Maximum number of installed CLI versions to retain after pruning. */
53
54
  exports.DEFAULT_RETAIN_VERSIONS = 5;
55
+ const INSTALLED_RUNTIME_PACKAGE_PAYLOAD_PATHS = [
56
+ "assets",
57
+ "dist",
58
+ "RepairGuide.ouro",
59
+ "SerpentGuide.ouro",
60
+ "skills",
61
+ "changelog.json",
62
+ "package.json",
63
+ ];
64
+ const INSTALLED_RUNTIME_TEXT_EXTENSIONS = new Set([
65
+ ".cjs",
66
+ ".css",
67
+ ".html",
68
+ ".js",
69
+ ".json",
70
+ ".md",
71
+ ".mjs",
72
+ ".txt",
73
+ ]);
74
+ const BLOCKED_INSTALLED_RUNTIME_TEXT_SIGNATURES = [
75
+ {
76
+ label: "removed BlueBubbles timeout notice",
77
+ base64: "bGl2ZSBpTWVzc2FnZSB0dXJuIHRpbWVkIG91dDsgSSBjYXB0dXJlZCBpdCBmb3IgcmVjb3ZlcnkgaW5zdGVhZCBvZiBzaWxlbnRseSBoYW5naW5n",
78
+ },
79
+ ];
54
80
  function getOuroCliHome(homeDir) {
55
81
  /* v8 ignore next -- dep default: tests always inject @preserve */
56
82
  const home = homeDir ?? os.homedir();
@@ -98,6 +124,94 @@ function listInstalledVersions(deps) {
98
124
  return [];
99
125
  }
100
126
  }
127
+ function validateInstalledVersionForActivation(version, deps = {}) {
128
+ const cliHome = getOuroCliHome(deps.homeDir);
129
+ /* v8 ignore start -- dep defaults: tests inject @preserve */
130
+ const existsSync = deps.existsSync ?? fs.existsSync;
131
+ const readdirSync = deps.readdirSync ?? ((p, opts) => fs.readdirSync(p, opts));
132
+ const readFileSync = deps.readFileSync ?? fs.readFileSync;
133
+ /* v8 ignore stop */
134
+ const packageRoot = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli");
135
+ const packageJsonPath = path.join(packageRoot, "package.json");
136
+ if (!existsSync(packageJsonPath)) {
137
+ const message = `installed runtime ${version} is missing its @ouro.bot/cli package; run 'ouro up' to restore the current release`;
138
+ (0, runtime_1.emitNervesEvent)({
139
+ component: "daemon",
140
+ event: "daemon.cli_version_activation_blocked",
141
+ message: "blocked CLI version activation",
142
+ meta: { version, packageRoot, reason: "missing-package" },
143
+ level: "warn",
144
+ });
145
+ return { ok: false, version, packageRoot, violations: ["missing @ouro.bot/cli package"], message };
146
+ }
147
+ const violations = findBlockedInstalledRuntimePayloadText(packageRoot, { existsSync, readdirSync, readFileSync });
148
+ if (violations.length === 0) {
149
+ return { ok: true, version, packageRoot, violations, message: "installed runtime package passed activation guard" };
150
+ }
151
+ const message = `installed runtime ${version} contains blocked package assets (${violations.join(", ")}); run 'ouro up' to stay on the current release`;
152
+ (0, runtime_1.emitNervesEvent)({
153
+ component: "daemon",
154
+ event: "daemon.cli_version_activation_blocked",
155
+ message: "blocked CLI version activation",
156
+ meta: { version, packageRoot, reason: "blocked-package-assets", violations },
157
+ level: "warn",
158
+ });
159
+ return { ok: false, version, packageRoot, violations, message };
160
+ }
161
+ function findBlockedInstalledRuntimePayloadText(packageRoot, deps) {
162
+ const violations = [];
163
+ const blockedText = BLOCKED_INSTALLED_RUNTIME_TEXT_SIGNATURES.map((signature) => ({
164
+ label: signature.label,
165
+ text: Buffer.from(signature.base64, "base64").toString("utf8"),
166
+ }));
167
+ for (const payloadPath of INSTALLED_RUNTIME_PACKAGE_PAYLOAD_PATHS) {
168
+ const absolutePath = path.join(packageRoot, payloadPath);
169
+ if (!deps.existsSync(absolutePath))
170
+ continue;
171
+ collectBlockedTextViolations(packageRoot, absolutePath, payloadPath, blockedText, deps, violations);
172
+ }
173
+ return violations.sort();
174
+ }
175
+ function collectBlockedTextViolations(packageRoot, absolutePath, relativePath, blockedText, deps, violations) {
176
+ let entries = null;
177
+ try {
178
+ entries = deps.readdirSync(absolutePath, { withFileTypes: true });
179
+ }
180
+ catch {
181
+ entries = null;
182
+ }
183
+ if (entries !== null) {
184
+ for (const entry of entries) {
185
+ const childRelativePath = `${relativePath}/${entry.name}`;
186
+ if (entry.isDirectory()) {
187
+ collectBlockedTextViolations(packageRoot, path.join(absolutePath, entry.name), childRelativePath, blockedText, deps, violations);
188
+ }
189
+ else if (entry.isFile()) {
190
+ inspectInstalledRuntimePayloadFile(packageRoot, childRelativePath, blockedText, deps, violations);
191
+ }
192
+ }
193
+ return;
194
+ }
195
+ inspectInstalledRuntimePayloadFile(packageRoot, relativePath, blockedText, deps, violations);
196
+ }
197
+ function inspectInstalledRuntimePayloadFile(packageRoot, relativePath, blockedText, deps, violations) {
198
+ if (!INSTALLED_RUNTIME_TEXT_EXTENSIONS.has(path.extname(relativePath)))
199
+ return;
200
+ let content = "";
201
+ try {
202
+ content = deps.readFileSync(path.join(packageRoot, relativePath), "utf8");
203
+ }
204
+ catch (error) {
205
+ const reason = error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error);
206
+ violations.push(`${relativePath} could not be inspected: ${reason}`);
207
+ return;
208
+ }
209
+ for (const blocked of blockedText) {
210
+ if (content.includes(blocked.text)) {
211
+ violations.push(`${relativePath} contains ${blocked.label}`);
212
+ }
213
+ }
214
+ }
101
215
  function installVersion(version, deps) {
102
216
  const cliHome = getOuroCliHome(deps.homeDir);
103
217
  /* v8 ignore start -- dep defaults: tests always inject @preserve */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.638",
3
+ "version": "0.1.0-alpha.640",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",