@ouro.bot/cli 0.1.0-alpha.15 → 0.1.0-alpha.151

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 (150) hide show
  1. package/AdoptionSpecialist.ouro/psyche/SOUL.md +2 -2
  2. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  3. package/README.md +185 -191
  4. package/changelog.json +991 -0
  5. package/dist/heart/active-work.js +633 -0
  6. package/dist/heart/bridges/manager.js +358 -0
  7. package/dist/heart/bridges/state-machine.js +135 -0
  8. package/dist/heart/bridges/store.js +123 -0
  9. package/dist/heart/commitments.js +105 -0
  10. package/dist/heart/config.js +66 -21
  11. package/dist/heart/core.js +645 -112
  12. package/dist/heart/cross-chat-delivery.js +146 -0
  13. package/dist/heart/daemon/agent-discovery.js +81 -0
  14. package/dist/heart/daemon/agent-service.js +360 -0
  15. package/dist/heart/daemon/auth-flow.js +457 -0
  16. package/dist/heart/daemon/cadence.js +70 -0
  17. package/dist/heart/daemon/daemon-cli.js +2301 -205
  18. package/dist/heart/daemon/daemon-entry.js +131 -4
  19. package/dist/heart/daemon/daemon-runtime-sync.js +212 -0
  20. package/dist/heart/daemon/daemon-tombstone.js +106 -0
  21. package/dist/heart/daemon/daemon.js +372 -3
  22. package/dist/heart/daemon/habit-migration.js +181 -0
  23. package/dist/heart/daemon/habit-parser.js +119 -0
  24. package/dist/heart/daemon/habit-scheduler.js +252 -0
  25. package/dist/heart/daemon/hatch-animation.js +10 -3
  26. package/dist/heart/daemon/hatch-flow.js +28 -111
  27. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  28. package/dist/heart/daemon/inner-status.js +89 -0
  29. package/dist/heart/daemon/launchd.js +159 -0
  30. package/dist/heart/daemon/log-tailer.js +4 -3
  31. package/dist/heart/daemon/mcp-server.js +591 -0
  32. package/dist/heart/daemon/message-router.js +17 -8
  33. package/dist/heart/daemon/os-cron-deps.js +134 -0
  34. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  35. package/dist/heart/daemon/ouro-path-installer.js +78 -34
  36. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  37. package/dist/heart/daemon/process-manager.js +75 -0
  38. package/dist/heart/daemon/run-hooks.js +37 -0
  39. package/dist/heart/daemon/runtime-logging.js +58 -15
  40. package/dist/heart/daemon/runtime-metadata.js +219 -0
  41. package/dist/heart/daemon/runtime-mode.js +67 -0
  42. package/dist/heart/daemon/sense-manager.js +50 -2
  43. package/dist/heart/daemon/session-id-resolver.js +131 -0
  44. package/dist/heart/daemon/skill-management-installer.js +94 -0
  45. package/dist/heart/daemon/socket-client.js +202 -0
  46. package/dist/heart/daemon/specialist-orchestrator.js +2 -2
  47. package/dist/heart/daemon/specialist-prompt.js +10 -7
  48. package/dist/heart/daemon/specialist-tools.js +53 -4
  49. package/dist/heart/daemon/staged-restart.js +114 -0
  50. package/dist/heart/daemon/task-scheduler.js +3 -25
  51. package/dist/heart/daemon/thoughts.js +507 -0
  52. package/dist/heart/daemon/update-checker.js +111 -0
  53. package/dist/heart/daemon/update-hooks.js +138 -0
  54. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  55. package/dist/heart/delegation.js +62 -0
  56. package/dist/heart/identity.js +64 -21
  57. package/dist/heart/kicks.js +2 -20
  58. package/dist/heart/model-capabilities.js +48 -0
  59. package/dist/heart/obligations.js +197 -0
  60. package/dist/heart/progress-story.js +42 -0
  61. package/dist/heart/provider-failover.js +88 -0
  62. package/dist/heart/provider-ping.js +159 -0
  63. package/dist/heart/providers/anthropic-token.js +163 -0
  64. package/dist/heart/providers/anthropic.js +195 -34
  65. package/dist/heart/providers/azure.js +115 -9
  66. package/dist/heart/providers/github-copilot.js +157 -0
  67. package/dist/heart/providers/minimax.js +33 -3
  68. package/dist/heart/providers/openai-codex.js +49 -14
  69. package/dist/heart/session-activity.js +173 -0
  70. package/dist/heart/session-recall.js +216 -0
  71. package/dist/heart/streaming.js +117 -33
  72. package/dist/heart/target-resolution.js +127 -0
  73. package/dist/heart/tool-loop.js +200 -0
  74. package/dist/heart/turn-coordinator.js +28 -0
  75. package/dist/mind/associative-recall.js +122 -32
  76. package/dist/mind/bundle-manifest.js +14 -0
  77. package/dist/mind/context.js +125 -14
  78. package/dist/mind/{memory.js → diary.js} +70 -56
  79. package/dist/mind/first-impressions.js +16 -2
  80. package/dist/mind/friends/channel.js +44 -0
  81. package/dist/mind/friends/group-context.js +144 -0
  82. package/dist/mind/friends/store-file.js +19 -0
  83. package/dist/mind/friends/trust-explanation.js +74 -0
  84. package/dist/mind/friends/types.js +8 -0
  85. package/dist/mind/journal-index.js +161 -0
  86. package/dist/mind/obligation-steering.js +221 -0
  87. package/dist/mind/obligations.js +134 -0
  88. package/dist/mind/pending.js +76 -9
  89. package/dist/mind/phrases.js +1 -0
  90. package/dist/mind/prompt.js +612 -88
  91. package/dist/mind/token-estimate.js +8 -12
  92. package/dist/nerves/cli-logging.js +15 -2
  93. package/dist/nerves/coverage/run-artifacts.js +1 -1
  94. package/dist/nerves/index.js +59 -2
  95. package/dist/nerves/runtime.js +5 -1
  96. package/dist/repertoire/ado-client.js +4 -2
  97. package/dist/repertoire/ado-semantic.js +6 -0
  98. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  99. package/dist/repertoire/coding/context-pack.js +254 -0
  100. package/dist/repertoire/coding/feedback.js +301 -0
  101. package/dist/repertoire/coding/index.js +4 -1
  102. package/dist/repertoire/coding/manager.js +210 -4
  103. package/dist/repertoire/coding/spawner.js +58 -12
  104. package/dist/repertoire/coding/tools.js +176 -4
  105. package/dist/repertoire/data/ado-endpoints.json +188 -0
  106. package/dist/repertoire/guardrails.js +290 -0
  107. package/dist/repertoire/mcp-client.js +254 -0
  108. package/dist/repertoire/mcp-manager.js +198 -0
  109. package/dist/repertoire/skills.js +14 -23
  110. package/dist/repertoire/tasks/board.js +12 -0
  111. package/dist/repertoire/tasks/index.js +23 -9
  112. package/dist/repertoire/tasks/parser.js +0 -2
  113. package/dist/repertoire/tasks/transitions.js +1 -20
  114. package/dist/repertoire/tools-base.js +990 -273
  115. package/dist/repertoire/tools-bluebubbles.js +94 -0
  116. package/dist/repertoire/tools-github.js +1 -6
  117. package/dist/repertoire/tools-teams.js +65 -59
  118. package/dist/repertoire/tools.js +281 -91
  119. package/dist/scripts/claude-code-hook.js +41 -0
  120. package/dist/scripts/claude-code-stop-hook.js +47 -0
  121. package/dist/senses/attention-queue.js +97 -0
  122. package/dist/senses/bluebubbles-client.js +258 -5
  123. package/dist/senses/bluebubbles-entry.js +2 -0
  124. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  125. package/dist/senses/bluebubbles-media.js +339 -0
  126. package/dist/senses/bluebubbles-model.js +12 -4
  127. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  128. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  129. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  130. package/dist/senses/bluebubbles.js +981 -45
  131. package/dist/senses/cli-entry.js +1 -1
  132. package/dist/senses/cli-layout.js +187 -0
  133. package/dist/senses/cli.js +374 -131
  134. package/dist/senses/contextual-heartbeat.js +106 -0
  135. package/dist/senses/continuity.js +94 -0
  136. package/dist/senses/debug-activity.js +154 -0
  137. package/dist/senses/inner-dialog-worker.js +112 -19
  138. package/dist/senses/inner-dialog.js +621 -84
  139. package/dist/senses/pipeline.js +463 -0
  140. package/dist/senses/shared-turn.js +196 -0
  141. package/dist/senses/surface-tool.js +95 -0
  142. package/dist/senses/teams.js +796 -141
  143. package/dist/senses/trust-gate.js +112 -2
  144. package/package.json +12 -4
  145. package/skills/configure-dev-tools.md +81 -0
  146. package/subagents/README.md +4 -70
  147. package/dist/heart/daemon/subagent-installer.js +0 -134
  148. package/subagents/work-doer.md +0 -233
  149. package/subagents/work-merger.md +0 -624
  150. package/subagents/work-planner.md +0 -373
@@ -34,26 +34,46 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ensureDaemonRunning = ensureDaemonRunning;
37
+ exports.listGithubCopilotModels = listGithubCopilotModels;
38
+ exports.pingGithubCopilotModel = pingGithubCopilotModel;
37
39
  exports.parseOuroCommand = parseOuroCommand;
40
+ exports.readFirstBundleMetaVersion = readFirstBundleMetaVersion;
38
41
  exports.discoverExistingCredentials = discoverExistingCredentials;
39
42
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
40
43
  exports.runOuroCli = runOuroCli;
41
44
  const child_process_1 = require("child_process");
45
+ const crypto_1 = require("crypto");
42
46
  const fs = __importStar(require("fs"));
43
- const net = __importStar(require("net"));
44
47
  const os = __importStar(require("os"));
45
48
  const path = __importStar(require("path"));
49
+ const semver = __importStar(require("semver"));
46
50
  const identity_1 = require("../identity");
47
51
  const runtime_1 = require("../../nerves/runtime");
48
52
  const store_file_1 = require("../../mind/friends/store-file");
49
53
  const types_1 = require("../../mind/friends/types");
50
54
  const ouro_uti_1 = require("./ouro-uti");
51
55
  const ouro_path_installer_1 = require("./ouro-path-installer");
52
- const subagent_installer_1 = require("./subagent-installer");
56
+ const ouro_version_manager_1 = require("./ouro-version-manager");
57
+ const skill_management_installer_1 = require("./skill-management-installer");
53
58
  const hatch_flow_1 = require("./hatch-flow");
54
59
  const specialist_orchestrator_1 = require("./specialist-orchestrator");
55
60
  const specialist_prompt_1 = require("./specialist-prompt");
56
61
  const specialist_tools_1 = require("./specialist-tools");
62
+ const runtime_metadata_1 = require("./runtime-metadata");
63
+ const runtime_mode_1 = require("./runtime-mode");
64
+ const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
65
+ const agent_discovery_1 = require("./agent-discovery");
66
+ const update_hooks_1 = require("./update-hooks");
67
+ const bundle_meta_1 = require("./hooks/bundle-meta");
68
+ const bundle_manifest_1 = require("../../mind/bundle-manifest");
69
+ const tasks_1 = require("../../repertoire/tasks");
70
+ const thoughts_1 = require("./thoughts");
71
+ const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
72
+ const launchd_1 = require("./launchd");
73
+ const socket_client_1 = require("./socket-client");
74
+ const daemon_tombstone_1 = require("./daemon-tombstone");
75
+ const session_activity_1 = require("../session-activity");
76
+ const auth_flow_1 = require("./auth-flow");
57
77
  function stringField(value) {
58
78
  return typeof value === "string" ? value : null;
59
79
  }
@@ -78,8 +98,14 @@ function parseStatusPayload(data) {
78
98
  daemon: stringField(overview.daemon) ?? "unknown",
79
99
  health: stringField(overview.health) ?? "unknown",
80
100
  socketPath: stringField(overview.socketPath) ?? "unknown",
101
+ version: stringField(overview.version) ?? "unknown",
102
+ lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
103
+ repoRoot: stringField(overview.repoRoot) ?? "unknown",
104
+ configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
81
105
  workerCount: numberField(overview.workerCount) ?? 0,
82
106
  senseCount: numberField(overview.senseCount) ?? 0,
107
+ entryPath: stringField(overview.entryPath) ?? "unknown",
108
+ mode: stringField(overview.mode) ?? "unknown",
83
109
  };
84
110
  const parsedSenses = senses.map((entry) => {
85
111
  if (!entry || typeof entry !== "object" || Array.isArray(entry))
@@ -120,6 +146,8 @@ function parseStatusPayload(data) {
120
146
  status,
121
147
  pid,
122
148
  restartCount,
149
+ lastExitCode: numberField(row.lastExitCode) ?? null,
150
+ lastSignal: stringField(row.lastSignal) ?? null,
123
151
  };
124
152
  });
125
153
  if (parsedSenses.some((row) => row === null) || parsedWorkers.some((row) => row === null))
@@ -158,6 +186,10 @@ function formatDaemonStatusOutput(response, fallback) {
158
186
  const overviewRows = [
159
187
  ["Daemon", payload.overview.daemon],
160
188
  ["Socket", payload.overview.socketPath],
189
+ ["Version", payload.overview.version],
190
+ ["Last Updated", payload.overview.lastUpdated],
191
+ ["Entry Path", payload.overview.entryPath],
192
+ ["Mode", payload.overview.mode],
161
193
  ["Workers", String(payload.overview.workerCount)],
162
194
  ["Senses", String(payload.overview.senseCount)],
163
195
  ["Health", payload.overview.health],
@@ -169,13 +201,23 @@ function formatDaemonStatusOutput(response, fallback) {
169
201
  row.status,
170
202
  row.detail,
171
203
  ]);
172
- const workerRows = payload.workers.map((row) => [
173
- row.agent,
174
- row.worker,
175
- row.status,
176
- row.pid === null ? "n/a" : String(row.pid),
177
- String(row.restartCount),
178
- ]);
204
+ const workerRows = payload.workers.map((row) => {
205
+ /* v8 ignore start — exit info branches tested via daemon-crash-context; v8 misreports conditional chains @preserve */
206
+ let exitInfo = "n/a";
207
+ if (row.lastExitCode !== null)
208
+ exitInfo = `code=${row.lastExitCode}`;
209
+ if (row.lastSignal !== null)
210
+ exitInfo = row.lastExitCode !== null ? `code=${row.lastExitCode} sig=${row.lastSignal}` : `sig=${row.lastSignal}`;
211
+ /* v8 ignore stop */
212
+ return [
213
+ row.agent,
214
+ row.worker,
215
+ row.status,
216
+ row.pid === null ? "n/a" : String(row.pid),
217
+ String(row.restartCount),
218
+ exitInfo,
219
+ ];
220
+ });
179
221
  return [
180
222
  "Overview",
181
223
  formatTable(["Item", "Value"], overviewRows),
@@ -184,16 +226,41 @@ function formatDaemonStatusOutput(response, fallback) {
184
226
  formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
185
227
  "",
186
228
  "Workers",
187
- formatTable(["Agent", "Worker", "State", "PID", "Restarts"], workerRows),
229
+ formatTable(["Agent", "Worker", "State", "PID", "Restarts", "Last Exit"], workerRows),
188
230
  ].join("\n");
189
231
  }
190
232
  async function ensureDaemonRunning(deps) {
191
233
  const alive = await deps.checkSocketAlive(deps.socketPath);
192
234
  if (alive) {
193
- return {
194
- alreadyRunning: true,
195
- message: `daemon already running (${deps.socketPath})`,
235
+ const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
236
+ let runningRuntimePromise = null;
237
+ const fetchRunningRuntimeMetadata = async () => {
238
+ runningRuntimePromise ??= (async () => {
239
+ const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
240
+ const payload = parseStatusPayload(status.data);
241
+ return {
242
+ version: payload?.overview.version ?? "unknown",
243
+ lastUpdated: payload?.overview.lastUpdated ?? "unknown",
244
+ repoRoot: payload?.overview.repoRoot ?? "unknown",
245
+ configFingerprint: payload?.overview.configFingerprint ?? "unknown",
246
+ };
247
+ })();
248
+ return runningRuntimePromise;
196
249
  };
250
+ return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
251
+ socketPath: deps.socketPath,
252
+ localVersion: localRuntime.version,
253
+ localLastUpdated: localRuntime.lastUpdated,
254
+ localRepoRoot: localRuntime.repoRoot,
255
+ localConfigFingerprint: localRuntime.configFingerprint,
256
+ fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
257
+ fetchRunningRuntimeMetadata,
258
+ stopDaemon: async () => {
259
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
260
+ },
261
+ cleanupStaleSocket: deps.cleanupStaleSocket,
262
+ startDaemonProcess: deps.startDaemonProcess,
263
+ });
197
264
  }
198
265
  deps.cleanupStaleSocket(deps.socketPath);
199
266
  const started = await deps.startDaemonProcess(deps.socketPath);
@@ -202,17 +269,117 @@ async function ensureDaemonRunning(deps) {
202
269
  message: `daemon started (pid ${started.pid ?? "unknown"})`,
203
270
  };
204
271
  }
272
+ /**
273
+ * Extract `--agent <name>` from an args array, returning the agent name and
274
+ * the remaining args with the flag pair removed.
275
+ */
276
+ function extractAgentFlag(args) {
277
+ const idx = args.indexOf("--agent");
278
+ if (idx === -1 || idx + 1 >= args.length)
279
+ return { rest: args };
280
+ const agent = args[idx + 1];
281
+ const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
282
+ return { agent, rest };
283
+ }
205
284
  function usage() {
206
285
  return [
207
286
  "Usage:",
208
287
  " ouro [up]",
209
- " ouro stop|status|logs|hatch",
288
+ " ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
289
+ " ouro stop|down|status|logs|hatch",
290
+ " ouro -v|--version",
291
+ " ouro config model --agent <name> <model-name>",
292
+ " ouro config models --agent <name>",
293
+ " ouro auth --agent <name> [--provider <provider>]",
294
+ " ouro auth verify --agent <name> [--provider <provider>]",
295
+ " ouro auth switch --agent <name> --provider <provider>",
210
296
  " ouro chat <agent>",
211
297
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
212
298
  " ouro poke <agent> --task <task-id>",
299
+ " ouro poke <agent> --habit <name>",
300
+ " ouro habit list [--agent <name>]",
301
+ " ouro habit create [--agent <name>] <name> [--cadence <interval>]",
213
302
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
303
+ " ouro task board [<status>] [--agent <name>]",
304
+ " ouro task create <title> [--type <type>] [--agent <name>]",
305
+ " ouro task update <id> <status> [--agent <name>]",
306
+ " ouro task show <id> [--agent <name>]",
307
+ " ouro task actionable|deps|sessions [--agent <name>]",
308
+ " ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
309
+ " ouro friend list [--agent <name>]",
310
+ " ouro friend show <id> [--agent <name>]",
311
+ " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
312
+ " ouro friend update <id> --trust <level> [--agent <name>]",
313
+ " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
314
+ " ouro inner [--agent <name>]",
315
+ " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
316
+ " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
317
+ " ouro whoami [--agent <name>]",
318
+ " ouro session list [--agent <name>]",
319
+ " ouro mcp list",
320
+ " ouro mcp call <server> <tool> [--args '{...}']",
321
+ " ouro rollback [<version>]",
322
+ " ouro versions",
214
323
  ].join("\n");
215
324
  }
325
+ function formatVersionOutput() {
326
+ const version = (0, runtime_metadata_1.getRuntimeMetadata)().version;
327
+ const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
328
+ /* v8 ignore start — cosmetic display toggle; dev mode always true in test env */
329
+ return mode === "dev" ? `${version} (dev)` : version;
330
+ /* v8 ignore stop */
331
+ }
332
+ function buildStoppedStatusPayload(socketPath) {
333
+ const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
334
+ const repoRoot = (0, identity_1.getRepoRoot)();
335
+ return {
336
+ overview: {
337
+ daemon: "stopped",
338
+ health: "warn",
339
+ socketPath,
340
+ version: metadata.version,
341
+ lastUpdated: metadata.lastUpdated,
342
+ repoRoot: metadata.repoRoot,
343
+ configFingerprint: metadata.configFingerprint,
344
+ workerCount: 0,
345
+ senseCount: 0,
346
+ entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
347
+ mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
348
+ },
349
+ senses: [],
350
+ workers: [],
351
+ };
352
+ }
353
+ function daemonUnavailableStatusOutput(socketPath) {
354
+ /* v8 ignore start — tombstone read tested in daemon-status-tombstone.test; branch misreported @preserve */
355
+ const tombstone = (0, daemon_tombstone_1.readDaemonTombstone)();
356
+ const deathLine = tombstone
357
+ ? `Last death: ${tombstone.timestamp} -- ${tombstone.reason}: ${tombstone.message}`
358
+ : null;
359
+ /* v8 ignore stop */
360
+ const lines = [
361
+ formatDaemonStatusOutput({
362
+ ok: true,
363
+ summary: "daemon not running",
364
+ data: buildStoppedStatusPayload(socketPath),
365
+ }, "daemon not running"),
366
+ "",
367
+ ];
368
+ /* v8 ignore start — tombstone presence requires real daemon crash @preserve */
369
+ if (deathLine) {
370
+ lines.push(deathLine);
371
+ lines.push("");
372
+ /* v8 ignore stop */
373
+ }
374
+ lines.push("daemon not running; run `ouro up`");
375
+ return lines.join("\n");
376
+ }
377
+ function isDaemonUnavailableError(error) {
378
+ const code = typeof error === "object" && error !== null && "code" in error
379
+ ? String(error.code ?? "")
380
+ : "";
381
+ return code === "ENOENT" || code === "ECONNREFUSED";
382
+ }
216
383
  function parseMessageCommand(args) {
217
384
  let to;
218
385
  let sessionId;
@@ -254,17 +421,56 @@ function parsePokeCommand(args) {
254
421
  if (!agent)
255
422
  throw new Error(`Usage\n${usage()}`);
256
423
  let taskId;
424
+ let habitName;
257
425
  for (let i = 1; i < args.length; i += 1) {
258
426
  if (args[i] === "--task") {
259
427
  taskId = args[i + 1];
260
428
  i += 1;
261
429
  }
430
+ if (args[i] === "--habit") {
431
+ habitName = args[i + 1];
432
+ i += 1;
433
+ }
262
434
  }
435
+ // --habit takes priority over --task
436
+ if (habitName)
437
+ return { kind: "habit.poke", agent, habitName };
263
438
  if (!taskId)
264
439
  throw new Error(`Usage\n${usage()}`);
265
440
  return { kind: "task.poke", agent, taskId };
266
441
  }
267
- function parseLinkCommand(args) {
442
+ function parseHabitCommand(args) {
443
+ const { agent, rest } = extractAgentFlag(args);
444
+ const sub = rest[0];
445
+ if (sub === "list") {
446
+ return { kind: "habit.list", ...(agent ? { agent } : {}) };
447
+ }
448
+ if (sub === "create") {
449
+ const nameArgs = rest.slice(1);
450
+ let name;
451
+ let cadence;
452
+ const positional = [];
453
+ for (let i = 0; i < nameArgs.length; i++) {
454
+ if (nameArgs[i] === "--cadence" && nameArgs[i + 1]) {
455
+ cadence = nameArgs[++i];
456
+ continue;
457
+ }
458
+ /* v8 ignore start -- defensive: --agent already extracted by extractAgentFlag; guard prevents regression if parsing flow changes @preserve */
459
+ if (nameArgs[i] === "--agent" && nameArgs[i + 1]) {
460
+ i++; // skip --agent value (already extracted)
461
+ continue;
462
+ }
463
+ /* v8 ignore stop */
464
+ positional.push(nameArgs[i]);
465
+ }
466
+ name = positional[0];
467
+ if (!name)
468
+ throw new Error(`Usage\n${usage()}`);
469
+ return { kind: "habit.create", name, ...(agent ? { agent } : {}), ...(cadence ? { cadence } : {}) };
470
+ }
471
+ throw new Error(`Usage\n${usage()}`);
472
+ }
473
+ function parseLinkCommand(args, kind = "friend.link") {
268
474
  const agent = args[0];
269
475
  if (!agent)
270
476
  throw new Error(`Usage\n${usage()}`);
@@ -296,7 +502,7 @@ function parseLinkCommand(args) {
296
502
  throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
297
503
  }
298
504
  return {
299
- kind: "friend.link",
505
+ kind,
300
506
  agent,
301
507
  friendId,
302
508
  provider: providerRaw,
@@ -304,7 +510,102 @@ function parseLinkCommand(args) {
304
510
  };
305
511
  }
306
512
  function isAgentProvider(value) {
307
- return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex";
513
+ return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
514
+ }
515
+ /* v8 ignore start -- hasStoredCredentials: per-provider branches tested via auth switch tests @preserve */
516
+ function hasStoredCredentials(provider, providerSecrets) {
517
+ if (provider === "anthropic")
518
+ return !!providerSecrets.setupToken;
519
+ if (provider === "openai-codex")
520
+ return !!providerSecrets.oauthAccessToken;
521
+ if (provider === "github-copilot")
522
+ return !!providerSecrets.githubToken;
523
+ if (provider === "minimax")
524
+ return !!providerSecrets.apiKey;
525
+ // azure
526
+ return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
527
+ }
528
+ /* v8 ignore stop */
529
+ /* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
530
+ async function verifyProviderCredentials(provider, providers) {
531
+ const config = providers[provider];
532
+ if (!config)
533
+ return "not configured";
534
+ try {
535
+ const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../../heart/provider-ping")));
536
+ const result = await pingProvider(provider, config);
537
+ return result.ok ? "ok" : `failed (${result.message})`;
538
+ }
539
+ catch (error) {
540
+ return `failed (${error instanceof Error ? error.message : String(error)})`;
541
+ }
542
+ }
543
+ async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
544
+ const url = `${baseUrl.replace(/\/+$/, "")}/models`;
545
+ const response = await fetchImpl(url, {
546
+ headers: { Authorization: `Bearer ${token}` },
547
+ });
548
+ if (!response.ok) {
549
+ throw new Error(`model listing failed (HTTP ${response.status})`);
550
+ }
551
+ const body = await response.json();
552
+ /* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
553
+ const items = Array.isArray(body) ? body : (body?.data ?? []);
554
+ return items.map((item) => {
555
+ const rec = item;
556
+ const capabilities = Array.isArray(rec.capabilities)
557
+ ? rec.capabilities.filter((c) => typeof c === "string")
558
+ : undefined;
559
+ return {
560
+ id: String(rec.id ?? rec.name ?? ""),
561
+ name: String(rec.name ?? rec.id ?? ""),
562
+ ...(capabilities ? { capabilities } : {}),
563
+ };
564
+ });
565
+ /* v8 ignore stop */
566
+ }
567
+ async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
568
+ const base = baseUrl.replace(/\/+$/, "");
569
+ const isClaude = model.startsWith("claude");
570
+ const url = isClaude ? `${base}/chat/completions` : `${base}/responses`;
571
+ const body = isClaude
572
+ ? JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 })
573
+ : JSON.stringify({ model, input: "ping", max_output_tokens: 16 });
574
+ try {
575
+ const response = await fetchImpl(url, {
576
+ method: "POST",
577
+ headers: {
578
+ Authorization: `Bearer ${token}`,
579
+ "Content-Type": "application/json",
580
+ },
581
+ body,
582
+ });
583
+ if (response.ok)
584
+ return { ok: true };
585
+ let detail = `HTTP ${response.status}`;
586
+ try {
587
+ const json = await response.json();
588
+ /* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
589
+ if (typeof json.error === "string")
590
+ detail = json.error;
591
+ else if (typeof json.error === "object" && json.error !== null) {
592
+ const errObj = json.error;
593
+ if (typeof errObj.message === "string")
594
+ detail = errObj.message;
595
+ }
596
+ else if (typeof json.message === "string")
597
+ detail = json.message;
598
+ /* v8 ignore stop */
599
+ }
600
+ catch {
601
+ // response body not JSON — keep HTTP status
602
+ }
603
+ return { ok: false, error: detail };
604
+ }
605
+ catch (err) {
606
+ /* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
607
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
608
+ }
308
609
  }
309
610
  function parseHatchCommand(args) {
310
611
  let agentName;
@@ -361,7 +662,7 @@ function parseHatchCommand(args) {
361
662
  }
362
663
  }
363
664
  if (providerRaw && !isAgentProvider(providerRaw)) {
364
- throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex.");
665
+ throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex|github-copilot.");
365
666
  }
366
667
  const provider = providerRaw && isAgentProvider(providerRaw) ? providerRaw : undefined;
367
668
  return {
@@ -373,13 +674,395 @@ function parseHatchCommand(args) {
373
674
  migrationPath,
374
675
  };
375
676
  }
677
+ function parseTaskCommand(args) {
678
+ const { agent, rest: cleaned } = extractAgentFlag(args);
679
+ const [sub, ...rest] = cleaned;
680
+ if (!sub)
681
+ throw new Error(`Usage\n${usage()}`);
682
+ if (sub === "board") {
683
+ const status = rest[0];
684
+ return status
685
+ ? { kind: "task.board", status, ...(agent ? { agent } : {}) }
686
+ : { kind: "task.board", ...(agent ? { agent } : {}) };
687
+ }
688
+ if (sub === "create") {
689
+ const title = rest[0];
690
+ if (!title)
691
+ throw new Error(`Usage\n${usage()}`);
692
+ let type;
693
+ for (let i = 1; i < rest.length; i++) {
694
+ if (rest[i] === "--type" && rest[i + 1]) {
695
+ type = rest[i + 1];
696
+ i += 1;
697
+ }
698
+ }
699
+ return type
700
+ ? { kind: "task.create", title, type, ...(agent ? { agent } : {}) }
701
+ : { kind: "task.create", title, ...(agent ? { agent } : {}) };
702
+ }
703
+ if (sub === "update") {
704
+ const id = rest[0];
705
+ const status = rest[1];
706
+ if (!id || !status)
707
+ throw new Error(`Usage\n${usage()}`);
708
+ return { kind: "task.update", id, status, ...(agent ? { agent } : {}) };
709
+ }
710
+ if (sub === "show") {
711
+ const id = rest[0];
712
+ if (!id)
713
+ throw new Error(`Usage\n${usage()}`);
714
+ return { kind: "task.show", id, ...(agent ? { agent } : {}) };
715
+ }
716
+ if (sub === "actionable")
717
+ return { kind: "task.actionable", ...(agent ? { agent } : {}) };
718
+ if (sub === "deps")
719
+ return { kind: "task.deps", ...(agent ? { agent } : {}) };
720
+ if (sub === "sessions")
721
+ return { kind: "task.sessions", ...(agent ? { agent } : {}) };
722
+ throw new Error(`Usage\n${usage()}`);
723
+ }
724
+ function parseAuthCommand(args) {
725
+ const first = args[0];
726
+ // Support both positional (`auth switch`) and flag (`auth --switch`) forms
727
+ if (first === "verify" || first === "switch" || first === "--verify" || first === "--switch") {
728
+ const subcommand = first.replace(/^--/, "");
729
+ const { agent, rest } = extractAgentFlag(args.slice(1));
730
+ let provider;
731
+ /* v8 ignore start -- provider flag parsing: branches tested via CLI parsing tests @preserve */
732
+ for (let i = 0; i < rest.length; i += 1) {
733
+ if (rest[i] === "--provider") {
734
+ const value = rest[i + 1];
735
+ if (!isAgentProvider(value))
736
+ throw new Error(`Usage\n${usage()}`);
737
+ provider = value;
738
+ i += 1;
739
+ continue;
740
+ }
741
+ }
742
+ /* v8 ignore stop */
743
+ /* v8 ignore next -- defensive: agent always provided in tests @preserve */
744
+ if (!agent)
745
+ throw new Error(`Usage\n${usage()}`);
746
+ if (subcommand === "switch") {
747
+ if (!provider)
748
+ throw new Error(`auth switch requires --provider.\n${usage()}`);
749
+ return { kind: "auth.switch", agent, provider };
750
+ }
751
+ return provider ? { kind: "auth.verify", agent, provider } : { kind: "auth.verify", agent };
752
+ }
753
+ const { agent, rest } = extractAgentFlag(args);
754
+ let provider;
755
+ for (let i = 0; i < rest.length; i += 1) {
756
+ if (rest[i] === "--provider") {
757
+ const value = rest[i + 1];
758
+ if (!isAgentProvider(value))
759
+ throw new Error(`Usage\n${usage()}`);
760
+ provider = value;
761
+ i += 1;
762
+ continue;
763
+ }
764
+ }
765
+ if (!agent) {
766
+ throw new Error([
767
+ "Usage:",
768
+ " ouro auth --agent <name> [--provider <provider>] Set up credentials",
769
+ " ouro auth verify --agent <name> [--provider <p>] Verify credentials work",
770
+ " ouro auth switch --agent <name> --provider <p> Switch active provider",
771
+ ].join("\n"));
772
+ }
773
+ return provider ? { kind: "auth.run", agent, provider } : { kind: "auth.run", agent };
774
+ }
775
+ function parseReminderCommand(args) {
776
+ const { agent, rest: cleaned } = extractAgentFlag(args);
777
+ const [sub, ...rest] = cleaned;
778
+ if (!sub)
779
+ throw new Error(`Usage\n${usage()}`);
780
+ if (sub === "create") {
781
+ const title = rest[0];
782
+ if (!title)
783
+ throw new Error(`Usage\n${usage()}`);
784
+ let body;
785
+ let scheduledAt;
786
+ let cadence;
787
+ let category;
788
+ let requester;
789
+ for (let i = 1; i < rest.length; i++) {
790
+ if (rest[i] === "--body" && rest[i + 1]) {
791
+ body = rest[i + 1];
792
+ i += 1;
793
+ }
794
+ else if (rest[i] === "--at" && rest[i + 1]) {
795
+ scheduledAt = rest[i + 1];
796
+ i += 1;
797
+ }
798
+ else if (rest[i] === "--cadence" && rest[i + 1]) {
799
+ cadence = rest[i + 1];
800
+ i += 1;
801
+ }
802
+ else if (rest[i] === "--category" && rest[i + 1]) {
803
+ category = rest[i + 1];
804
+ i += 1;
805
+ }
806
+ else if (rest[i] === "--requester" && rest[i + 1]) {
807
+ requester = rest[i + 1];
808
+ i += 1;
809
+ }
810
+ }
811
+ if (!body)
812
+ throw new Error(`Usage\n${usage()}`);
813
+ if (!scheduledAt && !cadence)
814
+ throw new Error(`Usage\n${usage()}`);
815
+ return {
816
+ kind: "reminder.create",
817
+ title,
818
+ body,
819
+ ...(scheduledAt ? { scheduledAt } : {}),
820
+ ...(cadence ? { cadence } : {}),
821
+ ...(category ? { category } : {}),
822
+ ...(requester ? { requester } : {}),
823
+ ...(agent ? { agent } : {}),
824
+ };
825
+ }
826
+ throw new Error(`Usage\n${usage()}`);
827
+ }
828
+ function parseSessionCommand(args) {
829
+ const { agent, rest: cleaned } = extractAgentFlag(args);
830
+ const [sub] = cleaned;
831
+ if (!sub)
832
+ throw new Error(`Usage\n${usage()}`);
833
+ if (sub === "list")
834
+ return { kind: "session.list", ...(agent ? { agent } : {}) };
835
+ throw new Error(`Usage\n${usage()}`);
836
+ }
837
+ function parseAttentionCommand(args) {
838
+ const { agent, rest: cleaned } = extractAgentFlag(args);
839
+ const sub = cleaned[0];
840
+ if (sub === "show" && cleaned[1]) {
841
+ return { kind: "attention.show", id: cleaned[1], ...(agent ? { agent } : {}) };
842
+ }
843
+ if (sub === "history") {
844
+ return { kind: "attention.history", ...(agent ? { agent } : {}) };
845
+ }
846
+ return { kind: "attention.list", ...(agent ? { agent } : {}) };
847
+ }
848
+ function parseThoughtsCommand(args) {
849
+ const { agent, rest: cleaned } = extractAgentFlag(args);
850
+ let last;
851
+ let json = false;
852
+ let follow = false;
853
+ for (let i = 0; i < cleaned.length; i++) {
854
+ if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
855
+ last = Number.parseInt(cleaned[i + 1], 10);
856
+ i++;
857
+ }
858
+ if (cleaned[i] === "--json")
859
+ json = true;
860
+ if (cleaned[i] === "--follow" || cleaned[i] === "-f")
861
+ follow = true;
862
+ }
863
+ return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
864
+ }
865
+ function parseFriendCommand(args) {
866
+ const { agent, rest: cleaned } = extractAgentFlag(args);
867
+ const [sub, ...rest] = cleaned;
868
+ if (!sub)
869
+ throw new Error(`Usage\n${usage()}`);
870
+ if (sub === "list")
871
+ return { kind: "friend.list", ...(agent ? { agent } : {}) };
872
+ if (sub === "show") {
873
+ const friendId = rest[0];
874
+ if (!friendId)
875
+ throw new Error(`Usage\n${usage()}`);
876
+ return { kind: "friend.show", friendId, ...(agent ? { agent } : {}) };
877
+ }
878
+ if (sub === "create") {
879
+ let name;
880
+ let trustLevel;
881
+ for (let i = 0; i < rest.length; i++) {
882
+ if (rest[i] === "--name" && rest[i + 1]) {
883
+ name = rest[i + 1];
884
+ i += 1;
885
+ }
886
+ else if (rest[i] === "--trust" && rest[i + 1]) {
887
+ trustLevel = rest[i + 1];
888
+ i += 1;
889
+ }
890
+ }
891
+ if (!name)
892
+ throw new Error(`Usage\n${usage()}`);
893
+ return {
894
+ kind: "friend.create",
895
+ name,
896
+ ...(trustLevel ? { trustLevel } : {}),
897
+ ...(agent ? { agent } : {}),
898
+ };
899
+ }
900
+ if (sub === "update") {
901
+ const friendId = rest[0];
902
+ if (!friendId)
903
+ throw new Error(`Usage: ouro friend update <id> --trust <level>`);
904
+ let trustLevel;
905
+ /* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
906
+ for (let i = 1; i < rest.length; i++) {
907
+ if (rest[i] === "--trust" && rest[i + 1]) {
908
+ trustLevel = rest[i + 1];
909
+ i += 1;
910
+ }
911
+ }
912
+ /* v8 ignore stop */
913
+ const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
914
+ if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
915
+ throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
916
+ }
917
+ return {
918
+ kind: "friend.update",
919
+ friendId,
920
+ trustLevel: trustLevel,
921
+ ...(agent ? { agent } : {}),
922
+ };
923
+ }
924
+ if (sub === "link")
925
+ return parseLinkCommand(rest, "friend.link");
926
+ if (sub === "unlink")
927
+ return parseLinkCommand(rest, "friend.unlink");
928
+ throw new Error(`Usage\n${usage()}`);
929
+ }
930
+ function parseConfigCommand(args) {
931
+ const { agent, rest: cleaned } = extractAgentFlag(args);
932
+ const [sub, ...rest] = cleaned;
933
+ if (!sub)
934
+ throw new Error(`Usage\n${usage()}`);
935
+ if (sub === "model") {
936
+ if (!agent)
937
+ throw new Error("--agent is required for config model");
938
+ const modelName = rest[0];
939
+ if (!modelName)
940
+ throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
941
+ return { kind: "config.model", agent, modelName };
942
+ }
943
+ if (sub === "models") {
944
+ if (!agent)
945
+ throw new Error("--agent is required for config models");
946
+ return { kind: "config.models", agent };
947
+ }
948
+ throw new Error(`Usage\n${usage()}`);
949
+ }
950
+ function parseMcpCommand(args) {
951
+ const [sub, ...rest] = args;
952
+ if (!sub)
953
+ throw new Error(`Usage\n${usage()}`);
954
+ if (sub === "list")
955
+ return { kind: "mcp.list" };
956
+ if (sub === "call") {
957
+ const server = rest[0];
958
+ const tool = rest[1];
959
+ if (!server || !tool)
960
+ throw new Error(`Usage\n${usage()}`);
961
+ const argsIdx = rest.indexOf("--args");
962
+ const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
963
+ return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
964
+ }
965
+ throw new Error(`Usage\n${usage()}`);
966
+ }
967
+ function parseMcpServeCommand(args) {
968
+ let agent;
969
+ let friendId;
970
+ let socketOverride;
971
+ for (let i = 0; i < args.length; i++) {
972
+ if (args[i] === "--agent" && args[i + 1]) {
973
+ agent = args[++i];
974
+ continue;
975
+ }
976
+ if (args[i] === "--friend" && args[i + 1]) {
977
+ friendId = args[++i];
978
+ continue;
979
+ }
980
+ if (args[i] === "--socket" && args[i + 1]) {
981
+ socketOverride = args[++i];
982
+ continue;
983
+ }
984
+ }
985
+ if (!agent)
986
+ throw new Error("mcp-serve requires --agent <name>");
987
+ return { kind: "mcp-serve", agent, ...(friendId ? { friendId } : {}), ...(socketOverride ? { socketOverride } : {}) };
988
+ }
989
+ function parseSetupCommand(args) {
990
+ let tool;
991
+ let agent;
992
+ for (let i = 0; i < args.length; i++) {
993
+ if (args[i] === "--tool" && args[i + 1]) {
994
+ tool = args[++i];
995
+ continue;
996
+ }
997
+ if (args[i] === "--agent" && args[i + 1]) {
998
+ agent = args[++i];
999
+ continue;
1000
+ }
1001
+ }
1002
+ if (!tool)
1003
+ throw new Error("setup requires --tool (claude-code | codex)");
1004
+ if (tool !== "claude-code" && tool !== "codex")
1005
+ throw new Error(`Unknown tool: ${tool}. Supported: claude-code, codex`);
1006
+ if (!agent)
1007
+ throw new Error("setup requires --agent <name>");
1008
+ return { kind: "setup", tool, agent };
1009
+ }
376
1010
  function parseOuroCommand(args) {
377
1011
  const [head, second] = args;
378
1012
  if (!head)
379
1013
  return { kind: "daemon.up" };
1014
+ if (head === "--agent" && second) {
1015
+ return parseOuroCommand(args.slice(2));
1016
+ }
1017
+ if (head === "hook") {
1018
+ const hookArgs = args.slice(1);
1019
+ let event;
1020
+ let hookAgent;
1021
+ for (let i = 0; i < hookArgs.length; i++) {
1022
+ if (hookArgs[i] === "--agent" && hookArgs[i + 1]) {
1023
+ hookAgent = hookArgs[++i];
1024
+ continue;
1025
+ }
1026
+ /* v8 ignore start -- false branch: extra positional args after event are ignored */
1027
+ if (!event) {
1028
+ event = hookArgs[i];
1029
+ }
1030
+ /* v8 ignore stop */
1031
+ }
1032
+ if (!event)
1033
+ throw new Error("hook requires an event name (session-start, stop, post-tool-use)");
1034
+ if (!hookAgent)
1035
+ throw new Error("hook requires --agent <name>");
1036
+ return { kind: "hook", event, agent: hookAgent };
1037
+ }
380
1038
  if (head === "up")
381
1039
  return { kind: "daemon.up" };
382
- if (head === "stop")
1040
+ if (head === "dev") {
1041
+ const devArgs = args.slice(1);
1042
+ let repoPath;
1043
+ let clone = false;
1044
+ let clonePath;
1045
+ for (let i = 0; i < devArgs.length; i++) {
1046
+ if (devArgs[i] === "--repo-path" && devArgs[i + 1]) {
1047
+ repoPath = devArgs[++i];
1048
+ continue;
1049
+ }
1050
+ if (devArgs[i] === "--clone") {
1051
+ clone = true;
1052
+ continue;
1053
+ }
1054
+ if (devArgs[i] === "--clone-path" && devArgs[i + 1]) {
1055
+ clonePath = devArgs[++i];
1056
+ continue;
1057
+ }
1058
+ }
1059
+ return { kind: "daemon.dev", repoPath, clone, clonePath };
1060
+ }
1061
+ if (head === "rollback")
1062
+ return { kind: "rollback", ...(second ? { version: second } : {}) };
1063
+ if (head === "versions")
1064
+ return { kind: "versions" };
1065
+ if (head === "stop" || head === "down")
383
1066
  return { kind: "daemon.stop" };
384
1067
  if (head === "status")
385
1068
  return { kind: "daemon.status" };
@@ -387,6 +1070,44 @@ function parseOuroCommand(args) {
387
1070
  return { kind: "daemon.logs" };
388
1071
  if (head === "hatch")
389
1072
  return parseHatchCommand(args.slice(1));
1073
+ if (head === "auth")
1074
+ return parseAuthCommand(args.slice(1));
1075
+ if (head === "task")
1076
+ return parseTaskCommand(args.slice(1));
1077
+ if (head === "reminder")
1078
+ return parseReminderCommand(args.slice(1));
1079
+ if (head === "habit")
1080
+ return parseHabitCommand(args.slice(1));
1081
+ if (head === "friend")
1082
+ return parseFriendCommand(args.slice(1));
1083
+ if (head === "config")
1084
+ return parseConfigCommand(args.slice(1));
1085
+ if (head === "mcp")
1086
+ return parseMcpCommand(args.slice(1));
1087
+ if (head === "whoami") {
1088
+ const { agent } = extractAgentFlag(args.slice(1));
1089
+ return { kind: "whoami", ...(agent ? { agent } : {}) };
1090
+ }
1091
+ if (head === "session")
1092
+ return parseSessionCommand(args.slice(1));
1093
+ if (head === "changelog") {
1094
+ const sliced = args.slice(1);
1095
+ const { agent, rest: remaining } = extractAgentFlag(sliced);
1096
+ let from;
1097
+ const fromIdx = remaining.indexOf("--from");
1098
+ if (fromIdx !== -1 && remaining[fromIdx + 1]) {
1099
+ from = remaining[fromIdx + 1];
1100
+ }
1101
+ return { kind: "changelog", ...(from ? { from } : {}), ...(agent ? { agent } : {}) };
1102
+ }
1103
+ if (head === "thoughts")
1104
+ return parseThoughtsCommand(args.slice(1));
1105
+ if (head === "attention")
1106
+ return parseAttentionCommand(args.slice(1));
1107
+ if (head === "inner") {
1108
+ const { agent } = extractAgentFlag(args.slice(1));
1109
+ return { kind: "inner.status", ...(agent ? { agent } : {}) };
1110
+ }
390
1111
  if (head === "chat") {
391
1112
  if (!second)
392
1113
  throw new Error(`Usage\n${usage()}`);
@@ -398,40 +1119,12 @@ function parseOuroCommand(args) {
398
1119
  return parsePokeCommand(args.slice(1));
399
1120
  if (head === "link")
400
1121
  return parseLinkCommand(args.slice(1));
1122
+ if (head === "mcp-serve")
1123
+ return parseMcpServeCommand(args.slice(1));
1124
+ if (head === "setup")
1125
+ return parseSetupCommand(args.slice(1));
401
1126
  throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
402
1127
  }
403
- function defaultSendCommand(socketPath, command) {
404
- return new Promise((resolve, reject) => {
405
- const client = net.createConnection(socketPath);
406
- let raw = "";
407
- client.on("connect", () => {
408
- client.write(JSON.stringify(command));
409
- client.end();
410
- });
411
- client.on("data", (chunk) => {
412
- raw += chunk.toString("utf-8");
413
- });
414
- client.on("error", reject);
415
- client.on("end", () => {
416
- const trimmed = raw.trim();
417
- if (trimmed.length === 0 && command.kind === "daemon.stop") {
418
- resolve({ ok: true, message: "daemon stopped" });
419
- return;
420
- }
421
- if (trimmed.length === 0) {
422
- reject(new Error("Daemon returned empty response."));
423
- return;
424
- }
425
- try {
426
- const parsed = JSON.parse(trimmed);
427
- resolve(parsed);
428
- }
429
- catch (error) {
430
- reject(error);
431
- }
432
- });
433
- });
434
- }
435
1128
  function defaultStartDaemonProcess(socketPath) {
436
1129
  const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
437
1130
  const child = (0, child_process_1.spawn)("node", [entry, "--socket", socketPath], {
@@ -445,45 +1138,32 @@ function defaultWriteStdout(text) {
445
1138
  // eslint-disable-next-line no-console -- terminal UX: CLI command output
446
1139
  console.log(text);
447
1140
  }
448
- function defaultCheckSocketAlive(socketPath) {
449
- return new Promise((resolve) => {
450
- const client = net.createConnection(socketPath);
451
- let raw = "";
452
- let done = false;
453
- const finalize = (alive) => {
454
- if (done)
455
- return;
456
- done = true;
457
- resolve(alive);
458
- };
459
- if ("setTimeout" in client && typeof client.setTimeout === "function") {
460
- client.setTimeout(800, () => {
461
- client.destroy();
462
- finalize(false);
463
- });
1141
+ /**
1142
+ * Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
1143
+ * Returns undefined if none found or unreadable.
1144
+ */
1145
+ function readFirstBundleMetaVersion(bundlesRoot) {
1146
+ try {
1147
+ if (!fs.existsSync(bundlesRoot))
1148
+ return undefined;
1149
+ const entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
1150
+ for (const entry of entries) {
1151
+ /* v8 ignore next -- skip non-.ouro dirs: tested via version-detect tests @preserve */
1152
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
1153
+ continue;
1154
+ const metaPath = path.join(bundlesRoot, entry.name, "bundle-meta.json");
1155
+ if (!fs.existsSync(metaPath))
1156
+ continue;
1157
+ const raw = fs.readFileSync(metaPath, "utf-8");
1158
+ const meta = JSON.parse(raw);
1159
+ if (meta.runtimeVersion)
1160
+ return meta.runtimeVersion;
464
1161
  }
465
- client.on("connect", () => {
466
- client.write(JSON.stringify({ kind: "daemon.status" }));
467
- client.end();
468
- });
469
- client.on("data", (chunk) => {
470
- raw += chunk.toString("utf-8");
471
- });
472
- client.on("error", () => finalize(false));
473
- client.on("end", () => {
474
- if (raw.trim().length === 0) {
475
- finalize(false);
476
- return;
477
- }
478
- try {
479
- JSON.parse(raw);
480
- finalize(true);
481
- }
482
- catch {
483
- finalize(false);
484
- }
485
- });
486
- });
1162
+ }
1163
+ catch {
1164
+ // Best effort — return undefined on any error
1165
+ }
1166
+ return undefined;
487
1167
  }
488
1168
  function defaultCleanupStaleSocket(socketPath) {
489
1169
  if (fs.existsSync(socketPath)) {
@@ -519,9 +1199,38 @@ function defaultFallbackPendingMessage(command) {
519
1199
  });
520
1200
  return pendingPath;
521
1201
  }
522
- async function defaultInstallSubagents() {
523
- return (0, subagent_installer_1.installSubagentsForAvailableCli)({
524
- repoRoot: (0, identity_1.getRepoRoot)(),
1202
+ function defaultEnsureDaemonBootPersistence(socketPath) {
1203
+ if (process.platform !== "darwin") {
1204
+ return;
1205
+ }
1206
+ const homeDir = os.homedir();
1207
+ const launchdDeps = {
1208
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd, { stdio: "ignore" }); },
1209
+ writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
1210
+ removeFile: (filePath) => fs.rmSync(filePath, { force: true }),
1211
+ existsFile: (filePath) => fs.existsSync(filePath),
1212
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
1213
+ homeDir,
1214
+ userUid: process.getuid?.() ?? 0,
1215
+ };
1216
+ const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
1217
+ /* v8 ignore next -- covered via mock in daemon-cli-defaults.test.ts; v8 on CI attributes the real fs.existsSync branch to the non-mock load @preserve */
1218
+ if (!fs.existsSync(entryPath)) {
1219
+ (0, runtime_1.emitNervesEvent)({
1220
+ level: "warn",
1221
+ component: "daemon",
1222
+ event: "daemon.entry_path_missing",
1223
+ message: "entryPath does not exist on disk — plist may point to a stale location. Run 'ouro daemon install' from the correct location.",
1224
+ meta: { entryPath },
1225
+ });
1226
+ }
1227
+ const logDir = (0, identity_1.getAgentDaemonLogsDir)();
1228
+ (0, launchd_1.installLaunchAgent)(launchdDeps, {
1229
+ nodePath: process.execPath,
1230
+ entryPath,
1231
+ socketPath,
1232
+ logDir,
1233
+ envPath: process.env.PATH,
525
1234
  });
526
1235
  }
527
1236
  async function defaultPromptInput(question) {
@@ -539,61 +1248,11 @@ async function defaultPromptInput(question) {
539
1248
  }
540
1249
  }
541
1250
  function defaultListDiscoveredAgents() {
542
- const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
543
- let entries;
544
- try {
545
- entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
546
- }
547
- catch {
548
- return [];
549
- }
550
- const discovered = [];
551
- for (const entry of entries) {
552
- if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
553
- continue;
554
- const agentName = entry.name.slice(0, -5);
555
- const configPath = path.join(bundlesRoot, entry.name, "agent.json");
556
- let enabled = true;
557
- try {
558
- const raw = fs.readFileSync(configPath, "utf-8");
559
- const parsed = JSON.parse(raw);
560
- if (typeof parsed.enabled === "boolean") {
561
- enabled = parsed.enabled;
562
- }
563
- }
564
- catch {
565
- continue;
566
- }
567
- if (enabled) {
568
- discovered.push(agentName);
569
- }
570
- }
571
- return discovered.sort((left, right) => left.localeCompare(right));
572
- }
573
- async function defaultLinkFriendIdentity(command) {
574
- const friendStore = new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends"));
575
- const current = await friendStore.get(command.friendId);
576
- if (!current) {
577
- return `friend not found: ${command.friendId}`;
578
- }
579
- const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
580
- if (alreadyLinked) {
581
- return `identity already linked: ${command.provider}:${command.externalId}`;
582
- }
583
- const now = new Date().toISOString();
584
- await friendStore.put(command.friendId, {
585
- ...current,
586
- externalIds: [
587
- ...current.externalIds,
588
- {
589
- provider: command.provider,
590
- externalId: command.externalId,
591
- linkedAt: now,
592
- },
593
- ],
594
- updatedAt: now,
1251
+ return (0, agent_discovery_1.listEnabledBundleAgents)({
1252
+ bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
1253
+ readdirSync: fs.readdirSync,
1254
+ readFileSync: fs.readFileSync,
595
1255
  });
596
- return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
597
1256
  }
598
1257
  function discoverExistingCredentials(secretsRoot) {
599
1258
  const found = [];
@@ -626,16 +1285,16 @@ function discoverExistingCredentials(secretsRoot) {
626
1285
  continue;
627
1286
  for (const [provName, provConfig] of Object.entries(parsed.providers)) {
628
1287
  if (provName === "anthropic" && provConfig.setupToken) {
629
- found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken } });
1288
+ found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
630
1289
  }
631
1290
  else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
632
- found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken } });
1291
+ found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
633
1292
  }
634
1293
  else if (provName === "minimax" && provConfig.apiKey) {
635
- found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey } });
1294
+ found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
636
1295
  }
637
1296
  else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
638
- found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment } });
1297
+ found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
639
1298
  }
640
1299
  }
641
1300
  }
@@ -653,7 +1312,7 @@ function discoverExistingCredentials(secretsRoot) {
653
1312
  async function defaultRunAdoptionSpecialist() {
654
1313
  const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
655
1314
  const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
656
- const { setAgentName } = await Promise.resolve().then(() => __importStar(require("../identity")));
1315
+ const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
657
1316
  const readlinePromises = await Promise.resolve().then(() => __importStar(require("readline/promises")));
658
1317
  const crypto = await Promise.resolve().then(() => __importStar(require("crypto")));
659
1318
  // Phase 1: cold CLI — collect provider/credentials with a simple readline
@@ -664,16 +1323,29 @@ async function defaultRunAdoptionSpecialist() {
664
1323
  };
665
1324
  let providerRaw;
666
1325
  let credentials = {};
1326
+ let providerConfig = {};
667
1327
  const tempDir = path.join(os.tmpdir(), `ouro-hatch-${crypto.randomUUID()}`);
668
1328
  try {
669
1329
  const secretsRoot = path.join(os.homedir(), ".agentsecrets");
670
1330
  const discovered = discoverExistingCredentials(secretsRoot);
1331
+ const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
1332
+ const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
1333
+ // Default models per provider (used when entering new credentials)
1334
+ const defaultModels = {
1335
+ anthropic: "claude-opus-4-6",
1336
+ minimax: "MiniMax-Text-01",
1337
+ "openai-codex": "gpt-5.4",
1338
+ "github-copilot": "claude-sonnet-4.6",
1339
+ azure: "",
1340
+ };
671
1341
  if (discovered.length > 0) {
672
- process.stdout.write("\n\ud83d\udc0d welcome to ouro! let's hatch your first agent.\n");
1342
+ process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
673
1343
  process.stdout.write("i found existing API credentials:\n\n");
674
1344
  const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
675
1345
  for (let i = 0; i < unique.length; i++) {
676
- process.stdout.write(` ${i + 1}. ${unique[i].provider} (from ${unique[i].agentName})\n`);
1346
+ const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
1347
+ const modelLabel = model ? `, ${model}` : "";
1348
+ process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (from ${unique[i].agentName})\n`);
677
1349
  }
678
1350
  process.stdout.write("\n");
679
1351
  const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
@@ -681,15 +1353,17 @@ async function defaultRunAdoptionSpecialist() {
681
1353
  if (idx >= 0 && idx < unique.length) {
682
1354
  providerRaw = unique[idx].provider;
683
1355
  credentials = unique[idx].credentials;
1356
+ providerConfig = unique[idx].providerConfig;
684
1357
  }
685
1358
  else {
686
- const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
1359
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
687
1360
  if (!isAgentProvider(pRaw)) {
688
1361
  process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
689
1362
  coldRl.close();
690
1363
  return null;
691
1364
  }
692
1365
  providerRaw = pRaw;
1366
+ providerConfig = { model: defaultModels[providerRaw] };
693
1367
  if (providerRaw === "anthropic")
694
1368
  credentials.setupToken = await coldPrompt("API key: ");
695
1369
  if (providerRaw === "openai-codex")
@@ -704,15 +1378,16 @@ async function defaultRunAdoptionSpecialist() {
704
1378
  }
705
1379
  }
706
1380
  else {
707
- process.stdout.write("\n\ud83d\udc0d welcome to ouro! let's hatch your first agent.\n");
1381
+ process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
708
1382
  process.stdout.write("i need an API key to power our conversation.\n\n");
709
- const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
1383
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
710
1384
  if (!isAgentProvider(pRaw)) {
711
1385
  process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
712
1386
  coldRl.close();
713
1387
  return null;
714
1388
  }
715
1389
  providerRaw = pRaw;
1390
+ providerConfig = { model: defaultModels[providerRaw] };
716
1391
  if (providerRaw === "anthropic")
717
1392
  credentials.setupToken = await coldPrompt("API key: ");
718
1393
  if (providerRaw === "openai-codex")
@@ -731,17 +1406,31 @@ async function defaultRunAdoptionSpecialist() {
731
1406
  const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
732
1407
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
733
1408
  const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
734
- // Configure provider credentials in runtime config
735
- patchRuntimeConfig({
736
- providers: {
737
- [providerRaw]: credentials,
738
- },
739
- });
1409
+ // Suppress non-critical log noise during adoption (no secrets.json, etc.)
1410
+ const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
1411
+ const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
1412
+ setRuntimeLogger(createLogger({ level: "error" }));
1413
+ // Configure runtime: set agent identity + config override so runAgent
1414
+ // doesn't try to read from ~/AgentBundles/AdoptionSpecialist.ouro/
740
1415
  setAgentName("AdoptionSpecialist");
741
1416
  // Build specialist system prompt
742
1417
  const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
743
1418
  const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
744
1419
  const identity = (0, specialist_orchestrator_1.pickRandomIdentity)(identitiesDir);
1420
+ // Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
1421
+ const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
1422
+ const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
1423
+ setAgentConfigOverride({
1424
+ version: 1,
1425
+ enabled: true,
1426
+ provider: providerRaw,
1427
+ phrases,
1428
+ });
1429
+ patchRuntimeConfig({
1430
+ providers: {
1431
+ [providerRaw]: { ...providerConfig, ...credentials },
1432
+ },
1433
+ });
745
1434
  const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
746
1435
  const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
747
1436
  tempDir,
@@ -763,6 +1452,10 @@ async function defaultRunAdoptionSpecialist() {
763
1452
  tools: specialistTools,
764
1453
  execTool: specialistExecTool,
765
1454
  exitOnToolCall: "complete_adoption",
1455
+ autoFirstTurn: true,
1456
+ banner: false,
1457
+ disableCommands: true,
1458
+ skipSystemPromptRefresh: true,
766
1459
  messages: [
767
1460
  { role: "system", content: systemPrompt },
768
1461
  { role: "user", content: "hi" },
@@ -776,11 +1469,21 @@ async function defaultRunAdoptionSpecialist() {
776
1469
  }
777
1470
  return null;
778
1471
  }
779
- catch {
1472
+ catch (err) {
1473
+ process.stderr.write(`\nouro adoption error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
780
1474
  coldRl.close();
781
1475
  return null;
782
1476
  }
783
1477
  finally {
1478
+ // Clear specialist config/identity so the hatched agent gets its own
1479
+ setAgentConfigOverride(null);
1480
+ const { resetProviderRuntime } = await Promise.resolve().then(() => __importStar(require("../core")));
1481
+ resetProviderRuntime();
1482
+ const { resetConfigCache } = await Promise.resolve().then(() => __importStar(require("../config")));
1483
+ resetConfigCache();
1484
+ // Restore default logging
1485
+ const { setRuntimeLogger: restoreLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
1486
+ restoreLogger(null);
784
1487
  // Clean up temp dir if it still exists
785
1488
  try {
786
1489
  if (fs.existsSync(tempDir)) {
@@ -793,30 +1496,118 @@ async function defaultRunAdoptionSpecialist() {
793
1496
  }
794
1497
  }
795
1498
  /* v8 ignore stop */
796
- function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
1499
+ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH) {
797
1500
  return {
798
1501
  socketPath,
799
- sendCommand: defaultSendCommand,
1502
+ sendCommand: socket_client_1.sendDaemonCommand,
800
1503
  startDaemonProcess: defaultStartDaemonProcess,
801
1504
  writeStdout: defaultWriteStdout,
802
- checkSocketAlive: defaultCheckSocketAlive,
1505
+ checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
803
1506
  cleanupStaleSocket: defaultCleanupStaleSocket,
804
1507
  fallbackPendingMessage: defaultFallbackPendingMessage,
805
- installSubagents: defaultInstallSubagents,
806
- linkFriendIdentity: defaultLinkFriendIdentity,
807
1508
  listDiscoveredAgents: defaultListDiscoveredAgents,
808
1509
  runHatchFlow: hatch_flow_1.runHatchFlow,
809
1510
  promptInput: defaultPromptInput,
810
1511
  runAdoptionSpecialist: defaultRunAdoptionSpecialist,
1512
+ runAuthFlow: auth_flow_1.runRuntimeAuthFlow,
811
1513
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
812
1514
  installOuroCommand: ouro_path_installer_1.installOuroCommand,
1515
+ /* v8 ignore start -- self-healing: ensures active symlink matches running runtime version @preserve */
1516
+ ensureCurrentVersionInstalled: () => {
1517
+ const linkedVersion = (0, ouro_version_manager_1.getCurrentVersion)({});
1518
+ const version = (0, bundle_manifest_1.getPackageVersion)();
1519
+ if (linkedVersion === version)
1520
+ return;
1521
+ (0, ouro_version_manager_1.ensureLayout)({});
1522
+ const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
1523
+ const versionEntry = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
1524
+ if (!fs.existsSync(versionEntry)) {
1525
+ (0, ouro_version_manager_1.installVersion)(version, {});
1526
+ }
1527
+ (0, ouro_version_manager_1.activateVersion)(version, {});
1528
+ },
1529
+ /* v8 ignore stop */
1530
+ /* v8 ignore start -- CLI version management defaults: integration code @preserve */
1531
+ checkForCliUpdate: async () => {
1532
+ const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require("./update-checker")));
1533
+ return checkForUpdate((0, bundle_manifest_1.getPackageVersion)(), {
1534
+ fetchRegistryJson: async () => {
1535
+ const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
1536
+ return res.json();
1537
+ },
1538
+ distTag: "alpha",
1539
+ });
1540
+ },
1541
+ installCliVersion: async (version) => { (0, ouro_version_manager_1.installVersion)(version, {}); },
1542
+ activateCliVersion: (version) => { (0, ouro_version_manager_1.activateVersion)(version, {}); },
1543
+ getCurrentCliVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({}),
1544
+ getPreviousCliVersion: () => (0, ouro_version_manager_1.getPreviousVersion)({}),
1545
+ listCliVersions: () => (0, ouro_version_manager_1.listInstalledVersions)({}),
1546
+ reExecFromNewVersion: (reArgs) => {
1547
+ const entry = path.join((0, ouro_version_manager_1.getOuroCliHome)(), "CurrentVersion", "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
1548
+ require("child_process").execFileSync("node", [entry, ...reArgs], { stdio: "inherit" });
1549
+ process.exit(0);
1550
+ },
1551
+ /* v8 ignore stop */
1552
+ syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
1553
+ ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
1554
+ ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
1555
+ /* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
1556
+ detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
1557
+ getInstalledBinaryPath: () => {
1558
+ const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
1559
+ const binaryPath = path.join(cliHome, "bin", "ouro");
1560
+ return fs.existsSync(binaryPath) ? binaryPath : null;
1561
+ },
1562
+ execInstalledBinary: (binaryPath, binArgs) => {
1563
+ const { execFileSync } = require("child_process");
1564
+ execFileSync(binaryPath, binArgs, { stdio: "inherit" });
1565
+ process.exit(0);
1566
+ },
1567
+ /* v8 ignore stop */
813
1568
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
814
1569
  startChat: async (agentName) => {
815
1570
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
816
1571
  await main(agentName);
817
1572
  },
1573
+ scanSessions: async () => {
1574
+ const agentName = (0, identity_1.getAgentName)();
1575
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
1576
+ return (0, session_activity_1.listSessionActivity)({
1577
+ sessionsDir: path.join(agentRoot, "state", "sessions"),
1578
+ friendsDir: path.join(agentRoot, "friends"),
1579
+ agentName,
1580
+ }).map((entry) => ({
1581
+ friendId: entry.friendId,
1582
+ friendName: entry.friendName,
1583
+ channel: entry.channel,
1584
+ lastActivity: entry.lastActivityAt,
1585
+ }));
1586
+ },
818
1587
  };
819
1588
  }
1589
+ function formatMcpResponse(command, response) {
1590
+ if (command.kind === "mcp.list") {
1591
+ const allTools = response.data;
1592
+ if (!allTools || allTools.length === 0) {
1593
+ return response.message ?? "no tools available from connected MCP servers";
1594
+ }
1595
+ const lines = [];
1596
+ for (const entry of allTools) {
1597
+ lines.push(`[${entry.server}]`);
1598
+ for (const tool of entry.tools) {
1599
+ lines.push(` ${tool.name}: ${tool.description}`);
1600
+ }
1601
+ }
1602
+ return lines.join("\n");
1603
+ }
1604
+ // mcp.call
1605
+ const result = response.data;
1606
+ if (!result) {
1607
+ return response.message ?? "no result";
1608
+ }
1609
+ return result.content.map((c) => c.text).join("\n");
1610
+ }
820
1611
  function toDaemonCommand(command) {
821
1612
  return command;
822
1613
  }
@@ -824,28 +1615,17 @@ async function resolveHatchInput(command, deps) {
824
1615
  const prompt = deps.promptInput;
825
1616
  const agentName = command.agentName ?? (prompt ? await prompt("Hatchling name: ") : "");
826
1617
  const humanName = command.humanName ?? (prompt ? await prompt("Your name: ") : os.userInfo().username);
827
- const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex): ") : "");
1618
+ const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex|github-copilot): ") : "");
828
1619
  if (!agentName || !humanName || !isAgentProvider(providerRaw)) {
829
1620
  throw new Error(`Usage\n${usage()}`);
830
1621
  }
831
- const credentials = { ...(command.credentials ?? {}) };
832
- if (providerRaw === "anthropic" && !credentials.setupToken && prompt) {
833
- credentials.setupToken = await prompt("Anthropic setup-token: ");
834
- }
835
- if (providerRaw === "openai-codex" && !credentials.oauthAccessToken && prompt) {
836
- credentials.oauthAccessToken = await prompt("OpenAI Codex OAuth token: ");
837
- }
838
- if (providerRaw === "minimax" && !credentials.apiKey && prompt) {
839
- credentials.apiKey = await prompt("MiniMax API key: ");
840
- }
841
- if (providerRaw === "azure") {
842
- if (!credentials.apiKey && prompt)
843
- credentials.apiKey = await prompt("Azure API key: ");
844
- if (!credentials.endpoint && prompt)
845
- credentials.endpoint = await prompt("Azure endpoint: ");
846
- if (!credentials.deployment && prompt)
847
- credentials.deployment = await prompt("Azure deployment: ");
848
- }
1622
+ const credentials = await (0, auth_flow_1.resolveHatchCredentials)({
1623
+ agentName,
1624
+ provider: providerRaw,
1625
+ credentials: command.credentials,
1626
+ promptInput: prompt,
1627
+ runAuthFlow: deps.runAuthFlow,
1628
+ });
849
1629
  return {
850
1630
  agentName,
851
1631
  humanName,
@@ -875,7 +1655,11 @@ async function performSystemSetup(deps) {
875
1655
  // Install ouro command to PATH (non-blocking)
876
1656
  if (deps.installOuroCommand) {
877
1657
  try {
878
- deps.installOuroCommand();
1658
+ const installResult = deps.installOuroCommand();
1659
+ /* v8 ignore next -- old-launcher repair hint: fires when stale ~/.local/bin/ouro is fixed @preserve */
1660
+ if (installResult.repairedOldLauncher) {
1661
+ deps.writeStdout("repaired stale ouro launcher at ~/.local/bin/ouro");
1662
+ }
879
1663
  }
880
1664
  catch (error) {
881
1665
  (0, runtime_1.emitNervesEvent)({
@@ -887,28 +1671,337 @@ async function performSystemSetup(deps) {
887
1671
  });
888
1672
  }
889
1673
  }
890
- // Install subagents (claude/codex skills)
891
- try {
892
- await deps.installSubagents();
893
- }
894
- catch (error) {
895
- (0, runtime_1.emitNervesEvent)({
896
- level: "warn",
897
- component: "daemon",
898
- event: "daemon.subagent_install_error",
899
- message: "subagent auto-install failed",
900
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
901
- });
902
- }
1674
+ // Self-healing: ensure current version is installed in ~/.ouro-cli/ layout.
1675
+ // Handles the case where the wrapper exists but CurrentVersion is missing
1676
+ // (e.g., first run after migration from old npx wrapper).
1677
+ if (deps.ensureCurrentVersionInstalled) {
1678
+ try {
1679
+ deps.ensureCurrentVersionInstalled();
1680
+ }
1681
+ catch (error) {
1682
+ (0, runtime_1.emitNervesEvent)({
1683
+ level: "warn",
1684
+ component: "daemon",
1685
+ event: "daemon.system_setup_version_install_error",
1686
+ message: "failed to ensure current version installed",
1687
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) },
1688
+ });
1689
+ }
1690
+ }
1691
+ if (deps.syncGlobalOuroBotWrapper) {
1692
+ try {
1693
+ await Promise.resolve(deps.syncGlobalOuroBotWrapper());
1694
+ }
1695
+ catch (error) {
1696
+ (0, runtime_1.emitNervesEvent)({
1697
+ level: "warn",
1698
+ component: "daemon",
1699
+ event: "daemon.system_setup_ouro_bot_wrapper_error",
1700
+ message: "failed to sync global ouro.bot wrapper",
1701
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1702
+ });
1703
+ }
1704
+ }
1705
+ // Ensure skill-management skill is available
1706
+ if (deps.ensureSkillManagement) {
1707
+ try {
1708
+ await deps.ensureSkillManagement();
1709
+ /* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
1710
+ }
1711
+ catch (error) {
1712
+ (0, runtime_1.emitNervesEvent)({
1713
+ level: "warn",
1714
+ component: "daemon",
1715
+ event: "daemon.system_setup_skill_management_error",
1716
+ message: "failed to ensure skill-management skill",
1717
+ meta: { error: error instanceof Error ? error.message : String(error) },
1718
+ });
1719
+ }
1720
+ /* v8 ignore stop */
1721
+ }
903
1722
  // Register .ouro bundle type (UTI on macOS)
904
1723
  await registerOuroBundleTypeNonBlocking(deps);
905
1724
  }
1725
+ function executeTaskCommand(command, taskMod) {
1726
+ if (command.kind === "task.board") {
1727
+ if (command.status) {
1728
+ const lines = taskMod.boardStatus(command.status);
1729
+ return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
1730
+ }
1731
+ const board = taskMod.getBoard();
1732
+ return board.full || board.compact || "no tasks found";
1733
+ }
1734
+ if (command.kind === "task.create") {
1735
+ try {
1736
+ const created = taskMod.createTask({
1737
+ title: command.title,
1738
+ type: command.type ?? "one-shot",
1739
+ category: "general",
1740
+ body: "",
1741
+ });
1742
+ return `created: ${created}`;
1743
+ }
1744
+ catch (error) {
1745
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1746
+ }
1747
+ }
1748
+ if (command.kind === "task.update") {
1749
+ const result = taskMod.updateStatus(command.id, command.status);
1750
+ if (!result.ok) {
1751
+ return `error: ${result.reason ?? "status update failed"}`;
1752
+ }
1753
+ const archivedSuffix = result.archived && result.archived.length > 0
1754
+ ? ` | archived: ${result.archived.join(", ")}`
1755
+ : "";
1756
+ return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
1757
+ }
1758
+ if (command.kind === "task.show") {
1759
+ const task = taskMod.getTask(command.id);
1760
+ if (!task)
1761
+ return `task not found: ${command.id}`;
1762
+ return [
1763
+ `title: ${task.title}`,
1764
+ `type: ${task.type}`,
1765
+ `status: ${task.status}`,
1766
+ `category: ${task.category}`,
1767
+ `created: ${task.created}`,
1768
+ `updated: ${task.updated}`,
1769
+ `path: ${task.path}`,
1770
+ task.body ? `\n${task.body}` : "",
1771
+ ].filter(Boolean).join("\n");
1772
+ }
1773
+ if (command.kind === "task.actionable") {
1774
+ const lines = taskMod.boardAction();
1775
+ return lines.length > 0 ? lines.join("\n") : "no action required";
1776
+ }
1777
+ if (command.kind === "task.deps") {
1778
+ const lines = taskMod.boardDeps();
1779
+ return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
1780
+ }
1781
+ // command.kind === "task.sessions"
1782
+ const lines = taskMod.boardSessions();
1783
+ return lines.length > 0 ? lines.join("\n") : "no active sessions";
1784
+ }
1785
+ const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
1786
+ /* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
1787
+ function higherTrust(a, b) {
1788
+ const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
1789
+ const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
1790
+ return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
1791
+ }
1792
+ /* v8 ignore stop */
1793
+ async function executeFriendCommand(command, store) {
1794
+ if (command.kind === "friend.list") {
1795
+ const listAll = store.listAll;
1796
+ if (!listAll)
1797
+ return "friend store does not support listing";
1798
+ const friends = await listAll.call(store);
1799
+ if (friends.length === 0)
1800
+ return "no friends found";
1801
+ const lines = friends.map((f) => {
1802
+ const trust = f.trustLevel ?? "unknown";
1803
+ return `${f.id} ${f.name} ${trust}`;
1804
+ });
1805
+ return lines.join("\n");
1806
+ }
1807
+ if (command.kind === "friend.show") {
1808
+ const record = await store.get(command.friendId);
1809
+ if (!record)
1810
+ return `friend not found: ${command.friendId}`;
1811
+ return JSON.stringify(record, null, 2);
1812
+ }
1813
+ if (command.kind === "friend.create") {
1814
+ const now = new Date().toISOString();
1815
+ const id = (0, crypto_1.randomUUID)();
1816
+ const trustLevel = (command.trustLevel ?? "acquaintance");
1817
+ await store.put(id, {
1818
+ id,
1819
+ name: command.name,
1820
+ trustLevel,
1821
+ externalIds: [],
1822
+ tenantMemberships: [],
1823
+ toolPreferences: {},
1824
+ notes: {},
1825
+ totalTokens: 0,
1826
+ createdAt: now,
1827
+ updatedAt: now,
1828
+ schemaVersion: 1,
1829
+ });
1830
+ return `created: ${id} (${command.name}, ${trustLevel})`;
1831
+ }
1832
+ if (command.kind === "friend.update") {
1833
+ const current = await store.get(command.friendId);
1834
+ if (!current)
1835
+ return `friend not found: ${command.friendId}`;
1836
+ const now = new Date().toISOString();
1837
+ await store.put(command.friendId, {
1838
+ ...current,
1839
+ trustLevel: command.trustLevel,
1840
+ role: command.trustLevel,
1841
+ updatedAt: now,
1842
+ });
1843
+ return `updated: ${command.friendId} → trust=${command.trustLevel}`;
1844
+ }
1845
+ if (command.kind === "friend.link") {
1846
+ const current = await store.get(command.friendId);
1847
+ if (!current)
1848
+ return `friend not found: ${command.friendId}`;
1849
+ const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
1850
+ if (alreadyLinked)
1851
+ return `identity already linked: ${command.provider}:${command.externalId}`;
1852
+ const now = new Date().toISOString();
1853
+ const newExternalIds = [
1854
+ ...current.externalIds,
1855
+ { provider: command.provider, externalId: command.externalId, linkedAt: now },
1856
+ ];
1857
+ // Orphan cleanup: check if another friend has this externalId
1858
+ const orphan = await store.findByExternalId(command.provider, command.externalId);
1859
+ let mergeMessage = "";
1860
+ let mergedNotes = { ...current.notes };
1861
+ let mergedTrust = current.trustLevel;
1862
+ let orphanExternalIds = [];
1863
+ if (orphan && orphan.id !== command.friendId) {
1864
+ // Merge orphan's notes (target's notes take priority)
1865
+ mergedNotes = { ...orphan.notes, ...current.notes };
1866
+ // Keep higher trust level
1867
+ mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
1868
+ // Collect orphan's other externalIds (excluding the one being linked)
1869
+ orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
1870
+ await store.delete(orphan.id);
1871
+ mergeMessage = ` (merged orphan ${orphan.id})`;
1872
+ }
1873
+ await store.put(command.friendId, {
1874
+ ...current,
1875
+ externalIds: [...newExternalIds, ...orphanExternalIds],
1876
+ notes: mergedNotes,
1877
+ trustLevel: mergedTrust,
1878
+ updatedAt: now,
1879
+ });
1880
+ return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
1881
+ }
1882
+ // command.kind === "friend.unlink"
1883
+ const current = await store.get(command.friendId);
1884
+ if (!current)
1885
+ return `friend not found: ${command.friendId}`;
1886
+ const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
1887
+ if (idx === -1)
1888
+ return `identity not linked: ${command.provider}:${command.externalId}`;
1889
+ const now = new Date().toISOString();
1890
+ const filtered = current.externalIds.filter((_, i) => i !== idx);
1891
+ await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
1892
+ return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
1893
+ }
1894
+ function executeReminderCommand(command, taskMod) {
1895
+ try {
1896
+ const created = taskMod.createTask({
1897
+ title: command.title,
1898
+ type: command.cadence ? "ongoing" : "one-shot",
1899
+ category: command.category ?? "reminder",
1900
+ body: command.body,
1901
+ scheduledAt: command.scheduledAt,
1902
+ cadence: command.cadence,
1903
+ requester: command.requester,
1904
+ });
1905
+ return `created: ${created}`;
1906
+ }
1907
+ catch (error) {
1908
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1909
+ }
1910
+ }
1911
+ /* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
1912
+ function getDevConfigPath() {
1913
+ return path.join((0, ouro_version_manager_1.getOuroCliHome)(), "dev-config.json");
1914
+ }
1915
+ function readPersistedDevPath() {
1916
+ try {
1917
+ const data = JSON.parse(fs.readFileSync(getDevConfigPath(), "utf-8"));
1918
+ return typeof data.repoPath === "string" ? data.repoPath : null;
1919
+ }
1920
+ catch {
1921
+ return null;
1922
+ }
1923
+ }
1924
+ function persistDevPath(repoPath) {
1925
+ try {
1926
+ fs.mkdirSync(path.dirname(getDevConfigPath()), { recursive: true });
1927
+ fs.writeFileSync(getDevConfigPath(), JSON.stringify({ repoPath }, null, 2));
1928
+ }
1929
+ catch { /* best effort */ }
1930
+ }
1931
+ function resolveDevRepoCwd(command, checkExists, deps) {
1932
+ // 1. Explicit --repo-path: use it and persist for next time
1933
+ if (command.repoPath) {
1934
+ const resolved = path.resolve(command.repoPath);
1935
+ persistDevPath(resolved);
1936
+ return resolved;
1937
+ }
1938
+ // 2. Clone request
1939
+ if (command.clone)
1940
+ return resolveClonePath(command, checkExists, deps);
1941
+ // 3. If test/internal deps provide getRepoCwd, use it directly
1942
+ if (deps.getRepoCwd) {
1943
+ return deps.getRepoCwd();
1944
+ }
1945
+ // 4. Check CWD — if we're inside a harness repo, use it
1946
+ const cwd = process.cwd();
1947
+ if (checkExists(path.join(cwd, ".git")) && checkExists(path.join(cwd, "src", "heart", "daemon"))) {
1948
+ persistDevPath(cwd);
1949
+ return cwd;
1950
+ }
1951
+ // 5. Read persisted path from last --repo-path
1952
+ const persisted = readPersistedDevPath();
1953
+ if (persisted && checkExists(path.join(persisted, ".git"))) {
1954
+ deps.writeStdout(`using remembered dev path: ${persisted}`);
1955
+ return persisted;
1956
+ }
1957
+ // 6. Fall back to getRepoRoot (works when running from installed binary → resolves to npm package)
1958
+ return (0, identity_1.getRepoRoot)();
1959
+ }
1960
+ /* v8 ignore stop */
1961
+ /* v8 ignore start -- clone/build: requires real git clone + npm install on disk @preserve */
1962
+ function resolveClonePath(command, checkExists, deps) {
1963
+ const cloneTarget = command.clonePath
1964
+ ? path.resolve(command.clonePath)
1965
+ : path.join(os.homedir(), "Projects", "ouroboros");
1966
+ if (!checkExists(path.join(cloneTarget, ".git"))) {
1967
+ deps.writeStdout(`cloning ouroboros to ${cloneTarget}...`);
1968
+ try {
1969
+ (0, child_process_1.execSync)(`git clone ${identity_1.HARNESS_CANONICAL_REPO_URL} "${cloneTarget}"`, { stdio: "inherit" });
1970
+ }
1971
+ catch {
1972
+ throw new Error(`clone failed. check your network and try again, or clone manually and use --repo-path.`);
1973
+ }
1974
+ }
1975
+ else {
1976
+ deps.writeStdout(`repo already exists at ${cloneTarget}, pulling latest...`);
1977
+ try {
1978
+ (0, child_process_1.execSync)("git pull --ff-only", { cwd: cloneTarget, stdio: "inherit" });
1979
+ }
1980
+ catch {
1981
+ deps.writeStdout("pull failed (may have local changes). continuing with existing code.");
1982
+ }
1983
+ }
1984
+ deps.writeStdout("building...");
1985
+ try {
1986
+ (0, child_process_1.execSync)("npm install && npm run build", { cwd: cloneTarget, stdio: "inherit" });
1987
+ }
1988
+ catch {
1989
+ throw new Error(`build failed in ${cloneTarget}. check the output above.`);
1990
+ }
1991
+ return cloneTarget;
1992
+ }
1993
+ /* v8 ignore stop */
906
1994
  async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
907
- if (args.includes("--help") || args.includes("-h")) {
1995
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
908
1996
  const text = usage();
909
1997
  deps.writeStdout(text);
910
1998
  return text;
911
1999
  }
2000
+ if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
2001
+ const text = formatVersionOutput();
2002
+ deps.writeStdout(text);
2003
+ return text;
2004
+ }
912
2005
  let command;
913
2006
  try {
914
2007
  command = parseOuroCommand(args);
@@ -979,21 +2072,1014 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
979
2072
  meta: { kind: command.kind },
980
2073
  });
981
2074
  if (command.kind === "daemon.up") {
2075
+ // ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
2076
+ /* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
2077
+ try {
2078
+ const devConfigPath = getDevConfigPath();
2079
+ if (fs.existsSync(devConfigPath)) {
2080
+ fs.unlinkSync(devConfigPath);
2081
+ }
2082
+ }
2083
+ catch { /* best effort */ }
2084
+ /* v8 ignore stop */
2085
+ // ── dev mode delegation: ouro up from a dev repo delegates to installed binary ──
2086
+ // Only runs when detectMode is explicitly injected (via createDefaultOuroCliDeps or tests)
2087
+ if (deps.detectMode) {
2088
+ const runtimeMode = deps.detectMode();
2089
+ if (runtimeMode === "dev") {
2090
+ /* v8 ignore next -- defensive: getInstalledBinaryPath always injected in tests @preserve */
2091
+ const installedBinary = deps.getInstalledBinaryPath ? deps.getInstalledBinaryPath() : null;
2092
+ if (installedBinary) {
2093
+ deps.writeStdout("delegating to installed ouro...");
2094
+ /* v8 ignore next 3 -- defensive: execInstalledBinary always injected; missing branch unreachable @preserve */
2095
+ if (deps.execInstalledBinary) {
2096
+ deps.execInstalledBinary(installedBinary, args);
2097
+ }
2098
+ /* v8 ignore next 2 -- unreachable after exec replaces process @preserve */
2099
+ return "";
2100
+ }
2101
+ const message = "no installed version found. run: npx @ouro.bot/cli@alpha";
2102
+ deps.writeStdout(message);
2103
+ return message;
2104
+ }
2105
+ }
2106
+ const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
2107
+ // ── versioned CLI update check ──
2108
+ if (deps.checkForCliUpdate) {
2109
+ let pendingReExec = false;
2110
+ try {
2111
+ const updateResult = await deps.checkForCliUpdate();
2112
+ if (updateResult.available && updateResult.latestVersion) {
2113
+ /* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
2114
+ const currentVersion = linkedVersionBeforeUp ?? "unknown";
2115
+ await deps.installCliVersion(updateResult.latestVersion);
2116
+ deps.activateCliVersion(updateResult.latestVersion);
2117
+ deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
2118
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
2119
+ /* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
2120
+ if (changelogCommand) {
2121
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
2122
+ }
2123
+ pendingReExec = true;
2124
+ }
2125
+ /* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
2126
+ }
2127
+ catch (error) {
2128
+ (0, runtime_1.emitNervesEvent)({
2129
+ level: "warn",
2130
+ component: "daemon",
2131
+ event: "daemon.cli_update_check_error",
2132
+ message: "CLI update check failed",
2133
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
2134
+ });
2135
+ }
2136
+ /* v8 ignore stop */
2137
+ if (pendingReExec) {
2138
+ deps.reExecFromNewVersion(args);
2139
+ }
2140
+ }
982
2141
  await performSystemSetup(deps);
2142
+ const linkedVersionAfterSetup = deps.getCurrentCliVersion?.() ?? null;
2143
+ const runtimeVersion = (0, bundle_manifest_1.getPackageVersion)();
2144
+ if (linkedVersionBeforeUp && linkedVersionBeforeUp !== runtimeVersion && linkedVersionAfterSetup === runtimeVersion) {
2145
+ deps.writeStdout(`ouro updated to ${runtimeVersion} (was ${linkedVersionBeforeUp})`);
2146
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(linkedVersionBeforeUp, runtimeVersion);
2147
+ if (changelogCommand) {
2148
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
2149
+ }
2150
+ }
2151
+ if (deps.ensureDaemonBootPersistence) {
2152
+ try {
2153
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
2154
+ }
2155
+ catch (error) {
2156
+ (0, runtime_1.emitNervesEvent)({
2157
+ level: "warn",
2158
+ component: "daemon",
2159
+ event: "daemon.system_setup_launchd_error",
2160
+ message: "failed to persist daemon boot startup",
2161
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
2162
+ });
2163
+ }
2164
+ }
2165
+ // Run update hooks before starting daemon so user sees the output
2166
+ (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
2167
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
2168
+ const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
2169
+ // Snapshot the previous CLI version from the first bundle-meta before
2170
+ // hooks overwrite it. This detects when npx downloaded a newer CLI.
2171
+ const previousCliVersion = readFirstBundleMetaVersion(bundlesRoot);
2172
+ const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
2173
+ // Notify about CLI binary update (npx downloaded a new version).
2174
+ // Skip when the symlink already points to the running version — that
2175
+ // means path 1 (checkForCliUpdate + reExecFromNewVersion) already
2176
+ // printed the update message before re-exec.
2177
+ /* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
2178
+ if (previousCliVersion && previousCliVersion !== currentVersion && linkedVersionBeforeUp !== currentVersion) {
2179
+ deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
2180
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
2181
+ /* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
2182
+ if (changelogCommand) {
2183
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
2184
+ }
2185
+ }
2186
+ /* v8 ignore stop */
2187
+ if (updateSummary.updated.length > 0) {
2188
+ const agents = updateSummary.updated.map((e) => e.agent);
2189
+ const from = updateSummary.updated[0].from;
2190
+ const to = updateSummary.updated[0].to;
2191
+ const fromStr = from ? ` (was ${from})` : "";
2192
+ const count = agents.length;
2193
+ deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
2194
+ }
983
2195
  const daemonResult = await ensureDaemonRunning(deps);
984
2196
  deps.writeStdout(daemonResult.message);
985
2197
  return daemonResult.message;
986
2198
  }
2199
+ if (command.kind === "daemon.dev") {
2200
+ /* v8 ignore next -- defensive: existsSync always injected in tests @preserve */
2201
+ const checkExists = deps.existsSync ?? fs.existsSync;
2202
+ /* v8 ignore next -- repo resolution dispatched to v8-ignored helper @preserve */
2203
+ let repoCwd = resolveDevRepoCwd(command, checkExists, deps);
2204
+ let entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2205
+ if (!checkExists(entryPath) || !checkExists(path.join(repoCwd, ".git"))) {
2206
+ if (command.repoPath) {
2207
+ // Explicit --repo-path didn't have a valid repo — error
2208
+ const message = `no harness repo found at ${repoCwd}. run npm run build first.`;
2209
+ deps.writeStdout(message);
2210
+ return message;
2211
+ }
2212
+ /* v8 ignore start -- auto-clone: interactive prompt + existing repo discovery + real git/npm @preserve */
2213
+ const defaultClonePath = path.join(os.homedir(), "Projects", "ouroboros");
2214
+ if (checkExists(path.join(defaultClonePath, ".git"))) {
2215
+ deps.writeStdout(`found existing repo at ${defaultClonePath}`);
2216
+ try {
2217
+ repoCwd = resolveClonePath({ clonePath: defaultClonePath }, checkExists, deps);
2218
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2219
+ }
2220
+ catch (err) {
2221
+ const message = err instanceof Error ? err.message : String(err);
2222
+ deps.writeStdout(message);
2223
+ return message;
2224
+ }
2225
+ }
2226
+ else if (deps.promptInput) {
2227
+ deps.writeStdout("no harness repo found.");
2228
+ const answer = await deps.promptInput(`already have a checkout? enter its path, or press enter to clone to ${defaultClonePath}: `);
2229
+ const cloneTarget = answer.trim() || defaultClonePath;
2230
+ try {
2231
+ repoCwd = resolveClonePath({ clonePath: cloneTarget }, checkExists, deps);
2232
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2233
+ }
2234
+ catch (err) {
2235
+ const message = err instanceof Error ? err.message : String(err);
2236
+ deps.writeStdout(message);
2237
+ return message;
2238
+ }
2239
+ }
2240
+ else {
2241
+ const message = `no harness repo found. run: ouro dev --repo-path /path/to/ouroboros`;
2242
+ deps.writeStdout(message);
2243
+ return message;
2244
+ }
2245
+ /* v8 ignore stop */
2246
+ }
2247
+ // Auto-build: always rebuild in dev mode so dist/ matches source
2248
+ /* v8 ignore start -- dev auto-build: execSync in repo cwd, tested manually @preserve */
2249
+ deps.writeStdout(`building from ${repoCwd}...`);
2250
+ try {
2251
+ (0, child_process_1.execSync)("npm run build", { cwd: repoCwd, stdio: "inherit" });
2252
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2253
+ }
2254
+ catch {
2255
+ const message = `build failed in ${repoCwd}. fix compilation errors and retry.`;
2256
+ deps.writeStdout(message);
2257
+ return message;
2258
+ }
2259
+ /* v8 ignore stop */
2260
+ /* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
2261
+ if (deps.ensureDaemonBootPersistence) {
2262
+ try {
2263
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
2264
+ /* v8 ignore next -- defensive: boot persistence error should not block dev mode @preserve */
2265
+ }
2266
+ catch (error) {
2267
+ (0, runtime_1.emitNervesEvent)({
2268
+ level: "warn",
2269
+ component: "daemon",
2270
+ event: "daemon.dev_boot_persistence_error",
2271
+ message: "failed to persist daemon boot startup in dev mode",
2272
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
2273
+ });
2274
+ }
2275
+ /* v8 ignore stop */
2276
+ }
2277
+ // Disable launchd KeepAlive before killing — prevents the installed daemon from respawning
2278
+ /* v8 ignore start -- dev launchd disable: requires real launchctl + plist on disk @preserve */
2279
+ const launchdDevDeps = {
2280
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd); },
2281
+ existsFile: (p) => fs.existsSync(p),
2282
+ removeFile: (p) => { try {
2283
+ fs.unlinkSync(p);
2284
+ }
2285
+ catch { /* best effort */ } },
2286
+ homeDir: os.homedir(),
2287
+ userUid: process.getuid?.() ?? 0,
2288
+ };
2289
+ if ((0, launchd_1.isDaemonInstalled)(launchdDevDeps)) {
2290
+ (0, launchd_1.uninstallLaunchAgent)(launchdDevDeps);
2291
+ deps.writeStdout("disabled launchd auto-restart for dev mode");
2292
+ }
2293
+ /* v8 ignore stop */
2294
+ // Always force-restart in dev mode — you rebuilt, you want this code running
2295
+ /* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
2296
+ const alive = await deps.checkSocketAlive(deps.socketPath);
2297
+ if (alive) {
2298
+ try {
2299
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
2300
+ }
2301
+ catch { /* already stopping */ }
2302
+ }
2303
+ deps.cleanupStaleSocket(deps.socketPath);
2304
+ const devEntry = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2305
+ const startDevDaemon = deps.startDaemonProcess === defaultStartDaemonProcess
2306
+ ? async (sp) => {
2307
+ const child = (0, child_process_1.spawn)("node", [devEntry, "--socket", sp], { detached: true, stdio: "ignore" });
2308
+ child.unref();
2309
+ return { pid: child.pid ?? null };
2310
+ }
2311
+ : deps.startDaemonProcess;
2312
+ /* v8 ignore stop */
2313
+ const started = await startDevDaemon(deps.socketPath);
2314
+ /* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
2315
+ const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})\nrun 'ouro up' to return to production mode`;
2316
+ deps.writeStdout(message);
2317
+ return message;
2318
+ }
2319
+ // ── rollback command (local, no daemon socket needed for symlinks) ──
2320
+ /* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
2321
+ if (command.kind === "rollback") {
2322
+ const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
2323
+ if (command.version) {
2324
+ // Rollback to a specific version
2325
+ const installed = deps.listCliVersions?.() ?? [];
2326
+ if (!installed.includes(command.version)) {
2327
+ try {
2328
+ await deps.installCliVersion(command.version);
2329
+ }
2330
+ catch (error) {
2331
+ const message = `failed to install version ${command.version}: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
2332
+ deps.writeStdout(message);
2333
+ return message;
2334
+ }
2335
+ }
2336
+ deps.activateCliVersion(command.version);
2337
+ }
2338
+ else {
2339
+ // Rollback to previous version
2340
+ const previousVersion = deps.getPreviousCliVersion?.();
2341
+ if (!previousVersion) {
2342
+ const message = "no previous version to roll back to";
2343
+ deps.writeStdout(message);
2344
+ return message;
2345
+ }
2346
+ deps.activateCliVersion(previousVersion);
2347
+ command = { ...command, version: previousVersion };
2348
+ }
2349
+ // Stop daemon (non-fatal if not running)
2350
+ try {
2351
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
2352
+ }
2353
+ catch {
2354
+ // Daemon may not be running — that's fine
2355
+ }
2356
+ const message = `rolled back to ${command.version} (was ${currentVersion})`;
2357
+ deps.writeStdout(message);
2358
+ return message;
2359
+ }
2360
+ // ── versions command (local install list + published update truth, no daemon socket needed) ──
2361
+ if (command.kind === "versions") {
2362
+ const versions = deps.listCliVersions?.() ?? [];
2363
+ const current = deps.getCurrentCliVersion?.();
2364
+ const previous = deps.getPreviousCliVersion?.();
2365
+ const localSection = versions.length === 0
2366
+ ? "no versions installed"
2367
+ : versions.map((v) => {
2368
+ let line = v;
2369
+ if (v === current)
2370
+ line += " * current";
2371
+ if (v === previous)
2372
+ line += " (previous)";
2373
+ return line;
2374
+ }).join("\n");
2375
+ const sections = [localSection];
2376
+ if (deps.checkForCliUpdate) {
2377
+ try {
2378
+ const updateResult = await deps.checkForCliUpdate();
2379
+ if (updateResult.latestVersion) {
2380
+ sections.push(`published alpha: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
2381
+ }
2382
+ else if (updateResult.error) {
2383
+ sections.push(`published alpha: unavailable (${updateResult.error})`);
2384
+ }
2385
+ }
2386
+ catch (err) {
2387
+ const reason = err instanceof Error ? err.message : String(err);
2388
+ sections.push(`published alpha: unavailable (${reason})`);
2389
+ }
2390
+ }
2391
+ const message = sections.join("\n\n");
2392
+ deps.writeStdout(message);
2393
+ return message;
2394
+ }
2395
+ /* v8 ignore stop */
987
2396
  if (command.kind === "daemon.logs" && deps.tailLogs) {
988
2397
  deps.tailLogs();
989
2398
  return "";
990
2399
  }
991
- if (command.kind === "friend.link") {
992
- const linker = deps.linkFriendIdentity ?? defaultLinkFriendIdentity;
993
- const message = await linker(command);
2400
+ // ── hook: handle Claude Code lifecycle hooks ──
2401
+ /* v8 ignore start -- hook handler: reads real stdin, sends to real daemon @preserve */
2402
+ if (command.kind === "hook") {
2403
+ let stdinData = "";
2404
+ try {
2405
+ stdinData = require("fs").readFileSync(0, "utf-8");
2406
+ }
2407
+ catch { /* no stdin */ }
2408
+ let event = {};
2409
+ try {
2410
+ event = JSON.parse(stdinData);
2411
+ }
2412
+ catch { /* malformed */ }
2413
+ const eventType = command.event;
2414
+ const sessionId = event.session_id ?? "unknown";
2415
+ const cwd = event.cwd ?? "";
2416
+ // Build notification content based on event type
2417
+ let content;
2418
+ if (eventType === "session-start") {
2419
+ const model = event.model ?? "";
2420
+ const source = event.source ?? "";
2421
+ content = `[Claude Code session started: ${sessionId}, cwd: ${cwd}${model ? `, model: ${model}` : ""}${source ? `, source: ${source}` : ""}]`;
2422
+ }
2423
+ else if (eventType === "stop") {
2424
+ const lastMsg = event.last_assistant_message ?? "";
2425
+ content = `[Claude Code session ended: ${sessionId}${lastMsg ? `, last: ${lastMsg.slice(0, 200)}` : ""}]`;
2426
+ }
2427
+ else if (eventType === "post-tool-use") {
2428
+ const toolName = event.tool_name ?? "";
2429
+ content = `[Claude Code used ${toolName} in session ${sessionId}]`;
2430
+ }
2431
+ else {
2432
+ content = `[Claude Code hook: ${eventType} in session ${sessionId}]`;
2433
+ }
2434
+ // Send to the specific agent configured for this hook
2435
+ try {
2436
+ await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: command.agent, content }).catch(() => { });
2437
+ await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: command.agent }).catch(() => { });
2438
+ }
2439
+ catch { /* daemon not running — silent */ }
2440
+ // Output for Claude Code hook system
2441
+ deps.writeStdout(JSON.stringify({ continue: true }));
2442
+ return JSON.stringify({ continue: true });
2443
+ }
2444
+ /* v8 ignore stop */
2445
+ // ── setup: configure dev tool integration ──
2446
+ if (command.kind === "setup") {
2447
+ const { tool, agent: setupAgent } = command;
2448
+ const sourceRoot = (0, identity_1.getRepoRoot)();
2449
+ const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
2450
+ const mcpServeCommand = runtimeMode === "dev"
2451
+ ? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
2452
+ : `ouro mcp-serve --agent ${setupAgent}`;
2453
+ if (tool === "claude-code") {
2454
+ // 1. Register MCP server with Claude Code
2455
+ const mcpAddCmd = `claude mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
2456
+ (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
2457
+ // 2. Write hooks config to ~/.claude/settings.json
2458
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2459
+ let settings = {};
2460
+ if (fs.existsSync(settingsPath)) {
2461
+ try {
2462
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
2463
+ }
2464
+ catch { /* start fresh */ }
2465
+ }
2466
+ // Use `ouro hook <event>` — resolves the right code based on dev vs installed mode.
2467
+ // Bare `ouro` works because ouro is on PATH via ~/.ouro-cli/bin/.
2468
+ settings.hooks = {
2469
+ ...(settings.hooks ?? {}),
2470
+ SessionStart: [{ hooks: [{ type: "command", command: `ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
2471
+ Stop: [{ hooks: [{ type: "command", command: `ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
2472
+ PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
2473
+ };
2474
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
2475
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
2476
+ (0, runtime_1.emitNervesEvent)({
2477
+ component: "daemon",
2478
+ event: "daemon.setup_complete",
2479
+ message: "dev tool setup complete",
2480
+ meta: { tool, agent: setupAgent, runtimeMode },
2481
+ });
2482
+ // 3. Write conversation formatting instructions to ~/.claude/CLAUDE.md
2483
+ const claudeMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
2484
+ const agentInstructions = `\n## Agent conversations (ouro)\nWhen using MCP \`send_message\` to talk to an ouro agent, format the exchange clearly:\n- Before the tool call, briefly say what you're asking/telling the agent\n- After the response, quote the agent's reply in a blockquote, then add your reaction\n- Example: **Me → Agent:** "question" / > **Agent:** "response" / Your synthesis here\n`;
2485
+ let existingClaudeMd = "";
2486
+ if (fs.existsSync(claudeMdPath)) {
2487
+ existingClaudeMd = fs.readFileSync(claudeMdPath, "utf-8");
2488
+ }
2489
+ if (!existingClaudeMd.includes("Agent conversations (ouro)")) {
2490
+ fs.writeFileSync(claudeMdPath, existingClaudeMd + agentInstructions);
2491
+ }
2492
+ const message = `setup complete: claude-code + ${setupAgent}\n MCP server registered\n hooks configured\n conversation formatting instructions added`;
2493
+ deps.writeStdout(message);
2494
+ return message;
2495
+ }
2496
+ else {
2497
+ // tool === "codex" (parseSetupCommand validates tool, so this is the only remaining option)
2498
+ const mcpAddCmd = `codex mcp add ouro-${setupAgent} -- ${mcpServeCommand}`;
2499
+ (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
2500
+ (0, runtime_1.emitNervesEvent)({
2501
+ component: "daemon",
2502
+ event: "daemon.setup_complete",
2503
+ message: "dev tool setup complete",
2504
+ meta: { tool, agent: setupAgent, runtimeMode },
2505
+ });
2506
+ const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
2507
+ deps.writeStdout(message);
2508
+ return message;
2509
+ }
2510
+ }
2511
+ /* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
2512
+ // ── mcp-serve: start MCP server in-process on stdin/stdout ──
2513
+ if (command.kind === "mcp-serve") {
2514
+ const { createMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcp-server")));
2515
+ const friendId = command.friendId ?? `local-${os.userInfo().username}`;
2516
+ const mcpSocketPath = command.socketOverride ?? deps.socketPath;
2517
+ const server = createMcpServer({
2518
+ agent: command.agent,
2519
+ friendId,
2520
+ socketPath: mcpSocketPath,
2521
+ stdin: process.stdin,
2522
+ stdout: process.stdout,
2523
+ });
2524
+ server.start();
2525
+ (0, runtime_1.emitNervesEvent)({
2526
+ component: "daemon",
2527
+ event: "daemon.mcp_serve_started",
2528
+ message: "MCP server started via CLI",
2529
+ meta: { agent: command.agent, friendId },
2530
+ });
2531
+ // Keep process alive until stdin closes
2532
+ await new Promise((resolve) => {
2533
+ process.stdin.on("end", () => {
2534
+ server.stop();
2535
+ resolve();
2536
+ });
2537
+ });
2538
+ return "";
2539
+ }
2540
+ /* v8 ignore stop */
2541
+ // ── mcp subcommands (routed through daemon socket) ──
2542
+ if (command.kind === "mcp.list" || command.kind === "mcp.call") {
2543
+ const daemonCommand = toDaemonCommand(command);
2544
+ let response;
2545
+ try {
2546
+ response = await deps.sendCommand(deps.socketPath, daemonCommand);
2547
+ }
2548
+ catch {
2549
+ const message = "daemon unavailable — start with `ouro up` first";
2550
+ deps.writeStdout(message);
2551
+ return message;
2552
+ }
2553
+ if (!response.ok) {
2554
+ const message = response.error ?? "unknown error";
2555
+ deps.writeStdout(message);
2556
+ return message;
2557
+ }
2558
+ const message = formatMcpResponse(command, response);
2559
+ deps.writeStdout(message);
2560
+ return message;
2561
+ }
2562
+ // ── task subcommands (local, no daemon socket needed) ──
2563
+ if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
2564
+ command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
2565
+ command.kind === "task.sessions") {
2566
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2567
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
2568
+ /* v8 ignore stop */
2569
+ const message = executeTaskCommand(command, taskMod);
2570
+ deps.writeStdout(message);
2571
+ return message;
2572
+ }
2573
+ // ── reminder subcommands (local, no daemon socket needed) ──
2574
+ if (command.kind === "reminder.create") {
2575
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2576
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
2577
+ /* v8 ignore stop */
2578
+ const message = executeReminderCommand(command, taskMod);
2579
+ deps.writeStdout(message);
2580
+ return message;
2581
+ }
2582
+ // ── habit subcommands (local, no daemon socket needed) ──
2583
+ if (command.kind === "habit.list" || command.kind === "habit.create") {
2584
+ const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("./habit-parser")));
2585
+ /* v8 ignore start -- production default: uses real bundle root @preserve */
2586
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2587
+ const bundleRoot = deps.agentBundleRoot ?? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
2588
+ /* v8 ignore stop */
2589
+ const habitsDir = path.join(bundleRoot, "habits");
2590
+ if (command.kind === "habit.list") {
2591
+ let files;
2592
+ try {
2593
+ files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md");
2594
+ }
2595
+ catch {
2596
+ const message = "no habits found";
2597
+ deps.writeStdout(message);
2598
+ return message;
2599
+ }
2600
+ if (files.length === 0) {
2601
+ const message = "no habits found";
2602
+ deps.writeStdout(message);
2603
+ return message;
2604
+ }
2605
+ const lines = [];
2606
+ for (const file of files) {
2607
+ const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
2608
+ const habit = parseHabitFile(content, path.join(habitsDir, file));
2609
+ const lastRunStr = habit.lastRun ?? "never";
2610
+ lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
2611
+ }
2612
+ const message = lines.join("\n");
2613
+ deps.writeStdout(message);
2614
+ return message;
2615
+ }
2616
+ // habit.create
2617
+ const filePath = path.join(habitsDir, `${command.name}.md`);
2618
+ if (fs.existsSync(filePath)) {
2619
+ const message = `error: habit '${command.name}' already exists`;
2620
+ deps.writeStdout(message);
2621
+ return message;
2622
+ }
2623
+ fs.mkdirSync(habitsDir, { recursive: true });
2624
+ const now = new Date().toISOString();
2625
+ const content = renderHabitFile({
2626
+ title: command.name,
2627
+ cadence: command.cadence ?? "null",
2628
+ status: "active",
2629
+ lastRun: now,
2630
+ created: now,
2631
+ }, `Habit: ${command.name}`);
2632
+ fs.writeFileSync(filePath, content, "utf-8");
2633
+ const message = `created: ${filePath}`;
2634
+ deps.writeStdout(message);
2635
+ return message;
2636
+ }
2637
+ // ── friend subcommands (local, no daemon socket needed) ──
2638
+ if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
2639
+ command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
2640
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2641
+ let store = deps.friendStore;
2642
+ if (!store) {
2643
+ // Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
2644
+ const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
2645
+ const friendsDir = agentName
2646
+ ? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
2647
+ : path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
2648
+ store = new store_file_1.FileFriendStore(friendsDir);
2649
+ }
2650
+ /* v8 ignore stop */
2651
+ const message = await executeFriendCommand(command, store);
2652
+ deps.writeStdout(message);
2653
+ return message;
2654
+ }
2655
+ // ── auth (local, no daemon socket needed) ──
2656
+ if (command.kind === "auth.run") {
2657
+ const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent).config.provider;
2658
+ /* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
2659
+ const authRunner = deps.runAuthFlow ?? auth_flow_1.runRuntimeAuthFlow;
2660
+ const result = await authRunner({
2661
+ agentName: command.agent,
2662
+ provider,
2663
+ promptInput: deps.promptInput,
2664
+ });
2665
+ // Behavior: ouro auth stores credentials only — does NOT switch provider.
2666
+ // Use `ouro auth switch` to change the active provider.
2667
+ deps.writeStdout(result.message);
2668
+ // Verify the credentials actually work by pinging the provider
2669
+ /* v8 ignore start -- integration: real API ping after auth @preserve */
2670
+ try {
2671
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2672
+ const status = await verifyProviderCredentials(provider, secrets.providers);
2673
+ deps.writeStdout(`${provider}: ${status}`);
2674
+ }
2675
+ catch {
2676
+ // Verification failure is non-blocking — credentials were saved regardless
2677
+ }
2678
+ /* v8 ignore stop */
2679
+ return result.message;
2680
+ }
2681
+ // ── auth verify (local, no daemon socket needed) ──
2682
+ /* v8 ignore start -- auth verify/switch: tested in daemon-cli.test.ts but v8 traces differ in CI @preserve */
2683
+ if (command.kind === "auth.verify") {
2684
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2685
+ const providers = secrets.providers;
2686
+ if (command.provider) {
2687
+ const status = await verifyProviderCredentials(command.provider, providers);
2688
+ const message = `${command.provider}: ${status}`;
2689
+ deps.writeStdout(message);
2690
+ return message;
2691
+ }
2692
+ const lines = [];
2693
+ for (const p of Object.keys(providers)) {
2694
+ const status = await verifyProviderCredentials(p, providers);
2695
+ lines.push(`${p}: ${status}`);
2696
+ }
2697
+ const message = lines.join("\n");
994
2698
  deps.writeStdout(message);
995
2699
  return message;
996
2700
  }
2701
+ // ── auth switch (local, no daemon socket needed) ──
2702
+ if (command.kind === "auth.switch") {
2703
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2704
+ const providerSecrets = secrets.providers[command.provider];
2705
+ if (!providerSecrets || !hasStoredCredentials(command.provider, providerSecrets)) {
2706
+ const message = `no credentials stored for ${command.provider}. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`;
2707
+ deps.writeStdout(message);
2708
+ return message;
2709
+ }
2710
+ // Verify credentials actually work before switching
2711
+ const status = await verifyProviderCredentials(command.provider, secrets.providers);
2712
+ if (!status.startsWith("ok")) {
2713
+ const message = `${command.provider}: ${status}. fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` before switching.`;
2714
+ deps.writeStdout(message);
2715
+ return message;
2716
+ }
2717
+ (0, auth_flow_1.writeAgentProviderSelection)(command.agent, command.provider);
2718
+ const message = `switched ${command.agent} to ${command.provider} (verified working)`;
2719
+ deps.writeStdout(message);
2720
+ return message;
2721
+ }
2722
+ /* v8 ignore stop */
2723
+ // ── config models (local, no daemon socket needed) ──
2724
+ /* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
2725
+ if (command.kind === "config.models") {
2726
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
2727
+ const provider = config.provider;
2728
+ if (provider !== "github-copilot") {
2729
+ const message = `model listing not available for ${provider} — check provider documentation.`;
2730
+ deps.writeStdout(message);
2731
+ return message;
2732
+ }
2733
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2734
+ const ghConfig = secrets.providers["github-copilot"];
2735
+ if (!ghConfig.githubToken || !ghConfig.baseUrl) {
2736
+ throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
2737
+ }
2738
+ const fetchFn = deps.fetchImpl ?? fetch;
2739
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
2740
+ if (models.length === 0) {
2741
+ const message = "no models found";
2742
+ deps.writeStdout(message);
2743
+ return message;
2744
+ }
2745
+ const lines = ["available models:"];
2746
+ for (const m of models) {
2747
+ const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
2748
+ lines.push(` ${m.id}${caps}`);
2749
+ }
2750
+ const message = lines.join("\n");
2751
+ deps.writeStdout(message);
2752
+ return message;
2753
+ }
2754
+ /* v8 ignore stop */
2755
+ // ── config model (local, no daemon socket needed) ──
2756
+ /* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
2757
+ if (command.kind === "config.model") {
2758
+ // Validate model availability for github-copilot before writing
2759
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
2760
+ if (config.provider === "github-copilot") {
2761
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2762
+ const ghConfig = secrets.providers["github-copilot"];
2763
+ if (ghConfig.githubToken && ghConfig.baseUrl) {
2764
+ const fetchFn = deps.fetchImpl ?? fetch;
2765
+ try {
2766
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
2767
+ const available = models.map((m) => m.id);
2768
+ if (available.length > 0 && !available.includes(command.modelName)) {
2769
+ const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
2770
+ deps.writeStdout(message);
2771
+ return message;
2772
+ }
2773
+ }
2774
+ catch {
2775
+ // Catalog validation failed — fall through to ping test
2776
+ }
2777
+ // Ping test: verify the model actually works before switching
2778
+ const pingResult = await pingGithubCopilotModel(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
2779
+ if (!pingResult.ok) {
2780
+ const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
2781
+ deps.writeStdout(message);
2782
+ return message;
2783
+ }
2784
+ }
2785
+ }
2786
+ const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, command.modelName);
2787
+ const message = previousModel
2788
+ ? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
2789
+ : `set ${command.agent} model on ${provider}: ${command.modelName}`;
2790
+ deps.writeStdout(message);
2791
+ return message;
2792
+ }
2793
+ /* v8 ignore stop */
2794
+ // ── whoami (local, no daemon socket needed) ──
2795
+ if (command.kind === "whoami") {
2796
+ if (command.agent) {
2797
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
2798
+ const message = [
2799
+ `agent: ${command.agent}`,
2800
+ `home: ${agentRoot}`,
2801
+ `bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
2802
+ ].join("\n");
2803
+ deps.writeStdout(message);
2804
+ return message;
2805
+ }
2806
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2807
+ try {
2808
+ const info = deps.whoamiInfo
2809
+ ? deps.whoamiInfo()
2810
+ : {
2811
+ agentName: (0, identity_1.getAgentName)(),
2812
+ homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
2813
+ bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
2814
+ };
2815
+ const message = [
2816
+ `agent: ${info.agentName}`,
2817
+ `home: ${info.homePath}`,
2818
+ `bones: ${info.bonesVersion}`,
2819
+ ].join("\n");
2820
+ deps.writeStdout(message);
2821
+ return message;
2822
+ }
2823
+ catch {
2824
+ const message = "error: no agent context — use --agent <name> to specify";
2825
+ deps.writeStdout(message);
2826
+ return message;
2827
+ }
2828
+ /* v8 ignore stop */
2829
+ }
2830
+ // ── changelog (local, no daemon socket needed) ──
2831
+ if (command.kind === "changelog") {
2832
+ try {
2833
+ const changelogPath = deps.getChangelogPath
2834
+ ? deps.getChangelogPath()
2835
+ : (0, bundle_manifest_1.getChangelogPath)();
2836
+ const raw = fs.readFileSync(changelogPath, "utf-8");
2837
+ const parsed = JSON.parse(raw);
2838
+ const entries = Array.isArray(parsed) ? parsed : (parsed.versions ?? []);
2839
+ let filtered = entries;
2840
+ if (command.from) {
2841
+ const fromVersion = command.from;
2842
+ filtered = entries.filter((e) => semver.valid(e.version) && semver.gt(e.version, fromVersion));
2843
+ }
2844
+ if (filtered.length === 0) {
2845
+ const message = "no changelog entries found.";
2846
+ deps.writeStdout(message);
2847
+ return message;
2848
+ }
2849
+ const lines = [];
2850
+ for (const entry of filtered) {
2851
+ lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
2852
+ if (entry.changes) {
2853
+ for (const change of entry.changes) {
2854
+ lines.push(`- ${change}`);
2855
+ }
2856
+ }
2857
+ lines.push("");
2858
+ }
2859
+ const message = lines.join("\n").trim();
2860
+ deps.writeStdout(message);
2861
+ return message;
2862
+ }
2863
+ catch {
2864
+ const message = "no changelog entries found.";
2865
+ deps.writeStdout(message);
2866
+ return message;
2867
+ }
2868
+ }
2869
+ // ── thoughts (local, no daemon socket needed) ──
2870
+ if (command.kind === "thoughts") {
2871
+ try {
2872
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2873
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
2874
+ const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
2875
+ if (command.json) {
2876
+ try {
2877
+ const raw = fs.readFileSync(sessionFilePath, "utf-8");
2878
+ deps.writeStdout(raw);
2879
+ return raw;
2880
+ }
2881
+ catch {
2882
+ const message = "no inner dialog session found";
2883
+ deps.writeStdout(message);
2884
+ return message;
2885
+ }
2886
+ }
2887
+ const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
2888
+ const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
2889
+ deps.writeStdout(message);
2890
+ if (command.follow) {
2891
+ deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
2892
+ /* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
2893
+ const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
2894
+ deps.writeStdout("\n" + formatted);
2895
+ });
2896
+ /* v8 ignore stop */
2897
+ // Block until process exit; cleanup watcher on SIGINT/SIGTERM
2898
+ return new Promise((resolve) => {
2899
+ const cleanup = () => { stop(); resolve(message); };
2900
+ process.once("SIGINT", cleanup);
2901
+ process.once("SIGTERM", cleanup);
2902
+ });
2903
+ }
2904
+ return message;
2905
+ }
2906
+ catch {
2907
+ const message = "error: no agent context — use --agent <name> to specify";
2908
+ deps.writeStdout(message);
2909
+ return message;
2910
+ }
2911
+ }
2912
+ // ── attention queue (local, no daemon socket needed) ──
2913
+ /* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
2914
+ if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
2915
+ try {
2916
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2917
+ const { listActiveObligations, readObligation } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
2918
+ if (command.kind === "attention.list") {
2919
+ const obligations = listActiveObligations(agentName);
2920
+ if (obligations.length === 0) {
2921
+ const message = "nothing held — attention queue is empty";
2922
+ deps.writeStdout(message);
2923
+ return message;
2924
+ }
2925
+ const lines = obligations.map((o) => `[${o.id}] ${o.origin.friendId} via ${o.origin.channel}/${o.origin.key} — ${o.delegatedContent.slice(0, 60)}${o.delegatedContent.length > 60 ? "..." : ""} (${o.status})`);
2926
+ const message = lines.join("\n");
2927
+ deps.writeStdout(message);
2928
+ return message;
2929
+ }
2930
+ if (command.kind === "attention.show") {
2931
+ const obligation = readObligation(agentName, command.id);
2932
+ if (!obligation) {
2933
+ const message = `no obligation found with id ${command.id}`;
2934
+ deps.writeStdout(message);
2935
+ return message;
2936
+ }
2937
+ const message = JSON.stringify(obligation, null, 2);
2938
+ deps.writeStdout(message);
2939
+ return message;
2940
+ }
2941
+ // attention.history: show returned obligations
2942
+ const { getObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
2943
+ const obligationsDir = getObligationsDir(agentName);
2944
+ let entries = [];
2945
+ try {
2946
+ entries = fs.readdirSync(obligationsDir);
2947
+ }
2948
+ catch { /* empty */ }
2949
+ const returned = entries
2950
+ .filter((e) => e.endsWith(".json"))
2951
+ .map((e) => { try {
2952
+ return JSON.parse(fs.readFileSync(path.join(obligationsDir, e), "utf-8"));
2953
+ }
2954
+ catch {
2955
+ return null;
2956
+ } })
2957
+ .filter((o) => o?.status === "returned")
2958
+ .sort((a, b) => (b.returnedAt ?? 0) - (a.returnedAt ?? 0))
2959
+ .slice(0, 20);
2960
+ if (returned.length === 0) {
2961
+ const message = "no surfacing history yet";
2962
+ deps.writeStdout(message);
2963
+ return message;
2964
+ }
2965
+ const lines = returned.map((o) => {
2966
+ const when = o.returnedAt ? new Date(o.returnedAt).toISOString() : "unknown";
2967
+ return `[${o.id}] → ${o.origin.friendId} via ${o.returnTarget ?? "unknown"} at ${when}`;
2968
+ });
2969
+ const message = lines.join("\n");
2970
+ deps.writeStdout(message);
2971
+ return message;
2972
+ }
2973
+ catch {
2974
+ const message = "error: no agent context — use --agent <name> to specify";
2975
+ deps.writeStdout(message);
2976
+ return message;
2977
+ }
2978
+ }
2979
+ /* v8 ignore stop */
2980
+ // ── inner dialog status (local, no daemon socket needed) ──
2981
+ /* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
2982
+ if (command.kind === "inner.status") {
2983
+ try {
2984
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2985
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
2986
+ const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
2987
+ const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
2988
+ const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
2989
+ const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../repertoire/tasks/parser")));
2990
+ const { listActiveObligations } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
2991
+ // Read runtime state
2992
+ const innerSessionPath = getSessionPath("inner-dialog", "inner", "session");
2993
+ const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
2994
+ let runtimeState = null;
2995
+ try {
2996
+ const raw = fs.readFileSync(runtimeJsonPath, "utf-8");
2997
+ runtimeState = JSON.parse(raw);
2998
+ }
2999
+ catch { /* missing or corrupt — will show "unknown" */ }
3000
+ // Read journal files
3001
+ const journalDir = path.join(agentRoot, "journal");
3002
+ let journalFiles = [];
3003
+ try {
3004
+ const entries = fs.readdirSync(journalDir, { withFileTypes: true });
3005
+ journalFiles = entries
3006
+ .filter((e) => e.isFile() && !e.name.startsWith("."))
3007
+ .map((e) => {
3008
+ const stat = fs.statSync(path.join(journalDir, e.name));
3009
+ return { name: e.name, mtimeMs: stat.mtimeMs };
3010
+ });
3011
+ }
3012
+ catch { /* missing dir — will show (empty) */ }
3013
+ // Read heartbeat cadence
3014
+ let heartbeat = null;
3015
+ try {
3016
+ const habitsDir = path.join(agentRoot, "habits");
3017
+ const heartbeatPath = path.join(habitsDir, "heartbeat.md");
3018
+ let cadenceMs = DEFAULT_CADENCE_MS;
3019
+ if (fs.existsSync(heartbeatPath)) {
3020
+ const content = fs.readFileSync(heartbeatPath, "utf-8");
3021
+ const lines = content.split(/\r?\n/);
3022
+ if (lines[0]?.trim() === "---") {
3023
+ const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
3024
+ if (closing !== -1) {
3025
+ const rawFrontmatter = lines.slice(1, closing).join("\n");
3026
+ const frontmatter = parseFrontmatter(rawFrontmatter);
3027
+ const parsed = parseCadenceMs(frontmatter.cadence);
3028
+ if (parsed !== null)
3029
+ cadenceMs = parsed;
3030
+ }
3031
+ }
3032
+ }
3033
+ let lastCompletedAt = null;
3034
+ if (runtimeState?.lastCompletedAt) {
3035
+ const ms = new Date(runtimeState.lastCompletedAt).getTime();
3036
+ if (!Number.isNaN(ms))
3037
+ lastCompletedAt = ms;
3038
+ }
3039
+ heartbeat = { cadenceMs, lastCompletedAt };
3040
+ }
3041
+ catch { /* no habits — heartbeat unknown */ }
3042
+ // Attention count
3043
+ const activeObligations = listActiveObligations(agentName);
3044
+ const message = buildInnerStatusOutput({
3045
+ agentName,
3046
+ runtimeState,
3047
+ journalFiles,
3048
+ heartbeat,
3049
+ attentionCount: activeObligations.length,
3050
+ now: Date.now(),
3051
+ });
3052
+ deps.writeStdout(message);
3053
+ return message;
3054
+ }
3055
+ catch {
3056
+ const message = "error: no agent context — use --agent <name> to specify";
3057
+ deps.writeStdout(message);
3058
+ return message;
3059
+ }
3060
+ }
3061
+ /* v8 ignore stop */
3062
+ // ── session list (local, no daemon socket needed) ──
3063
+ if (command.kind === "session.list") {
3064
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
3065
+ const scanner = deps.scanSessions ?? (async () => []);
3066
+ /* v8 ignore stop */
3067
+ const sessions = await scanner();
3068
+ if (sessions.length === 0) {
3069
+ const message = "no active sessions";
3070
+ deps.writeStdout(message);
3071
+ return message;
3072
+ }
3073
+ const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
3074
+ const message = lines.join("\n");
3075
+ deps.writeStdout(message);
3076
+ return message;
3077
+ }
3078
+ if (command.kind === "chat.connect" && deps.startChat) {
3079
+ await ensureDaemonRunning(deps);
3080
+ await deps.startChat(command.agent);
3081
+ return "";
3082
+ }
997
3083
  if (command.kind === "hatch.start") {
998
3084
  // Route through adoption specialist when no explicit hatch args were provided
999
3085
  const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
@@ -1041,6 +3127,16 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1041
3127
  deps.writeStdout(message);
1042
3128
  return message;
1043
3129
  }
3130
+ if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
3131
+ const message = daemonUnavailableStatusOutput(deps.socketPath);
3132
+ deps.writeStdout(message);
3133
+ return message;
3134
+ }
3135
+ if (command.kind === "daemon.stop" && isDaemonUnavailableError(error)) {
3136
+ const message = "daemon not running";
3137
+ deps.writeStdout(message);
3138
+ return message;
3139
+ }
1044
3140
  throw error;
1045
3141
  }
1046
3142
  const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);