@ouro.bot/cli 0.1.0-alpha.655 → 0.1.0-alpha.658

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/README.md +13 -13
  2. package/changelog.json +21 -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 +14 -5
  8. package/dist/heart/daemon/agent-config-check.js +1 -1
  9. package/dist/heart/daemon/agent-service.js +18 -17
  10. package/dist/heart/daemon/cli-exec.js +134 -15
  11. package/dist/heart/daemon/cli-help.js +21 -2
  12. package/dist/heart/daemon/cli-parse.js +31 -3
  13. package/dist/heart/daemon/daemon-entry.js +1 -1
  14. package/dist/heart/daemon/daemon.js +3 -3
  15. package/dist/heart/daemon/hooks/bundle-meta.js +29 -9
  16. package/dist/heart/daemon/inner-status.js +4 -15
  17. package/dist/heart/daemon/sense-manager.js +16 -1
  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/identity.js +4 -1
  22. package/dist/heart/kept-notes.js +5 -73
  23. package/dist/heart/mailbox/readers/runtime-readers.js +21 -49
  24. package/dist/heart/mcp/mcp-server.js +8 -8
  25. package/dist/heart/sense-truth.js +2 -0
  26. package/dist/heart/session-events.js +1 -31
  27. package/dist/heart/start-of-turn-packet.js +8 -2
  28. package/dist/heart/tool-description.js +15 -3
  29. package/dist/heart/turn-context.js +34 -7
  30. package/dist/heart/work-card.js +386 -0
  31. package/dist/mailbox-ui/assets/{index-9-AxCxuB.js → index-Cbasiy6y.js} +1 -1
  32. package/dist/mailbox-ui/index.html +1 -1
  33. package/dist/mind/bundle-manifest.js +9 -3
  34. package/dist/mind/context.js +1 -2
  35. package/dist/mind/desk-section.js +53 -1
  36. package/dist/mind/diary.js +2 -3
  37. package/dist/mind/note-search.js +36 -106
  38. package/dist/mind/prompt.js +45 -102
  39. package/dist/mind/record-paths.js +312 -0
  40. package/dist/repertoire/bundle-templates.js +4 -5
  41. package/dist/repertoire/tools-bundle.js +1 -1
  42. package/dist/repertoire/tools-evolution.js +4 -4
  43. package/dist/repertoire/tools-notes.js +42 -62
  44. package/dist/repertoire/tools-record.js +16 -11
  45. package/dist/repertoire/tools-session.js +4 -4
  46. package/dist/repertoire/tools.js +1 -1
  47. package/dist/senses/habit-turn-message.js +19 -5
  48. package/dist/senses/inner-dialog-worker.js +58 -9
  49. package/dist/senses/inner-dialog.js +30 -11
  50. package/dist/senses/pipeline.js +135 -1
  51. package/dist/util/frontmatter.js +17 -1
  52. package/package.json +3 -3
  53. package/skills/configure-dev-tools.md +1 -1
  54. package/skills/travel-planning.md +1 -1
  55. package/dist/mind/journal-index.js +0 -162
@@ -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,7 @@ function agentResolutionFailureMode(command) {
365
365
  case "attention.list":
366
366
  case "attention.show":
367
367
  case "attention.history":
368
+ case "work.card":
368
369
  case "inner.status":
369
370
  case "session.list":
370
371
  return "return-message";
@@ -2496,25 +2497,64 @@ function enableAgentSense(agent, sense, deps) {
2496
2497
  mail: senses.mail ?? { enabled: false },
2497
2498
  voice: senses.voice ?? { enabled: false },
2498
2499
  a2a: senses.a2a ?? { enabled: false },
2500
+ workbench: senses.workbench ?? { enabled: false },
2499
2501
  [sense]: { ...existing, enabled: true },
2500
2502
  };
2501
2503
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
2502
2504
  }
2503
- const CONNECT_MENU_PROMPT = "Choose [1-8] or type a name: ";
2505
+ const CONNECT_MENU_PROMPT = "Choose [1-9] or type a name: ";
2504
2506
  function connectMenuIsTTY(deps) {
2505
2507
  return deps.isTTY ?? process.stdout.isTTY === true;
2506
2508
  }
2507
2509
  function readConnectBaySenseFlags(agent, deps) {
2508
2510
  const configPath = path.join(providerCliAgentRoot({ agent }, deps), "agent.json");
2509
2511
  const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2512
+ const workbenchCommand = parsed.mcpServers?.ouro_workbench?.command;
2510
2513
  return {
2511
2514
  teamsEnabled: parsed.senses?.teams?.enabled === true,
2512
2515
  blueBubblesEnabled: parsed.senses?.bluebubbles?.enabled === true,
2513
2516
  mailEnabled: parsed.senses?.mail?.enabled === true,
2514
2517
  voiceEnabled: parsed.senses?.voice?.enabled === true,
2515
2518
  a2aEnabled: parsed.senses?.a2a?.enabled === true,
2519
+ workbenchEnabled: parsed.senses?.workbench?.enabled === true,
2520
+ workbenchMcpCommand: typeof workbenchCommand === "string" && workbenchCommand.trim().length > 0
2521
+ ? workbenchCommand
2522
+ : null,
2516
2523
  };
2517
2524
  }
2525
+ function defaultWorkbenchMcpCandidates(deps) {
2526
+ const homeDir = deps.homeDir ?? os.homedir();
2527
+ return [
2528
+ path.join(homeDir, "Applications", "Ouro Workbench.app", "Contents", "MacOS", "OuroWorkbenchMCP"),
2529
+ path.join("/Applications", "Ouro Workbench.app", "Contents", "MacOS", "OuroWorkbenchMCP"),
2530
+ ];
2531
+ }
2532
+ function cliPathExists(deps, filePath) {
2533
+ return !!filePath && (deps.existsSync ?? fs.existsSync)(filePath);
2534
+ }
2535
+ function findInstalledWorkbenchMcp(deps, preferred) {
2536
+ const candidates = [
2537
+ ...(preferred ? [preferred] : []),
2538
+ ...defaultWorkbenchMcpCandidates(deps),
2539
+ ];
2540
+ return candidates.find((candidate) => cliPathExists(deps, candidate)) ?? null;
2541
+ }
2542
+ function writeWorkbenchMcpRegistration(agent, executablePath, deps) {
2543
+ const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
2544
+ enableAgentSense(agent, "workbench", deps);
2545
+ const nextRaw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2546
+ const mcpServers = nextRaw.mcpServers && typeof nextRaw.mcpServers === "object" && !Array.isArray(nextRaw.mcpServers)
2547
+ ? nextRaw.mcpServers
2548
+ : {};
2549
+ nextRaw.mcpServers = {
2550
+ ...mcpServers,
2551
+ ouro_workbench: {
2552
+ command: executablePath,
2553
+ args: [],
2554
+ },
2555
+ };
2556
+ fs.writeFileSync(configPath, `${JSON.stringify(nextRaw, null, 2)}\n`, "utf-8");
2557
+ }
2518
2558
  async function buildConnectMenu(agent, deps, onProgress) {
2519
2559
  const bundlesRoot = path.dirname(providerCliAgentRoot({ agent }, deps));
2520
2560
  let providerHealth;
@@ -2542,7 +2582,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
2542
2582
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2543
2583
  onProgress?.("loading this machine's settings");
2544
2584
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2545
- const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled, a2aEnabled } = readConnectBaySenseFlags(agent, deps);
2585
+ const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled, a2aEnabled, workbenchEnabled, workbenchMcpCommand, } = readConnectBaySenseFlags(agent, deps);
2546
2586
  const perplexityApiKey = runtimeConfig.ok
2547
2587
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2548
2588
  : null;
@@ -2698,6 +2738,26 @@ async function buildConnectMenu(agent, deps, onProgress) {
2698
2738
  : "missing"
2699
2739
  : runtimeConfigReadStatus(runtimeConfig);
2700
2740
  const a2aStatus = a2aEnabled ? "ready" : "missing";
2741
+ const installedWorkbenchMcp = findInstalledWorkbenchMcp(deps, workbenchMcpCommand);
2742
+ const configuredWorkbenchMcpExists = cliPathExists(deps, workbenchMcpCommand);
2743
+ const workbenchStatus = workbenchEnabled && configuredWorkbenchMcpExists
2744
+ ? "ready"
2745
+ : workbenchEnabled || workbenchMcpCommand
2746
+ ? "needs attention"
2747
+ : installedWorkbenchMcp
2748
+ ? "not attached"
2749
+ : "missing";
2750
+ const workbenchDetailLines = [
2751
+ workbenchEnabled ? "senses.workbench.enabled = true" : "senses.workbench.enabled is not enabled",
2752
+ workbenchMcpCommand
2753
+ ? configuredWorkbenchMcpExists
2754
+ ? `MCP command registered: ${workbenchMcpCommand}`
2755
+ : `registered MCP command is missing: ${workbenchMcpCommand}`
2756
+ : "mcpServers.ouro_workbench is not registered",
2757
+ installedWorkbenchMcp
2758
+ ? `OuroWorkbenchMCP found: ${installedWorkbenchMcp}`
2759
+ : "OuroWorkbenchMCP not found in ~/Applications or /Applications",
2760
+ ];
2701
2761
  const entries = [
2702
2762
  {
2703
2763
  option: "1",
@@ -2826,6 +2886,20 @@ async function buildConnectMenu(agent, deps, onProgress) {
2826
2886
  status: a2aStatus,
2827
2887
  }) ? `ouro connect a2a --agent ${agent}` : undefined,
2828
2888
  },
2889
+ {
2890
+ option: "9",
2891
+ name: "Ouro Workbench",
2892
+ section: "This machine",
2893
+ status: workbenchStatus,
2894
+ description: "Native terminal-agent control room and local MCP sense.",
2895
+ detailLines: workbenchDetailLines,
2896
+ nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
2897
+ option: "9",
2898
+ name: "Ouro Workbench",
2899
+ section: "This machine",
2900
+ status: workbenchStatus,
2901
+ }) ? `ouro connect workbench --agent ${agent}` : undefined,
2902
+ },
2829
2903
  ];
2830
2904
  const isTTY = connectMenuIsTTY(deps);
2831
2905
  return (0, connect_bay_1.renderConnectBay)(entries, {
@@ -4364,6 +4438,8 @@ function connectMenuTarget(answer) {
4364
4438
  /* v8 ignore next -- direct `ouro connect a2a` covers behavior; interactive menu requires broader provider/vault readiness work @preserve */
4365
4439
  if (normalized === "8" || normalized === "a2a" || normalized === "agent2agent" || normalized === "agent-to-agent")
4366
4440
  return "a2a";
4441
+ if (normalized === "9" || normalized === "workbench" || normalized === "ouro-workbench" || normalized === "terminal-workbench")
4442
+ return "workbench";
4367
4443
  return "cancel";
4368
4444
  }
4369
4445
  async function executeConnectVoice(agent, deps) {
@@ -4421,6 +4497,30 @@ async function executeConnectA2A(agent, deps) {
4421
4497
  deps.writeStdout(message);
4422
4498
  return message;
4423
4499
  }
4500
+ async function executeConnectWorkbench(agent, deps) {
4501
+ const { workbenchMcpCommand } = readConnectBaySenseFlags(agent, deps);
4502
+ const executablePath = findInstalledWorkbenchMcp(deps, workbenchMcpCommand);
4503
+ if (!executablePath) {
4504
+ throw new Error([
4505
+ `Ouro Workbench is not installed for ${agent} on this machine.`,
4506
+ "Expected the MCP executable at one of:",
4507
+ ...defaultWorkbenchMcpCandidates(deps).map((candidate) => ` ${candidate}`),
4508
+ "Install or copy Ouro Workbench.app there, then run:",
4509
+ ` ouro connect workbench --agent ${agent}`,
4510
+ ].join("\n"));
4511
+ }
4512
+ writeWorkbenchMcpRegistration(agent, executablePath, deps);
4513
+ const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
4514
+ const message = [
4515
+ `Workbench connected for ${agent}`,
4516
+ "agent.json: senses.workbench.enabled = true",
4517
+ `agent.json: mcpServers.ouro_workbench.command = ${executablePath}`,
4518
+ "Workbench is a local machine sense; provider secrets stay in the agent vault.",
4519
+ ...(syncSummary ? [syncSummary] : []),
4520
+ ].join("\n");
4521
+ deps.writeStdout(message);
4522
+ return message;
4523
+ }
4424
4524
  async function executeConnect(command, deps) {
4425
4525
  if (command.target === "providers")
4426
4526
  return executeConnectProviders(command.agent, deps);
@@ -4438,6 +4538,8 @@ async function executeConnect(command, deps) {
4438
4538
  return executeConnectVoice(command.agent, deps);
4439
4539
  if (command.target === "a2a")
4440
4540
  return executeConnectA2A(command.agent, deps);
4541
+ if (command.target === "workbench")
4542
+ return executeConnectWorkbench(command.agent, deps);
4441
4543
  const progress = createHumanCommandProgress(deps, "connect");
4442
4544
  let menu;
4443
4545
  try {
@@ -4449,7 +4551,7 @@ async function executeConnect(command, deps) {
4449
4551
  const promptInput = deps.promptInput;
4450
4552
  if (!promptInput) {
4451
4553
  const message = [
4452
- menu.replace(/\nChoose \[1-8\] or type a name: $/, ""),
4554
+ menu.replace(/\nChoose \[1-9\] or type a name: $/, ""),
4453
4555
  "",
4454
4556
  `Run: ouro connect providers --agent ${command.agent}`,
4455
4557
  `Run: ouro connect perplexity --agent ${command.agent}`,
@@ -4459,6 +4561,7 @@ async function executeConnect(command, deps) {
4459
4561
  `Run: ouro connect mail --agent ${command.agent}`,
4460
4562
  `Run: ouro connect voice --agent ${command.agent}`,
4461
4563
  `Run: ouro connect a2a --agent ${command.agent}`,
4564
+ `Run: ouro connect workbench --agent ${command.agent}`,
4462
4565
  ].join("\n");
4463
4566
  deps.writeStdout(message);
4464
4567
  return message;
@@ -4481,6 +4584,8 @@ async function executeConnect(command, deps) {
4481
4584
  /* v8 ignore next -- direct `ouro connect a2a` covers behavior; interactive menu requires broader provider/vault readiness work @preserve */
4482
4585
  if (answer === "a2a")
4483
4586
  return executeConnectA2A(command.agent, deps);
4587
+ if (answer === "workbench")
4588
+ return executeConnectWorkbench(command.agent, deps);
4484
4589
  const message = "connect cancelled.";
4485
4590
  deps.writeStdout(message);
4486
4591
  return message;
@@ -7173,6 +7278,18 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7173
7278
  }
7174
7279
  }
7175
7280
  /* v8 ignore stop */
7281
+ // ── work card (local, no daemon socket needed) ──
7282
+ if (command.kind === "work.card") {
7283
+ const { buildWorkCard, formatWorkCardText } = await Promise.resolve().then(() => __importStar(require("../work-card")));
7284
+ if (!command.agent)
7285
+ throw new Error("work card requires --agent <name>");
7286
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
7287
+ const agentRoot = deps.agentBundleRoot ?? path.join(bundlesRoot, `${command.agent}.ouro`);
7288
+ const card = buildWorkCard(command.agent, agentRoot);
7289
+ const message = command.format === "json" ? JSON.stringify(card, null, 2) : formatWorkCardText(card);
7290
+ deps.writeStdout(message);
7291
+ return message;
7292
+ }
7176
7293
  // ── inner dialog status (local, no daemon socket needed) ──
7177
7294
  /* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
7178
7295
  if (command.kind === "inner.status") {
@@ -7182,6 +7299,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7182
7299
  const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
7183
7300
  const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../util/frontmatter")));
7184
7301
  const { listActiveReturnObligations } = await Promise.resolve().then(() => __importStar(require("../../arc/obligations")));
7302
+ const { resolveDeskRecordPaths } = await Promise.resolve().then(() => __importStar(require("../../mind/record-paths")));
7185
7303
  // Read runtime state
7186
7304
  const innerSessionPath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
7187
7305
  const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
@@ -7191,19 +7309,20 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7191
7309
  runtimeState = JSON.parse(raw);
7192
7310
  }
7193
7311
  catch { /* missing or corrupt — will show "unknown" */ }
7194
- // Read journal files
7195
- const journalDir = path.join(agentRoot, "journal");
7196
- let journalFiles = [];
7312
+ // Read canonical Desk record summary
7313
+ const recordPaths = resolveDeskRecordPaths(agentRoot);
7314
+ const recordSummary = { diaryFactCount: 0, noteCount: 0 };
7197
7315
  try {
7198
- const journalEntries = fs.readdirSync(journalDir, { withFileTypes: true });
7199
- journalFiles = journalEntries
7200
- .filter((e) => e.isFile() && !e.name.startsWith("."))
7201
- .map((e) => {
7202
- const stat = fs.statSync(path.join(journalDir, e.name));
7203
- return { name: e.name, mtimeMs: stat.mtimeMs };
7204
- });
7316
+ const rawFacts = fs.readFileSync(recordPaths.factsPath, "utf-8");
7317
+ recordSummary.diaryFactCount = rawFacts.split(/\r?\n/).filter((line) => line.trim().length > 0).length;
7318
+ }
7319
+ catch { /* missing facts file — count stays zero */ }
7320
+ try {
7321
+ recordSummary.noteCount = fs.readdirSync(recordPaths.notesRoot, { withFileTypes: true })
7322
+ .filter((e) => e.isFile() && !e.name.startsWith(".") && e.name.endsWith(".md"))
7323
+ .length;
7205
7324
  }
7206
- catch { /* missing dir — will show (empty) */ }
7325
+ catch { /* missing notes dir — count stays zero */ }
7207
7326
  // Read heartbeat cadence
7208
7327
  let heartbeat = null;
7209
7328
  try {
@@ -7238,7 +7357,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7238
7357
  const message = buildInnerStatusOutput({
7239
7358
  agentName: command.agent,
7240
7359
  runtimeState,
7241
- journalFiles,
7360
+ recordSummary,
7242
7361
  heartbeat,
7243
7362
  attentionCount: activeObligations.length,
7244
7363
  now: Date.now(),
@@ -151,6 +151,20 @@ 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 the agent's durable Work Card compiled from arc records.",
157
+ usage: "ouro work card [--agent <name>] [--format text|json|--json]",
158
+ example: "ouro work card --agent slugger --format json",
159
+ subcommands: ["card"],
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
+ },
154
168
  "migrate-to-desk": {
155
169
  category: "Tasks",
156
170
  description: "Migrate a legacy `tasks/` tree into the new `desk/` shape (copy semantics — source untouched).",
@@ -187,9 +201,9 @@ exports.COMMAND_REGISTRY = {
187
201
  connect: {
188
202
  category: "Auth",
189
203
  description: "Set up providers, portable integrations, and local senses from one guided screen",
190
- usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>]",
204
+ usage: "ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>]",
191
205
  example: "ouro connect",
192
- subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles", "mail", "voice", "a2a"],
206
+ subcommands: ["providers", "perplexity", "embeddings", "teams", "bluebubbles", "mail", "voice", "a2a", "workbench"],
193
207
  },
194
208
  a2a: {
195
209
  category: "Friends",
@@ -342,6 +356,11 @@ const SUBCOMMAND_HELP = {
342
356
  usage: "ouro connect a2a [--agent <name>]",
343
357
  example: "ouro connect a2a --agent <agent>",
344
358
  },
359
+ "connect workbench": {
360
+ description: "Attach native Ouro Workbench on this machine and register its MCP sense",
361
+ usage: "ouro connect workbench [--agent <name>]",
362
+ example: "ouro connect workbench --agent <agent>",
363
+ },
345
364
  "a2a card": {
346
365
  description: "Print this agent's A2A Agent Card",
347
366
  usage: "ouro a2a card [--agent <name>] [--base-url <url>] [--json]",
@@ -84,7 +84,7 @@ function usage() {
84
84
  " ouro -v|--version",
85
85
  " ouro auth [--agent <name>] [--provider <provider>]",
86
86
  " ouro account ensure [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
87
- " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
87
+ " ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]",
88
88
  " ouro mail import-mbox --file <path> [--owner-email <email>] [--source <label>] [--agent <name>] [--foreground]",
89
89
  " ouro mail backfill-indexes [--agent <name>] [--foreground]",
90
90
  " ouro auth verify [--agent <name>] [--provider <provider>]",
@@ -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 [--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>",
@@ -744,7 +745,9 @@ function normalizeConnectTarget(value) {
744
745
  return "voice";
745
746
  if (value === "a2a" || value === "agent2agent" || value === "agent-to-agent")
746
747
  return "a2a";
747
- throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>]");
748
+ if (value === "workbench" || value === "ouro-workbench" || value === "terminal-workbench")
749
+ return "workbench";
750
+ throw new Error("Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>]");
748
751
  }
749
752
  function extractMailSourceFlags(args, usageText) {
750
753
  const rest = [];
@@ -797,7 +800,7 @@ function extractMailSourceFlags(args, usageText) {
797
800
  };
798
801
  }
799
802
  function parseConnectCommand(args) {
800
- const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
803
+ const usageText = "Usage: ouro connect [providers|perplexity|embeddings|teams|bluebubbles|mail|voice|a2a|workbench] [--agent <name>] [--owner-email <email> --source <label>|--no-delegated-source] [--rotate-missing-mail-keys]";
801
804
  const { agent, rest: afterAgent } = extractAgentFlag(args);
802
805
  const mailFlags = extractMailSourceFlags(afterAgent, usageText);
803
806
  if (mailFlags.rest.length > 1)
@@ -998,6 +1001,29 @@ function parseAttentionCommand(args) {
998
1001
  }
999
1002
  return { kind: "attention.list", ...(agent ? { agent } : {}) };
1000
1003
  }
1004
+ function parseWorkCommand(args) {
1005
+ const { agent, rest: cleaned } = extractAgentFlag(args);
1006
+ const sub = cleaned[0];
1007
+ if (sub !== "card")
1008
+ throw new Error("Usage: ouro work card [--agent <name>] [--format text|json|--json]");
1009
+ let format = "text";
1010
+ for (let i = 1; i < cleaned.length; i += 1) {
1011
+ if (cleaned[i] === "--json") {
1012
+ format = "json";
1013
+ continue;
1014
+ }
1015
+ if (cleaned[i] === "--format" && cleaned[i + 1]) {
1016
+ const value = cleaned[++i];
1017
+ if (value !== "text" && value !== "json") {
1018
+ throw new Error("--format must be text or json");
1019
+ }
1020
+ format = value;
1021
+ continue;
1022
+ }
1023
+ throw new Error("Usage: ouro work card [--agent <name>] [--format text|json|--json]");
1024
+ }
1025
+ return { kind: "work.card", ...(agent ? { agent } : {}), ...(format !== "text" ? { format } : {}) };
1026
+ }
1001
1027
  function parseThoughtsCommand(args) {
1002
1028
  const { agent, rest: cleaned } = extractAgentFlag(args);
1003
1029
  let last;
@@ -1581,6 +1607,8 @@ function parseOuroCommand(args) {
1581
1607
  return parseThoughtsCommand(args.slice(1));
1582
1608
  if (head === "attention")
1583
1609
  return parseAttentionCommand(args.slice(1));
1610
+ if (head === "work")
1611
+ return parseWorkCommand(args.slice(1));
1584
1612
  if (head === "inner") {
1585
1613
  const { agent } = extractAgentFlag(args.slice(1));
1586
1614
  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}`,
@@ -37,10 +37,11 @@ exports.bundleMetaHook = bundleMetaHook;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const runtime_1 = require("../../../nerves/runtime");
40
+ const record_paths_1 = require("../../../mind/record-paths");
40
41
  /**
41
42
  * Migrate bundle from schema 1 to schema 2:
42
43
  * - Move state/{episodes,obligations,cares,intentions}/* to arc/{name}/*
43
- * - Move the old psyche note store to diary/
44
+ * - Move legacy record stores into desk/_record/
44
45
  * Idempotent: skips missing sources; on collision, newer mtime wins.
45
46
  */
46
47
  function migrateToSchema2(agentRoot) {
@@ -56,7 +57,7 @@ function migrateToSchema2(agentRoot) {
56
57
  const dest = path.join(agentRoot, "arc", name);
57
58
  migrateDirectory(src, dest);
58
59
  }
59
- // Migrate diary from the old pre-diary bundle layout.
60
+ // Stage legacy diary files for schema 3 to canonicalize into Desk record.
60
61
  const legacyDiarySrc = path.join(agentRoot, "psyche", "mem" + "ory");
61
62
  const diaryDest = path.join(agentRoot, "diary");
62
63
  migrateDirectory(legacyDiarySrc, diaryDest);
@@ -70,7 +71,7 @@ function migrateToSchema2(agentRoot) {
70
71
  });
71
72
  }
72
73
  /**
73
- * Ensure bundle .gitignore has state/ ignored and does NOT ignore arc/, diary/, journal/.
74
+ * Ensure bundle .gitignore has state/ ignored and does NOT ignore tracked record roots.
74
75
  */
75
76
  function updateBundleGitignore(agentRoot) {
76
77
  const gitignorePath = path.join(agentRoot, ".gitignore");
@@ -83,8 +84,8 @@ function updateBundleGitignore(agentRoot) {
83
84
  catch {
84
85
  // If we can't read, start fresh
85
86
  }
86
- // Remove arc/, diary/, journal/ from ignore (they should be tracked)
87
- const toRemove = new Set(["arc/", "diary/", "journal/"]);
87
+ // Remove tracked bundle roots from ignore.
88
+ const toRemove = new Set(["arc/", "desk/", "diary/", "journal/"]);
88
89
  lines = lines.filter((line) => !toRemove.has(line.trim()));
89
90
  // Ensure state/ is in the ignore list
90
91
  const hasState = lines.some((line) => line.trim() === "state/");
@@ -100,6 +101,10 @@ function updateBundleGitignore(agentRoot) {
100
101
  // Non-blocking: if we can't write .gitignore, migration still succeeds
101
102
  }
102
103
  }
104
+ function migrateToSchema3(agentRoot) {
105
+ (0, record_paths_1.migrateLegacyRecordStores)(agentRoot);
106
+ updateBundleGitignore(agentRoot);
107
+ }
103
108
  /**
104
109
  * Recursively copy files from src to dest.
105
110
  * Creates destination directories as needed. Skips if source doesn't exist.
@@ -165,14 +170,29 @@ async function bundleMetaHook(ctx) {
165
170
  // Malformed JSON -- treat as missing, will overwrite with fresh
166
171
  existing = undefined;
167
172
  }
168
- // Run schema-2 migration if needed
173
+ // Run schema migrations if needed.
169
174
  const currentSchema = existing?.bundleSchemaVersion ?? 1;
170
- if (currentSchema < 2) {
171
- migrateToSchema2(ctx.agentRoot);
175
+ try {
176
+ if (currentSchema < 2) {
177
+ migrateToSchema2(ctx.agentRoot);
178
+ }
179
+ if (currentSchema < 3) {
180
+ migrateToSchema3(ctx.agentRoot);
181
+ }
182
+ }
183
+ catch (err) {
184
+ const errorMessage = err instanceof Error ? err.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(err);
185
+ (0, runtime_1.emitNervesEvent)({
186
+ component: "daemon",
187
+ event: "daemon.bundle_meta_hook_error",
188
+ message: "bundle-meta hook migration failed",
189
+ meta: { agentRoot: ctx.agentRoot, error: errorMessage },
190
+ });
191
+ return { ok: false, error: errorMessage };
172
192
  }
173
193
  const updated = {
174
194
  runtimeVersion: ctx.currentVersion,
175
- bundleSchemaVersion: currentSchema < 2 ? 2 : currentSchema,
195
+ bundleSchemaVersion: currentSchema < 3 ? 3 : currentSchema,
176
196
  lastUpdated: new Date().toISOString(),
177
197
  };
178
198
  // Save old runtimeVersion as previousRuntimeVersion (if there was one)