@ouro.bot/cli 0.1.0-alpha.724 → 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 (54) hide show
  1. package/changelog.json +12 -0
  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 +72 -14
  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 +21 -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/pipeline.js +90 -24
  50. package/dist/senses/private-runtime-worker.js +21 -20
  51. package/dist/senses/private-runtime.js +79 -29
  52. package/dist/senses/shared-turn.js +1 -0
  53. package/dist/senses/teams.js +1 -0
  54. package/package.json +6 -1
@@ -488,6 +488,13 @@ function isValidHabitPokeCommand(command) {
488
488
  && typeof command.habitName === "string"
489
489
  && command.habitName.trim().length > 0;
490
490
  }
491
+ function isValidHabitProbeCommand(command) {
492
+ return typeof command.agent === "string"
493
+ && command.agent.trim().length > 0
494
+ && typeof command.habitName === "string"
495
+ && command.habitName.trim().length > 0
496
+ && (command.noSend === undefined || command.noSend === true);
497
+ }
491
498
  function taskIdFromTaskPokePrivateWakeCommand(command) {
492
499
  if (command.kind !== "private.wake" || command.triggerSource !== "task-poke")
493
500
  return null;
@@ -1162,6 +1169,10 @@ class OuroDaemon {
1162
1169
  originRefs: [{ kind: "daemon-command", id: "inner.wake" }],
1163
1170
  };
1164
1171
  }
1172
+ const hasNoSendCapability = command.originRefs?.some((ref) => ref.kind === "capability" && ref.id === "no-send") ?? false;
1173
+ if ((command.noSend === true) !== hasNoSendCapability) {
1174
+ throw new Error("private-runtime no-send capability does not match its bound origin reference");
1175
+ }
1165
1176
  return {
1166
1177
  agent: command.agent,
1167
1178
  origin: "daemon.private.wake",
@@ -1210,37 +1221,110 @@ class OuroDaemon {
1210
1221
  ],
1211
1222
  };
1212
1223
  }
1213
- resolveHabitPokeOccurrenceId(command, trigger) {
1214
- const agentRoot = path.join(this.bundlesRoot, `${command.agent}.ouro`);
1215
- const habitPath = path.join(agentRoot, "habits", `${command.habitName}.md`);
1216
- let parsedHabit = null;
1217
- if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
1218
- if (!fs.existsSync(habitPath))
1219
- return { skipReason: "RSVP habit file not found" };
1224
+ resolveHabitDispatch(options) {
1225
+ const agentRoot = path.join(this.bundlesRoot, `${options.agent}.ouro`);
1226
+ const habitPath = path.join(agentRoot, "habits", `${options.habitName}.md`);
1227
+ const isRsvp = (0, habit_policy_1.isRsvpHabitName)(options.habitName);
1228
+ let readFailure = null;
1229
+ let habit;
1230
+ if (!fs.existsSync(habitPath)) {
1231
+ readFailure = "missing";
1232
+ habit = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", "habit file not found");
1233
+ }
1234
+ else {
1220
1235
  try {
1221
- parsedHabit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1236
+ habit = (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1222
1237
  }
1223
1238
  catch (error) {
1224
- return { skipReason: `RSVP habit metadata invalid: ${messageFromHabitPokeError(error)}` };
1239
+ readFailure = "read";
1240
+ habit = (0, habit_parser_1.createDegradedHabitFile)(habitPath, "read_error", "", messageFromHabitPokeError(error));
1225
1241
  }
1226
- if (!parsedHabit.rsvp)
1227
- return { skipReason: "RSVP habit metadata is required before private runtime wake" };
1228
1242
  }
1229
- if (trigger === "poke" || trigger === "manual")
1230
- return undefined;
1231
- if (typeof command.occurrenceId === "string" && command.occurrenceId.trim().length > 0)
1232
- return command.occurrenceId.trim();
1233
- if (!fs.existsSync(habitPath))
1234
- return { skipReason: "habit file not found" };
1235
- const habit = parsedHabit ?? (0, habit_runtime_state_1.applyHabitRuntimeState)(agentRoot, (0, habit_parser_1.parseHabitFile)(fs.readFileSync(habitPath, "utf-8"), habitPath));
1236
- if (habit.cadence === null)
1237
- return { skipReason: "habit has no cadence" };
1238
- const cadence = habit.cadence;
1239
- if (habit.lastRun === null)
1240
- return `${trigger}:first-run:${cadence}`;
1241
- return `${trigger}:last-run:${habit.lastRun}:cadence:${cadence}`;
1243
+ if (habit.status !== "active") {
1244
+ let skipReason = `habit status ${habit.status} is non-executable`;
1245
+ if (isRsvp && habit.status === "degraded") {
1246
+ if (readFailure === "missing") {
1247
+ skipReason = "RSVP habit file not found";
1248
+ }
1249
+ else {
1250
+ skipReason = `RSVP habit metadata invalid: ${habit.degradedDetail ?? habit.degradedReason}`;
1251
+ }
1252
+ }
1253
+ else if (readFailure === "missing"
1254
+ && options.source === "habit-poke"
1255
+ && options.trigger !== "manual"
1256
+ && options.trigger !== "poke") {
1257
+ skipReason = "habit file not found";
1258
+ }
1259
+ return { kind: "skipped", habit, skipReason };
1260
+ }
1261
+ if (isRsvp && !habit.rsvp) {
1262
+ return {
1263
+ kind: "skipped",
1264
+ habit,
1265
+ skipReason: "RSVP habit metadata is required before private runtime wake",
1266
+ };
1267
+ }
1268
+ if (options.trigger === "poke" || options.trigger === "manual") {
1269
+ return { kind: "ready", habit };
1270
+ }
1271
+ if (typeof options.occurrenceId === "string" && options.occurrenceId.trim().length > 0) {
1272
+ return { kind: "ready", habit, occurrenceId: options.occurrenceId.trim() };
1273
+ }
1274
+ if (habit.cadence === null) {
1275
+ return { kind: "skipped", habit, skipReason: "habit has no cadence" };
1276
+ }
1277
+ if (habit.lastRun === null) {
1278
+ return { kind: "ready", habit, occurrenceId: `${options.trigger}:first-run:${habit.cadence}` };
1279
+ }
1280
+ return {
1281
+ kind: "ready",
1282
+ habit,
1283
+ occurrenceId: `${options.trigger}:last-run:${habit.lastRun}:cadence:${habit.cadence}`,
1284
+ };
1285
+ }
1286
+ emitHabitDispatchSkipped(options) {
1287
+ (0, runtime_1.emitNervesEvent)({
1288
+ level: "warn",
1289
+ component: "daemon",
1290
+ event: "daemon.habit_dispatch_skipped",
1291
+ message: "habit dispatch rejected by current lifecycle state",
1292
+ meta: {
1293
+ agent: options.agent,
1294
+ habitName: options.habitName,
1295
+ trigger: options.trigger,
1296
+ status: options.resolution.habit.status,
1297
+ degradedReason: options.resolution.habit.status === "degraded"
1298
+ ? options.resolution.habit.degradedReason
1299
+ : null,
1300
+ detail: options.resolution.habit.status === "degraded"
1301
+ ? options.resolution.habit.degradedDetail
1302
+ : null,
1303
+ reason: options.resolution.skipReason,
1304
+ },
1305
+ });
1242
1306
  }
1243
- async runNativeRsvpHabit(command, trigger, occurrenceId) {
1307
+ emitHabitDispatchReady(options) {
1308
+ (0, runtime_1.emitNervesEvent)({
1309
+ component: "daemon",
1310
+ event: "daemon.habit_dispatch_ready",
1311
+ message: "habit dispatch accepted by current lifecycle state",
1312
+ meta: {
1313
+ agent: options.agent,
1314
+ habitName: options.habitName,
1315
+ trigger: options.trigger,
1316
+ status: options.resolution.habit.status,
1317
+ occurrenceId: options.resolution.occurrenceId ?? null,
1318
+ },
1319
+ });
1320
+ }
1321
+ skippedHabitDispatchResponse(options) {
1322
+ return {
1323
+ ok: true,
1324
+ message: `skipped scheduled habit ${options.habitName} for ${options.agent}: ${options.skipReason}`,
1325
+ };
1326
+ }
1327
+ async runNativeRsvpHabit(command, trigger, occurrenceId, noSend = false) {
1244
1328
  const runner = this.rsvpHabitRunner ?? (async (input) => {
1245
1329
  const { runNativeRsvpHabit } = await Promise.resolve().then(() => __importStar(require("../../rsvp/native-habit-runner")));
1246
1330
  return runNativeRsvpHabit(input);
@@ -1251,7 +1335,48 @@ class OuroDaemon {
1251
1335
  habitName: command.habitName,
1252
1336
  trigger,
1253
1337
  ...(occurrenceId ? { occurrenceId } : {}),
1338
+ ...(noSend ? { noSend: true } : {}),
1254
1339
  });
1340
+ if (noSend) {
1341
+ const payload = result.payload;
1342
+ const status = payload?.status;
1343
+ const transportInvocationCount = payload?.transportInvocationCount;
1344
+ const contractError = "native RSVP habit probe violated the immutable no-send result contract";
1345
+ if (!result.ok) {
1346
+ return {
1347
+ ok: false,
1348
+ error: result.message,
1349
+ data: {
1350
+ ok: false,
1351
+ noSend: true,
1352
+ status: typeof status === "string" ? status : "degraded",
1353
+ transportInvocationCount: null,
1354
+ error: result.message,
1355
+ },
1356
+ };
1357
+ }
1358
+ if (payload?.noSend !== true
1359
+ || typeof status !== "string"
1360
+ || typeof transportInvocationCount !== "number"
1361
+ || transportInvocationCount !== 0) {
1362
+ return {
1363
+ ok: false,
1364
+ error: contractError,
1365
+ data: {
1366
+ ok: false,
1367
+ noSend: true,
1368
+ status: typeof status === "string" ? status : "degraded",
1369
+ transportInvocationCount: null,
1370
+ error: contractError,
1371
+ },
1372
+ };
1373
+ }
1374
+ return {
1375
+ ok: true,
1376
+ message: result.message,
1377
+ data: { noSend: true, status, transportInvocationCount: 0 },
1378
+ };
1379
+ }
1255
1380
  return {
1256
1381
  ok: result.ok,
1257
1382
  message: result.message,
@@ -1281,6 +1406,7 @@ class OuroDaemon {
1281
1406
  type: "habit",
1282
1407
  habitName: habitMessage.habitName,
1283
1408
  trigger: habitMessage.trigger,
1409
+ ...(habitMessage.noSend ? { noSend: true } : {}),
1284
1410
  privateTurnDecision: decision,
1285
1411
  };
1286
1412
  }
@@ -1616,8 +1742,43 @@ class OuroDaemon {
1616
1742
  }
1617
1743
  case "inner.wake":
1618
1744
  return this.handlePrivateRuntimeWake(command);
1619
- case "private.wake":
1745
+ case "private.wake": {
1746
+ const scheduledHabit = (0, habit_private_wake_1.habitMessageFromPrivateWakeCommand)(command);
1747
+ if (!scheduledHabit || !this.hasManagedPrivateRuntime(command.agent)) {
1748
+ return this.handlePrivateRuntimeWake(command);
1749
+ }
1750
+ const occurrenceRef = command.originRefs?.find((ref) => ref.kind === "habit-occurrence");
1751
+ const occurrenceId = typeof occurrenceRef?.id === "string" && occurrenceRef.id.trim().length > 0
1752
+ ? occurrenceRef.id.trim()
1753
+ : undefined;
1754
+ const resolution = this.resolveHabitDispatch({
1755
+ agent: command.agent,
1756
+ habitName: scheduledHabit.habitName,
1757
+ trigger: scheduledHabit.trigger,
1758
+ source: "private-wake",
1759
+ ...(occurrenceId ? { occurrenceId } : {}),
1760
+ });
1761
+ if (resolution.kind === "skipped") {
1762
+ this.emitHabitDispatchSkipped({
1763
+ agent: command.agent,
1764
+ habitName: scheduledHabit.habitName,
1765
+ trigger: scheduledHabit.trigger,
1766
+ resolution,
1767
+ });
1768
+ return this.skippedHabitDispatchResponse({
1769
+ agent: command.agent,
1770
+ habitName: scheduledHabit.habitName,
1771
+ skipReason: resolution.skipReason,
1772
+ });
1773
+ }
1774
+ this.emitHabitDispatchReady({
1775
+ agent: command.agent,
1776
+ habitName: scheduledHabit.habitName,
1777
+ trigger: scheduledHabit.trigger,
1778
+ resolution,
1779
+ });
1620
1780
  return this.handlePrivateRuntimeWake(command);
1781
+ }
1621
1782
  case "chat.connect":
1622
1783
  await this.processManager.startAgent(command.agent);
1623
1784
  return {
@@ -1663,23 +1824,115 @@ class OuroDaemon {
1663
1824
  error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
1664
1825
  };
1665
1826
  }
1666
- const occurrenceId = this.resolveHabitPokeOccurrenceId(command, trigger);
1667
- if (typeof occurrenceId === "object") {
1827
+ const resolution = this.resolveHabitDispatch({
1828
+ agent: command.agent,
1829
+ habitName: command.habitName,
1830
+ trigger,
1831
+ source: "habit-poke",
1832
+ ...(command.occurrenceId ? { occurrenceId: command.occurrenceId } : {}),
1833
+ });
1834
+ if (resolution.kind === "skipped") {
1835
+ this.emitHabitDispatchSkipped({
1836
+ agent: command.agent,
1837
+ habitName: command.habitName,
1838
+ trigger,
1839
+ resolution,
1840
+ });
1841
+ return this.skippedHabitDispatchResponse({
1842
+ agent: command.agent,
1843
+ habitName: command.habitName,
1844
+ skipReason: resolution.skipReason,
1845
+ });
1846
+ }
1847
+ this.emitHabitDispatchReady({
1848
+ agent: command.agent,
1849
+ habitName: command.habitName,
1850
+ trigger,
1851
+ resolution,
1852
+ });
1853
+ if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
1854
+ return this.runNativeRsvpHabit(command, trigger, resolution.occurrenceId);
1855
+ }
1856
+ return this.handlePrivateRuntimeWake((0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
1857
+ agent: command.agent,
1858
+ habitName: command.habitName,
1859
+ trigger,
1860
+ sourceRef: { kind: "daemon-command", id: "habit.poke" },
1861
+ occurrenceId: resolution.occurrenceId,
1862
+ }));
1863
+ }
1864
+ case "habit.probe": {
1865
+ if (!isValidHabitProbeCommand(command)) {
1866
+ return {
1867
+ ok: false,
1868
+ error: "Invalid habit.probe payload: expected non-empty agent/habitName and noSend=true.",
1869
+ };
1870
+ }
1871
+ const trigger = "manual";
1872
+ if (!(0, habit_policy_1.isRsvpHabitName)(command.habitName) && !this.hasManagedPrivateRuntime(command.agent)) {
1873
+ return {
1874
+ ok: false,
1875
+ error: `No managed agent '${command.agent}' is registered with daemon-managed private runtime.`,
1876
+ };
1877
+ }
1878
+ const resolution = this.resolveHabitDispatch({
1879
+ agent: command.agent,
1880
+ habitName: command.habitName,
1881
+ trigger,
1882
+ source: "habit-probe",
1883
+ });
1884
+ if (resolution.kind === "skipped") {
1885
+ this.emitHabitDispatchSkipped({
1886
+ agent: command.agent,
1887
+ habitName: command.habitName,
1888
+ trigger,
1889
+ resolution,
1890
+ });
1668
1891
  return {
1669
1892
  ok: true,
1670
- message: `skipped scheduled habit ${command.habitName} for ${command.agent}: ${occurrenceId.skipReason}`,
1893
+ message: `habit probe rejected by lifecycle: status=${resolution.habit.status}`,
1894
+ data: {
1895
+ noSend: true,
1896
+ status: resolution.habit.status,
1897
+ transportInvocationCount: 0,
1898
+ },
1671
1899
  };
1672
1900
  }
1901
+ this.emitHabitDispatchReady({
1902
+ agent: command.agent,
1903
+ habitName: command.habitName,
1904
+ trigger,
1905
+ resolution,
1906
+ });
1673
1907
  if ((0, habit_policy_1.isRsvpHabitName)(command.habitName)) {
1674
- return this.runNativeRsvpHabit(command, trigger, occurrenceId);
1908
+ return this.runNativeRsvpHabit(command, trigger, resolution.occurrenceId, true);
1675
1909
  }
1676
- return this.handlePrivateRuntimeWake((0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
1910
+ const wake = await this.handlePrivateRuntimeWake((0, habit_private_wake_1.buildHabitPrivateWakeCommand)({
1677
1911
  agent: command.agent,
1678
1912
  habitName: command.habitName,
1679
1913
  trigger,
1680
- sourceRef: { kind: "daemon-command", id: "habit.poke" },
1681
- occurrenceId,
1914
+ sourceRef: { kind: "daemon-command", id: "habit.probe" },
1915
+ occurrenceId: resolution.occurrenceId,
1916
+ noSend: true,
1682
1917
  }));
1918
+ if (!wake.ok)
1919
+ return wake;
1920
+ const denied = wake.data
1921
+ ?.decision?.executable === false;
1922
+ const error = denied
1923
+ ? "non-RSVP habit probe was not executed because private-runtime policy denied execution"
1924
+ : "non-RSVP habit probe completion is asynchronous; zero-transport evidence is unverified";
1925
+ return {
1926
+ ok: false,
1927
+ error,
1928
+ data: {
1929
+ ok: false,
1930
+ noSend: true,
1931
+ status: resolution.habit.status,
1932
+ transportInvocationCount: null,
1933
+ error,
1934
+ },
1935
+ };
1683
1936
  }
1684
1937
  case "await.poke": {
1685
1938
  return this.handlePrivateRuntimeWake(this.buildAwaitPrivateWakeCommand(command));
@@ -503,19 +503,34 @@ function checkHabits(deps) {
503
503
  }
504
504
  checks.push({ label: `${agentDir} habits dir`, status: "pass", detail: habitsDir });
505
505
  const activeScheduledHabits = [];
506
+ const habitLifecycleRows = [];
506
507
  let unreadableHabits = 0;
507
- for (const file of deps.readdirSync(habitsDir).filter((entry) => entry.endsWith(".md"))) {
508
+ for (const file of deps.readdirSync(habitsDir).filter((entry) => entry.endsWith(".md") && entry !== "README.md").sort()) {
508
509
  const filePath = `${habitsDir}/${file}`;
510
+ let habit;
509
511
  try {
510
- const habit = (0, habit_parser_1.parseHabitFile)(deps.readFileSync(filePath), filePath);
511
- if (habit.status === "active" && habit.cadence && (0, cadence_1.parseCadenceToCron)(habit.cadence) !== null) {
512
- activeScheduledHabits.push(habit.name);
513
- }
512
+ habit = (0, habit_parser_1.parseHabitFile)(deps.readFileSync(filePath), filePath);
514
513
  }
515
- catch {
514
+ catch (error) {
516
515
  unreadableHabits += 1;
516
+ habit = (0, habit_parser_1.createDegradedHabitFile)(filePath, "read_error", "", error instanceof Error ? error.message : String(error));
517
+ }
518
+ habitLifecycleRows.push(habit);
519
+ if (habit.status === "active" && habit.cadence && (0, cadence_1.parseCadenceToCron)(habit.cadence) !== null) {
520
+ activeScheduledHabits.push(habit.name);
517
521
  }
518
522
  }
523
+ if (habitLifecycleRows.length > 0) {
524
+ const hasDegradedHabit = habitLifecycleRows.some((habit) => habit.status === "degraded");
525
+ checks.push({
526
+ id: "habits.lifecycle",
527
+ label: `${agentDir} habit lifecycle`,
528
+ status: hasDegradedHabit ? "fail" : "pass",
529
+ detail: habitLifecycleRows.map((habit) => habit.status === "degraded"
530
+ ? `${habit.name}=degraded(${habit.degradedReason})`
531
+ : `${habit.name}=${habit.status}`).join(", "),
532
+ });
533
+ }
519
534
  if (unreadableHabits > 0) {
520
535
  checks.push({
521
536
  label: `${agentDir} habit files`,
@@ -32,7 +32,9 @@ function buildHabitPrivateWakeCommand(options) {
32
32
  { kind: "habit-trigger", id: options.trigger },
33
33
  { kind: "habit-occurrence", id: occurrenceId },
34
34
  options.sourceRef,
35
+ ...(options.noSend ? [{ kind: "capability", id: "no-send" }] : []),
35
36
  ],
37
+ ...(options.noSend ? { noSend: true } : {}),
36
38
  };
37
39
  }
38
40
  function habitMessageFromPrivateWakeCommand(command) {
@@ -42,8 +44,11 @@ function habitMessageFromPrivateWakeCommand(command) {
42
44
  return null;
43
45
  const habitRef = command.originRefs?.find((ref) => ref.kind === "habit" && typeof ref.id === "string");
44
46
  const triggerRef = command.originRefs?.find((ref) => ref.kind === "habit-trigger" && typeof ref.id === "string");
47
+ const noSendRef = command.originRefs?.some((ref) => ref.kind === "capability" && ref.id === "no-send") ?? false;
45
48
  if (!habitRef || typeof habitRef.id !== "string" || !triggerRef || typeof triggerRef.id !== "string")
46
49
  return null;
50
+ if ((command.noSend === true) !== noSendRef)
51
+ return null;
47
52
  const habitName = habitRef.id.trim();
48
53
  const trigger = triggerRef.id.trim();
49
54
  if (habitName.length === 0)
@@ -52,5 +57,5 @@ function habitMessageFromPrivateWakeCommand(command) {
52
57
  return null;
53
58
  if (command.triggerSource !== `habit-${trigger}`)
54
59
  return null;
55
- return { habitName, trigger };
60
+ return { habitName, trigger, ...(noSendRef ? { noSend: true } : {}) };
56
61
  }