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

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,19 @@
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.668",
6
+ "changes": [
7
+ "Harden flight-recorder reconciliation so terminal fulfilled-only repairs are not continuable work, unverifiable active obligation ids are preserved and degraded, parseable-invalid obligation records are excluded from verified recovery state, synthesized recovery actions get fresh provenance, reads remain side-effect-free, and legacy obligation records stay visible in Mailbox, Workbench, and Work Card summaries."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.667",
12
+ "changes": [
13
+ "Add stateful habit session summaries, summary tooling, mailbox APIs, nerves review, and habit history visibility.",
14
+ "Harden habit summary receipts as the canonical summary snapshot, validate agent-scoped Mailbox routes, await post-turn persistence, and preserve session-summary recovery through malformed locators and projected session edge cases."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.666",
6
19
  "changes": [
@@ -50,6 +50,7 @@ const path = __importStar(require("path"));
50
50
  const crypto_1 = require("crypto");
51
51
  const session_events_1 = require("../heart/session-events");
52
52
  const runtime_1 = require("../nerves/runtime");
53
+ const obligations_1 = require("./obligations");
53
54
  function isHabitRunTrigger(value) {
54
55
  return value === "cron"
55
56
  || value === "launchd"
@@ -201,7 +202,7 @@ function normalizeResumeInvariants(resume) {
201
202
  ];
202
203
  const missingChanged = resume.missing.join("\n") !== missing.join("\n");
203
204
  const issues = [
204
- ...(resume.canContinue && !resume.hasCompleteState ? ["canContinue true while hasCompleteState false"] : []),
205
+ ...(resume.canContinue && !hasCompleteState ? ["canContinue true while hasCompleteState false"] : []),
205
206
  ...(resume.canContinue && !hasCurrentAsk ? ["canContinue true without currentAsk"] : []),
206
207
  ...(resume.canContinue && !hasNextSafeAction ? ["canContinue true without nextSafeAction"] : []),
207
208
  ...(resume.canContinue && resume.blockedBecause.length > 0 ? ["canContinue true while blocked"] : []),
@@ -226,7 +227,117 @@ function normalizeResumeInvariants(resume) {
226
227
  : resume.recorderHealth,
227
228
  };
228
229
  }
229
- /* v8 ignore stop */
230
+ const FLIGHT_RECORDER_RECONCILE_SOURCE_ID = "reconcile:active-obligations";
231
+ function remainingArcWorkDescriptions(resume) {
232
+ return [
233
+ ...resume.activeReturnObligationIds.map((id) => `return obligation ${id}`),
234
+ ...resume.activePacketIds.map((id) => `packet ${id}`),
235
+ ...resume.openEvolutionCaseIds.map((id) => `evolution case ${id}`),
236
+ ];
237
+ }
238
+ function nextSafeActionAfterObligationReconcile(resume, activeObligations, staleActiveObligationIds, unverifiableActiveObligationIds) {
239
+ if (unverifiableActiveObligationIds.length > 0) {
240
+ return (0, session_events_1.capStructuredRecordString)(`inspect unverifiable active obligations before acting: ${unverifiableActiveObligationIds.join(", ")}`);
241
+ }
242
+ const firstActive = activeObligations[0];
243
+ if (firstActive) {
244
+ const detail = firstActive.nextAction?.trim() || firstActive.content;
245
+ return (0, session_events_1.capStructuredRecordString)(`continue open obligation ${firstActive.id}: ${detail}`);
246
+ }
247
+ const remainingWork = remainingArcWorkDescriptions(resume);
248
+ if (remainingWork.length > 0) {
249
+ return (0, session_events_1.capStructuredRecordString)(`continue remaining Arc work: ${remainingWork.slice(0, 5).join(", ")}`);
250
+ }
251
+ return (0, session_events_1.capStructuredRecordString)(`wait for new input; reconciled completed or missing obligations: ${staleActiveObligationIds.join(", ")}`);
252
+ }
253
+ function reconcileActiveObligations(agentRoot, resume) {
254
+ const obligations = (0, obligations_1.readVerifiedObligations)(agentRoot);
255
+ const activeObligations = (0, obligations_1.readVerifiedPendingObligations)(agentRoot);
256
+ const canonicalIdSet = new Set(obligations.map((obligation) => obligation.id));
257
+ const openIdSet = new Set(activeObligations.map((obligation) => obligation.id));
258
+ const resumeIdSet = new Set(resume.activeObligationIds);
259
+ const staleActiveObligationIds = resume.activeObligationIds.filter((id) => canonicalIdSet.has(id) && !openIdSet.has(id));
260
+ const unverifiableActiveObligationIds = resume.activeObligationIds.filter((id) => !canonicalIdSet.has(id));
261
+ const missingActiveObligationIds = activeObligations
262
+ .map((obligation) => obligation.id)
263
+ .filter((id) => !resumeIdSet.has(id));
264
+ if (staleActiveObligationIds.length === 0
265
+ && missingActiveObligationIds.length === 0
266
+ && unverifiableActiveObligationIds.length === 0) {
267
+ return { resume, staleActiveObligationIds, missingActiveObligationIds, unverifiableActiveObligationIds };
268
+ }
269
+ const activeObligationIds = uniqueStrings([
270
+ ...resume.activeObligationIds.filter((id) => !staleActiveObligationIds.includes(id)),
271
+ ...missingActiveObligationIds,
272
+ ]);
273
+ const hasActiveArcContinuation = activeObligationIds.length > 0
274
+ || resume.activeReturnObligationIds.length > 0
275
+ || resume.activePacketIds.length > 0
276
+ || resume.openEvolutionCaseIds.length > 0;
277
+ const isTerminalWait = staleActiveObligationIds.length > 0 && !hasActiveArcContinuation;
278
+ const mustSynthesizeAction = staleActiveObligationIds.length > 0
279
+ || unverifiableActiveObligationIds.length > 0
280
+ || !nonEmpty(resume.nextSafeAction.value);
281
+ const nextSafeActionValue = mustSynthesizeAction
282
+ ? nextSafeActionAfterObligationReconcile(resume, activeObligations, staleActiveObligationIds, unverifiableActiveObligationIds)
283
+ : resume.nextSafeAction.value;
284
+ const recorderHealth = unverifiableActiveObligationIds.length > 0
285
+ ? {
286
+ status: resume.recorderHealth.status === "unavailable" ? "unavailable" : "degraded",
287
+ issues: uniqueStrings([
288
+ ...resume.recorderHealth.issues,
289
+ `active obligation ids could not be verified in arc/obligations: ${unverifiableActiveObligationIds.join(", ")}`,
290
+ ]),
291
+ }
292
+ : resume.recorderHealth;
293
+ const canContinue = nonEmpty(resume.currentAsk.value)
294
+ && nonEmpty(nextSafeActionValue)
295
+ && resume.blockedBecause.length === 0
296
+ && recorderHealth.status === "ok"
297
+ && hasActiveArcContinuation
298
+ && !isTerminalWait;
299
+ return {
300
+ resume: normalizeResumeInvariants({
301
+ ...resume,
302
+ canContinue,
303
+ recorderHealth,
304
+ activeObligationIds,
305
+ nextSafeAction: {
306
+ ...resume.nextSafeAction,
307
+ value: nextSafeActionValue,
308
+ sourceEventIds: mustSynthesizeAction
309
+ ? [FLIGHT_RECORDER_RECONCILE_SOURCE_ID]
310
+ : resume.nextSafeAction.sourceEventIds,
311
+ stopBefore: unverifiableActiveObligationIds.length > 0
312
+ ? uniqueStrings([...resume.nextSafeAction.stopBefore, "acting on unverifiable obligation state"])
313
+ : resume.nextSafeAction.stopBefore,
314
+ },
315
+ }),
316
+ staleActiveObligationIds,
317
+ missingActiveObligationIds,
318
+ unverifiableActiveObligationIds,
319
+ };
320
+ }
321
+ function normalizeResumeForAgentRoot(agentRoot, resume) {
322
+ return reconcileActiveObligations(agentRoot, normalizeResumeInvariants(resume));
323
+ }
324
+ function emitFlightRecorderReconciled(agentRoot, staleActiveObligationIds, missingActiveObligationIds, unverifiableActiveObligationIds) {
325
+ if (staleActiveObligationIds.length === 0
326
+ && missingActiveObligationIds.length === 0
327
+ && unverifiableActiveObligationIds.length === 0)
328
+ return;
329
+ (0, runtime_1.emitNervesEvent)({
330
+ component: "mind",
331
+ event: "mind.flight_recorder_resume_reconciled",
332
+ message: "flight recorder resume reconciled with canonical Arc state",
333
+ meta: {
334
+ agentRoot,
335
+ staleActiveObligationIds,
336
+ missingActiveObligationIds,
337
+ unverifiableActiveObligationIds,
338
+ },
339
+ });
340
+ }
230
341
  function latestFromEvent(event, previous) {
231
342
  const currentAskValue = event.currentAsk !== undefined ? event.currentAsk : previous.currentAsk.value;
232
343
  const nextSafeActionValue = event.nextSafeAction !== undefined ? event.nextSafeAction : previous.nextSafeAction.value;
@@ -279,7 +390,12 @@ function readFlightRecorderResume(agentRoot) {
279
390
  if (!isFlightRecorderResume(parsed)) {
280
391
  throw new Error("latest.json has invalid flight-recorder resume shape");
281
392
  }
282
- const resume = normalizeResumeInvariants(parsed);
393
+ const { resume, staleActiveObligationIds, missingActiveObligationIds, unverifiableActiveObligationIds, } = normalizeResumeForAgentRoot(agentRoot, parsed);
394
+ if (staleActiveObligationIds.length > 0
395
+ || missingActiveObligationIds.length > 0
396
+ || unverifiableActiveObligationIds.length > 0) {
397
+ emitFlightRecorderReconciled(agentRoot, staleActiveObligationIds, missingActiveObligationIds, unverifiableActiveObligationIds);
398
+ }
283
399
  (0, runtime_1.emitNervesEvent)({
284
400
  component: "mind",
285
401
  event: "mind.flight_recorder_resume_read",
@@ -302,8 +418,9 @@ function readFlightRecorderResume(agentRoot) {
302
418
  }
303
419
  }
304
420
  function writeFlightRecorderResume(agentRoot, resume) {
305
- const safeResume = normalizeResumeInvariants(resume);
421
+ const { resume: safeResume, staleActiveObligationIds, missingActiveObligationIds, unverifiableActiveObligationIds, } = normalizeResumeForAgentRoot(agentRoot, resume);
306
422
  atomicWriteJson(flightRecorderLatestPath(agentRoot), safeResume);
423
+ emitFlightRecorderReconciled(agentRoot, staleActiveObligationIds, missingActiveObligationIds, unverifiableActiveObligationIds);
307
424
  (0, runtime_1.emitNervesEvent)({
308
425
  component: "mind",
309
426
  event: "mind.flight_recorder_resume_written",
@@ -397,6 +514,52 @@ function isHabitToolPolicy(value) {
397
514
  && isStringArray(value.deniedTools)
398
515
  && typeof value.outwardMessagingAllowed === "boolean";
399
516
  }
517
+ function defaultHabitRunSummarySnapshot(receipt) {
518
+ if (receipt.errors.length > 0) {
519
+ return {
520
+ summary: `Habit ${receipt.habitName} finished with errors: ${receipt.errors.join("; ")}`,
521
+ decisions: [],
522
+ nextLikelyStep: null,
523
+ };
524
+ }
525
+ const surface = receipt.surfaceAttempts.find((attempt) => attempt.result !== "blocked" && attempt.result !== "failed" && attempt.result !== "unavailable");
526
+ if (surface) {
527
+ return {
528
+ summary: `Habit ${receipt.habitName} surfaced via ${surface.recipient}/${surface.channel}.`,
529
+ decisions: [],
530
+ nextLikelyStep: null,
531
+ };
532
+ }
533
+ const produced = receipt.producedRefs.find((ref) => ref.kind !== "none");
534
+ if (produced) {
535
+ return {
536
+ summary: `Habit ${receipt.habitName} produced ${produced.kind}: ${produced.locator}.`,
537
+ decisions: [],
538
+ nextLikelyStep: null,
539
+ };
540
+ }
541
+ return {
542
+ summary: `Habit ${receipt.habitName} finished with ${receipt.outcome}.`,
543
+ decisions: [],
544
+ nextLikelyStep: null,
545
+ };
546
+ }
547
+ function normalizeHabitRunSummarySnapshot(value, fallback) {
548
+ const snapshot = isPlainRecord(value) ? value : {};
549
+ const summary = typeof snapshot.summary === "string" && snapshot.summary.trim().length > 0
550
+ ? snapshot.summary
551
+ : fallback.summary;
552
+ const nextLikelyStep = snapshot.nextLikelyStep === null
553
+ ? null
554
+ : typeof snapshot.nextLikelyStep === "string" && snapshot.nextLikelyStep.trim().length > 0
555
+ ? snapshot.nextLikelyStep
556
+ : fallback.nextLikelyStep;
557
+ return {
558
+ summary: (0, session_events_1.capStructuredRecordString)(summary),
559
+ decisions: cappedArray(isStringArray(snapshot.decisions) ? snapshot.decisions : fallback.decisions),
560
+ nextLikelyStep: nextLikelyStep === null ? null : (0, session_events_1.capStructuredRecordString)(nextLikelyStep),
561
+ };
562
+ }
400
563
  function isHabitRunReceipt(value) {
401
564
  if (!isPlainRecord(value))
402
565
  return false;
@@ -423,9 +586,11 @@ function isHabitRunReceipt(value) {
423
586
  && typeof value.pendingLocator === "string"
424
587
  && typeof value.runtimeStateLocator === "string"
425
588
  && typeof value.receiptLocator === "string"
589
+ && (value.operationId === undefined || value.operationId === null || typeof value.operationId === "string")
426
590
  && (value.nextRunAt === null || typeof value.nextRunAt === "string")
427
591
  && isHabitPermissionEnvelope(value.permissionEnvelope)
428
592
  && isHabitToolPolicy(value.toolPolicy)
593
+ && (value.summarySnapshot === undefined || isPlainRecord(value.summarySnapshot))
429
594
  && isProducedRefArray(value.producedRefs)
430
595
  && isHabitSurfaceAttemptArray(value.surfaceAttempts)
431
596
  && isStringArray(value.errors);
@@ -479,6 +644,7 @@ function normalizeLegacyHabitRunReceipt(receipt) {
479
644
  pendingLocator: `state/habit-sessions/${receipt.runId}/pending`,
480
645
  runtimeStateLocator: `state/habits/${receipt.habitName}.json`,
481
646
  receiptLocator: `arc/flight-recorder/habit-receipts/${receipt.runId}.json`,
647
+ operationId: null,
482
648
  nextRunAt: null,
483
649
  permissionEnvelope: {
484
650
  schemaVersion: 1,
@@ -493,12 +659,14 @@ function normalizeLegacyHabitRunReceipt(receipt) {
493
659
  deniedTools: sawSurface ? [] : ["send_message", "surface"],
494
660
  outwardMessagingAllowed: sawSurface,
495
661
  },
662
+ summarySnapshot: defaultHabitRunSummarySnapshot(receipt),
496
663
  producedRefs: receipt.producedRefs,
497
664
  surfaceAttempts: receipt.surfaceAttempts,
498
665
  errors: receipt.errors,
499
666
  };
500
667
  }
501
668
  function capHabitRunReceipt(receipt) {
669
+ const fallbackSnapshot = defaultHabitRunSummarySnapshot(receipt);
502
670
  return {
503
671
  ...receipt,
504
672
  habitName: (0, session_events_1.capStructuredRecordString)(receipt.habitName),
@@ -507,6 +675,7 @@ function capHabitRunReceipt(receipt) {
507
675
  pendingLocator: (0, session_events_1.capStructuredRecordString)(receipt.pendingLocator),
508
676
  runtimeStateLocator: (0, session_events_1.capStructuredRecordString)(receipt.runtimeStateLocator),
509
677
  receiptLocator: (0, session_events_1.capStructuredRecordString)(receipt.receiptLocator),
678
+ operationId: receipt.operationId ? (0, session_events_1.capStructuredRecordString)(receipt.operationId) : null,
510
679
  permissionEnvelope: {
511
680
  ...receipt.permissionEnvelope,
512
681
  returnRoutes: receipt.permissionEnvelope.returnRoutes.map((route) => ({
@@ -526,6 +695,7 @@ function capHabitRunReceipt(receipt) {
526
695
  deniedTools: cappedArray(receipt.toolPolicy.deniedTools),
527
696
  outwardMessagingAllowed: receipt.toolPolicy.outwardMessagingAllowed,
528
697
  },
698
+ summarySnapshot: normalizeHabitRunSummarySnapshot(receipt.summarySnapshot, fallbackSnapshot),
529
699
  producedRefs: receipt.producedRefs.map((ref) => ({ ...ref, locator: (0, session_events_1.capStructuredRecordString)(ref.locator) })),
530
700
  surfaceAttempts: receipt.surfaceAttempts.map((attempt) => ({
531
701
  ...attempt,
@@ -562,7 +732,7 @@ function readHabitRunReceipt(agentRoot, runId) {
562
732
  message: "flight recorder habit receipt read",
563
733
  meta: { agentRoot, runId },
564
734
  });
565
- return receipt;
735
+ return capHabitRunReceipt(receipt);
566
736
  }
567
737
  catch (error) {
568
738
  warnMalformedHabitReceipt(agentRoot, runId, error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error));
@@ -593,7 +763,10 @@ function writeHabitRunReceipt(agentRoot, receipt) {
593
763
  recordedAt: safeReceipt.endedAt,
594
764
  summary: `habit ${safeReceipt.habitName} finished with ${safeReceipt.outcome}`,
595
765
  producedRefs: safeReceipt.producedRefs,
596
- meta: { receiptPath: path.join("arc", "flight-recorder", "habit-receipts", `${safeReceipt.runId}.json`) },
766
+ meta: {
767
+ receiptPath: path.join("arc", "flight-recorder", "habit-receipts", `${safeReceipt.runId}.json`),
768
+ operationId: safeReceipt.operationId ?? null,
769
+ },
597
770
  });
598
771
  (0, runtime_1.emitNervesEvent)({
599
772
  component: "mind",
@@ -38,6 +38,8 @@ exports.isOpenObligation = isOpenObligation;
38
38
  exports.createObligation = createObligation;
39
39
  exports.readObligations = readObligations;
40
40
  exports.readPendingObligations = readPendingObligations;
41
+ exports.readVerifiedObligations = readVerifiedObligations;
42
+ exports.readVerifiedPendingObligations = readVerifiedPendingObligations;
41
43
  exports.advanceObligation = advanceObligation;
42
44
  exports.fulfillObligation = fulfillObligation;
43
45
  exports.findPendingObligationForOrigin = findPendingObligationForOrigin;
@@ -64,6 +66,31 @@ function isOpenObligationStatus(status) {
64
66
  function isOpenObligation(obligation) {
65
67
  return isOpenObligationStatus(obligation.status);
66
68
  }
69
+ function isReadableObligation(value) {
70
+ if (!value || typeof value !== "object" || Array.isArray(value))
71
+ return false;
72
+ const obligation = value;
73
+ return typeof obligation.id === "string"
74
+ && typeof obligation.content === "string";
75
+ }
76
+ function isVerifiedObligationStatus(value) {
77
+ return value === "pending"
78
+ || value === "investigating"
79
+ || value === "waiting_for_merge"
80
+ || value === "updating_runtime"
81
+ || value === "fulfilled";
82
+ }
83
+ function isVerifiedObligation(value) {
84
+ if (!isReadableObligation(value))
85
+ return false;
86
+ const obligation = value;
87
+ return isVerifiedObligationStatus(obligation.status)
88
+ && typeof obligation.createdAt === "string"
89
+ && !!obligation.origin
90
+ && typeof obligation.origin.friendId === "string"
91
+ && typeof obligation.origin.channel === "string"
92
+ && typeof obligation.origin.key === "string";
93
+ }
67
94
  function createObligation(agentRoot, input) {
68
95
  const now = new Date().toISOString();
69
96
  const id = (0, json_store_1.generateTimestampId)();
@@ -92,11 +119,18 @@ function createObligation(agentRoot, input) {
92
119
  }
93
120
  function readObligations(agentRoot) {
94
121
  const all = (0, json_store_1.readJsonDir)(obligationsDir(agentRoot));
95
- return all.filter((parsed) => typeof parsed.id === "string" && typeof parsed.content === "string");
122
+ return all.filter(isReadableObligation);
96
123
  }
97
124
  function readPendingObligations(agentRoot) {
98
125
  return readObligations(agentRoot).filter(isOpenObligation);
99
126
  }
127
+ function readVerifiedObligations(agentRoot) {
128
+ const all = (0, json_store_1.readJsonDir)(obligationsDir(agentRoot));
129
+ return all.filter(isVerifiedObligation);
130
+ }
131
+ function readVerifiedPendingObligations(agentRoot) {
132
+ return readVerifiedObligations(agentRoot).filter(isOpenObligation);
133
+ }
100
134
  function advanceObligation(agentRoot, obligationId, update) {
101
135
  const dir = obligationsDir(agentRoot);
102
136
  const obligation = (0, json_store_1.readJsonFile)(dir, obligationId);
@@ -96,6 +96,7 @@ const cli_help_1 = require("./cli-help");
96
96
  const plugin_cli_1 = require("./plugin-cli");
97
97
  const cli_desk_1 = require("./cli-desk");
98
98
  const migrate_to_desk_1 = require("./migrate-to-desk");
99
+ const core_2 = require("../../nerves/review/core");
99
100
  const cli_render_1 = require("./cli-render");
100
101
  const cli_defaults_1 = require("./cli-defaults");
101
102
  const agent_config_check_1 = require("./agent-config-check");
@@ -150,6 +151,58 @@ function returnCliFailure(deps, message, exitCode = 1) {
150
151
  deps.writeStdout(message);
151
152
  return message;
152
153
  }
154
+ function renderHabitSummaryCli(summary) {
155
+ return [
156
+ `${summary.runId} habit=${summary.habitName} outcome=${summary.status} completedAt=${summary.completedAt}`,
157
+ summary.operationId ? `operation=${summary.operationId}` : null,
158
+ `summary=${summary.summary}`,
159
+ summary.nextLikelyStep ? `next=${summary.nextLikelyStep}` : null,
160
+ summary.decisions.length > 0 ? `decisions=${summary.decisions.join("; ")}` : null,
161
+ `pending=${summary.pending.count}${summary.pending.files.length > 0 ? ` (${summary.pending.files.join(", ")})` : ""}`,
162
+ `messages=${summary.messagesSent.length}`,
163
+ `tools=${summary.toolsUsed.length > 0 ? summary.toolsUsed.join(",") : "none"}`,
164
+ summary.producedRefs.length > 0 ? `refs=${summary.producedRefs.map((ref) => `${ref.kind}:${ref.locator}`).join(",")}` : null,
165
+ summary.errors.length > 0 ? `errors=${summary.errors.join("; ")}` : null,
166
+ summary.warnings.length > 0 ? `warnings=${summary.warnings.join("; ")}` : null,
167
+ `receipt=${summary.sources.receipt}`,
168
+ `session=${summary.sources.session}`,
169
+ `runtime=${summary.sources.runtimeState}`,
170
+ ].filter((line) => Boolean(line)).join("\n");
171
+ }
172
+ function executeNervesReviewCommand(command, deps) {
173
+ let sinceMs;
174
+ if (command.since) {
175
+ const parsed = (0, core_2.parseDuration)(command.since);
176
+ if (parsed === null) {
177
+ const message = `nerves-review: --since '${command.since}' is not a valid duration (e.g. 5m, 2h, 1d)`;
178
+ deps.setExitCode?.(2);
179
+ deps.writeStdout(message);
180
+ return message;
181
+ }
182
+ sinceMs = parsed;
183
+ }
184
+ const logsDir = (0, identity_1.getAgentDaemonLogsDir)(command.agent);
185
+ const filePath = path.join(logsDir, `${command.process}.ndjson`);
186
+ const entries = (0, core_2.reviewNerveEvents)(filePath, {
187
+ componentSubstring: command.component,
188
+ eventSubstring: command.event,
189
+ level: command.level,
190
+ sinceMs,
191
+ limit: command.limit,
192
+ nowMs: Date.now(),
193
+ });
194
+ const message = entries.length === 0
195
+ ? `(no matching nerves events in ${filePath})`
196
+ : entries.map((entry) => command.json ? entry.raw : (0, core_2.formatNerveEntry)(entry)).join("\n");
197
+ deps.writeStdout(message);
198
+ (0, runtime_1.emitNervesEvent)({
199
+ component: "daemon",
200
+ event: "daemon.nerves_review_cli_read",
201
+ message: "nerves review CLI read local log events",
202
+ meta: { agent: command.agent, process: command.process, count: entries.length, json: command.json },
203
+ });
204
+ return message;
205
+ }
153
206
  function summarizeDaemonStartupFailure(result) {
154
207
  if (result.startupFailureReason && result.startupFailureReason.trim().length > 0) {
155
208
  return result.startupFailureReason;
@@ -366,6 +419,8 @@ function agentResolutionFailureMode(command) {
366
419
  case "habit.create":
367
420
  case "habit.runs":
368
421
  case "habit.inspect":
422
+ case "habit.summary":
423
+ case "nerves-review":
369
424
  case "thoughts":
370
425
  case "attention.list":
371
426
  case "attention.show":
@@ -5684,6 +5739,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5684
5739
  if (command.kind === "hook") {
5685
5740
  await refreshHookSentinel(command, deps);
5686
5741
  }
5742
+ if (command.kind === "nerves-review") {
5743
+ return executeNervesReviewCommand(command, deps);
5744
+ }
5687
5745
  if (args.length === 0) {
5688
5746
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : (0, cli_defaults_1.defaultListDiscoveredAgents)());
5689
5747
  /* v8 ignore start -- the interactive home shell is exercised extensively in daemon-cli tests; V8 miscounts this orchestrator because it chains through recursive command handoffs and early chat health exits @preserve */
@@ -6843,10 +6901,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6843
6901
  return result.summary;
6844
6902
  }
6845
6903
  // ── habit subcommands (local, no daemon socket needed) ──
6846
- if (command.kind === "habit.list" || command.kind === "habit.create" || command.kind === "habit.runs" || command.kind === "habit.inspect") {
6904
+ if (command.kind === "habit.list" || command.kind === "habit.create" || command.kind === "habit.runs" || command.kind === "habit.inspect" || command.kind === "habit.summary") {
6847
6905
  const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("../habits/habit-parser")));
6848
6906
  const { applyHabitRuntimeState } = await Promise.resolve().then(() => __importStar(require("../habits/habit-runtime-state")));
6849
6907
  const { listHabitRunReceipts, readHabitRunReceipt } = await Promise.resolve().then(() => __importStar(require("../../arc/flight-recorder")));
6908
+ const { readHabitSessionSummary } = await Promise.resolve().then(() => __importStar(require("../habits/habit-session-summary")));
6850
6909
  /* v8 ignore start -- production default: uses real bundle root @preserve */
6851
6910
  const bundleRoot = deps.agentBundleRoot ?? path.join(deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
6852
6911
  /* v8 ignore stop */
@@ -6923,6 +6982,36 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6923
6982
  });
6924
6983
  return message;
6925
6984
  }
6985
+ if (command.kind === "habit.summary") {
6986
+ const summary = readHabitSessionSummary(bundleRoot, {
6987
+ ...(command.runId ? { runId: command.runId } : {}),
6988
+ ...(command.habitName ? { habitName: command.habitName } : {}),
6989
+ ...(command.operationId ? { operationId: command.operationId } : {}),
6990
+ ...(command.which ? { which: command.which } : {}),
6991
+ });
6992
+ if (!summary) {
6993
+ const message = "error: habit summary not found";
6994
+ deps.writeStdout(message);
6995
+ deps.setExitCode?.(1);
6996
+ (0, runtime_1.emitNervesEvent)({
6997
+ level: "warn",
6998
+ component: "daemon",
6999
+ event: "daemon.habit_summary_cli_read_missing",
7000
+ message: "habit run summary not found from CLI",
7001
+ meta: { agent: command.agent, runId: command.runId, habitName: command.habitName, operationId: command.operationId, which: command.which },
7002
+ });
7003
+ return message;
7004
+ }
7005
+ const message = command.json ? `${JSON.stringify(summary, null, 2)}\n` : renderHabitSummaryCli(summary);
7006
+ deps.writeStdout(message);
7007
+ (0, runtime_1.emitNervesEvent)({
7008
+ component: "daemon",
7009
+ event: "daemon.habit_summary_cli_read",
7010
+ message: "habit run summary read from CLI",
7011
+ meta: { agent: command.agent, runId: summary.runId, habitName: summary.habitName, json: command.json },
7012
+ });
7013
+ return message;
7014
+ }
6926
7015
  // habit.create
6927
7016
  const filePath = path.join(habitsDir, `${command.name}.md`);
6928
7017
  if (fs.existsSync(filePath)) {
@@ -135,7 +135,7 @@ exports.COMMAND_REGISTRY = {
135
135
  description: "Manage agent habits",
136
136
  usage: "ouro habit <subcommand> [--agent <name>]",
137
137
  example: "ouro habit list",
138
- subcommands: ["list", "create", "poke"],
138
+ subcommands: ["list", "create", "runs", "inspect", "summary", "poke"],
139
139
  },
140
140
  desk: {
141
141
  category: "Tasks",
@@ -158,6 +158,12 @@ exports.COMMAND_REGISTRY = {
158
158
  example: "ouro work sentinel --agent slugger --format json",
159
159
  subcommands: ["card", "gauntlet", "sentinel"],
160
160
  },
161
+ "nerves-review": {
162
+ category: "Internal",
163
+ description: "Read-only review of recent nerves events from an agent log stream",
164
+ usage: "ouro nerves-review [--agent <name>] [--process <name>] [--component <substr>] [--event <substr>] [--level <level>] [--since <duration>] [--limit <n>] [--json]",
165
+ example: "ouro nerves-review --agent slugger --component daemon --event habit --since 30m --json",
166
+ },
161
167
  "work card": {
162
168
  category: "Tasks",
163
169
  description: "Show the agent's durable Work Card compiled from arc records.",
@@ -395,6 +401,11 @@ const SUBCOMMAND_HELP = {
395
401
  usage: "ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
396
402
  example: "ouro account ensure --agent <agent> --owner-email you@example.com --source hey",
397
403
  },
404
+ "habit summary": {
405
+ description: "Read a habit run summary from receipts and session artifacts without contacting the daemon",
406
+ usage: "ouro habit summary [--agent <name>] (--run-id <id>|--habit <name>|--operation-id <id>) [--which latest|previous|latest-success|latest-failure] [--json]",
407
+ example: "ouro habit summary --agent slugger --operation-id habit:standup --which latest --json",
408
+ },
398
409
  "mail import-mbox": {
399
410
  description: "Import a HEY or other MBOX export into an existing delegated Mailroom source grant",
400
411
  usage: "ouro mail import-mbox (--file <path>|--discover) [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
@@ -110,6 +110,7 @@ function usage() {
110
110
  " ouro habit create [--agent <name>] <name> [--cadence <interval>]",
111
111
  " ouro habit runs [--agent <name>] [--limit <n>]",
112
112
  " ouro habit inspect [--agent <name>] <runId>",
113
+ " ouro habit summary [--agent <name>] (--run-id <id>|--habit <name>|--operation-id <id>) [--which latest|previous|latest-success|latest-failure] [--json]",
113
114
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
114
115
  " ouro bluebubbles replay [--agent <name>] --message-guid <guid> [--event-type new-message|updated-message] [--json]",
115
116
  " ouro friend list [--agent <name>]",
@@ -118,6 +119,7 @@ function usage() {
118
119
  " ouro friend update <id> --trust <level> [--agent <name>]",
119
120
  " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
120
121
  " ouro work card|gauntlet|sentinel [refresh] [--agent <name>] [--format text|json|--json]",
122
+ " ouro nerves-review [--agent <name>] [--process <name>] [--component <substr>] [--event <substr>] [--level <level>] [--since <duration>] [--limit <n>] [--json]",
121
123
  " ouro inner [--agent <name>]",
122
124
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
123
125
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
@@ -261,6 +263,53 @@ function parseHabitCommand(args) {
261
263
  throw new Error(`Usage\n${usage()}`);
262
264
  return { kind: "habit.inspect", ...(agent ? { agent } : {}), runId: positional[0] };
263
265
  }
266
+ if (sub === "summary") {
267
+ let runId;
268
+ let habitName;
269
+ let operationId;
270
+ let which;
271
+ let json = false;
272
+ const options = rest.slice(1);
273
+ for (let i = 0; i < options.length; i += 1) {
274
+ const option = options[i];
275
+ if (option === "--json") {
276
+ json = true;
277
+ continue;
278
+ }
279
+ if ((option === "--run-id" || option === "--habit" || option === "--operation-id" || option === "--which") && options[i + 1]) {
280
+ const value = options[++i];
281
+ if (option === "--run-id")
282
+ runId = value;
283
+ if (option === "--habit")
284
+ habitName = value;
285
+ if (option === "--operation-id")
286
+ operationId = value;
287
+ if (option === "--which") {
288
+ if (!["latest", "previous", "latest-success", "latest-failure"].includes(value)) {
289
+ throw new Error("--which must be latest, previous, latest-success, or latest-failure");
290
+ }
291
+ which = value;
292
+ }
293
+ continue;
294
+ }
295
+ throw new Error(`Usage\n${usage()}`);
296
+ }
297
+ if (runId !== undefined && (habitName !== undefined || operationId !== undefined || which !== undefined)) {
298
+ throw new Error("--run-id cannot be combined with --habit, --operation-id, or --which");
299
+ }
300
+ if (runId === undefined && habitName === undefined && operationId === undefined) {
301
+ throw new Error("provide --run-id, --habit, or --operation-id");
302
+ }
303
+ return {
304
+ kind: "habit.summary",
305
+ ...(agent ? { agent } : {}),
306
+ ...(runId ? { runId } : {}),
307
+ ...(habitName ? { habitName } : {}),
308
+ ...(operationId ? { operationId } : {}),
309
+ ...(which ? { which } : {}),
310
+ json,
311
+ };
312
+ }
264
313
  throw new Error(`Usage\n${usage()}`);
265
314
  }
266
315
  function parseLinkCommand(args, kind = "friend.link") {
@@ -1537,6 +1586,57 @@ function parseBlueBubblesCommand(args) {
1537
1586
  ...(json ? { json: true } : {}),
1538
1587
  };
1539
1588
  }
1589
+ function parseNervesReviewCommand(args) {
1590
+ const { agent, rest } = extractAgentFlag(args);
1591
+ let processName = "daemon";
1592
+ let component;
1593
+ let event;
1594
+ let level;
1595
+ let since;
1596
+ let limit;
1597
+ let json = false;
1598
+ for (let i = 0; i < rest.length; i += 1) {
1599
+ const token = rest[i];
1600
+ const next = rest[i + 1];
1601
+ if (token === "--json") {
1602
+ json = true;
1603
+ continue;
1604
+ }
1605
+ if ((token === "--process" || token === "--component" || token === "--event" || token === "--level" || token === "--since" || token === "--limit") && next) {
1606
+ if (token === "--process")
1607
+ processName = next;
1608
+ if (token === "--component")
1609
+ component = next;
1610
+ if (token === "--event")
1611
+ event = next;
1612
+ if (token === "--level")
1613
+ level = next;
1614
+ if (token === "--since")
1615
+ since = next;
1616
+ if (token === "--limit") {
1617
+ const parsed = Number.parseInt(next, 10);
1618
+ if (!Number.isInteger(parsed) || String(parsed) !== next || parsed < 1 || parsed > 1000) {
1619
+ throw new Error("--limit must be an integer between 1 and 1000");
1620
+ }
1621
+ limit = parsed;
1622
+ }
1623
+ i += 1;
1624
+ continue;
1625
+ }
1626
+ throw new Error(`Usage\n${usage()}`);
1627
+ }
1628
+ return {
1629
+ kind: "nerves-review",
1630
+ ...(agent ? { agent } : {}),
1631
+ process: processName,
1632
+ ...(component ? { component } : {}),
1633
+ ...(event ? { event } : {}),
1634
+ ...(level ? { level } : {}),
1635
+ ...(since ? { since } : {}),
1636
+ ...(limit ? { limit } : {}),
1637
+ json,
1638
+ };
1639
+ }
1540
1640
  // ── Main dispatch ──
1541
1641
  function parseOuroCommand(args) {
1542
1642
  const [head, second] = args;
@@ -1704,6 +1804,8 @@ function parseOuroCommand(args) {
1704
1804
  return parseMessageCommand(args.slice(1));
1705
1805
  if (head === "poke")
1706
1806
  return parsePokeCommand(args.slice(1));
1807
+ if (head === "nerves-review")
1808
+ return parseNervesReviewCommand(args.slice(1));
1707
1809
  if (head === "link")
1708
1810
  return parseLinkCommand(args.slice(1));
1709
1811
  if (head === "mcp-serve")