@ouro.bot/cli 0.1.0-alpha.665 → 0.1.0-alpha.666

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.
@@ -364,6 +364,8 @@ function agentResolutionFailureMode(command) {
364
364
  case "friend.update":
365
365
  case "habit.list":
366
366
  case "habit.create":
367
+ case "habit.runs":
368
+ case "habit.inspect":
367
369
  case "thoughts":
368
370
  case "attention.list":
369
371
  case "attention.show":
@@ -6841,9 +6843,10 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6841
6843
  return result.summary;
6842
6844
  }
6843
6845
  // ── habit subcommands (local, no daemon socket needed) ──
6844
- if (command.kind === "habit.list" || command.kind === "habit.create") {
6846
+ if (command.kind === "habit.list" || command.kind === "habit.create" || command.kind === "habit.runs" || command.kind === "habit.inspect") {
6845
6847
  const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
6846
6848
  const { applyHabitRuntimeState } = await Promise.resolve().then(() => __importStar(require("../habits/habit-runtime-state")));
6849
+ const { listHabitRunReceipts, readHabitRunReceipt } = await Promise.resolve().then(() => __importStar(require("../../arc/flight-recorder")));
6847
6850
  /* v8 ignore start -- production default: uses real bundle root @preserve */
6848
6851
  const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
6849
6852
  /* v8 ignore stop */
@@ -6874,6 +6877,52 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6874
6877
  deps.writeStdout(message);
6875
6878
  return message;
6876
6879
  }
6880
+ if (command.kind === "habit.runs") {
6881
+ const receipts = listHabitRunReceipts(bundleRoot, { limit: command.limit });
6882
+ const message = receipts.length === 0
6883
+ ? "no habit runs found"
6884
+ : receipts.map((receipt) => [
6885
+ receipt.runId,
6886
+ `habit=${receipt.habitName}`,
6887
+ `trigger=${receipt.trigger}`,
6888
+ `outcome=${receipt.outcome}`,
6889
+ `endedAt=${receipt.endedAt}`,
6890
+ `receipt=${receipt.receiptLocator}`,
6891
+ ].join(" ")).join("\n");
6892
+ deps.writeStdout(message);
6893
+ (0, runtime_1.emitNervesEvent)({
6894
+ component: "daemon",
6895
+ event: "daemon.habit_runs_cli_read",
6896
+ message: "habit run receipts listed from CLI",
6897
+ meta: { agent: command.agent, limit: command.limit, count: receipts.length },
6898
+ });
6899
+ return message;
6900
+ }
6901
+ if (command.kind === "habit.inspect") {
6902
+ const receipt = readHabitRunReceipt(bundleRoot, command.runId);
6903
+ if (!receipt) {
6904
+ const message = `error: habit run '${command.runId}' not found`;
6905
+ deps.writeStdout(message);
6906
+ deps.setExitCode?.(1);
6907
+ (0, runtime_1.emitNervesEvent)({
6908
+ level: "warn",
6909
+ component: "daemon",
6910
+ event: "daemon.habit_run_cli_read_missing",
6911
+ message: "habit run receipt not found from CLI",
6912
+ meta: { agent: command.agent, runId: command.runId },
6913
+ });
6914
+ return message;
6915
+ }
6916
+ const message = `${JSON.stringify(receipt, null, 2)}\n`;
6917
+ deps.writeStdout(message);
6918
+ (0, runtime_1.emitNervesEvent)({
6919
+ component: "daemon",
6920
+ event: "daemon.habit_run_cli_read",
6921
+ message: "habit run receipt read from CLI",
6922
+ meta: { agent: command.agent, runId: command.runId },
6923
+ });
6924
+ return message;
6925
+ }
6877
6926
  // habit.create
6878
6927
  const filePath = path.join(habitsDir, `${command.name}.md`);
6879
6928
  if (fs.existsSync(filePath)) {
@@ -127,7 +127,7 @@ exports.COMMAND_REGISTRY = {
127
127
  poke: {
128
128
  category: "Tasks",
129
129
  description: "Poke an agent about a task or habit",
130
- usage: "ouro poke <agent> --task <task-id> | --habit <name>",
130
+ usage: "ouro poke <agent> --task <task-id> | --habit <name> [--trigger poke|launchd|cron|overdue|manual]",
131
131
  example: "ouro poke ouroboros --task abc123",
132
132
  },
133
133
  habit: {
@@ -17,6 +17,7 @@ exports.parseOuroCommand = parseOuroCommand;
17
17
  const types_1 = require("../../mind/friends/types");
18
18
  const cli_help_1 = require("./cli-help");
19
19
  const cli_desk_1 = require("./cli-desk");
20
+ const flight_recorder_1 = require("../../arc/flight-recorder");
20
21
  const vault_items_1 = require("./vault-items");
21
22
  // ── Shared helpers ──
22
23
  function extractAgentFlag(args) {
@@ -104,9 +105,11 @@ function usage() {
104
105
  " ouro chat <agent>",
105
106
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
106
107
  " ouro poke <agent> --task <task-id>",
107
- " ouro poke <agent> --habit <name>",
108
+ " ouro poke <agent> --habit <name> [--trigger poke|launchd|cron|overdue|manual]",
108
109
  " ouro habit list [--agent <name>]",
109
110
  " ouro habit create [--agent <name>] <name> [--cadence <interval>]",
111
+ " ouro habit runs [--agent <name>] [--limit <n>]",
112
+ " ouro habit inspect [--agent <name>] <runId>",
110
113
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
111
114
  " ouro bluebubbles replay [--agent <name>] --message-guid <guid> [--event-type new-message|updated-message] [--json]",
112
115
  " ouro friend list [--agent <name>]",
@@ -175,6 +178,7 @@ function parsePokeCommand(args) {
175
178
  let taskId;
176
179
  let habitName;
177
180
  let awaitName;
181
+ let trigger;
178
182
  for (let i = 1; i < args.length; i += 1) {
179
183
  if (args[i] === "--task") {
180
184
  taskId = args[i + 1];
@@ -188,12 +192,21 @@ function parsePokeCommand(args) {
188
192
  awaitName = args[i + 1];
189
193
  i += 1;
190
194
  }
195
+ if (args[i] === "--trigger") {
196
+ const rawTrigger = args[i + 1];
197
+ if (!(0, flight_recorder_1.isHabitRunTrigger)(rawTrigger))
198
+ throw new Error("invalid habit trigger");
199
+ trigger = rawTrigger;
200
+ i += 1;
201
+ }
191
202
  }
192
203
  // Priority order: --await > --habit > --task
193
204
  if (awaitName)
194
205
  return { kind: "await.poke", agent, awaitName };
195
206
  if (habitName)
196
- return { kind: "habit.poke", agent, habitName };
207
+ return { kind: "habit.poke", agent, habitName, trigger: trigger ?? "poke" };
208
+ if (trigger)
209
+ throw new Error(`Usage\n${usage()}`);
197
210
  if (!taskId)
198
211
  throw new Error(`Usage\n${usage()}`);
199
212
  return { kind: "task.poke", agent, taskId };
@@ -227,6 +240,27 @@ function parseHabitCommand(args) {
227
240
  throw new Error(`Usage\n${usage()}`);
228
241
  return { kind: "habit.create", name, ...(agent ? { agent } : {}), ...(cadence ? { cadence } : {}) };
229
242
  }
243
+ if (sub === "runs") {
244
+ let limit = 20;
245
+ const options = rest.slice(1);
246
+ for (let i = 0; i < options.length; i += 1) {
247
+ if (options[i] !== "--limit" || !options[i + 1])
248
+ throw new Error(`Usage\n${usage()}`);
249
+ const parsedLimit = Number.parseInt(options[i + 1], 10);
250
+ if (!Number.isInteger(parsedLimit) || String(parsedLimit) !== options[i + 1] || parsedLimit < 1 || parsedLimit > 100) {
251
+ throw new Error("--limit must be an integer between 1 and 100");
252
+ }
253
+ limit = parsedLimit;
254
+ i += 1;
255
+ }
256
+ return { kind: "habit.runs", ...(agent ? { agent } : {}), limit };
257
+ }
258
+ if (sub === "inspect") {
259
+ const positional = rest.slice(1);
260
+ if (positional.length !== 1 || !positional[0])
261
+ throw new Error(`Usage\n${usage()}`);
262
+ return { kind: "habit.inspect", ...(agent ? { agent } : {}), runId: positional[0] };
263
+ }
230
264
  throw new Error(`Usage\n${usage()}`);
231
265
  }
232
266
  function parseLinkCommand(args, kind = "friend.link") {
@@ -149,9 +149,30 @@ const processManager = new process_manager_1.DaemonProcessManager({
149
149
  },
150
150
  /* v8 ignore stop */
151
151
  });
152
- const scheduler = new task_scheduler_1.TaskDrivenScheduler({
152
+ const taskScheduler = new task_scheduler_1.TaskDrivenScheduler({
153
153
  agents: [...managedAgents],
154
154
  });
155
+ const habitSchedulers = [];
156
+ const awaitSchedulers = [];
157
+ const scheduler = {
158
+ listJobs: () => [
159
+ ...taskScheduler.listJobs(),
160
+ ...habitSchedulers.flatMap((habitScheduler) => habitScheduler.listJobs()),
161
+ ],
162
+ triggerJob: (jobId) => taskScheduler.triggerJob(jobId),
163
+ triggerHabitJob: async (jobId) => {
164
+ for (const habitScheduler of habitSchedulers) {
165
+ const result = await habitScheduler.triggerJob(jobId, "cron");
166
+ if (result.ok)
167
+ return result;
168
+ }
169
+ return { ok: false, message: `unknown habit job: ${jobId}` };
170
+ },
171
+ start: () => taskScheduler.start(),
172
+ stop: () => taskScheduler.stop(),
173
+ reconcile: () => taskScheduler.reconcile(),
174
+ recordTaskRun: (agent, taskId) => taskScheduler.recordTaskRun(agent, taskId),
175
+ };
155
176
  const router = new message_router_1.FileMessageRouter();
156
177
  const senseManager = new sense_manager_1.DaemonSenseManager({
157
178
  agents: [...managedAgents],
@@ -201,8 +222,6 @@ const healthMonitor = new health_monitor_1.HealthMonitor({
201
222
  catch { /* recovery is best-effort */ }
202
223
  },
203
224
  });
204
- const habitSchedulers = [];
205
- const awaitSchedulers = [];
206
225
  let entryRuntimeStopping = false;
207
226
  let stopCommandExitScheduled = false;
208
227
  function stopEntryRuntime() {
@@ -393,8 +412,8 @@ void daemon.start().then(async () => {
393
412
  agent,
394
413
  habitsDir,
395
414
  osCronManager,
396
- onHabitFire: (habitName) => {
397
- processManager.sendToAgent(agent, { type: "habit", habitName, trigger: "overdue" });
415
+ onHabitFire: (habitName, trigger) => {
416
+ processManager.sendToAgent(agent, { type: "habit", habitName, trigger });
398
417
  },
399
418
  deps: {
400
419
  readdir: (dir) => fs.readdirSync(dir),
@@ -67,6 +67,7 @@ const mailbox_read_1 = require("../mailbox/mailbox-read");
67
67
  const mailbox_view_1 = require("../mailbox/mailbox-view");
68
68
  const provider_visibility_1 = require("../provider-visibility");
69
69
  const socket_client_1 = require("./socket-client");
70
+ const flight_recorder_1 = require("../../arc/flight-recorder");
70
71
  const PIDFILE_PATH = path.join(os.homedir(), ".ouro-cli", "daemon.pids");
71
72
  /**
72
73
  * Defense-in-depth: detect if we're running under vitest. The pidfile lives
@@ -1236,6 +1237,10 @@ class OuroDaemon {
1236
1237
  return { ok: true, summary, data: jobs };
1237
1238
  }
1238
1239
  case "cron.trigger": {
1240
+ const habitResult = await this.scheduler.triggerHabitJob?.(command.jobId);
1241
+ if (habitResult?.ok) {
1242
+ return { ok: true, message: habitResult.message };
1243
+ }
1239
1244
  const result = await this.scheduler.triggerJob(command.jobId);
1240
1245
  return { ok: result.ok, message: result.message };
1241
1246
  }
@@ -1302,7 +1307,11 @@ class OuroDaemon {
1302
1307
  };
1303
1308
  }
1304
1309
  case "habit.poke": {
1305
- this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName, trigger: "poke" });
1310
+ const trigger = command.trigger ?? "poke";
1311
+ if (!(0, flight_recorder_1.isHabitRunTrigger)(trigger)) {
1312
+ return { ok: false, error: `invalid habit trigger: ${String(trigger)}` };
1313
+ }
1314
+ this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName, trigger });
1306
1315
  return {
1307
1316
  ok: true,
1308
1317
  message: `poked habit ${command.habitName} for ${command.agent}`,
@@ -108,7 +108,7 @@ class HabitScheduler {
108
108
  message: "firing overdue habit (never run)",
109
109
  meta: { habitName: habit.name, agent: this.agent },
110
110
  });
111
- this.onHabitFire(habit.name);
111
+ this.onHabitFire(habit.name, "overdue");
112
112
  continue;
113
113
  }
114
114
  const lastRunMs = new Date(habit.lastRun).getTime();
@@ -120,7 +120,7 @@ class HabitScheduler {
120
120
  message: "firing overdue habit",
121
121
  meta: { habitName: habit.name, agent: this.agent, elapsedMs: elapsed },
122
122
  });
123
- this.onHabitFire(habit.name);
123
+ this.onHabitFire(habit.name, "overdue");
124
124
  }
125
125
  }
126
126
  }
@@ -174,6 +174,25 @@ class HabitScheduler {
174
174
  return null;
175
175
  }
176
176
  }
177
+ listJobs() {
178
+ return this.buildJobs(this.scanHabits())
179
+ .map((job) => ({ id: job.id, schedule: job.schedule, lastRun: job.lastRun }))
180
+ .sort((a, b) => a.id.localeCompare(b.id));
181
+ }
182
+ async triggerJob(jobId, trigger = "cron") {
183
+ const job = this.buildJobs(this.scanHabits()).find((candidate) => candidate.id === jobId);
184
+ if (!job) {
185
+ return { ok: false, message: `unknown habit job: ${jobId}` };
186
+ }
187
+ this.onHabitFire(job.taskId, trigger);
188
+ (0, runtime_1.emitNervesEvent)({
189
+ component: "daemon",
190
+ event: "daemon.habit_job_triggered",
191
+ message: "habit scheduler job triggered",
192
+ meta: { agent: job.agent, habitName: job.taskId, jobId, trigger },
193
+ });
194
+ return { ok: true, message: `triggered habit ${jobId}` };
195
+ }
177
196
  watchForChanges() {
178
197
  const watchFn = this.deps.watch;
179
198
  if (!watchFn)
@@ -278,7 +297,7 @@ class HabitScheduler {
278
297
  const output = this.execForVerify("crontab -l");
279
298
  const lines = output.split("\n");
280
299
  for (const line of lines) {
281
- const match = line.match(/ouro poke \S+ --habit (\S+)/);
300
+ const match = line.match(/ouro poke \S+ --habit (\S+)(?:\s+--trigger\s+\S+)?/);
282
301
  if (match) {
283
302
  verified.add(match[1]);
284
303
  }
@@ -293,7 +312,7 @@ class HabitScheduler {
293
312
  createTimerFallback(habitName, cadenceMs) {
294
313
  const schedule = () => {
295
314
  const timer = setTimeout(() => {
296
- this.onHabitFire(habitName);
315
+ this.onHabitFire(habitName, "overdue");
297
316
  schedule();
298
317
  }, cadenceMs);
299
318
  this.timerFallbacks.set(habitName, timer);
@@ -362,7 +381,7 @@ class HabitScheduler {
362
381
  taskId: habit.name,
363
382
  schedule: cronSchedule,
364
383
  lastRun: habit.lastRun,
365
- command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name}`,
384
+ command: `${this.deps.ouroPath} poke ${this.agent} --habit ${habit.name} --trigger launchd`,
366
385
  taskPath: path.join(this.habitsDir, `${habit.name}.md`),
367
386
  });
368
387
  }