@ouro.bot/cli 0.1.0-alpha.723 → 0.1.0-alpha.725

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.
Files changed (55) hide show
  1. package/changelog.json +295 -275
  2. package/dist/heart/active-work.js +10 -3
  3. package/dist/heart/core.js +343 -77
  4. package/dist/heart/daemon/cli-exec.js +132 -15
  5. package/dist/heart/daemon/cli-help.js +13 -2
  6. package/dist/heart/daemon/cli-parse.js +82 -1
  7. package/dist/heart/daemon/daemon.js +286 -33
  8. package/dist/heart/daemon/doctor.js +82 -6
  9. package/dist/heart/daemon/habit-private-wake.js +6 -1
  10. package/dist/heart/habits/habit-cancel.js +810 -0
  11. package/dist/heart/habits/habit-lifecycle.js +1327 -0
  12. package/dist/heart/habits/habit-migration.js +28 -23
  13. package/dist/heart/habits/habit-parser.js +72 -8
  14. package/dist/heart/habits/habit-runtime-state.js +3 -24
  15. package/dist/heart/habits/habit-scheduler.js +268 -50
  16. package/dist/heart/habits/habit-session.js +16 -0
  17. package/dist/heart/hatch/hatch-flow.js +6 -4
  18. package/dist/heart/mailbox/readers/runtime-readers.js +26 -27
  19. package/dist/heart/orientation-frame.js +58 -1
  20. package/dist/heart/provider-attempt.js +4 -1
  21. package/dist/heart/providers/anthropic.js +23 -2
  22. package/dist/heart/start-of-turn-packet.js +14 -11
  23. package/dist/heart/streaming.js +565 -185
  24. package/dist/mind/obligation-steering.js +15 -9
  25. package/dist/mind/prompt.js +58 -26
  26. package/dist/nerves/coverage/run-artifacts.js +55 -35
  27. package/dist/nerves/review/cli.js +2 -2
  28. package/dist/nerves/review/core.js +14 -7
  29. package/dist/repertoire/tools-base.js +2 -0
  30. package/dist/repertoire/tools-habits.js +103 -0
  31. package/dist/repertoire/tools.js +18 -3
  32. package/dist/rsvp/cli.js +126 -20
  33. package/dist/rsvp/diagnostics.js +12 -2
  34. package/dist/rsvp/habit-policy.js +7 -0
  35. package/dist/rsvp/habit-stage.js +19 -4
  36. package/dist/rsvp/native-habit-runner.js +151 -11
  37. package/dist/rsvp/outbound-state.js +360 -0
  38. package/dist/senses/bluebubbles/client.js +90 -17
  39. package/dist/senses/bluebubbles/inbound-log.js +1 -0
  40. package/dist/senses/bluebubbles/index.js +662 -425
  41. package/dist/senses/bluebubbles/model.js +57 -13
  42. package/dist/senses/bluebubbles/mutation-log.js +21 -10
  43. package/dist/senses/bluebubbles/reaction-policy.js +55 -0
  44. package/dist/senses/bluebubbles/semantic-receipts.js +1818 -0
  45. package/dist/senses/cli/tui-store.js +1 -0
  46. package/dist/senses/cli.js +1 -0
  47. package/dist/senses/habit-lifecycle-guard.js +24 -0
  48. package/dist/senses/ingress-evidence.js +2 -0
  49. package/dist/senses/mail.js +129 -0
  50. package/dist/senses/pipeline.js +90 -24
  51. package/dist/senses/private-runtime-worker.js +21 -20
  52. package/dist/senses/private-runtime.js +79 -29
  53. package/dist/senses/shared-turn.js +1 -0
  54. package/dist/senses/teams.js +1 -0
  55. package/package.json +6 -1
@@ -1444,8 +1444,41 @@ async function checkManualCloneBundles(deps) {
1444
1444
  }
1445
1445
  // ── toDaemonCommand ──
1446
1446
  function toDaemonCommand(command) {
1447
+ if (command.kind === "habit.probe") {
1448
+ return {
1449
+ kind: "habit.probe",
1450
+ agent: command.agent,
1451
+ habitName: command.habitName,
1452
+ noSend: true,
1453
+ };
1454
+ }
1447
1455
  return command;
1448
1456
  }
1457
+ function habitProbeJsonResponse(response) {
1458
+ const data = response.data && typeof response.data === "object" && !Array.isArray(response.data)
1459
+ ? response.data
1460
+ : null;
1461
+ const status = typeof data?.status === "string" ? data.status : "degraded";
1462
+ if (response.ok
1463
+ && data?.noSend === true
1464
+ && typeof data.transportInvocationCount === "number"
1465
+ && data.transportInvocationCount === 0) {
1466
+ return JSON.stringify({
1467
+ noSend: true,
1468
+ status,
1469
+ transportInvocationCount: 0,
1470
+ }, null, 2);
1471
+ }
1472
+ return JSON.stringify({
1473
+ ok: false,
1474
+ noSend: true,
1475
+ status,
1476
+ transportInvocationCount: null,
1477
+ error: response.error
1478
+ ?? (!response.ok ? response.message : undefined)
1479
+ ?? "habit probe did not produce verified zero-transport evidence",
1480
+ }, null, 2);
1481
+ }
1449
1482
  // ── Hatch input resolution ──
1450
1483
  async function resolveHatchInput(command, deps) {
1451
1484
  const prompt = deps.promptInput;
@@ -3707,6 +3740,57 @@ function hostedMissingMailKeys(body, keys) {
3707
3740
  rotateSourceGrant: missingSourceGrant,
3708
3741
  };
3709
3742
  }
3743
+ /**
3744
+ * Key ids the public registry declares for this agent, or `[]` when the
3745
+ * registry cannot be consulted.
3746
+ *
3747
+ * `requiredHostedKeyIds` derives the asserted set from the ensure response
3748
+ * alone, so anything the response omits is never asserted — and the server
3749
+ * omits `sourceGrant` unless a source alias is in play. That is how the
3750
+ * 2026-07-24 gap passed: the registry declared a source-grant key, the vault
3751
+ * never received its private half, and setup reported success. The registry is
3752
+ * the authority on what mail is currently being encrypted to, so it is
3753
+ * consulted too.
3754
+ *
3755
+ * Never throws. An unreachable registry degrades to the response-only
3756
+ * assertion, because refusing a setup that previously succeeded — on a machine
3757
+ * whose Azure credentials happen to be unavailable — would be a worse failure
3758
+ * than the one this guards against.
3759
+ *
3760
+ * The agent match is case-insensitive: the harness sends `agentId: agent`
3761
+ * verbatim in the ensure payload while registry records can carry the
3762
+ * lowercased form.
3763
+ */
3764
+ async function hostedRegistryDeclaredKeyIds(input) {
3765
+ try {
3766
+ const publicRegistry = isPlainRecord(input.body.publicRegistry) ? input.body.publicRegistry : {};
3767
+ const managedIdentityClientId = stringField(input.existingMailroom ?? {}, "azureManagedIdentityClientId");
3768
+ const registry = await input.readRegistry({
3769
+ mailboxAddress: "",
3770
+ privateKeys: {},
3771
+ registryAzureAccountUrl: stringField(publicRegistry, "azureAccountUrl"),
3772
+ registryContainer: stringField(publicRegistry, "container"),
3773
+ registryBlob: stringField(publicRegistry, "blob"),
3774
+ ...(managedIdentityClientId ? { azureManagedIdentityClientId: managedIdentityClientId } : {}),
3775
+ });
3776
+ const wanted = input.agent.toLowerCase();
3777
+ const isThisAgent = (agentId) => agentId.toLowerCase() === wanted;
3778
+ return [
3779
+ ...registry.mailboxes.filter((mailbox) => isThisAgent(mailbox.agentId)).map((mailbox) => mailbox.keyId),
3780
+ ...registry.sourceGrants.filter((grant) => isThisAgent(grant.agentId) && grant.enabled).map((grant) => grant.keyId),
3781
+ ].filter((keyId) => keyId.trim().length > 0);
3782
+ }
3783
+ catch (error) {
3784
+ (0, runtime_1.emitNervesEvent)({
3785
+ level: "warn",
3786
+ component: "cli",
3787
+ event: "cli.hosted_mail_registry_assertion_skipped",
3788
+ message: "hosted mail key assertion could not consult the public registry",
3789
+ meta: { agent: input.agent, error: error instanceof Error ? error.message : String(error) },
3790
+ });
3791
+ return [];
3792
+ }
3793
+ }
3710
3794
  function assertHostedPrivateKeys(input) {
3711
3795
  for (const keyId of input.requiredKeyIds) {
3712
3796
  if (input.keys[keyId])
@@ -3811,10 +3895,18 @@ async function ensureAgentMailroom(agent, input, deps, progressLabel) {
3811
3895
  generatedPrivateKeys = responsePrivateKeys(body.generatedPrivateKeys);
3812
3896
  Object.assign(privateKeys, generatedPrivateKeys);
3813
3897
  }
3898
+ const registryKeyIds = await hostedRegistryDeclaredKeyIds({
3899
+ agent,
3900
+ body,
3901
+ existingMailroom,
3902
+ readRegistry: deps.readMailroomRegistry ?? reader_1.readMailroomRegistry,
3903
+ });
3814
3904
  assertHostedPrivateKeys({
3815
3905
  agent,
3816
3906
  keys: privateKeys,
3817
- requiredKeyIds: requiredHostedKeyIds(body),
3907
+ // Union, not replacement: the response still names keys it just minted,
3908
+ // which a registry read taken before the write would not yet show.
3909
+ requiredKeyIds: [...new Set([...requiredHostedKeyIds(body), ...registryKeyIds])],
3818
3910
  });
3819
3911
  const publicRegistry = requiredResponseRecord(body.publicRegistry, "publicRegistry");
3820
3912
  const blobStore = requiredResponseRecord(body.blobStore, "blobStore");
@@ -7159,8 +7251,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7159
7251
  return result.summary;
7160
7252
  }
7161
7253
  // ── habit subcommands (local, no daemon socket needed) ──
7162
- if (command.kind === "habit.list" || command.kind === "habit.create" || command.kind === "habit.runs" || command.kind === "habit.inspect" || command.kind === "habit.summary") {
7163
- const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
7254
+ if (command.kind === "habit.list" || command.kind === "habit.create" || command.kind === "habit.runs" || command.kind === "habit.inspect" || command.kind === "habit.summary" || command.kind === "habit.cancel") {
7255
+ const { createDegradedHabitFile, parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
7256
+ const { publishNewHabitDefinition } = await Promise.resolve().then(() => __importStar(require("../habits/habit-lifecycle")));
7164
7257
  const { applyHabitRuntimeState } = await Promise.resolve().then(() => __importStar(require("../habits/habit-runtime-state")));
7165
7258
  const { listHabitRunReceipts, readHabitRunReceipt } = await Promise.resolve().then(() => __importStar(require("../../arc/flight-recorder")));
7166
7259
  const { readHabitSessionSummary } = await Promise.resolve().then(() => __importStar(require("../habits/habit-session-summary")));
@@ -7168,10 +7261,21 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7168
7261
  const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
7169
7262
  /* v8 ignore stop */
7170
7263
  const habitsDir = path.join(bundleRoot, "habits");
7264
+ if (command.kind === "habit.cancel") {
7265
+ const { cancelHabit } = await Promise.resolve().then(() => __importStar(require("../habits/habit-cancel")));
7266
+ const receipt = await cancelHabit({
7267
+ agentRoot: bundleRoot,
7268
+ habitId: command.habitName,
7269
+ evidenceLocator: command.evidenceLocator,
7270
+ authority: { kind: "offline_bridge" },
7271
+ }, deps.habitCancelDeps);
7272
+ deps.writeStdout(receipt.acknowledgement);
7273
+ return receipt.acknowledgement;
7274
+ }
7171
7275
  if (command.kind === "habit.list") {
7172
7276
  let files;
7173
7277
  try {
7174
- files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md");
7278
+ files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md").sort();
7175
7279
  }
7176
7280
  catch {
7177
7281
  const message = "no habits found";
@@ -7185,10 +7289,18 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7185
7289
  }
7186
7290
  const lines = [];
7187
7291
  for (const file of files) {
7188
- const fileContent = fs.readFileSync(path.join(habitsDir, file), "utf-8");
7189
- const habit = applyHabitRuntimeState(bundleRoot, parseHabitFile(fileContent, path.join(habitsDir, file)));
7292
+ const filePath = path.join(habitsDir, file);
7293
+ let habit;
7294
+ try {
7295
+ const fileContent = fs.readFileSync(filePath, "utf-8");
7296
+ habit = applyHabitRuntimeState(bundleRoot, parseHabitFile(fileContent, filePath));
7297
+ }
7298
+ catch (error) {
7299
+ habit = createDegradedHabitFile(filePath, "read_error", "", error instanceof Error ? error.message : String(error));
7300
+ }
7190
7301
  const lastRunStr = habit.lastRun ?? "never";
7191
- lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
7302
+ const degradedReason = habit.status === "degraded" ? ` degradedReason=${habit.degradedReason}` : "";
7303
+ lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status}${degradedReason} lastRun=${lastRunStr}`);
7192
7304
  }
7193
7305
  const message = lines.join("\n");
7194
7306
  deps.writeStdout(message);
@@ -7272,12 +7384,6 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7272
7384
  }
7273
7385
  // habit.create
7274
7386
  const filePath = path.join(habitsDir, `${command.name}.md`);
7275
- if (fs.existsSync(filePath)) {
7276
- const message = `error: habit '${command.name}' already exists`;
7277
- deps.writeStdout(message);
7278
- return message;
7279
- }
7280
- fs.mkdirSync(habitsDir, { recursive: true });
7281
7387
  const now = new Date().toISOString();
7282
7388
  const habitContent = renderHabitFile({
7283
7389
  title: command.name,
@@ -7285,7 +7391,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7285
7391
  status: "active",
7286
7392
  created: now,
7287
7393
  }, `Habit: ${command.name}`);
7288
- fs.writeFileSync(filePath, habitContent, "utf-8");
7394
+ const publication = publishNewHabitDefinition({
7395
+ agentRoot: bundleRoot,
7396
+ habitId: command.name,
7397
+ bytes: habitContent,
7398
+ });
7399
+ if (publication === "exists") {
7400
+ const message = `error: habit '${command.name}' already exists`;
7401
+ deps.writeStdout(message);
7402
+ return message;
7403
+ }
7289
7404
  const message = `created: ${filePath}`;
7290
7405
  deps.writeStdout(message);
7291
7406
  return message;
@@ -8258,7 +8373,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
8258
8373
  ? command.json
8259
8374
  ? formatDaemonStatusJsonOutput(response)
8260
8375
  : (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage)
8261
- : fallbackMessage;
8376
+ : command.kind === "habit.probe" && command.json
8377
+ ? habitProbeJsonResponse(response)
8378
+ : fallbackMessage;
8262
8379
  deps.writeStdout(message);
8263
8380
  return message;
8264
8381
  }
@@ -10,6 +10,7 @@ exports.levenshteinDistance = levenshteinDistance;
10
10
  exports.suggestCommand = suggestCommand;
11
11
  exports.getGroupedHelp = getGroupedHelp;
12
12
  exports.getCommandHelp = getCommandHelp;
13
+ const core_1 = require("../../nerves/review/core");
13
14
  // ── Registry ──
14
15
  exports.COMMAND_REGISTRY = {
15
16
  up: {
@@ -142,7 +143,7 @@ exports.COMMAND_REGISTRY = {
142
143
  description: "Manage agent habits",
143
144
  usage: "ouro habit <subcommand> [--agent <name>]",
144
145
  example: "ouro habit list",
145
- subcommands: ["list", "create", "runs", "inspect", "summary", "poke"],
146
+ subcommands: ["list", "create", "runs", "inspect", "summary", "poke", "cancel", "probe"],
146
147
  },
147
148
  rsvp: {
148
149
  category: "Habits",
@@ -186,7 +187,7 @@ exports.COMMAND_REGISTRY = {
186
187
  },
187
188
  "nerves-review": {
188
189
  category: "Internal",
189
- description: "Read-only review of recent nerves events from an agent log stream",
190
+ description: `Read-only review of recent nerves events from one agent log stream (default: ${core_1.DEFAULT_NERVES_PROCESS})`,
190
191
  usage: "ouro nerves-review [--agent <name>] [--process <name>] [--component <substr>] [--event <substr>] [--level <level>] [--since <duration>] [--limit <n>] [--json]",
191
192
  example: "ouro nerves-review --agent slugger --component daemon --event habit --since 30m --json",
192
193
  },
@@ -368,6 +369,16 @@ exports.COMMAND_REGISTRY = {
368
369
  },
369
370
  };
370
371
  const SUBCOMMAND_HELP = {
372
+ "habit probe": {
373
+ description: "Probe one habit through its real dispatch path with an irrevocable no-send boundary; it cannot send and reports zero transport invocations",
374
+ usage: "ouro habit probe --agent <name> --habit <name> --no-send [--json]",
375
+ example: "ouro habit probe --agent slugger --habit report-name --no-send --json",
376
+ },
377
+ "habit cancel": {
378
+ description: "Cancel a habit offline from a previously verified evidence bridge",
379
+ usage: "ouro habit cancel --agent <name> --habit <name> --evidence <bridge-id>",
380
+ example: "ouro habit cancel --agent slugger --habit report-name --evidence pre-v1-bluebubbles-event-id",
381
+ },
371
382
  "private decisions": {
372
383
  description: "Read recent private-runtime allow/deny decisions from the policy ledger",
373
384
  usage: "ouro private decisions [--agent <name>] [--limit <n>] [--json]",
@@ -18,6 +18,7 @@ const friends_1 = require("@ouro.bot/friends");
18
18
  const cli_help_1 = require("./cli-help");
19
19
  const cli_desk_1 = require("./cli-desk");
20
20
  const flight_recorder_1 = require("../../arc/flight-recorder");
21
+ const core_1 = require("../../nerves/review/core");
21
22
  const vault_items_1 = require("./vault-items");
22
23
  // ── Shared helpers ──
23
24
  function extractAgentFlag(args) {
@@ -112,6 +113,8 @@ function usage() {
112
113
  " ouro habit runs [--agent <name>] [--limit <n>]",
113
114
  " ouro habit inspect [--agent <name>] <runId>",
114
115
  " ouro habit summary [--agent <name>] (--run-id <id>|--habit <name>|--operation-id <id>) [--which latest|previous|latest-success|latest-failure] [--json]",
116
+ " ouro habit cancel --agent <name> --habit <name> --evidence <bridge-id>",
117
+ " ouro habit probe --agent <name> --habit <name> --no-send [--json]",
115
118
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
116
119
  " ouro bluebubbles replay [--agent <name>] --message-guid <guid> [--event-type new-message|updated-message] [--json]",
117
120
  " ouro bluebubbles context-smoke [--agent <name>] --message-guid <guid> [--persist] [--json]",
@@ -314,6 +317,84 @@ function parsePokeCommand(args) {
314
317
  function parseHabitCommand(args) {
315
318
  const { agent, rest } = extractAgentFlag(args);
316
319
  const sub = rest[0];
320
+ if (sub === "probe") {
321
+ if (args.includes("--allow-send")) {
322
+ throw new Error("habit probe cannot allow send");
323
+ }
324
+ if (args.filter((value) => value === "--agent").length !== 1
325
+ || !agent
326
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent))
327
+ throw new Error("habit probe requires --agent <name>");
328
+ let habitName;
329
+ let json = false;
330
+ let sawNoSend = false;
331
+ const options = rest.slice(1);
332
+ for (let index = 0; index < options.length; index += 1) {
333
+ const option = options[index];
334
+ if (option === "--json") {
335
+ if (json)
336
+ throw new Error("habit probe received duplicate --json");
337
+ json = true;
338
+ continue;
339
+ }
340
+ if (option === "--no-send") {
341
+ if (sawNoSend)
342
+ throw new Error("habit probe received duplicate --no-send");
343
+ sawNoSend = true;
344
+ continue;
345
+ }
346
+ if (option === "--habit" && options[index + 1]) {
347
+ if (habitName !== undefined)
348
+ throw new Error("habit probe received duplicate --habit");
349
+ habitName = options[index + 1];
350
+ index += 1;
351
+ continue;
352
+ }
353
+ throw new Error("Usage: ouro habit probe --agent <name> --habit <name> --no-send [--json]");
354
+ }
355
+ if (!habitName)
356
+ throw new Error("habit probe requires --habit <name>");
357
+ if (!/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(habitName)) {
358
+ throw new Error("habit probe requires a valid --habit name");
359
+ }
360
+ return { kind: "habit.probe", agent, habitName, noSend: true, json };
361
+ }
362
+ if (sub === "cancel") {
363
+ if (args.filter((value) => value === "--agent").length !== 1
364
+ || !agent
365
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(agent)) {
366
+ throw new Error(`Usage\n${usage()}`);
367
+ }
368
+ let habitName;
369
+ let evidenceLocator;
370
+ const options = rest.slice(1);
371
+ for (let index = 0; index < options.length; index += 1) {
372
+ const option = options[index];
373
+ const value = options[index + 1];
374
+ if ((option !== "--habit" && option !== "--evidence") || value === undefined) {
375
+ throw new Error(`Usage\n${usage()}`);
376
+ }
377
+ if (option === "--habit") {
378
+ if (habitName !== undefined)
379
+ throw new Error(`Usage\n${usage()}`);
380
+ habitName = value;
381
+ }
382
+ else {
383
+ if (evidenceLocator !== undefined)
384
+ throw new Error(`Usage\n${usage()}`);
385
+ evidenceLocator = value;
386
+ }
387
+ index += 1;
388
+ }
389
+ if (!habitName || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(habitName)) {
390
+ throw new Error("habit cancellation requires a valid --habit name");
391
+ }
392
+ if (!evidenceLocator
393
+ || evidenceLocator.startsWith("capture:")
394
+ || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(evidenceLocator))
395
+ throw new Error("habit cancellation requires an offline evidence bridge ID");
396
+ return { kind: "habit.cancel", agent, habitName, evidenceLocator };
397
+ }
317
398
  if (sub === "list") {
318
399
  return { kind: "habit.list", ...(agent ? { agent } : {}) };
319
400
  }
@@ -2225,7 +2306,7 @@ function parseRsvpSmokeCommand(args) {
2225
2306
  }
2226
2307
  function parseNervesReviewCommand(args) {
2227
2308
  const { agent, rest } = extractAgentFlag(args);
2228
- let processName = "daemon";
2309
+ let processName = core_1.DEFAULT_NERVES_PROCESS;
2229
2310
  let component;
2230
2311
  let event;
2231
2312
  let level;