@ouro.bot/cli 0.1.0-alpha.657 → 0.1.0-alpha.659

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 (60) hide show
  1. package/README.md +13 -13
  2. package/changelog.json +15 -0
  3. package/dist/arc/evolution.js +1 -1
  4. package/dist/arc/flight-recorder.js +369 -0
  5. package/dist/arc/obligations.js +24 -2
  6. package/dist/heart/active-work.js +1 -1
  7. package/dist/heart/config-registry.js +5 -5
  8. package/dist/heart/context-loss-gauntlet.js +354 -0
  9. package/dist/heart/daemon/agent-config-check.js +1 -1
  10. package/dist/heart/daemon/agent-service.js +18 -17
  11. package/dist/heart/daemon/cli-exec.js +40 -12
  12. package/dist/heart/daemon/cli-help.js +21 -0
  13. package/dist/heart/daemon/cli-parse.js +27 -0
  14. package/dist/heart/daemon/daemon-entry.js +1 -1
  15. package/dist/heart/daemon/daemon.js +3 -3
  16. package/dist/heart/daemon/hooks/bundle-meta.js +29 -9
  17. package/dist/heart/daemon/inner-status.js +4 -15
  18. package/dist/heart/habits/habit-parser.js +64 -1
  19. package/dist/heart/hatch/hatch-flow.js +17 -9
  20. package/dist/heart/hatch/specialist-tools.js +15 -11
  21. package/dist/heart/kept-notes.js +5 -73
  22. package/dist/heart/mailbox/mailbox-http-hooks.js +1 -0
  23. package/dist/heart/mailbox/mailbox-http-routes.js +4 -0
  24. package/dist/heart/mailbox/mailbox-read.js +2 -1
  25. package/dist/heart/mailbox/readers/continuity-readers.js +5 -0
  26. package/dist/heart/mailbox/readers/runtime-readers.js +21 -49
  27. package/dist/heart/mcp/mcp-server.js +8 -8
  28. package/dist/heart/session-events.js +1 -31
  29. package/dist/heart/start-of-turn-packet.js +8 -2
  30. package/dist/heart/tool-description.js +15 -3
  31. package/dist/heart/turn-context.js +27 -7
  32. package/dist/heart/work-card.js +386 -0
  33. package/dist/mailbox-ui/assets/index-B-V9vRQ0.js +61 -0
  34. package/dist/mailbox-ui/assets/index-BOZbGbkL.css +1 -0
  35. package/dist/mailbox-ui/index.html +2 -2
  36. package/dist/mind/bundle-manifest.js +9 -3
  37. package/dist/mind/context.js +1 -2
  38. package/dist/mind/desk-section.js +53 -1
  39. package/dist/mind/diary.js +2 -3
  40. package/dist/mind/note-search.js +36 -106
  41. package/dist/mind/prompt.js +37 -102
  42. package/dist/mind/record-paths.js +312 -0
  43. package/dist/repertoire/bundle-templates.js +4 -5
  44. package/dist/repertoire/tools-bundle.js +1 -1
  45. package/dist/repertoire/tools-evolution.js +4 -4
  46. package/dist/repertoire/tools-notes.js +42 -62
  47. package/dist/repertoire/tools-record.js +16 -11
  48. package/dist/repertoire/tools-session.js +4 -4
  49. package/dist/repertoire/tools.js +1 -1
  50. package/dist/senses/habit-turn-message.js +19 -5
  51. package/dist/senses/inner-dialog-worker.js +58 -9
  52. package/dist/senses/inner-dialog.js +30 -11
  53. package/dist/senses/pipeline.js +135 -1
  54. package/dist/util/frontmatter.js +17 -1
  55. package/package.json +3 -3
  56. package/skills/configure-dev-tools.md +1 -1
  57. package/skills/travel-planning.md +1 -1
  58. package/dist/mailbox-ui/assets/index-9-AxCxuB.js +0 -61
  59. package/dist/mailbox-ui/assets/index-CWzt267f.css +0 -1
  60. package/dist/mind/journal-index.js +0 -162
@@ -0,0 +1,354 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.runContextLossGauntlet = runContextLossGauntlet;
37
+ exports.formatContextLossGauntletText = formatContextLossGauntletText;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const flight_recorder_1 = require("../arc/flight-recorder");
41
+ const record_paths_1 = require("../mind/record-paths");
42
+ const runtime_1 = require("../nerves/runtime");
43
+ const work_card_1 = require("./work-card");
44
+ function makeCheck(input) {
45
+ return input;
46
+ }
47
+ function flightRecorderEvidence(card) {
48
+ return card.sources.filter((source) => source.kind === "flight_recorder").slice(0, 1);
49
+ }
50
+ function currentAskCheck(card) {
51
+ const evidence = flightRecorderEvidence(card);
52
+ if (!card.currentAsk.available) {
53
+ return makeCheck({
54
+ id: "current_ask",
55
+ label: "Current ask",
56
+ status: "fail",
57
+ score: 0,
58
+ maxScore: 15,
59
+ detail: "No durable current ask is available after context loss.",
60
+ evidence,
61
+ });
62
+ }
63
+ if (card.currentAsk.confidence !== "current") {
64
+ return makeCheck({
65
+ id: "current_ask",
66
+ label: "Current ask",
67
+ status: "warn",
68
+ score: 10,
69
+ maxScore: 15,
70
+ detail: `Current ask is available but marked ${card.currentAsk.confidence}.`,
71
+ evidence,
72
+ });
73
+ }
74
+ return makeCheck({
75
+ id: "current_ask",
76
+ label: "Current ask",
77
+ status: "pass",
78
+ score: 15,
79
+ maxScore: 15,
80
+ detail: "Current ask is preserved in the flight recorder.",
81
+ evidence,
82
+ });
83
+ }
84
+ function nextSafeActionCheck(card) {
85
+ const evidence = card.nextAction.source ? [card.nextAction.source] : [];
86
+ if (card.nextAction.actor === "unknown") {
87
+ return makeCheck({
88
+ id: "next_safe_action",
89
+ label: "Next safe action",
90
+ status: "fail",
91
+ score: 0,
92
+ maxScore: 20,
93
+ detail: card.nextAction.summary,
94
+ evidence,
95
+ });
96
+ }
97
+ return makeCheck({
98
+ id: "next_safe_action",
99
+ label: "Next safe action",
100
+ status: "pass",
101
+ score: 20,
102
+ maxScore: 20,
103
+ detail: `${card.nextAction.actor}: ${card.nextAction.summary}`,
104
+ evidence,
105
+ });
106
+ }
107
+ function staleGuardCheck(resume, card) {
108
+ const evidence = flightRecorderEvidence(card);
109
+ if (resume.canContinue && resume.hasCompleteState && resume.recorderHealth.status === "ok" && resume.blockedBecause.length === 0) {
110
+ return makeCheck({
111
+ id: "stale_guard",
112
+ label: "Stale-state guard",
113
+ status: "pass",
114
+ score: 15,
115
+ maxScore: 15,
116
+ detail: "Flight recorder permits continuation with complete state.",
117
+ evidence,
118
+ });
119
+ }
120
+ const reasons = [
121
+ "canContinue is false",
122
+ ...(!resume.hasCompleteState ? ["incomplete resume state"] : []),
123
+ ...(resume.recorderHealth.status !== "ok" ? [`recorder health is ${resume.recorderHealth.status}`] : []),
124
+ ...(resume.blockedBecause.length > 0 ? resume.blockedBecause : []),
125
+ ];
126
+ return makeCheck({
127
+ id: "stale_guard",
128
+ label: "Stale-state guard",
129
+ status: "fail",
130
+ score: 0,
131
+ maxScore: 15,
132
+ detail: `Flight recorder says continuation is unsafe: ${reasons.join("; ")}.`,
133
+ evidence,
134
+ });
135
+ }
136
+ function obligationsCheck(card) {
137
+ if (card.counts.owed === 0) {
138
+ return makeCheck({
139
+ id: "obligations_visible",
140
+ label: "Obligations",
141
+ status: "not_applicable",
142
+ score: 0,
143
+ maxScore: 0,
144
+ detail: "No owed obligations are active.",
145
+ evidence: [],
146
+ });
147
+ }
148
+ const evidence = card.owed.map((item) => item.source);
149
+ return makeCheck({
150
+ id: "obligations_visible",
151
+ label: "Obligations",
152
+ status: "pass",
153
+ score: 10,
154
+ maxScore: 10,
155
+ detail: `${card.counts.owed} owed obligation(s) have source locators.`,
156
+ evidence,
157
+ });
158
+ }
159
+ function returnRoutesCheck(card) {
160
+ if (card.counts.returnObligations === 0) {
161
+ return makeCheck({
162
+ id: "return_routes_visible",
163
+ label: "Return routes",
164
+ status: "not_applicable",
165
+ score: 0,
166
+ maxScore: 0,
167
+ detail: "No active return obligations are queued.",
168
+ evidence: [],
169
+ });
170
+ }
171
+ const evidence = card.returnObligations.map((item) => item.source);
172
+ return makeCheck({
173
+ id: "return_routes_visible",
174
+ label: "Return routes",
175
+ status: "pass",
176
+ score: 10,
177
+ maxScore: 10,
178
+ detail: `${card.counts.returnObligations} return route(s) are visible.`,
179
+ evidence,
180
+ });
181
+ }
182
+ function blockersCheck(card) {
183
+ if (card.counts.waitingOnHuman === 0) {
184
+ return makeCheck({
185
+ id: "blockers_surface",
186
+ label: "Blockers",
187
+ status: "not_applicable",
188
+ score: 0,
189
+ maxScore: 0,
190
+ detail: "No waiting or blocked work is active.",
191
+ evidence: [],
192
+ });
193
+ }
194
+ const evidence = card.waitingOnOthers.map((item) => item.source);
195
+ return makeCheck({
196
+ id: "blockers_surface",
197
+ label: "Blockers",
198
+ status: "pass",
199
+ score: 10,
200
+ maxScore: 10,
201
+ detail: "Waiting work controls the next action.",
202
+ evidence,
203
+ });
204
+ }
205
+ function deskRecordCheck(agentRoot) {
206
+ const recordPaths = (0, record_paths_1.resolveDeskRecordPaths)(agentRoot);
207
+ const requiredPaths = [
208
+ recordPaths.recordRoot,
209
+ recordPaths.diaryRoot,
210
+ recordPaths.diaryDailyDir,
211
+ recordPaths.notesRoot,
212
+ recordPaths.factsPath,
213
+ recordPaths.entitiesPath,
214
+ ];
215
+ const missing = requiredPaths
216
+ .filter((entry) => !fs.existsSync(entry))
217
+ .map((entry) => path.relative(agentRoot, entry));
218
+ const legacyRoots = ["journal", "diary"]
219
+ .map((entry) => path.join(agentRoot, entry))
220
+ .filter((entry) => fs.existsSync(entry))
221
+ .map((entry) => path.relative(agentRoot, entry));
222
+ if (legacyRoots.length > 0) {
223
+ return makeCheck({
224
+ id: "desk_record_ready",
225
+ label: "Desk record",
226
+ status: "fail",
227
+ score: 0,
228
+ maxScore: 10,
229
+ detail: `Legacy active record root(s) still exist: ${legacyRoots.join(", ")}.`,
230
+ evidence: [],
231
+ });
232
+ }
233
+ if (missing.length > 0) {
234
+ return makeCheck({
235
+ id: "desk_record_ready",
236
+ label: "Desk record",
237
+ status: "warn",
238
+ score: 5,
239
+ maxScore: 10,
240
+ detail: `Canonical Desk record scaffold is incomplete: ${missing.join(", ")}.`,
241
+ evidence: [],
242
+ });
243
+ }
244
+ return makeCheck({
245
+ id: "desk_record_ready",
246
+ label: "Desk record",
247
+ status: "pass",
248
+ score: 10,
249
+ maxScore: 10,
250
+ detail: "Canonical Desk record scaffold is present and no legacy record roots are active.",
251
+ evidence: [],
252
+ });
253
+ }
254
+ function sourceProvenanceCheck(card) {
255
+ const evidence = card.sources;
256
+ if (card.degraded.issues.length > 0) {
257
+ return makeCheck({
258
+ id: "source_provenance",
259
+ label: "Source provenance",
260
+ status: "fail",
261
+ score: 0,
262
+ maxScore: 15,
263
+ detail: `${card.degraded.issues.length} Work Card source issue(s) are present.`,
264
+ evidence,
265
+ });
266
+ }
267
+ return makeCheck({
268
+ id: "source_provenance",
269
+ label: "Source provenance",
270
+ status: "pass",
271
+ score: 15,
272
+ maxScore: 15,
273
+ detail: "Work Card source locators are intact.",
274
+ evidence,
275
+ });
276
+ }
277
+ function summarize(verdict) {
278
+ if (verdict === "ready")
279
+ return "ready: durable Arc, flight recorder, and Desk state can carry context-loss recovery";
280
+ if (verdict === "watch")
281
+ return "watch: recovery is possible, but warnings need attention";
282
+ return "blocked: context-loss recovery would lose or mislead the agent";
283
+ }
284
+ function runContextLossGauntlet(agentName, agentRoot, options = {}) {
285
+ const flightRecorderResume = (0, flight_recorder_1.readFlightRecorderResume)(agentRoot);
286
+ const card = (0, work_card_1.buildWorkCard)(agentName, agentRoot, { ...options, flightRecorderResume });
287
+ const generatedAt = (options.now ?? (() => new Date()))().toISOString();
288
+ const checks = [
289
+ currentAskCheck(card),
290
+ nextSafeActionCheck(card),
291
+ staleGuardCheck(flightRecorderResume, card),
292
+ obligationsCheck(card),
293
+ returnRoutesCheck(card),
294
+ blockersCheck(card),
295
+ deskRecordCheck(agentRoot),
296
+ sourceProvenanceCheck(card),
297
+ ];
298
+ const earned = checks.reduce((sum, check) => sum + check.score, 0);
299
+ const possible = checks.reduce((sum, check) => sum + check.maxScore, 0);
300
+ const percentage = Math.round((earned / possible) * 100);
301
+ const verdict = checks.some((check) => check.status === "fail")
302
+ ? "blocked"
303
+ : checks.some((check) => check.status === "warn")
304
+ ? "watch"
305
+ : "ready";
306
+ const report = {
307
+ schemaVersion: 1,
308
+ agent: card.agent,
309
+ generatedAt,
310
+ verdict,
311
+ summary: summarize(verdict),
312
+ score: { earned, possible, percentage },
313
+ currentAsk: card.currentAsk,
314
+ nextAction: card.nextAction,
315
+ counts: card.counts,
316
+ checks,
317
+ workCard: card,
318
+ };
319
+ (0, runtime_1.emitNervesEvent)({
320
+ component: "engine",
321
+ event: "engine.context_loss_gauntlet_ran",
322
+ message: "context-loss gauntlet scored durable recovery state",
323
+ meta: {
324
+ agent: card.agent,
325
+ verdict: report.verdict,
326
+ scorePercentage: report.score.percentage,
327
+ failedChecks: report.checks.filter((check) => check.status === "fail").map((check) => check.id),
328
+ warnedChecks: report.checks.filter((check) => check.status === "warn").map((check) => check.id),
329
+ },
330
+ });
331
+ return report;
332
+ }
333
+ function statusToken(status) {
334
+ if (status === "not_applicable")
335
+ return "N/A";
336
+ return status.toUpperCase();
337
+ }
338
+ function formatContextLossGauntletText(report) {
339
+ return [
340
+ `Context-loss gauntlet - ${report.agent}`,
341
+ `generated: ${report.generatedAt}`,
342
+ `verdict: ${report.verdict} (${report.score.earned}/${report.score.possible}, ${report.score.percentage}%)`,
343
+ `summary: ${report.summary}`,
344
+ "",
345
+ "Recovery",
346
+ report.currentAsk.available
347
+ ? ` current ask: ${report.currentAsk.value} (${report.currentAsk.confidence})`
348
+ : ` current ask: unavailable (${report.currentAsk.source})`,
349
+ ` next action: ${report.nextAction.actor}: ${report.nextAction.summary}`,
350
+ "",
351
+ "Checks",
352
+ ...report.checks.map((check) => ` - ${statusToken(check.status)} ${check.id}: ${check.detail}`),
353
+ ].join("\n").trim();
354
+ }
@@ -256,7 +256,7 @@ function credentialRecordForLane(pool, provider) {
256
256
  return pool.providers[provider];
257
257
  }
258
258
  function laneAudienceLabel(lane) {
259
- return lane === "outward" ? "chat" : "inner dialog";
259
+ return lane === "outward" ? "chat" : "inner lane";
260
260
  }
261
261
  function bindingLabel(binding) {
262
262
  return `${binding.provider} / ${binding.model}`;
@@ -3,7 +3,7 @@
3
3
  * Agent service layer — handles MCP-facing daemon commands.
4
4
  * Each handler receives { agent, friendId, ...params } and returns DaemonResponse.
5
5
  *
6
- * DRY: uses the same shared functions the agent's own tools use (diary, session transcript).
6
+ * DRY: uses the same shared functions the agent's own tools use (Desk record diary, session transcript).
7
7
  * This file is a thin adapter — no reimplemented search, parsing, or state reading.
8
8
  */
9
9
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
@@ -45,7 +45,7 @@ exports.handleAgentAsk = handleAgentAsk;
45
45
  exports.handleAgentCatchup = handleAgentCatchup;
46
46
  exports.handleAgentDelegate = handleAgentDelegate;
47
47
  exports.handleAgentGetContext = handleAgentGetContext;
48
- exports.handleAgentSearchNotes = handleAgentSearchNotes;
48
+ exports.handleAgentSearchFacts = handleAgentSearchFacts;
49
49
  exports.handleAgentGetTask = handleAgentGetTask;
50
50
  exports.handleAgentCheckScope = handleAgentCheckScope;
51
51
  exports.handleAgentRequestDecision = handleAgentRequestDecision;
@@ -57,9 +57,10 @@ const fs = __importStar(require("fs"));
57
57
  const path = __importStar(require("path"));
58
58
  const identity_1 = require("../identity");
59
59
  const diary_1 = require("../../mind/diary");
60
+ const record_paths_1 = require("../../mind/record-paths");
60
61
  const runtime_1 = require("../../nerves/runtime");
61
62
  const socket_client_1 = require("./socket-client");
62
- /** Format diary hits the same way the search_notes tool does. */
63
+ /** Format diary hits the same way the search_facts tool does. */
63
64
  function formatDiaryHits(hits) {
64
65
  return hits.map((f) => `[diary] ${f.text} (source=${f.source}, createdAt=${f.createdAt})`);
65
66
  }
@@ -72,7 +73,7 @@ function readAgentFile(agent, ...segments) {
72
73
  }
73
74
  /** Resolve the diary root for a specific agent. */
74
75
  function agentDiaryRoot(agent) {
75
- return (0, diary_1.resolveDiaryRoot)(path.join((0, identity_1.getAgentRoot)(agent), "diary"));
76
+ return (0, record_paths_1.resolveRecordDiaryRoot)((0, identity_1.getAgentRoot)(agent));
76
77
  }
77
78
  /** Read inner dialog runtime status. */
78
79
  function readInnerDialogStatus(agent) {
@@ -339,12 +340,12 @@ async function handleAgentAsk(params) {
339
340
  emit("daemon.agent_service_error", "agent.ask missing question", { agent: params.agent });
340
341
  return { ok: false, error: "Missing required parameter: question" };
341
342
  }
342
- // Use the same searchDiaryEntries the search_notes tool uses (substring fallback no embedding provider in shim)
343
+ // Use the same searchDiaryEntries the search_facts tool uses (substring fallback; no embedding provider in shim).
343
344
  const diaryRoot = agentDiaryRoot(params.agent);
344
345
  const hits = await (0, diary_1.searchDiaryEntries)(question, (0, diary_1.readDiaryEntries)(diaryRoot));
345
346
  const context = hits.length > 0
346
347
  ? hits.slice(0, 10).map((f) => f.text).join("\n")
347
- : `No relevant notes found for: ${question}`;
348
+ : `No relevant facts found for: ${question}`;
348
349
  emit("daemon.agent_service_end", "completed agent.ask", { agent: params.agent });
349
350
  return { ok: true, message: context };
350
351
  }
@@ -393,17 +394,17 @@ async function handleAgentGetContext(params) {
393
394
  const innerStatus = readInnerDialogStatus(params.agent);
394
395
  const sessions = enumerateSessions(params.agent);
395
396
  const taskFiles = listTaskFiles(params.agent);
396
- let noteSummary = null;
397
+ let recordSummary = null;
397
398
  if (query) {
398
399
  const hits = await (0, diary_1.searchDiaryEntries)(query, facts);
399
- noteSummary = hits.length > 0
400
+ recordSummary = hits.length > 0
400
401
  ? hits.slice(0, 10).map((f) => f.text).join("\n")
401
- : `No relevant notes for: ${query}`;
402
+ : `No relevant facts for: ${query}`;
402
403
  }
403
404
  else {
404
405
  const recent = facts.slice(-10);
405
406
  if (recent.length > 0)
406
- noteSummary = recent.map((f) => f.text).join("\n");
407
+ recordSummary = recent.map((f) => f.text).join("\n");
407
408
  }
408
409
  emit("daemon.agent_service_end", "completed agent.getContext", { agent: params.agent });
409
410
  return {
@@ -412,25 +413,25 @@ async function handleAgentGetContext(params) {
412
413
  agent: params.agent,
413
414
  hasDiaryEntries: facts.length > 0,
414
415
  factCount: facts.length,
415
- noteSummary,
416
+ recordSummary,
416
417
  taskCount: taskFiles.length,
417
418
  sessionCount: sessions.length,
418
419
  innerStatus: innerStatus?.status ?? null,
419
420
  },
420
421
  };
421
422
  }
422
- async function handleAgentSearchNotes(params) {
423
- emit("daemon.agent_service_start", "handling agent.searchNotes", { agent: params.agent });
423
+ async function handleAgentSearchFacts(params) {
424
+ emit("daemon.agent_service_start", "handling agent.searchFacts", { agent: params.agent });
424
425
  const query = params.query;
425
426
  if (!query) {
426
- emit("daemon.agent_service_error", "agent.searchNotes missing query", { agent: params.agent });
427
+ emit("daemon.agent_service_error", "agent.searchFacts missing query", { agent: params.agent });
427
428
  return { ok: false, error: "Missing required parameter: query" };
428
429
  }
429
- // Same searchDiaryEntries as the search_notes tool
430
+ // Same searchDiaryEntries as the search_facts tool.
430
431
  const diaryRoot = agentDiaryRoot(params.agent);
431
432
  const hits = await (0, diary_1.searchDiaryEntries)(query, (0, diary_1.readDiaryEntries)(diaryRoot));
432
433
  const formatted = formatDiaryHits(hits.slice(0, 20));
433
- emit("daemon.agent_service_end", "completed agent.searchNotes", { agent: params.agent, matchCount: hits.length });
434
+ emit("daemon.agent_service_end", "completed agent.searchFacts", { agent: params.agent, matchCount: hits.length });
434
435
  return {
435
436
  ok: true,
436
437
  message: hits.length > 0 ? `Found ${hits.length} matches` : "No matches found",
@@ -481,7 +482,7 @@ async function handleAgentCheckGuidance(params) {
481
482
  emit("daemon.agent_service_error", "agent.checkGuidance missing topic", { agent: params.agent });
482
483
  return { ok: false, error: "Missing required parameter: topic" };
483
484
  }
484
- // Same searchDiaryEntries as the search_notes tool
485
+ // Same searchDiaryEntries as the search_facts tool.
485
486
  const diaryRoot = agentDiaryRoot(params.agent);
486
487
  const hits = await (0, diary_1.searchDiaryEntries)(topic, (0, diary_1.readDiaryEntries)(diaryRoot));
487
488
  const guidance = hits.length > 0
@@ -365,6 +365,8 @@ function agentResolutionFailureMode(command) {
365
365
  case "attention.list":
366
366
  case "attention.show":
367
367
  case "attention.history":
368
+ case "work.card":
369
+ case "work.gauntlet":
368
370
  case "inner.status":
369
371
  case "session.list":
370
372
  return "return-message";
@@ -7277,6 +7279,30 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7277
7279
  }
7278
7280
  }
7279
7281
  /* v8 ignore stop */
7282
+ // ── work card (local, no daemon socket needed) ──
7283
+ if (command.kind === "work.card") {
7284
+ const { buildWorkCard, formatWorkCardText } = await Promise.resolve().then(() => __importStar(require("../work-card")));
7285
+ if (!command.agent)
7286
+ throw new Error("work card requires --agent <name>");
7287
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
7288
+ const agentRoot = deps.agentBundleRoot ?? path.join(bundlesRoot, `${command.agent}.ouro`);
7289
+ const card = buildWorkCard(command.agent, agentRoot);
7290
+ const message = command.format === "json" ? JSON.stringify(card, null, 2) : formatWorkCardText(card);
7291
+ deps.writeStdout(message);
7292
+ return message;
7293
+ }
7294
+ // ── context-loss gauntlet (local, no daemon socket needed) ──
7295
+ if (command.kind === "work.gauntlet") {
7296
+ const { runContextLossGauntlet, formatContextLossGauntletText } = await Promise.resolve().then(() => __importStar(require("../context-loss-gauntlet")));
7297
+ if (!command.agent)
7298
+ throw new Error("work gauntlet requires --agent <name>");
7299
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
7300
+ const agentRoot = deps.agentBundleRoot ?? path.join(bundlesRoot, `${command.agent}.ouro`);
7301
+ const report = runContextLossGauntlet(command.agent, agentRoot, { homeDir: deps.homeDir });
7302
+ const message = command.format === "json" ? JSON.stringify(report, null, 2) : formatContextLossGauntletText(report);
7303
+ deps.writeStdout(message);
7304
+ return message;
7305
+ }
7280
7306
  // ── inner dialog status (local, no daemon socket needed) ──
7281
7307
  /* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
7282
7308
  if (command.kind === "inner.status") {
@@ -7286,6 +7312,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7286
7312
  const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
7287
7313
  const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../util/frontmatter")));
7288
7314
  const { listActiveReturnObligations } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
7315
+ const { resolveDeskRecordPaths } = await Promise.resolve().then(() => __importStar(require("../../mind/record-paths")));
7289
7316
  // Read runtime state
7290
7317
  const innerSessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
7291
7318
  const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
@@ -7295,19 +7322,20 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7295
7322
  runtimeState = JSON.parse(raw);
7296
7323
  }
7297
7324
  catch { /* missing or corrupt — will show "unknown" */ }
7298
- // Read journal files
7299
- const journalDir = path.join(agentRoot, "journal");
7300
- let journalFiles = [];
7325
+ // Read canonical Desk record summary
7326
+ const recordPaths = resolveDeskRecordPaths(agentRoot);
7327
+ const recordSummary = { diaryFactCount: 0, noteCount: 0 };
7301
7328
  try {
7302
- const journalEntries = fs.readdirSync(journalDir, { withFileTypes: true });
7303
- journalFiles = journalEntries
7304
- .filter((e) => e.isFile() && !e.name.startsWith("."))
7305
- .map((e) => {
7306
- const stat = fs.statSync(path.join(journalDir, e.name));
7307
- return { name: e.name, mtimeMs: stat.mtimeMs };
7308
- });
7329
+ const rawFacts = fs.readFileSync(recordPaths.factsPath, "utf-8");
7330
+ recordSummary.diaryFactCount = rawFacts.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
7331
+ }
7332
+ catch { /* missing facts file — count stays zero */ }
7333
+ try {
7334
+ recordSummary.noteCount = fs.readdirSync(recordPaths.notesRoot, { withFileTypes: true })
7335
+ .filter((e) => e.isFile() && !e.name.startsWith(".") && e.name.endsWith(".md"))
7336
+ .length;
7309
7337
  }
7310
- catch { /* missing dir — will show (empty) */ }
7338
+ catch { /* missing notes dir — count stays zero */ }
7311
7339
  // Read heartbeat cadence
7312
7340
  let heartbeat = null;
7313
7341
  try {
@@ -7342,7 +7370,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7342
7370
  const message = buildInnerStatusOutput({
7343
7371
  agentName: command.agent,
7344
7372
  runtimeState,
7345
- journalFiles,
7373
+ recordSummary,
7346
7374
  heartbeat,
7347
7375
  attentionCount: activeObligations.length,
7348
7376
  now: Date.now(),
@@ -151,6 +151,27 @@ exports.COMMAND_REGISTRY = {
151
151
  example: "ouro task list",
152
152
  subcommands: ["list", "new", "done", "archive", "show"],
153
153
  },
154
+ work: {
155
+ category: "Tasks",
156
+ description: "Show durable Arc work state or run the context-loss gauntlet.",
157
+ usage: "ouro work card|gauntlet [--agent <name>] [--format text|json|--json]",
158
+ example: "ouro work gauntlet --agent slugger --format json",
159
+ subcommands: ["card", "gauntlet"],
160
+ },
161
+ "work card": {
162
+ category: "Tasks",
163
+ description: "Show the agent's durable Work Card compiled from arc records.",
164
+ usage: "ouro work card [--agent <name>] [--format text|json|--json]",
165
+ example: "ouro work card --agent slugger --format json",
166
+ hidden: true,
167
+ },
168
+ "work gauntlet": {
169
+ category: "Tasks",
170
+ description: "Score whether durable Arc, flight recorder, and Desk state can recover after context loss.",
171
+ usage: "ouro work gauntlet [--agent <name>] [--format text|json|--json]",
172
+ example: "ouro work gauntlet --agent slugger --format json",
173
+ hidden: true,
174
+ },
154
175
  "migrate-to-desk": {
155
176
  category: "Tasks",
156
177
  description: "Migrate a legacy `tasks/` tree into the new `desk/` shape (copy semantics — source untouched).",
@@ -114,6 +114,7 @@ function usage() {
114
114
  " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
115
115
  " ouro friend update <id> --trust <level> [--agent <name>]",
116
116
  " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
117
+ " ouro work card|gauntlet [--agent <name>] [--format text|json|--json]",
117
118
  " ouro inner [--agent <name>]",
118
119
  " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
119
120
  " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
@@ -1000,6 +1001,30 @@ function parseAttentionCommand(args) {
1000
1001
  }
1001
1002
  return { kind: "attention.list", ...(agent ? { agent } : {}) };
1002
1003
  }
1004
+ function parseWorkCommand(args) {
1005
+ const { agent, rest: cleaned } = extractAgentFlag(args);
1006
+ const sub = cleaned[0];
1007
+ if (sub !== "card" && sub !== "gauntlet") {
1008
+ throw new Error("Usage: ouro work card|gauntlet [--agent <name>] [--format text|json|--json]");
1009
+ }
1010
+ let format = "text";
1011
+ for (let i = 1; i < cleaned.length; i += 1) {
1012
+ if (cleaned[i] === "--json") {
1013
+ format = "json";
1014
+ continue;
1015
+ }
1016
+ if (cleaned[i] === "--format" && cleaned[i + 1]) {
1017
+ const value = cleaned[++i];
1018
+ if (value !== "text" && value !== "json") {
1019
+ throw new Error("--format must be text or json");
1020
+ }
1021
+ format = value;
1022
+ continue;
1023
+ }
1024
+ throw new Error(`Usage: ouro work ${sub} [--agent <name>] [--format text|json|--json]`);
1025
+ }
1026
+ return { kind: sub === "card" ? "work.card" : "work.gauntlet", ...(agent ? { agent } : {}), ...(format !== "text" ? { format } : {}) };
1027
+ }
1003
1028
  function parseThoughtsCommand(args) {
1004
1029
  const { agent, rest: cleaned } = extractAgentFlag(args);
1005
1030
  let last;
@@ -1583,6 +1608,8 @@ function parseOuroCommand(args) {
1583
1608
  return parseThoughtsCommand(args.slice(1));
1584
1609
  if (head === "attention")
1585
1610
  return parseAttentionCommand(args.slice(1));
1611
+ if (head === "work")
1612
+ return parseWorkCommand(args.slice(1));
1586
1613
  if (head === "inner") {
1587
1614
  const { agent } = extractAgentFlag(args.slice(1));
1588
1615
  return { kind: "inner.status", ...(agent ? { agent } : {}) };
@@ -366,7 +366,7 @@ void daemon.start().then(() => {
366
366
  habitsDir,
367
367
  osCronManager,
368
368
  onHabitFire: (habitName) => {
369
- processManager.sendToAgent(agent, { type: "habit", habitName });
369
+ processManager.sendToAgent(agent, { type: "habit", habitName, trigger: "overdue" });
370
370
  },
371
371
  deps: {
372
372
  readdir: (dir) => fs.readdirSync(dir),
@@ -1205,8 +1205,8 @@ class OuroDaemon {
1205
1205
  return (0, agent_service_1.handleAgentDelegate)(command);
1206
1206
  case "agent.getContext":
1207
1207
  return (0, agent_service_1.handleAgentGetContext)(command);
1208
- case "agent.searchNotes":
1209
- return (0, agent_service_1.handleAgentSearchNotes)(command);
1208
+ case "agent.searchFacts":
1209
+ return (0, agent_service_1.handleAgentSearchFacts)(command);
1210
1210
  case "agent.getTask":
1211
1211
  return (0, agent_service_1.handleAgentGetTask)(command);
1212
1212
  case "agent.checkScope":
@@ -1298,7 +1298,7 @@ class OuroDaemon {
1298
1298
  };
1299
1299
  }
1300
1300
  case "habit.poke": {
1301
- this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName });
1301
+ this.processManager.sendToAgent?.(command.agent, { type: "habit", habitName: command.habitName, trigger: "poke" });
1302
1302
  return {
1303
1303
  ok: true,
1304
1304
  message: `poked habit ${command.habitName} for ${command.agent}`,