@ouro.bot/cli 0.1.0-alpha.20 → 0.1.0-alpha.201

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 (204) hide show
  1. package/README.md +187 -191
  2. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/agent.json +3 -2
  3. package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/SOUL.md +1 -1
  4. package/changelog.json +1254 -0
  5. package/dist/heart/active-work.js +832 -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/cares.js +179 -0
  10. package/dist/heart/commitments.js +111 -0
  11. package/dist/heart/config.js +54 -30
  12. package/dist/heart/core.js +620 -124
  13. package/dist/heart/cross-chat-delivery.js +146 -0
  14. package/dist/heart/daemon/agent-config-check.js +149 -0
  15. package/dist/heart/daemon/agent-discovery.js +81 -0
  16. package/dist/heart/daemon/agent-service.js +360 -0
  17. package/dist/heart/daemon/auth-flow.js +477 -0
  18. package/dist/heart/daemon/cadence.js +70 -0
  19. package/dist/heart/daemon/daemon-cli.js +2444 -217
  20. package/dist/heart/daemon/daemon-entry.js +182 -4
  21. package/dist/heart/daemon/daemon-health.js +136 -0
  22. package/dist/heart/daemon/daemon-runtime-sync.js +231 -0
  23. package/dist/heart/daemon/daemon-tombstone.js +123 -0
  24. package/dist/heart/daemon/daemon.js +460 -18
  25. package/dist/heart/daemon/habit-migration.js +181 -0
  26. package/dist/heart/daemon/habit-parser.js +119 -0
  27. package/dist/heart/daemon/habit-scheduler.js +369 -0
  28. package/dist/heart/daemon/hatch-flow.js +31 -112
  29. package/dist/heart/daemon/hatch-specialist.js +3 -3
  30. package/dist/heart/daemon/hooks/agent-config-v2.js +33 -0
  31. package/dist/heart/daemon/hooks/bundle-meta.js +206 -0
  32. package/dist/heart/daemon/inner-status.js +89 -0
  33. package/dist/heart/daemon/launchd.js +165 -0
  34. package/dist/heart/daemon/log-tailer.js +4 -3
  35. package/dist/heart/daemon/mcp-server.js +591 -0
  36. package/dist/heart/daemon/message-router.js +17 -8
  37. package/dist/heart/daemon/os-cron-deps.js +134 -0
  38. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  39. package/dist/heart/daemon/ouro-path-installer.js +78 -34
  40. package/dist/heart/daemon/ouro-version-manager.js +171 -0
  41. package/dist/heart/daemon/outlook-http.js +439 -0
  42. package/dist/heart/daemon/outlook-read.js +1618 -0
  43. package/dist/heart/daemon/outlook-render.js +1032 -0
  44. package/dist/heart/daemon/outlook-types.js +7 -0
  45. package/dist/heart/daemon/outlook-view.js +194 -0
  46. package/dist/heart/daemon/process-manager.js +93 -0
  47. package/dist/heart/daemon/run-hooks.js +39 -0
  48. package/dist/heart/daemon/runtime-logging.js +58 -15
  49. package/dist/heart/daemon/runtime-metadata.js +101 -0
  50. package/dist/heart/daemon/runtime-mode.js +67 -0
  51. package/dist/heart/daemon/safe-mode.js +161 -0
  52. package/dist/heart/daemon/sense-manager.js +50 -2
  53. package/dist/heart/daemon/session-id-resolver.js +131 -0
  54. package/dist/heart/daemon/skill-management-installer.js +94 -0
  55. package/dist/heart/daemon/socket-client.js +202 -0
  56. package/dist/heart/daemon/specialist-prompt.js +10 -7
  57. package/dist/heart/daemon/specialist-tools.js +49 -3
  58. package/dist/heart/daemon/staged-restart.js +138 -0
  59. package/dist/heart/daemon/task-scheduler.js +3 -25
  60. package/dist/heart/daemon/thoughts.js +507 -0
  61. package/dist/heart/daemon/update-checker.js +111 -0
  62. package/dist/heart/daemon/update-hooks.js +138 -0
  63. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  64. package/dist/heart/delegation.js +62 -0
  65. package/dist/heart/identity.js +147 -55
  66. package/dist/heart/intentions.js +173 -0
  67. package/dist/heart/kicks.js +2 -20
  68. package/dist/heart/migrate-config.js +127 -0
  69. package/dist/heart/model-capabilities.js +48 -0
  70. package/dist/heart/obligations.js +225 -0
  71. package/dist/heart/presence.js +185 -0
  72. package/dist/heart/progress-story.js +42 -0
  73. package/dist/heart/provider-failover.js +88 -0
  74. package/dist/heart/provider-ping.js +178 -0
  75. package/dist/heart/providers/anthropic-token.js +163 -0
  76. package/dist/heart/providers/anthropic.js +187 -41
  77. package/dist/heart/providers/azure.js +116 -10
  78. package/dist/heart/providers/github-copilot.js +157 -0
  79. package/dist/heart/providers/minimax.js +33 -3
  80. package/dist/heart/providers/openai-codex.js +51 -16
  81. package/dist/heart/session-activity.js +172 -0
  82. package/dist/heart/session-recall.js +216 -0
  83. package/dist/heart/start-of-turn-packet.js +232 -0
  84. package/dist/heart/streaming.js +36 -27
  85. package/dist/heart/sync.js +217 -0
  86. package/dist/heart/target-resolution.js +127 -0
  87. package/dist/heart/tempo.js +93 -0
  88. package/dist/heart/temporal-view.js +41 -0
  89. package/dist/heart/tool-activity-callbacks.js +36 -0
  90. package/dist/heart/tool-description.js +135 -0
  91. package/dist/heart/tool-loop.js +200 -0
  92. package/dist/heart/turn-coordinator.js +28 -0
  93. package/dist/mind/associative-recall.js +122 -32
  94. package/dist/mind/bundle-manifest.js +19 -1
  95. package/dist/mind/context.js +125 -14
  96. package/dist/mind/{memory.js → diary.js} +69 -56
  97. package/dist/mind/episodes.js +119 -0
  98. package/dist/mind/file-state.js +179 -0
  99. package/dist/mind/first-impressions.js +16 -2
  100. package/dist/mind/friends/channel.js +56 -0
  101. package/dist/mind/friends/group-context.js +144 -0
  102. package/dist/mind/friends/store-file.js +36 -0
  103. package/dist/mind/friends/trust-explanation.js +74 -0
  104. package/dist/mind/friends/types.js +8 -0
  105. package/dist/mind/journal-index.js +161 -0
  106. package/dist/mind/obligation-steering.js +221 -0
  107. package/dist/mind/obligations.js +138 -0
  108. package/dist/mind/pending.js +76 -9
  109. package/dist/mind/phrases.js +1 -0
  110. package/dist/mind/prompt.js +785 -103
  111. package/dist/mind/scrutiny.js +173 -0
  112. package/dist/mind/token-estimate.js +8 -12
  113. package/dist/nerves/coverage/audit.js +1 -1
  114. package/dist/nerves/coverage/run-artifacts.js +1 -1
  115. package/dist/nerves/event-buffer.js +111 -0
  116. package/dist/nerves/index.js +63 -3
  117. package/dist/nerves/observation.js +20 -0
  118. package/dist/nerves/runtime.js +5 -1
  119. package/dist/outlook-ui/assets/index-Ck8agNeO.js +61 -0
  120. package/dist/outlook-ui/assets/index-LwChZTgL.css +1 -0
  121. package/dist/outlook-ui/index.html +15 -0
  122. package/dist/repertoire/ado-semantic.js +11 -5
  123. package/dist/repertoire/coding/codex-jsonl.js +64 -0
  124. package/dist/repertoire/coding/context-pack.js +330 -0
  125. package/dist/repertoire/coding/feedback.js +197 -30
  126. package/dist/repertoire/coding/manager.js +149 -2
  127. package/dist/repertoire/coding/spawner.js +55 -9
  128. package/dist/repertoire/coding/tools.js +170 -7
  129. package/dist/repertoire/guardrails.js +291 -0
  130. package/dist/repertoire/mcp-client.js +254 -0
  131. package/dist/repertoire/mcp-manager.js +251 -0
  132. package/dist/repertoire/shell-sessions.js +133 -0
  133. package/dist/repertoire/skills.js +14 -23
  134. package/dist/repertoire/tasks/board.js +43 -5
  135. package/dist/repertoire/tasks/fix.js +182 -0
  136. package/dist/repertoire/tasks/index.js +28 -10
  137. package/dist/repertoire/tasks/lifecycle.js +2 -2
  138. package/dist/repertoire/tasks/parser.js +3 -2
  139. package/dist/repertoire/tasks/scanner.js +194 -37
  140. package/dist/repertoire/tasks/transitions.js +11 -28
  141. package/dist/repertoire/tools-base.js +1345 -320
  142. package/dist/repertoire/tools-bluebubbles.js +94 -0
  143. package/dist/repertoire/tools-github.js +1 -6
  144. package/dist/repertoire/tools-teams.js +12 -59
  145. package/dist/repertoire/tools.js +318 -124
  146. package/dist/scripts/claude-code-hook.js +41 -0
  147. package/dist/scripts/claude-code-stop-hook.js +47 -0
  148. package/dist/senses/attention-queue.js +97 -0
  149. package/dist/senses/bluebubbles-client.js +102 -3
  150. package/dist/senses/bluebubbles-entry.js +2 -0
  151. package/dist/senses/bluebubbles-inbound-log.js +109 -0
  152. package/dist/senses/bluebubbles-media.js +118 -23
  153. package/dist/senses/bluebubbles-model.js +12 -4
  154. package/dist/senses/bluebubbles-mutation-log.js +45 -5
  155. package/dist/senses/bluebubbles-runtime-state.js +109 -0
  156. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  157. package/dist/senses/bluebubbles.js +1008 -102
  158. package/dist/senses/cli/adapter.js +274 -0
  159. package/dist/senses/cli/ink-app.js +156 -0
  160. package/dist/senses/cli/inline-diff.js +64 -0
  161. package/dist/senses/cli/message-list.js +51 -0
  162. package/dist/senses/cli/ouro-tui.js +349 -0
  163. package/dist/senses/cli/spinner-imperative.js +135 -0
  164. package/dist/senses/cli/spinner.js +101 -0
  165. package/dist/senses/cli/status-line.js +60 -0
  166. package/dist/senses/cli/streaming-markdown.js +526 -0
  167. package/dist/senses/cli/tool-display.js +83 -0
  168. package/dist/senses/cli/tool-render.js +85 -0
  169. package/dist/senses/cli/tui-store.js +198 -0
  170. package/dist/senses/cli/virtual-list.js +35 -0
  171. package/dist/senses/cli-entry.js +1 -1
  172. package/dist/senses/cli-layout.js +187 -0
  173. package/dist/senses/cli.js +508 -243
  174. package/dist/senses/commands.js +65 -1
  175. package/dist/senses/continuity.js +94 -0
  176. package/dist/senses/habit-turn-message.js +108 -0
  177. package/dist/senses/inner-dialog-worker.js +112 -19
  178. package/dist/senses/inner-dialog.js +592 -84
  179. package/dist/senses/pipeline.js +693 -0
  180. package/dist/senses/shared-turn.js +196 -0
  181. package/dist/senses/surface-tool.js +95 -0
  182. package/dist/senses/teams.js +667 -120
  183. package/dist/senses/trust-gate.js +112 -2
  184. package/package.json +16 -6
  185. package/skills/configure-dev-tools.md +81 -0
  186. package/subagents/README.md +4 -70
  187. package/dist/heart/daemon/subagent-installer.js +0 -134
  188. package/dist/senses/debug-activity.js +0 -107
  189. package/subagents/work-doer.md +0 -235
  190. package/subagents/work-merger.md +0 -618
  191. package/subagents/work-planner.md +0 -382
  192. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/basilisk.md +0 -0
  193. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jafar.md +0 -0
  194. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/jormungandr.md +0 -0
  195. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/kaa.md +0 -0
  196. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/medusa.md +0 -0
  197. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/monty.md +0 -0
  198. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/nagini.md +0 -0
  199. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/ouroboros.md +0 -0
  200. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/python.md +0 -0
  201. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/quetzalcoatl.md +0 -0
  202. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/sir-hiss.md +0 -0
  203. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-serpent.md +0 -0
  204. /package/{AdoptionSpecialist.ouro → SerpentGuide.ouro}/psyche/identities/the-snake.md +0 -0
@@ -34,27 +34,48 @@ 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");
57
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 agent_config_v2_1 = require("./hooks/agent-config-v2");
69
+ const bundle_manifest_1 = require("../../mind/bundle-manifest");
70
+ const tasks_1 = require("../../repertoire/tasks");
71
+ const thoughts_1 = require("./thoughts");
72
+ const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
73
+ const launchd_1 = require("./launchd");
74
+ const socket_client_1 = require("./socket-client");
75
+ const daemon_tombstone_1 = require("./daemon-tombstone");
76
+ const daemon_health_1 = require("./daemon-health");
77
+ const session_activity_1 = require("../session-activity");
78
+ const auth_flow_1 = require("./auth-flow");
58
79
  function stringField(value) {
59
80
  return typeof value === "string" ? value : null;
60
81
  }
@@ -79,10 +100,15 @@ function parseStatusPayload(data) {
79
100
  daemon: stringField(overview.daemon) ?? "unknown",
80
101
  health: stringField(overview.health) ?? "unknown",
81
102
  socketPath: stringField(overview.socketPath) ?? "unknown",
103
+ outlookUrl: stringField(overview.outlookUrl) ?? "unavailable",
82
104
  version: stringField(overview.version) ?? "unknown",
83
105
  lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
106
+ repoRoot: stringField(overview.repoRoot) ?? "unknown",
107
+ configFingerprint: stringField(overview.configFingerprint) ?? "unknown",
84
108
  workerCount: numberField(overview.workerCount) ?? 0,
85
109
  senseCount: numberField(overview.senseCount) ?? 0,
110
+ entryPath: stringField(overview.entryPath) ?? "unknown",
111
+ mode: stringField(overview.mode) ?? "unknown",
86
112
  };
87
113
  const parsedSenses = senses.map((entry) => {
88
114
  if (!entry || typeof entry !== "object" || Array.isArray(entry))
@@ -123,6 +149,8 @@ function parseStatusPayload(data) {
123
149
  status,
124
150
  pid,
125
151
  restartCount,
152
+ lastExitCode: numberField(row.lastExitCode) ?? null,
153
+ lastSignal: stringField(row.lastSignal) ?? null,
126
154
  };
127
155
  });
128
156
  if (parsedSenses.some((row) => row === null) || parsedWorkers.some((row) => row === null))
@@ -146,7 +174,9 @@ function humanizeSenseName(sense, label) {
146
174
  }
147
175
  function formatTable(headers, rows) {
148
176
  const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
149
- const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
177
+ const renderRow = (row) => `| ${row.map((cell, index) => (index === row.length - 1
178
+ ? cell
179
+ : cell.padEnd(widths[index]))).join(" | ")} |`;
150
180
  const divider = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
151
181
  return [
152
182
  renderRow(headers),
@@ -163,6 +193,9 @@ function formatDaemonStatusOutput(response, fallback) {
163
193
  ["Socket", payload.overview.socketPath],
164
194
  ["Version", payload.overview.version],
165
195
  ["Last Updated", payload.overview.lastUpdated],
196
+ ["Outlook", payload.overview.outlookUrl],
197
+ ["Entry Path", payload.overview.entryPath],
198
+ ["Mode", payload.overview.mode],
166
199
  ["Workers", String(payload.overview.workerCount)],
167
200
  ["Senses", String(payload.overview.senseCount)],
168
201
  ["Health", payload.overview.health],
@@ -174,13 +207,23 @@ function formatDaemonStatusOutput(response, fallback) {
174
207
  row.status,
175
208
  row.detail,
176
209
  ]);
177
- const workerRows = payload.workers.map((row) => [
178
- row.agent,
179
- row.worker,
180
- row.status,
181
- row.pid === null ? "n/a" : String(row.pid),
182
- String(row.restartCount),
183
- ]);
210
+ const workerRows = payload.workers.map((row) => {
211
+ /* v8 ignore start — exit info branches tested via daemon-crash-context; v8 misreports conditional chains @preserve */
212
+ let exitInfo = "n/a";
213
+ if (row.lastExitCode !== null)
214
+ exitInfo = `code=${row.lastExitCode}`;
215
+ if (row.lastSignal !== null)
216
+ exitInfo = row.lastExitCode !== null ? `code=${row.lastExitCode} sig=${row.lastSignal}` : `sig=${row.lastSignal}`;
217
+ /* v8 ignore stop */
218
+ return [
219
+ row.agent,
220
+ row.worker,
221
+ row.status,
222
+ row.pid === null ? "n/a" : String(row.pid),
223
+ String(row.restartCount),
224
+ exitInfo,
225
+ ];
226
+ });
184
227
  return [
185
228
  "Overview",
186
229
  formatTable(["Item", "Value"], overviewRows),
@@ -189,65 +232,196 @@ function formatDaemonStatusOutput(response, fallback) {
189
232
  formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
190
233
  "",
191
234
  "Workers",
192
- formatTable(["Agent", "Worker", "State", "PID", "Restarts"], workerRows),
235
+ formatTable(["Agent", "Worker", "State", "PID", "Restarts", "Last Exit"], workerRows),
193
236
  ].join("\n");
194
237
  }
195
238
  async function ensureDaemonRunning(deps) {
196
239
  const alive = await deps.checkSocketAlive(deps.socketPath);
197
240
  if (alive) {
198
- return {
199
- alreadyRunning: true,
200
- message: `daemon already running (${deps.socketPath})`,
241
+ const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
242
+ let runningRuntimePromise = null;
243
+ const fetchRunningRuntimeMetadata = async () => {
244
+ runningRuntimePromise ??= (async () => {
245
+ const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
246
+ const payload = parseStatusPayload(status.data);
247
+ return {
248
+ version: payload?.overview.version ?? "unknown",
249
+ lastUpdated: payload?.overview.lastUpdated ?? "unknown",
250
+ repoRoot: payload?.overview.repoRoot ?? "unknown",
251
+ configFingerprint: payload?.overview.configFingerprint ?? "unknown",
252
+ };
253
+ })();
254
+ return runningRuntimePromise;
201
255
  };
256
+ return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
257
+ socketPath: deps.socketPath,
258
+ localVersion: localRuntime.version,
259
+ localLastUpdated: localRuntime.lastUpdated,
260
+ localRepoRoot: localRuntime.repoRoot,
261
+ localConfigFingerprint: localRuntime.configFingerprint,
262
+ fetchRunningVersion: async () => (await fetchRunningRuntimeMetadata()).version,
263
+ fetchRunningRuntimeMetadata,
264
+ stopDaemon: async () => {
265
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
266
+ },
267
+ cleanupStaleSocket: deps.cleanupStaleSocket,
268
+ startDaemonProcess: deps.startDaemonProcess,
269
+ checkSocketAlive: deps.checkSocketAlive,
270
+ });
202
271
  }
203
272
  deps.cleanupStaleSocket(deps.socketPath);
204
273
  const started = await deps.startDaemonProcess(deps.socketPath);
274
+ const pid = started.pid ?? "unknown";
275
+ // Verify the daemon actually comes up before reporting success
276
+ const verified = await verifyDaemonAlive(deps.checkSocketAlive, deps.socketPath);
277
+ /* v8 ignore start -- daemon liveness failure: requires real daemon crash timing @preserve */
278
+ if (!verified) {
279
+ return {
280
+ alreadyRunning: false,
281
+ message: `daemon spawned (pid ${pid}) but failed to respond within 10s — check \`ouro status\` or daemon logs`,
282
+ };
283
+ }
284
+ /* v8 ignore stop */
205
285
  return {
206
286
  alreadyRunning: false,
207
- message: `daemon started (pid ${started.pid ?? "unknown"})`,
287
+ message: `daemon started (pid ${pid})`,
208
288
  };
209
289
  }
290
+ /* v8 ignore start -- daemon liveness poll: real socket timing untestable in vitest @preserve */
291
+ async function verifyDaemonAlive(checkSocketAlive, socketPath, maxWaitMs = 10_000, pollIntervalMs = 500) {
292
+ const deadline = Date.now() + maxWaitMs;
293
+ while (Date.now() < deadline) {
294
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
295
+ if (await checkSocketAlive(socketPath))
296
+ return true;
297
+ }
298
+ return false;
299
+ }
300
+ /* v8 ignore stop */
301
+ /**
302
+ * Extract `--agent <name>` from an args array, returning the agent name and
303
+ * the remaining args with the flag pair removed.
304
+ */
305
+ function extractAgentFlag(args) {
306
+ const idx = args.indexOf("--agent");
307
+ if (idx === -1 || idx + 1 >= args.length)
308
+ return { rest: args };
309
+ const agent = args[idx + 1];
310
+ const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
311
+ return { agent, rest };
312
+ }
210
313
  function usage() {
211
314
  return [
212
315
  "Usage:",
213
316
  " ouro [up]",
214
- " ouro stop|status|logs|hatch",
317
+ " ouro dev [--repo-path <path>] [--clone [--clone-path <path>]]",
318
+ " ouro stop|down|status|logs|hatch",
319
+ " ouro outlook [--json]",
215
320
  " ouro -v|--version",
321
+ " ouro config model --agent <name> <model-name>",
322
+ " ouro config models --agent <name>",
323
+ " ouro auth --agent <name> [--provider <provider>]",
324
+ " ouro auth verify --agent <name> [--provider <provider>]",
325
+ " ouro auth switch --agent <name> --provider <provider>",
216
326
  " ouro chat <agent>",
217
327
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
218
328
  " ouro poke <agent> --task <task-id>",
329
+ " ouro poke <agent> --habit <name>",
330
+ " ouro habit list [--agent <name>]",
331
+ " ouro habit create [--agent <name>] <name> [--cadence <interval>]",
219
332
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
333
+ " ouro task board [<status>] [--agent <name>]",
334
+ " ouro task create <title> [--type <type>] [--agent <name>]",
335
+ " ouro task update <id> <status> [--agent <name>]",
336
+ " ouro task show <id> [--agent <name>]",
337
+ " ouro task fix [--safe|--all] [<id> [--option <N>]] [--agent <name>]",
338
+ " ouro task actionable|deps|sessions [--agent <name>]",
339
+ " ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
340
+ " ouro friend list [--agent <name>]",
341
+ " ouro friend show <id> [--agent <name>]",
342
+ " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
343
+ " ouro friend update <id> --trust <level> [--agent <name>]",
344
+ " ouro thoughts [--last <n>] [--json] [--follow] [--agent <name>]",
345
+ " ouro inner [--agent <name>]",
346
+ " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
347
+ " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
348
+ " ouro whoami [--agent <name>]",
349
+ " ouro session list [--agent <name>]",
350
+ " ouro mcp list",
351
+ " ouro mcp call <server> <tool> [--args '{...}']",
352
+ " ouro rollback [<version>]",
353
+ " ouro versions",
220
354
  ].join("\n");
221
355
  }
222
356
  function formatVersionOutput() {
223
- return (0, runtime_metadata_1.getRuntimeMetadata)().version;
357
+ const version = (0, runtime_metadata_1.getRuntimeMetadata)().version;
358
+ const mode = (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)());
359
+ /* v8 ignore start — cosmetic display toggle; dev mode always true in test env */
360
+ return mode === "dev" ? `${version} (dev)` : version;
361
+ /* v8 ignore stop */
224
362
  }
225
363
  function buildStoppedStatusPayload(socketPath) {
226
364
  const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
365
+ const repoRoot = (0, identity_1.getRepoRoot)();
227
366
  return {
228
367
  overview: {
229
368
  daemon: "stopped",
230
369
  health: "warn",
231
370
  socketPath,
371
+ outlookUrl: "unavailable",
232
372
  version: metadata.version,
233
373
  lastUpdated: metadata.lastUpdated,
374
+ repoRoot: metadata.repoRoot,
375
+ configFingerprint: metadata.configFingerprint,
234
376
  workerCount: 0,
235
377
  senseCount: 0,
378
+ entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
379
+ mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
236
380
  },
237
381
  senses: [],
238
382
  workers: [],
239
383
  };
240
384
  }
241
- function daemonUnavailableStatusOutput(socketPath) {
242
- return [
385
+ function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
386
+ /* v8 ignore start — tombstone read tested in daemon-status-tombstone.test; branch misreported @preserve */
387
+ const tombstone = (0, daemon_tombstone_1.readDaemonTombstone)();
388
+ const deathLine = tombstone
389
+ ? `Last death: ${tombstone.timestamp} -- ${tombstone.reason}: ${tombstone.message}`
390
+ : null;
391
+ /* v8 ignore stop */
392
+ const lines = [
243
393
  formatDaemonStatusOutput({
244
394
  ok: true,
245
395
  summary: "daemon not running",
246
396
  data: buildStoppedStatusPayload(socketPath),
247
397
  }, "daemon not running"),
248
398
  "",
249
- "daemon not running; run `ouro up`",
250
- ].join("\n");
399
+ ];
400
+ /* v8 ignore start — tombstone presence requires real daemon crash @preserve */
401
+ if (deathLine) {
402
+ lines.push(deathLine);
403
+ lines.push("");
404
+ /* v8 ignore stop */
405
+ }
406
+ // Read health file for last-known state (best-effort)
407
+ const resolvedHealthPath = healthFilePath ?? (0, daemon_health_1.getDefaultHealthPath)();
408
+ const health = (0, daemon_health_1.readHealth)(resolvedHealthPath);
409
+ if (health) {
410
+ lines.push(`Last known status: ${health.status} (pid ${health.pid}, uptime ${health.uptimeSeconds}s)`);
411
+ if (health.safeMode?.active) {
412
+ lines.push(`SAFE MODE: ${health.safeMode.reason}`);
413
+ }
414
+ if (health.degraded.length > 0) {
415
+ lines.push("");
416
+ lines.push("Degraded:");
417
+ for (const d of health.degraded) {
418
+ lines.push(` ${d.component}: ${d.reason} (since ${d.since})`);
419
+ }
420
+ }
421
+ lines.push("");
422
+ }
423
+ lines.push("daemon not running; run `ouro up`");
424
+ return lines.join("\n");
251
425
  }
252
426
  function isDaemonUnavailableError(error) {
253
427
  const code = typeof error === "object" && error !== null && "code" in error
@@ -296,17 +470,56 @@ function parsePokeCommand(args) {
296
470
  if (!agent)
297
471
  throw new Error(`Usage\n${usage()}`);
298
472
  let taskId;
473
+ let habitName;
299
474
  for (let i = 1; i < args.length; i += 1) {
300
475
  if (args[i] === "--task") {
301
476
  taskId = args[i + 1];
302
477
  i += 1;
303
478
  }
479
+ if (args[i] === "--habit") {
480
+ habitName = args[i + 1];
481
+ i += 1;
482
+ }
304
483
  }
484
+ // --habit takes priority over --task
485
+ if (habitName)
486
+ return { kind: "habit.poke", agent, habitName };
305
487
  if (!taskId)
306
488
  throw new Error(`Usage\n${usage()}`);
307
489
  return { kind: "task.poke", agent, taskId };
308
490
  }
309
- function parseLinkCommand(args) {
491
+ function parseHabitCommand(args) {
492
+ const { agent, rest } = extractAgentFlag(args);
493
+ const sub = rest[0];
494
+ if (sub === "list") {
495
+ return { kind: "habit.list", ...(agent ? { agent } : {}) };
496
+ }
497
+ if (sub === "create") {
498
+ const nameArgs = rest.slice(1);
499
+ let name;
500
+ let cadence;
501
+ const positional = [];
502
+ for (let i = 0; i < nameArgs.length; i++) {
503
+ if (nameArgs[i] === "--cadence" && nameArgs[i + 1]) {
504
+ cadence = nameArgs[++i];
505
+ continue;
506
+ }
507
+ /* v8 ignore start -- defensive: --agent already extracted by extractAgentFlag; guard prevents regression if parsing flow changes @preserve */
508
+ if (nameArgs[i] === "--agent" && nameArgs[i + 1]) {
509
+ i++; // skip --agent value (already extracted)
510
+ continue;
511
+ }
512
+ /* v8 ignore stop */
513
+ positional.push(nameArgs[i]);
514
+ }
515
+ name = positional[0];
516
+ if (!name)
517
+ throw new Error(`Usage\n${usage()}`);
518
+ return { kind: "habit.create", name, ...(agent ? { agent } : {}), ...(cadence ? { cadence } : {}) };
519
+ }
520
+ throw new Error(`Usage\n${usage()}`);
521
+ }
522
+ function parseLinkCommand(args, kind = "friend.link") {
310
523
  const agent = args[0];
311
524
  if (!agent)
312
525
  throw new Error(`Usage\n${usage()}`);
@@ -338,7 +551,7 @@ function parseLinkCommand(args) {
338
551
  throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
339
552
  }
340
553
  return {
341
- kind: "friend.link",
554
+ kind,
342
555
  agent,
343
556
  friendId,
344
557
  provider: providerRaw,
@@ -346,7 +559,102 @@ function parseLinkCommand(args) {
346
559
  };
347
560
  }
348
561
  function isAgentProvider(value) {
349
- return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex";
562
+ return value === "azure" || value === "anthropic" || value === "minimax" || value === "openai-codex" || value === "github-copilot";
563
+ }
564
+ /* v8 ignore start -- hasStoredCredentials: per-provider branches tested via auth switch tests @preserve */
565
+ function hasStoredCredentials(provider, providerSecrets) {
566
+ if (provider === "anthropic")
567
+ return !!providerSecrets.setupToken;
568
+ if (provider === "openai-codex")
569
+ return !!providerSecrets.oauthAccessToken;
570
+ if (provider === "github-copilot")
571
+ return !!providerSecrets.githubToken;
572
+ if (provider === "minimax")
573
+ return !!providerSecrets.apiKey;
574
+ // azure
575
+ return !!providerSecrets.endpoint && !!providerSecrets.apiKey;
576
+ }
577
+ /* v8 ignore stop */
578
+ /* v8 ignore start -- verifyProviderCredentials: delegates to pingProvider @preserve */
579
+ async function verifyProviderCredentials(provider, providers) {
580
+ const config = providers[provider];
581
+ if (!config)
582
+ return "not configured";
583
+ try {
584
+ const { pingProvider } = await Promise.resolve().then(() => __importStar(require("../../heart/provider-ping")));
585
+ const result = await pingProvider(provider, config);
586
+ return result.ok ? "ok" : `failed (${result.message})`;
587
+ }
588
+ catch (error) {
589
+ return `failed (${error instanceof Error ? error.message : String(error)})`;
590
+ }
591
+ }
592
+ async function listGithubCopilotModels(baseUrl, token, fetchImpl = fetch) {
593
+ const url = `${baseUrl.replace(/\/+$/, "")}/models`;
594
+ const response = await fetchImpl(url, {
595
+ headers: { Authorization: `Bearer ${token}` },
596
+ });
597
+ if (!response.ok) {
598
+ throw new Error(`model listing failed (HTTP ${response.status})`);
599
+ }
600
+ const body = await response.json();
601
+ /* v8 ignore start -- response shape handling: tested via config-models.test.ts @preserve */
602
+ const items = Array.isArray(body) ? body : (body?.data ?? []);
603
+ return items.map((item) => {
604
+ const rec = item;
605
+ const capabilities = Array.isArray(rec.capabilities)
606
+ ? rec.capabilities.filter((c) => typeof c === "string")
607
+ : undefined;
608
+ return {
609
+ id: String(rec.id ?? rec.name ?? ""),
610
+ name: String(rec.name ?? rec.id ?? ""),
611
+ ...(capabilities ? { capabilities } : {}),
612
+ };
613
+ });
614
+ /* v8 ignore stop */
615
+ }
616
+ async function pingGithubCopilotModel(baseUrl, token, model, fetchImpl = fetch) {
617
+ const base = baseUrl.replace(/\/+$/, "");
618
+ const isClaude = model.startsWith("claude");
619
+ const url = isClaude ? `${base}/chat/completions` : `${base}/responses`;
620
+ const body = isClaude
621
+ ? JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 1 })
622
+ : JSON.stringify({ model, input: "ping", max_output_tokens: 16 });
623
+ try {
624
+ const response = await fetchImpl(url, {
625
+ method: "POST",
626
+ headers: {
627
+ Authorization: `Bearer ${token}`,
628
+ "Content-Type": "application/json",
629
+ },
630
+ body,
631
+ });
632
+ if (response.ok)
633
+ return { ok: true };
634
+ let detail = `HTTP ${response.status}`;
635
+ try {
636
+ const json = await response.json();
637
+ /* v8 ignore start -- error format parsing: all branches tested via config-models.test.ts @preserve */
638
+ if (typeof json.error === "string")
639
+ detail = json.error;
640
+ else if (typeof json.error === "object" && json.error !== null) {
641
+ const errObj = json.error;
642
+ if (typeof errObj.message === "string")
643
+ detail = errObj.message;
644
+ }
645
+ else if (typeof json.message === "string")
646
+ detail = json.message;
647
+ /* v8 ignore stop */
648
+ }
649
+ catch {
650
+ // response body not JSON — keep HTTP status
651
+ }
652
+ return { ok: false, error: detail };
653
+ }
654
+ catch (err) {
655
+ /* v8 ignore next -- defensive: fetch errors are always Error instances @preserve */
656
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
657
+ }
350
658
  }
351
659
  function parseHatchCommand(args) {
352
660
  let agentName;
@@ -403,7 +711,7 @@ function parseHatchCommand(args) {
403
711
  }
404
712
  }
405
713
  if (providerRaw && !isAgentProvider(providerRaw)) {
406
- throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex.");
714
+ throw new Error("Unknown provider. Use azure|anthropic|minimax|openai-codex|github-copilot.");
407
715
  }
408
716
  const provider = providerRaw && isAgentProvider(providerRaw) ? providerRaw : undefined;
409
717
  return {
@@ -415,20 +723,480 @@ function parseHatchCommand(args) {
415
723
  migrationPath,
416
724
  };
417
725
  }
726
+ function parseTaskCommand(args) {
727
+ const { agent, rest: cleaned } = extractAgentFlag(args);
728
+ const [sub, ...rest] = cleaned;
729
+ if (!sub)
730
+ throw new Error(`Usage\n${usage()}`);
731
+ if (sub === "board") {
732
+ const status = rest[0];
733
+ return status
734
+ ? { kind: "task.board", status, ...(agent ? { agent } : {}) }
735
+ : { kind: "task.board", ...(agent ? { agent } : {}) };
736
+ }
737
+ if (sub === "create") {
738
+ const title = rest[0];
739
+ if (!title)
740
+ throw new Error(`Usage\n${usage()}`);
741
+ let type;
742
+ for (let i = 1; i < rest.length; i++) {
743
+ if (rest[i] === "--type" && rest[i + 1]) {
744
+ type = rest[i + 1];
745
+ i += 1;
746
+ }
747
+ }
748
+ return type
749
+ ? { kind: "task.create", title, type, ...(agent ? { agent } : {}) }
750
+ : { kind: "task.create", title, ...(agent ? { agent } : {}) };
751
+ }
752
+ if (sub === "update") {
753
+ const id = rest[0];
754
+ const status = rest[1];
755
+ if (!id || !status)
756
+ throw new Error(`Usage\n${usage()}`);
757
+ return { kind: "task.update", id, status, ...(agent ? { agent } : {}) };
758
+ }
759
+ if (sub === "show") {
760
+ const id = rest[0];
761
+ if (!id)
762
+ throw new Error(`Usage\n${usage()}`);
763
+ return { kind: "task.show", id, ...(agent ? { agent } : {}) };
764
+ }
765
+ if (sub === "actionable")
766
+ return { kind: "task.actionable", ...(agent ? { agent } : {}) };
767
+ if (sub === "deps")
768
+ return { kind: "task.deps", ...(agent ? { agent } : {}) };
769
+ if (sub === "sessions")
770
+ return { kind: "task.sessions", ...(agent ? { agent } : {}) };
771
+ if (sub === "fix") {
772
+ // fix --safe | fix --all | fix <id> [--option N] | fix (dry-run)
773
+ if (rest.length === 0)
774
+ return { kind: "task.fix", mode: "dry-run", ...(agent ? { agent } : {}) };
775
+ const first = rest[0];
776
+ if (first === "--safe" || first === "--all") {
777
+ return { kind: "task.fix", mode: "safe", ...(agent ? { agent } : {}) };
778
+ }
779
+ // first arg is an issue ID (contains a colon, e.g. schema-missing-kind:one-shots/foo.md)
780
+ const issueId = first;
781
+ let option;
782
+ for (let i = 1; i < rest.length; i++) {
783
+ if (rest[i] === "--option" && rest[i + 1]) {
784
+ option = parseInt(rest[i + 1], 10);
785
+ i += 1;
786
+ }
787
+ }
788
+ return {
789
+ kind: "task.fix",
790
+ mode: "single",
791
+ issueId,
792
+ ...(option !== undefined ? { option } : {}),
793
+ ...(agent ? { agent } : {}),
794
+ };
795
+ }
796
+ throw new Error(`Usage\n${usage()}`);
797
+ }
798
+ function extractFacingFlag(args) {
799
+ const idx = args.indexOf("--facing");
800
+ if (idx === -1 || idx + 1 >= args.length)
801
+ return { rest: args };
802
+ const value = args[idx + 1];
803
+ if (value !== "human" && value !== "agent") {
804
+ throw new Error(`--facing must be 'human' or 'agent'`);
805
+ }
806
+ const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
807
+ return { facing: value, rest };
808
+ }
809
+ function parseAuthCommand(args) {
810
+ const first = args[0];
811
+ // Support both positional (`auth switch`) and flag (`auth --switch`) forms
812
+ if (first === "verify" || first === "switch" || first === "--verify" || first === "--switch") {
813
+ const subcommand = first.replace(/^--/, "");
814
+ const { agent, rest: afterAgent } = extractAgentFlag(args.slice(1));
815
+ const { facing, rest } = extractFacingFlag(afterAgent);
816
+ let provider;
817
+ /* v8 ignore start -- provider flag parsing: branches tested via CLI parsing tests @preserve */
818
+ for (let i = 0; i < rest.length; i += 1) {
819
+ if (rest[i] === "--provider") {
820
+ const value = rest[i + 1];
821
+ if (!isAgentProvider(value))
822
+ throw new Error(`Usage\n${usage()}`);
823
+ provider = value;
824
+ i += 1;
825
+ continue;
826
+ }
827
+ }
828
+ /* v8 ignore stop */
829
+ /* v8 ignore next -- defensive: agent always provided in tests @preserve */
830
+ if (!agent)
831
+ throw new Error(`Usage\n${usage()}`);
832
+ if (subcommand === "switch") {
833
+ if (!provider)
834
+ throw new Error(`auth switch requires --provider.\n${usage()}`);
835
+ return facing ? { kind: "auth.switch", agent, provider, facing } : { kind: "auth.switch", agent, provider };
836
+ }
837
+ return provider ? { kind: "auth.verify", agent, provider } : { kind: "auth.verify", agent };
838
+ }
839
+ const { agent, rest } = extractAgentFlag(args);
840
+ let provider;
841
+ for (let i = 0; i < rest.length; i += 1) {
842
+ if (rest[i] === "--provider") {
843
+ const value = rest[i + 1];
844
+ if (!isAgentProvider(value))
845
+ throw new Error(`Usage\n${usage()}`);
846
+ provider = value;
847
+ i += 1;
848
+ continue;
849
+ }
850
+ }
851
+ if (!agent) {
852
+ throw new Error([
853
+ "Usage:",
854
+ " ouro auth --agent <name> [--provider <provider>] Set up credentials",
855
+ " ouro auth verify --agent <name> [--provider <p>] Verify credentials work",
856
+ " ouro auth switch --agent <name> --provider <p> Switch active provider",
857
+ ].join("\n"));
858
+ }
859
+ return provider ? { kind: "auth.run", agent, provider } : { kind: "auth.run", agent };
860
+ }
861
+ function parseReminderCommand(args) {
862
+ const { agent, rest: cleaned } = extractAgentFlag(args);
863
+ const [sub, ...rest] = cleaned;
864
+ if (!sub)
865
+ throw new Error(`Usage\n${usage()}`);
866
+ if (sub === "create") {
867
+ const title = rest[0];
868
+ if (!title)
869
+ throw new Error(`Usage\n${usage()}`);
870
+ let body;
871
+ let scheduledAt;
872
+ let cadence;
873
+ let category;
874
+ let requester;
875
+ for (let i = 1; i < rest.length; i++) {
876
+ if (rest[i] === "--body" && rest[i + 1]) {
877
+ body = rest[i + 1];
878
+ i += 1;
879
+ }
880
+ else if (rest[i] === "--at" && rest[i + 1]) {
881
+ scheduledAt = rest[i + 1];
882
+ i += 1;
883
+ }
884
+ else if (rest[i] === "--cadence" && rest[i + 1]) {
885
+ cadence = rest[i + 1];
886
+ i += 1;
887
+ }
888
+ else if (rest[i] === "--category" && rest[i + 1]) {
889
+ category = rest[i + 1];
890
+ i += 1;
891
+ }
892
+ else if (rest[i] === "--requester" && rest[i + 1]) {
893
+ requester = rest[i + 1];
894
+ i += 1;
895
+ }
896
+ }
897
+ if (!body)
898
+ throw new Error(`Usage\n${usage()}`);
899
+ if (!scheduledAt && !cadence)
900
+ throw new Error(`Usage\n${usage()}`);
901
+ return {
902
+ kind: "reminder.create",
903
+ title,
904
+ body,
905
+ ...(scheduledAt ? { scheduledAt } : {}),
906
+ ...(cadence ? { cadence } : {}),
907
+ ...(category ? { category } : {}),
908
+ ...(requester ? { requester } : {}),
909
+ ...(agent ? { agent } : {}),
910
+ };
911
+ }
912
+ throw new Error(`Usage\n${usage()}`);
913
+ }
914
+ function parseSessionCommand(args) {
915
+ const { agent, rest: cleaned } = extractAgentFlag(args);
916
+ const [sub] = cleaned;
917
+ if (!sub)
918
+ throw new Error(`Usage\n${usage()}`);
919
+ if (sub === "list")
920
+ return { kind: "session.list", ...(agent ? { agent } : {}) };
921
+ throw new Error(`Usage\n${usage()}`);
922
+ }
923
+ function parseAttentionCommand(args) {
924
+ const { agent, rest: cleaned } = extractAgentFlag(args);
925
+ const sub = cleaned[0];
926
+ if (sub === "show" && cleaned[1]) {
927
+ return { kind: "attention.show", id: cleaned[1], ...(agent ? { agent } : {}) };
928
+ }
929
+ if (sub === "history") {
930
+ return { kind: "attention.history", ...(agent ? { agent } : {}) };
931
+ }
932
+ return { kind: "attention.list", ...(agent ? { agent } : {}) };
933
+ }
934
+ function parseThoughtsCommand(args) {
935
+ const { agent, rest: cleaned } = extractAgentFlag(args);
936
+ let last;
937
+ let json = false;
938
+ let follow = false;
939
+ for (let i = 0; i < cleaned.length; i++) {
940
+ if (cleaned[i] === "--last" && i + 1 < cleaned.length) {
941
+ last = Number.parseInt(cleaned[i + 1], 10);
942
+ i++;
943
+ }
944
+ if (cleaned[i] === "--json")
945
+ json = true;
946
+ if (cleaned[i] === "--follow" || cleaned[i] === "-f")
947
+ follow = true;
948
+ }
949
+ return { kind: "thoughts", ...(agent ? { agent } : {}), ...(last ? { last } : {}), ...(json ? { json } : {}), ...(follow ? { follow } : {}) };
950
+ }
951
+ function parseFriendCommand(args) {
952
+ const { agent, rest: cleaned } = extractAgentFlag(args);
953
+ const [sub, ...rest] = cleaned;
954
+ if (!sub)
955
+ throw new Error(`Usage\n${usage()}`);
956
+ if (sub === "list")
957
+ return { kind: "friend.list", ...(agent ? { agent } : {}) };
958
+ if (sub === "show") {
959
+ const friendId = rest[0];
960
+ if (!friendId)
961
+ throw new Error(`Usage\n${usage()}`);
962
+ return { kind: "friend.show", friendId, ...(agent ? { agent } : {}) };
963
+ }
964
+ if (sub === "create") {
965
+ let name;
966
+ let trustLevel;
967
+ for (let i = 0; i < rest.length; i++) {
968
+ if (rest[i] === "--name" && rest[i + 1]) {
969
+ name = rest[i + 1];
970
+ i += 1;
971
+ }
972
+ else if (rest[i] === "--trust" && rest[i + 1]) {
973
+ trustLevel = rest[i + 1];
974
+ i += 1;
975
+ }
976
+ }
977
+ if (!name)
978
+ throw new Error(`Usage\n${usage()}`);
979
+ return {
980
+ kind: "friend.create",
981
+ name,
982
+ ...(trustLevel ? { trustLevel } : {}),
983
+ ...(agent ? { agent } : {}),
984
+ };
985
+ }
986
+ if (sub === "update") {
987
+ const friendId = rest[0];
988
+ if (!friendId)
989
+ throw new Error(`Usage: ouro friend update <id> --trust <level>`);
990
+ let trustLevel;
991
+ /* v8 ignore start -- flag parsing loop: tested via CLI parsing tests @preserve */
992
+ for (let i = 1; i < rest.length; i++) {
993
+ if (rest[i] === "--trust" && rest[i + 1]) {
994
+ trustLevel = rest[i + 1];
995
+ i += 1;
996
+ }
997
+ }
998
+ /* v8 ignore stop */
999
+ const VALID_TRUST_LEVELS = new Set(["stranger", "acquaintance", "friend", "family"]);
1000
+ if (!trustLevel || !VALID_TRUST_LEVELS.has(trustLevel)) {
1001
+ throw new Error(`Usage: ouro friend update <id> --trust <stranger|acquaintance|friend|family>`);
1002
+ }
1003
+ return {
1004
+ kind: "friend.update",
1005
+ friendId,
1006
+ trustLevel: trustLevel,
1007
+ ...(agent ? { agent } : {}),
1008
+ };
1009
+ }
1010
+ if (sub === "link")
1011
+ return parseLinkCommand(rest, "friend.link");
1012
+ if (sub === "unlink")
1013
+ return parseLinkCommand(rest, "friend.unlink");
1014
+ throw new Error(`Usage\n${usage()}`);
1015
+ }
1016
+ function parseConfigCommand(args) {
1017
+ const { agent, rest: afterAgent } = extractAgentFlag(args);
1018
+ const { facing, rest: cleaned } = extractFacingFlag(afterAgent);
1019
+ const [sub, ...rest] = cleaned;
1020
+ if (!sub)
1021
+ throw new Error(`Usage\n${usage()}`);
1022
+ if (sub === "model") {
1023
+ if (!agent)
1024
+ throw new Error("--agent is required for config model");
1025
+ const modelName = rest[0];
1026
+ if (!modelName)
1027
+ throw new Error(`Usage: ouro config model --agent <name> <model-name>`);
1028
+ return facing ? { kind: "config.model", agent, modelName, facing } : { kind: "config.model", agent, modelName };
1029
+ }
1030
+ if (sub === "models") {
1031
+ if (!agent)
1032
+ throw new Error("--agent is required for config models");
1033
+ return { kind: "config.models", agent };
1034
+ }
1035
+ throw new Error(`Usage\n${usage()}`);
1036
+ }
1037
+ function parseMcpCommand(args) {
1038
+ const [sub, ...rest] = args;
1039
+ if (!sub)
1040
+ throw new Error(`Usage\n${usage()}`);
1041
+ if (sub === "list")
1042
+ return { kind: "mcp.list" };
1043
+ if (sub === "call") {
1044
+ const server = rest[0];
1045
+ const tool = rest[1];
1046
+ if (!server || !tool)
1047
+ throw new Error(`Usage\n${usage()}`);
1048
+ const argsIdx = rest.indexOf("--args");
1049
+ const mcpArgs = argsIdx !== -1 && rest[argsIdx + 1] ? rest[argsIdx + 1] : undefined;
1050
+ return { kind: "mcp.call", server, tool, ...(mcpArgs ? { args: mcpArgs } : {}) };
1051
+ }
1052
+ throw new Error(`Usage\n${usage()}`);
1053
+ }
1054
+ function parseMcpServeCommand(args) {
1055
+ let agent;
1056
+ let friendId;
1057
+ let socketOverride;
1058
+ for (let i = 0; i < args.length; i++) {
1059
+ if (args[i] === "--agent" && args[i + 1]) {
1060
+ agent = args[++i];
1061
+ continue;
1062
+ }
1063
+ if (args[i] === "--friend" && args[i + 1]) {
1064
+ friendId = args[++i];
1065
+ continue;
1066
+ }
1067
+ if (args[i] === "--socket" && args[i + 1]) {
1068
+ socketOverride = args[++i];
1069
+ continue;
1070
+ }
1071
+ }
1072
+ if (!agent)
1073
+ throw new Error("mcp-serve requires --agent <name>");
1074
+ return { kind: "mcp-serve", agent, ...(friendId ? { friendId } : {}), ...(socketOverride ? { socketOverride } : {}) };
1075
+ }
1076
+ function parseSetupCommand(args) {
1077
+ let tool;
1078
+ let agent;
1079
+ for (let i = 0; i < args.length; i++) {
1080
+ if (args[i] === "--tool" && args[i + 1]) {
1081
+ tool = args[++i];
1082
+ continue;
1083
+ }
1084
+ if (args[i] === "--agent" && args[i + 1]) {
1085
+ agent = args[++i];
1086
+ continue;
1087
+ }
1088
+ }
1089
+ if (!tool)
1090
+ throw new Error("setup requires --tool (claude-code | codex)");
1091
+ if (tool !== "claude-code" && tool !== "codex")
1092
+ throw new Error(`Unknown tool: ${tool}. Supported: claude-code, codex`);
1093
+ if (!agent)
1094
+ throw new Error("setup requires --agent <name>");
1095
+ return { kind: "setup", tool, agent };
1096
+ }
418
1097
  function parseOuroCommand(args) {
419
1098
  const [head, second] = args;
420
1099
  if (!head)
421
1100
  return { kind: "daemon.up" };
1101
+ if (head === "--agent" && second) {
1102
+ return parseOuroCommand(args.slice(2));
1103
+ }
1104
+ if (head === "hook") {
1105
+ const hookArgs = args.slice(1);
1106
+ let event;
1107
+ let hookAgent;
1108
+ for (let i = 0; i < hookArgs.length; i++) {
1109
+ if (hookArgs[i] === "--agent" && hookArgs[i + 1]) {
1110
+ hookAgent = hookArgs[++i];
1111
+ continue;
1112
+ }
1113
+ /* v8 ignore start -- false branch: extra positional args after event are ignored */
1114
+ if (!event) {
1115
+ event = hookArgs[i];
1116
+ }
1117
+ /* v8 ignore stop */
1118
+ }
1119
+ if (!event)
1120
+ throw new Error("hook requires an event name (session-start, stop, post-tool-use)");
1121
+ if (!hookAgent)
1122
+ throw new Error("hook requires --agent <name>");
1123
+ return { kind: "hook", event, agent: hookAgent };
1124
+ }
422
1125
  if (head === "up")
423
1126
  return { kind: "daemon.up" };
424
- if (head === "stop")
1127
+ if (head === "dev") {
1128
+ const devArgs = args.slice(1);
1129
+ let repoPath;
1130
+ let clone = false;
1131
+ let clonePath;
1132
+ for (let i = 0; i < devArgs.length; i++) {
1133
+ if (devArgs[i] === "--repo-path" && devArgs[i + 1]) {
1134
+ repoPath = devArgs[++i];
1135
+ continue;
1136
+ }
1137
+ if (devArgs[i] === "--clone") {
1138
+ clone = true;
1139
+ continue;
1140
+ }
1141
+ if (devArgs[i] === "--clone-path" && devArgs[i + 1]) {
1142
+ clonePath = devArgs[++i];
1143
+ continue;
1144
+ }
1145
+ }
1146
+ return { kind: "daemon.dev", repoPath, clone, clonePath };
1147
+ }
1148
+ if (head === "rollback")
1149
+ return { kind: "rollback", ...(second ? { version: second } : {}) };
1150
+ if (head === "versions")
1151
+ return { kind: "versions" };
1152
+ if (head === "stop" || head === "down")
425
1153
  return { kind: "daemon.stop" };
426
1154
  if (head === "status")
427
1155
  return { kind: "daemon.status" };
428
1156
  if (head === "logs")
429
1157
  return { kind: "daemon.logs" };
1158
+ if (head === "outlook")
1159
+ return { kind: "outlook", ...(args.includes("--json") ? { json: true } : {}) };
430
1160
  if (head === "hatch")
431
1161
  return parseHatchCommand(args.slice(1));
1162
+ if (head === "auth")
1163
+ return parseAuthCommand(args.slice(1));
1164
+ if (head === "task")
1165
+ return parseTaskCommand(args.slice(1));
1166
+ if (head === "reminder")
1167
+ return parseReminderCommand(args.slice(1));
1168
+ if (head === "habit")
1169
+ return parseHabitCommand(args.slice(1));
1170
+ if (head === "friend")
1171
+ return parseFriendCommand(args.slice(1));
1172
+ if (head === "config")
1173
+ return parseConfigCommand(args.slice(1));
1174
+ if (head === "mcp")
1175
+ return parseMcpCommand(args.slice(1));
1176
+ if (head === "whoami") {
1177
+ const { agent } = extractAgentFlag(args.slice(1));
1178
+ return { kind: "whoami", ...(agent ? { agent } : {}) };
1179
+ }
1180
+ if (head === "session")
1181
+ return parseSessionCommand(args.slice(1));
1182
+ if (head === "changelog") {
1183
+ const sliced = args.slice(1);
1184
+ const { agent, rest: remaining } = extractAgentFlag(sliced);
1185
+ let from;
1186
+ const fromIdx = remaining.indexOf("--from");
1187
+ if (fromIdx !== -1 && remaining[fromIdx + 1]) {
1188
+ from = remaining[fromIdx + 1];
1189
+ }
1190
+ return { kind: "changelog", ...(from ? { from } : {}), ...(agent ? { agent } : {}) };
1191
+ }
1192
+ if (head === "thoughts")
1193
+ return parseThoughtsCommand(args.slice(1));
1194
+ if (head === "attention")
1195
+ return parseAttentionCommand(args.slice(1));
1196
+ if (head === "inner") {
1197
+ const { agent } = extractAgentFlag(args.slice(1));
1198
+ return { kind: "inner.status", ...(agent ? { agent } : {}) };
1199
+ }
432
1200
  if (head === "chat") {
433
1201
  if (!second)
434
1202
  throw new Error(`Usage\n${usage()}`);
@@ -440,92 +1208,56 @@ function parseOuroCommand(args) {
440
1208
  return parsePokeCommand(args.slice(1));
441
1209
  if (head === "link")
442
1210
  return parseLinkCommand(args.slice(1));
1211
+ if (head === "mcp-serve")
1212
+ return parseMcpServeCommand(args.slice(1));
1213
+ if (head === "setup")
1214
+ return parseSetupCommand(args.slice(1));
443
1215
  throw new Error(`Unknown command '${args.join(" ")}'.\n${usage()}`);
444
1216
  }
445
- function defaultSendCommand(socketPath, command) {
446
- return new Promise((resolve, reject) => {
447
- const client = net.createConnection(socketPath);
448
- let raw = "";
449
- client.on("connect", () => {
450
- client.write(JSON.stringify(command));
451
- client.end();
452
- });
453
- client.on("data", (chunk) => {
454
- raw += chunk.toString("utf-8");
455
- });
456
- client.on("error", reject);
457
- client.on("end", () => {
458
- const trimmed = raw.trim();
459
- if (trimmed.length === 0 && command.kind === "daemon.stop") {
460
- resolve({ ok: true, message: "daemon stopped" });
461
- return;
462
- }
463
- if (trimmed.length === 0) {
464
- reject(new Error("Daemon returned empty response."));
465
- return;
466
- }
467
- try {
468
- const parsed = JSON.parse(trimmed);
469
- resolve(parsed);
470
- }
471
- catch (error) {
472
- reject(error);
473
- }
474
- });
475
- });
476
- }
477
1217
  function defaultStartDaemonProcess(socketPath) {
478
1218
  const entry = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
1219
+ // Redirect stdio to /dev/null via file descriptors — using 'ignore' causes EPIPE
1220
+ // when the daemon's logging system writes to stderr after the parent exits.
1221
+ const outFd = fs.openSync(os.devNull, "w");
1222
+ const errFd = fs.openSync(os.devNull, "w");
479
1223
  const child = (0, child_process_1.spawn)("node", [entry, "--socket", socketPath], {
480
1224
  detached: true,
481
- stdio: "ignore",
1225
+ stdio: ["ignore", outFd, errFd],
482
1226
  });
483
1227
  child.unref();
1228
+ // Don't close fds — the child process needs them. They'll be cleaned up when the parent exits.
484
1229
  return Promise.resolve({ pid: child.pid ?? null });
485
1230
  }
486
1231
  function defaultWriteStdout(text) {
487
1232
  // eslint-disable-next-line no-console -- terminal UX: CLI command output
488
1233
  console.log(text);
489
1234
  }
490
- function defaultCheckSocketAlive(socketPath) {
491
- return new Promise((resolve) => {
492
- const client = net.createConnection(socketPath);
493
- let raw = "";
494
- let done = false;
495
- const finalize = (alive) => {
496
- if (done)
497
- return;
498
- done = true;
499
- resolve(alive);
500
- };
501
- if ("setTimeout" in client && typeof client.setTimeout === "function") {
502
- client.setTimeout(800, () => {
503
- client.destroy();
504
- finalize(false);
505
- });
1235
+ /**
1236
+ * Read the runtimeVersion from the first .ouro bundle's bundle-meta.json.
1237
+ * Returns undefined if none found or unreadable.
1238
+ */
1239
+ function readFirstBundleMetaVersion(bundlesRoot) {
1240
+ try {
1241
+ if (!fs.existsSync(bundlesRoot))
1242
+ return undefined;
1243
+ const entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
1244
+ for (const entry of entries) {
1245
+ /* v8 ignore next -- skip non-.ouro dirs: tested via version-detect tests @preserve */
1246
+ if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
1247
+ continue;
1248
+ const metaPath = path.join(bundlesRoot, entry.name, "bundle-meta.json");
1249
+ if (!fs.existsSync(metaPath))
1250
+ continue;
1251
+ const raw = fs.readFileSync(metaPath, "utf-8");
1252
+ const meta = JSON.parse(raw);
1253
+ if (meta.runtimeVersion)
1254
+ return meta.runtimeVersion;
506
1255
  }
507
- client.on("connect", () => {
508
- client.write(JSON.stringify({ kind: "daemon.status" }));
509
- client.end();
510
- });
511
- client.on("data", (chunk) => {
512
- raw += chunk.toString("utf-8");
513
- });
514
- client.on("error", () => finalize(false));
515
- client.on("end", () => {
516
- if (raw.trim().length === 0) {
517
- finalize(false);
518
- return;
519
- }
520
- try {
521
- JSON.parse(raw);
522
- finalize(true);
523
- }
524
- catch {
525
- finalize(false);
526
- }
527
- });
528
- });
1256
+ }
1257
+ catch {
1258
+ // Best effort — return undefined on any error
1259
+ }
1260
+ return undefined;
529
1261
  }
530
1262
  function defaultCleanupStaleSocket(socketPath) {
531
1263
  if (fs.existsSync(socketPath)) {
@@ -561,9 +1293,39 @@ function defaultFallbackPendingMessage(command) {
561
1293
  });
562
1294
  return pendingPath;
563
1295
  }
564
- async function defaultInstallSubagents() {
565
- return (0, subagent_installer_1.installSubagentsForAvailableCli)({
566
- repoRoot: (0, identity_1.getRepoRoot)(),
1296
+ function defaultEnsureDaemonBootPersistence(socketPath) {
1297
+ if (process.platform !== "darwin") {
1298
+ return;
1299
+ }
1300
+ const homeDir = os.homedir();
1301
+ const writeDeps = {
1302
+ writeFile: (filePath, content) => fs.writeFileSync(filePath, content, "utf-8"),
1303
+ mkdirp: (dir) => fs.mkdirSync(dir, { recursive: true }),
1304
+ homeDir,
1305
+ };
1306
+ const entryPath = path.join((0, identity_1.getRepoRoot)(), "dist", "heart", "daemon", "daemon-entry.js");
1307
+ /* 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 */
1308
+ if (!fs.existsSync(entryPath)) {
1309
+ (0, runtime_1.emitNervesEvent)({
1310
+ level: "warn",
1311
+ component: "daemon",
1312
+ event: "daemon.entry_path_missing",
1313
+ message: "entryPath does not exist on disk — plist may point to a stale location. Run 'ouro daemon install' from the correct location.",
1314
+ meta: { entryPath },
1315
+ });
1316
+ }
1317
+ const logDir = (0, identity_1.getAgentDaemonLogsDir)();
1318
+ // Write plist only — do NOT launchctl bootstrap.
1319
+ // The daemon is already running (started by ouro up). Bootstrapping would
1320
+ // start a SECOND daemon via launchd's RunAtLoad, causing a race where
1321
+ // killOrphanProcesses kills the first daemon and both end up dead.
1322
+ // The plist on disk is sufficient: launchd picks it up on login.
1323
+ (0, launchd_1.writeLaunchAgentPlist)(writeDeps, {
1324
+ nodePath: process.execPath,
1325
+ entryPath,
1326
+ socketPath,
1327
+ logDir,
1328
+ envPath: process.env.PATH,
567
1329
  });
568
1330
  }
569
1331
  async function defaultPromptInput(question) {
@@ -581,62 +1343,11 @@ async function defaultPromptInput(question) {
581
1343
  }
582
1344
  }
583
1345
  function defaultListDiscoveredAgents() {
584
- const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
585
- let entries;
586
- try {
587
- entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
588
- }
589
- catch {
590
- return [];
591
- }
592
- const discovered = [];
593
- for (const entry of entries) {
594
- if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
595
- continue;
596
- const agentName = entry.name.slice(0, -5);
597
- const configPath = path.join(bundlesRoot, entry.name, "agent.json");
598
- let enabled = true;
599
- try {
600
- const raw = fs.readFileSync(configPath, "utf-8");
601
- const parsed = JSON.parse(raw);
602
- if (typeof parsed.enabled === "boolean") {
603
- enabled = parsed.enabled;
604
- }
605
- }
606
- catch {
607
- continue;
608
- }
609
- if (enabled) {
610
- discovered.push(agentName);
611
- }
612
- }
613
- return discovered.sort((left, right) => left.localeCompare(right));
614
- }
615
- async function defaultLinkFriendIdentity(command) {
616
- const fp = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends");
617
- const friendStore = new store_file_1.FileFriendStore(fp);
618
- const current = await friendStore.get(command.friendId);
619
- if (!current) {
620
- return `friend not found: ${command.friendId}`;
621
- }
622
- const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
623
- if (alreadyLinked) {
624
- return `identity already linked: ${command.provider}:${command.externalId}`;
625
- }
626
- const now = new Date().toISOString();
627
- await friendStore.put(command.friendId, {
628
- ...current,
629
- externalIds: [
630
- ...current.externalIds,
631
- {
632
- provider: command.provider,
633
- externalId: command.externalId,
634
- linkedAt: now,
635
- },
636
- ],
637
- updatedAt: now,
1346
+ return (0, agent_discovery_1.listEnabledBundleAgents)({
1347
+ bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
1348
+ readdirSync: fs.readdirSync,
1349
+ readFileSync: fs.readFileSync,
638
1350
  });
639
- return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
640
1351
  }
641
1352
  function discoverExistingCredentials(secretsRoot) {
642
1353
  const found = [];
@@ -693,7 +1404,7 @@ function discoverExistingCredentials(secretsRoot) {
693
1404
  });
694
1405
  }
695
1406
  /* v8 ignore start -- integration: interactive terminal specialist session @preserve */
696
- async function defaultRunAdoptionSpecialist() {
1407
+ async function defaultRunSerpentGuide() {
697
1408
  const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
698
1409
  const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
699
1410
  const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
@@ -717,10 +1428,41 @@ async function defaultRunAdoptionSpecialist() {
717
1428
  // Default models per provider (used when entering new credentials)
718
1429
  const defaultModels = {
719
1430
  anthropic: "claude-opus-4-6",
720
- minimax: "MiniMax-Text-01",
1431
+ minimax: "MiniMax-M2.7",
721
1432
  "openai-codex": "gpt-5.4",
1433
+ "github-copilot": "claude-sonnet-4.6",
722
1434
  azure: "",
723
1435
  };
1436
+ // Scan environment variables for API keys
1437
+ const envKeys = [
1438
+ { provider: "anthropic", envVar: "ANTHROPIC_API_KEY", credKey: "setupToken" },
1439
+ { provider: "openai-codex", envVar: "OPENAI_API_KEY", credKey: "oauthAccessToken" },
1440
+ { provider: "azure", envVar: "AZURE_OPENAI_API_KEY", credKey: "apiKey" },
1441
+ { provider: "azure", envVar: "AZURE_OPENAI_KEY", credKey: "apiKey" },
1442
+ { provider: "minimax", envVar: "MINIMAX_API_KEY", credKey: "apiKey" },
1443
+ { provider: "github-copilot", envVar: "GITHUB_TOKEN", credKey: "token" },
1444
+ ];
1445
+ const envDiscovered = [];
1446
+ for (const { provider, envVar, credKey } of envKeys) {
1447
+ const value = process.env[envVar];
1448
+ if (value) {
1449
+ const envCred = { [credKey]: value };
1450
+ // For Azure, also check for endpoint and deployment env vars
1451
+ if (provider === "azure") {
1452
+ const endpoint = process.env.AZURE_OPENAI_ENDPOINT;
1453
+ const deployment = process.env.AZURE_OPENAI_DEPLOYMENT;
1454
+ if (endpoint)
1455
+ envCred.endpoint = endpoint;
1456
+ if (deployment)
1457
+ envCred.deployment = deployment;
1458
+ }
1459
+ const provCfg = { model: defaultModels[provider] };
1460
+ if (provider === "azure" && envCred.deployment)
1461
+ provCfg.deployment = envCred.deployment;
1462
+ envDiscovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg, envVar });
1463
+ discovered.push({ provider, agentName: "env", credentials: envCred, providerConfig: provCfg });
1464
+ }
1465
+ }
724
1466
  if (discovered.length > 0) {
725
1467
  process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
726
1468
  process.stdout.write("i found existing API credentials:\n\n");
@@ -728,7 +1470,9 @@ async function defaultRunAdoptionSpecialist() {
728
1470
  for (let i = 0; i < unique.length; i++) {
729
1471
  const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
730
1472
  const modelLabel = model ? `, ${model}` : "";
731
- process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (from ${unique[i].agentName})\n`);
1473
+ const envMatch = envDiscovered.find((e) => e.provider === unique[i].provider && unique[i].agentName === "env");
1474
+ const sourceLabel = envMatch ? `from env: $${envMatch.envVar}` : `from ${unique[i].agentName}`;
1475
+ process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (${sourceLabel})\n`);
732
1476
  }
733
1477
  process.stdout.write("\n");
734
1478
  const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
@@ -739,7 +1483,7 @@ async function defaultRunAdoptionSpecialist() {
739
1483
  providerConfig = unique[idx].providerConfig;
740
1484
  }
741
1485
  else {
742
- const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
1486
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
743
1487
  if (!isAgentProvider(pRaw)) {
744
1488
  process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
745
1489
  coldRl.close();
@@ -763,7 +1507,7 @@ async function defaultRunAdoptionSpecialist() {
763
1507
  else {
764
1508
  process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
765
1509
  process.stdout.write("i need an API key to power our conversation.\n\n");
766
- const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
1510
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex/github-copilot): ");
767
1511
  if (!isAgentProvider(pRaw)) {
768
1512
  process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
769
1513
  coldRl.close();
@@ -785,17 +1529,17 @@ async function defaultRunAdoptionSpecialist() {
785
1529
  }
786
1530
  coldRl.close();
787
1531
  process.stdout.write("\n");
788
- // Phase 2: configure runtime for adoption specialist
789
- const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
1532
+ // Phase 2: configure runtime for serpent guide
1533
+ const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "SerpentGuide.ouro");
790
1534
  const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
791
1535
  const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
792
- // Suppress non-critical log noise during adoption (no secrets.json, etc.)
1536
+ // Suppress non-critical log noise during hatch (no secrets.json, etc.)
793
1537
  const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
794
1538
  const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
795
1539
  setRuntimeLogger(createLogger({ level: "error" }));
796
1540
  // Configure runtime: set agent identity + config override so runAgent
797
- // doesn't try to read from ~/AgentBundles/AdoptionSpecialist.ouro/
798
- setAgentName("AdoptionSpecialist");
1541
+ // doesn't try to read from ~/AgentBundles/SerpentGuide.ouro/
1542
+ setAgentName("SerpentGuide");
799
1543
  // Build specialist system prompt
800
1544
  const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
801
1545
  const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
@@ -803,10 +1547,13 @@ async function defaultRunAdoptionSpecialist() {
803
1547
  // Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
804
1548
  const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
805
1549
  const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
1550
+ const resolvedModel = providerConfig.model || providerConfig.deployment || "";
806
1551
  setAgentConfigOverride({
807
- version: 1,
1552
+ version: 2,
808
1553
  enabled: true,
809
1554
  provider: providerRaw,
1555
+ humanFacing: { provider: providerRaw, model: resolvedModel },
1556
+ agentFacing: { provider: providerRaw, model: resolvedModel },
810
1557
  phrases,
811
1558
  });
812
1559
  patchRuntimeConfig({
@@ -818,6 +1565,7 @@ async function defaultRunAdoptionSpecialist() {
818
1565
  const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
819
1566
  tempDir,
820
1567
  provider: providerRaw,
1568
+ model: providerConfig.model ?? "",
821
1569
  });
822
1570
  // Build specialist tools
823
1571
  const specialistTools = (0, specialist_tools_1.getSpecialistTools)();
@@ -829,9 +1577,9 @@ async function defaultRunAdoptionSpecialist() {
829
1577
  secretsRoot: secretsRoot2,
830
1578
  animationWriter: (text) => process.stdout.write(text),
831
1579
  });
832
- // Run the adoption specialist session via runCliSession
1580
+ // Run the serpent guide session via runCliSession
833
1581
  const result = await runCliSession({
834
- agentName: "AdoptionSpecialist",
1582
+ agentName: "SerpentGuide",
835
1583
  tools: specialistTools,
836
1584
  execTool: specialistExecTool,
837
1585
  exitOnToolCall: "complete_adoption",
@@ -853,7 +1601,7 @@ async function defaultRunAdoptionSpecialist() {
853
1601
  return null;
854
1602
  }
855
1603
  catch (err) {
856
- process.stderr.write(`\nouro adoption error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
1604
+ process.stderr.write(`\nouro hatch error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
857
1605
  coldRl.close();
858
1606
  return null;
859
1607
  }
@@ -879,30 +1627,118 @@ async function defaultRunAdoptionSpecialist() {
879
1627
  }
880
1628
  }
881
1629
  /* v8 ignore stop */
882
- function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
1630
+ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SOCKET_PATH) {
883
1631
  return {
884
1632
  socketPath,
885
- sendCommand: defaultSendCommand,
1633
+ sendCommand: socket_client_1.sendDaemonCommand,
886
1634
  startDaemonProcess: defaultStartDaemonProcess,
887
1635
  writeStdout: defaultWriteStdout,
888
- checkSocketAlive: defaultCheckSocketAlive,
1636
+ checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
889
1637
  cleanupStaleSocket: defaultCleanupStaleSocket,
890
1638
  fallbackPendingMessage: defaultFallbackPendingMessage,
891
- installSubagents: defaultInstallSubagents,
892
- linkFriendIdentity: defaultLinkFriendIdentity,
893
1639
  listDiscoveredAgents: defaultListDiscoveredAgents,
894
1640
  runHatchFlow: hatch_flow_1.runHatchFlow,
895
1641
  promptInput: defaultPromptInput,
896
- runAdoptionSpecialist: defaultRunAdoptionSpecialist,
1642
+ runSerpentGuide: defaultRunSerpentGuide,
1643
+ runAuthFlow: auth_flow_1.runRuntimeAuthFlow,
897
1644
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
898
1645
  installOuroCommand: ouro_path_installer_1.installOuroCommand,
1646
+ /* v8 ignore start -- self-healing: ensures active symlink matches running runtime version @preserve */
1647
+ ensureCurrentVersionInstalled: () => {
1648
+ const linkedVersion = (0, ouro_version_manager_1.getCurrentVersion)({});
1649
+ const version = (0, bundle_manifest_1.getPackageVersion)();
1650
+ if (linkedVersion === version)
1651
+ return;
1652
+ (0, ouro_version_manager_1.ensureLayout)({});
1653
+ const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
1654
+ const versionEntry = path.join(cliHome, "versions", version, "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
1655
+ if (!fs.existsSync(versionEntry)) {
1656
+ (0, ouro_version_manager_1.installVersion)(version, {});
1657
+ }
1658
+ (0, ouro_version_manager_1.activateVersion)(version, {});
1659
+ },
1660
+ /* v8 ignore stop */
1661
+ /* v8 ignore start -- CLI version management defaults: integration code @preserve */
1662
+ checkForCliUpdate: async () => {
1663
+ const { checkForUpdate } = await Promise.resolve().then(() => __importStar(require("./update-checker")));
1664
+ return checkForUpdate((0, bundle_manifest_1.getPackageVersion)(), {
1665
+ fetchRegistryJson: async () => {
1666
+ const res = await fetch("https://registry.npmjs.org/@ouro.bot/cli");
1667
+ return res.json();
1668
+ },
1669
+ distTag: "alpha",
1670
+ });
1671
+ },
1672
+ installCliVersion: async (version) => { (0, ouro_version_manager_1.installVersion)(version, {}); },
1673
+ activateCliVersion: (version) => { (0, ouro_version_manager_1.activateVersion)(version, {}); },
1674
+ getCurrentCliVersion: () => (0, ouro_version_manager_1.getCurrentVersion)({}),
1675
+ getPreviousCliVersion: () => (0, ouro_version_manager_1.getPreviousVersion)({}),
1676
+ listCliVersions: () => (0, ouro_version_manager_1.listInstalledVersions)({}),
1677
+ reExecFromNewVersion: (reArgs) => {
1678
+ const entry = path.join((0, ouro_version_manager_1.getOuroCliHome)(), "CurrentVersion", "node_modules", "@ouro.bot", "cli", "dist", "heart", "daemon", "ouro-entry.js");
1679
+ require("child_process").execFileSync("node", [entry, ...reArgs], { stdio: "inherit" });
1680
+ process.exit(0);
1681
+ },
1682
+ /* v8 ignore stop */
1683
+ syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
1684
+ ensureSkillManagement: skill_management_installer_1.ensureSkillManagement,
1685
+ ensureDaemonBootPersistence: defaultEnsureDaemonBootPersistence,
1686
+ /* v8 ignore start -- dev-mode defaults: tests inject mocks for mode detection and binary resolution @preserve */
1687
+ detectMode: () => (0, runtime_mode_1.detectRuntimeMode)((0, identity_1.getRepoRoot)()),
1688
+ getInstalledBinaryPath: () => {
1689
+ const cliHome = (0, ouro_version_manager_1.getOuroCliHome)();
1690
+ const binaryPath = path.join(cliHome, "bin", "ouro");
1691
+ return fs.existsSync(binaryPath) ? binaryPath : null;
1692
+ },
1693
+ execInstalledBinary: (binaryPath, binArgs) => {
1694
+ const { execFileSync } = require("child_process");
1695
+ execFileSync(binaryPath, binArgs, { stdio: "inherit" });
1696
+ process.exit(0);
1697
+ },
1698
+ /* v8 ignore stop */
899
1699
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
900
1700
  startChat: async (agentName) => {
901
1701
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
902
1702
  await main(agentName);
903
1703
  },
1704
+ scanSessions: async () => {
1705
+ const agentName = (0, identity_1.getAgentName)();
1706
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
1707
+ return (0, session_activity_1.listSessionActivity)({
1708
+ sessionsDir: path.join(agentRoot, "state", "sessions"),
1709
+ friendsDir: path.join(agentRoot, "friends"),
1710
+ agentName,
1711
+ }).map((entry) => ({
1712
+ friendId: entry.friendId,
1713
+ friendName: entry.friendName,
1714
+ channel: entry.channel,
1715
+ lastActivity: entry.lastActivityAt,
1716
+ }));
1717
+ },
904
1718
  };
905
1719
  }
1720
+ function formatMcpResponse(command, response) {
1721
+ if (command.kind === "mcp.list") {
1722
+ const allTools = response.data;
1723
+ if (!allTools || allTools.length === 0) {
1724
+ return response.message ?? "no tools available from connected MCP servers";
1725
+ }
1726
+ const lines = [];
1727
+ for (const entry of allTools) {
1728
+ lines.push(`[${entry.server}]`);
1729
+ for (const tool of entry.tools) {
1730
+ lines.push(` ${tool.name}: ${tool.description}`);
1731
+ }
1732
+ }
1733
+ return lines.join("\n");
1734
+ }
1735
+ // mcp.call
1736
+ const result = response.data;
1737
+ if (!result) {
1738
+ return response.message ?? "no result";
1739
+ }
1740
+ return result.content.map((c) => c.text).join("\n");
1741
+ }
906
1742
  function toDaemonCommand(command) {
907
1743
  return command;
908
1744
  }
@@ -910,28 +1746,17 @@ async function resolveHatchInput(command, deps) {
910
1746
  const prompt = deps.promptInput;
911
1747
  const agentName = command.agentName ?? (prompt ? await prompt("Hatchling name: ") : "");
912
1748
  const humanName = command.humanName ?? (prompt ? await prompt("Your name: ") : os.userInfo().username);
913
- const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex): ") : "");
1749
+ const providerRaw = command.provider ?? (prompt ? await prompt("Provider (azure|anthropic|minimax|openai-codex|github-copilot): ") : "");
914
1750
  if (!agentName || !humanName || !isAgentProvider(providerRaw)) {
915
1751
  throw new Error(`Usage\n${usage()}`);
916
1752
  }
917
- const credentials = { ...(command.credentials ?? {}) };
918
- if (providerRaw === "anthropic" && !credentials.setupToken && prompt) {
919
- credentials.setupToken = await prompt("Anthropic setup-token: ");
920
- }
921
- if (providerRaw === "openai-codex" && !credentials.oauthAccessToken && prompt) {
922
- credentials.oauthAccessToken = await prompt("OpenAI Codex OAuth token: ");
923
- }
924
- if (providerRaw === "minimax" && !credentials.apiKey && prompt) {
925
- credentials.apiKey = await prompt("MiniMax API key: ");
926
- }
927
- if (providerRaw === "azure") {
928
- if (!credentials.apiKey && prompt)
929
- credentials.apiKey = await prompt("Azure API key: ");
930
- if (!credentials.endpoint && prompt)
931
- credentials.endpoint = await prompt("Azure endpoint: ");
932
- if (!credentials.deployment && prompt)
933
- credentials.deployment = await prompt("Azure deployment: ");
934
- }
1753
+ const credentials = await (0, auth_flow_1.resolveHatchCredentials)({
1754
+ agentName,
1755
+ provider: providerRaw,
1756
+ credentials: command.credentials,
1757
+ promptInput: prompt,
1758
+ runAuthFlow: deps.runAuthFlow,
1759
+ });
935
1760
  return {
936
1761
  agentName,
937
1762
  humanName,
@@ -961,7 +1786,11 @@ async function performSystemSetup(deps) {
961
1786
  // Install ouro command to PATH (non-blocking)
962
1787
  if (deps.installOuroCommand) {
963
1788
  try {
964
- deps.installOuroCommand();
1789
+ const installResult = deps.installOuroCommand();
1790
+ /* v8 ignore next -- old-launcher repair hint: fires when stale ~/.local/bin/ouro is fixed @preserve */
1791
+ if (installResult.repairedOldLauncher) {
1792
+ deps.writeStdout("repaired stale ouro launcher at ~/.local/bin/ouro");
1793
+ }
965
1794
  }
966
1795
  catch (error) {
967
1796
  (0, runtime_1.emitNervesEvent)({
@@ -973,24 +1802,382 @@ async function performSystemSetup(deps) {
973
1802
  });
974
1803
  }
975
1804
  }
976
- // Install subagents (claude/codex skills)
977
- try {
978
- await deps.installSubagents();
979
- }
980
- catch (error) {
981
- (0, runtime_1.emitNervesEvent)({
982
- level: "warn",
983
- component: "daemon",
984
- event: "daemon.subagent_install_error",
985
- message: "subagent auto-install failed",
986
- meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
987
- });
1805
+ // Self-healing: ensure current version is installed in ~/.ouro-cli/ layout.
1806
+ // Handles the case where the wrapper exists but CurrentVersion is missing
1807
+ // (e.g., first run after migration from old npx wrapper).
1808
+ if (deps.ensureCurrentVersionInstalled) {
1809
+ try {
1810
+ deps.ensureCurrentVersionInstalled();
1811
+ }
1812
+ catch (error) {
1813
+ (0, runtime_1.emitNervesEvent)({
1814
+ level: "warn",
1815
+ component: "daemon",
1816
+ event: "daemon.system_setup_version_install_error",
1817
+ message: "failed to ensure current version installed",
1818
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive @preserve */ String(error) },
1819
+ });
1820
+ }
1821
+ }
1822
+ if (deps.syncGlobalOuroBotWrapper) {
1823
+ try {
1824
+ await Promise.resolve(deps.syncGlobalOuroBotWrapper());
1825
+ }
1826
+ catch (error) {
1827
+ (0, runtime_1.emitNervesEvent)({
1828
+ level: "warn",
1829
+ component: "daemon",
1830
+ event: "daemon.system_setup_ouro_bot_wrapper_error",
1831
+ message: "failed to sync global ouro.bot wrapper",
1832
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1833
+ });
1834
+ }
1835
+ }
1836
+ // Ensure skill-management skill is available
1837
+ if (deps.ensureSkillManagement) {
1838
+ try {
1839
+ await deps.ensureSkillManagement();
1840
+ /* v8 ignore start -- defensive: ensureSkillManagement handles its own errors internally @preserve */
1841
+ }
1842
+ catch (error) {
1843
+ (0, runtime_1.emitNervesEvent)({
1844
+ level: "warn",
1845
+ component: "daemon",
1846
+ event: "daemon.system_setup_skill_management_error",
1847
+ message: "failed to ensure skill-management skill",
1848
+ meta: { error: error instanceof Error ? error.message : String(error) },
1849
+ });
1850
+ }
1851
+ /* v8 ignore stop */
988
1852
  }
989
1853
  // Register .ouro bundle type (UTI on macOS)
990
1854
  await registerOuroBundleTypeNonBlocking(deps);
991
1855
  }
1856
+ function executeTaskCommand(command, taskMod) {
1857
+ if (command.kind === "task.board") {
1858
+ if (command.status) {
1859
+ const lines = taskMod.boardStatus(command.status);
1860
+ return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
1861
+ }
1862
+ const board = taskMod.getBoard();
1863
+ return board.full || board.compact || "no tasks found";
1864
+ }
1865
+ if (command.kind === "task.create") {
1866
+ try {
1867
+ const created = taskMod.createTask({
1868
+ title: command.title,
1869
+ type: command.type ?? "one-shot",
1870
+ category: "general",
1871
+ body: "",
1872
+ });
1873
+ return `created: ${created}`;
1874
+ }
1875
+ catch (error) {
1876
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1877
+ }
1878
+ }
1879
+ if (command.kind === "task.update") {
1880
+ const result = taskMod.updateStatus(command.id, command.status);
1881
+ if (!result.ok) {
1882
+ return `error: ${result.reason ?? "status update failed"}`;
1883
+ }
1884
+ const archivedSuffix = result.archived && result.archived.length > 0
1885
+ ? ` | archived: ${result.archived.join(", ")}`
1886
+ : "";
1887
+ return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
1888
+ }
1889
+ if (command.kind === "task.show") {
1890
+ const task = taskMod.getTask(command.id);
1891
+ if (!task)
1892
+ return `task not found: ${command.id}`;
1893
+ return [
1894
+ `title: ${task.title}`,
1895
+ `type: ${task.type}`,
1896
+ `status: ${task.status}`,
1897
+ `category: ${task.category}`,
1898
+ `created: ${task.created}`,
1899
+ `updated: ${task.updated}`,
1900
+ `path: ${task.path}`,
1901
+ task.body ? `\n${task.body}` : "",
1902
+ ].filter(Boolean).join("\n");
1903
+ }
1904
+ if (command.kind === "task.actionable") {
1905
+ const lines = taskMod.boardAction();
1906
+ return lines.length > 0 ? lines.join("\n") : "no action required";
1907
+ }
1908
+ if (command.kind === "task.deps") {
1909
+ const lines = taskMod.boardDeps();
1910
+ return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
1911
+ }
1912
+ if (command.kind === "task.fix") {
1913
+ try {
1914
+ const fixOptions = {
1915
+ mode: command.mode,
1916
+ ...(command.issueId ? { issueId: command.issueId } : {}),
1917
+ ...(command.option !== undefined ? { option: command.option } : {}),
1918
+ };
1919
+ const result = taskMod.fix(fixOptions);
1920
+ if (command.mode === "dry-run") {
1921
+ if (result.remaining.length === 0) {
1922
+ return `task health: clean`;
1923
+ }
1924
+ const safeIssues = result.remaining.filter((i) => i.confidence === "safe");
1925
+ const reviewIssues = result.remaining.filter((i) => i.confidence === "needs_review");
1926
+ const lines = [`${result.remaining.length} issues found`];
1927
+ if (safeIssues.length > 0) {
1928
+ lines.push("", `safe fixes (${safeIssues.length}):`);
1929
+ for (const issue of safeIssues) {
1930
+ lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
1931
+ }
1932
+ }
1933
+ if (reviewIssues.length > 0) {
1934
+ lines.push("", `needs review (${reviewIssues.length}):`);
1935
+ for (const issue of reviewIssues) {
1936
+ lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
1937
+ }
1938
+ }
1939
+ lines.push("", `task health: ${result.health}`);
1940
+ return lines.join("\n");
1941
+ }
1942
+ // safe, single, or --all modes: show what was done
1943
+ const lines = [];
1944
+ if (result.applied.length > 0) {
1945
+ lines.push(`${result.applied.length} applied:`);
1946
+ for (const issue of result.applied) {
1947
+ lines.push(` ${issue.code}:${issue.target}`);
1948
+ }
1949
+ }
1950
+ if (result.remaining.length > 0) {
1951
+ lines.push(`${result.remaining.length} remaining:`);
1952
+ for (const issue of result.remaining) {
1953
+ lines.push(` ${issue.code}:${issue.target} -- ${issue.description}`);
1954
+ }
1955
+ }
1956
+ if (result.applied.length === 0 && result.remaining.length === 0) {
1957
+ lines.push("no issues");
1958
+ }
1959
+ lines.push(`task health: ${result.health}`);
1960
+ return lines.join("\n");
1961
+ }
1962
+ catch (error) {
1963
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1964
+ }
1965
+ }
1966
+ // command.kind === "task.sessions"
1967
+ const lines = taskMod.boardSessions();
1968
+ return lines.length > 0 ? lines.join("\n") : "no active sessions";
1969
+ }
1970
+ const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
1971
+ /* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
1972
+ function higherTrust(a, b) {
1973
+ const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
1974
+ const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
1975
+ return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
1976
+ }
1977
+ /* v8 ignore stop */
1978
+ async function executeFriendCommand(command, store) {
1979
+ if (command.kind === "friend.list") {
1980
+ const listAll = store.listAll;
1981
+ if (!listAll)
1982
+ return "friend store does not support listing";
1983
+ const friends = await listAll.call(store);
1984
+ if (friends.length === 0)
1985
+ return "no friends found";
1986
+ const lines = friends.map((f) => {
1987
+ const trust = f.trustLevel ?? "unknown";
1988
+ return `${f.id} ${f.name} ${trust}`;
1989
+ });
1990
+ return lines.join("\n");
1991
+ }
1992
+ if (command.kind === "friend.show") {
1993
+ const record = await store.get(command.friendId);
1994
+ if (!record)
1995
+ return `friend not found: ${command.friendId}`;
1996
+ return JSON.stringify(record, null, 2);
1997
+ }
1998
+ if (command.kind === "friend.create") {
1999
+ const now = new Date().toISOString();
2000
+ const id = (0, crypto_1.randomUUID)();
2001
+ const trustLevel = (command.trustLevel ?? "acquaintance");
2002
+ await store.put(id, {
2003
+ id,
2004
+ name: command.name,
2005
+ trustLevel,
2006
+ externalIds: [],
2007
+ tenantMemberships: [],
2008
+ toolPreferences: {},
2009
+ notes: {},
2010
+ totalTokens: 0,
2011
+ createdAt: now,
2012
+ updatedAt: now,
2013
+ schemaVersion: 1,
2014
+ });
2015
+ return `created: ${id} (${command.name}, ${trustLevel})`;
2016
+ }
2017
+ if (command.kind === "friend.update") {
2018
+ const current = await store.get(command.friendId);
2019
+ if (!current)
2020
+ return `friend not found: ${command.friendId}`;
2021
+ const now = new Date().toISOString();
2022
+ await store.put(command.friendId, {
2023
+ ...current,
2024
+ trustLevel: command.trustLevel,
2025
+ role: command.trustLevel,
2026
+ updatedAt: now,
2027
+ });
2028
+ return `updated: ${command.friendId} → trust=${command.trustLevel}`;
2029
+ }
2030
+ if (command.kind === "friend.link") {
2031
+ const current = await store.get(command.friendId);
2032
+ if (!current)
2033
+ return `friend not found: ${command.friendId}`;
2034
+ const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
2035
+ if (alreadyLinked)
2036
+ return `identity already linked: ${command.provider}:${command.externalId}`;
2037
+ const now = new Date().toISOString();
2038
+ const newExternalIds = [
2039
+ ...current.externalIds,
2040
+ { provider: command.provider, externalId: command.externalId, linkedAt: now },
2041
+ ];
2042
+ // Orphan cleanup: check if another friend has this externalId
2043
+ const orphan = await store.findByExternalId(command.provider, command.externalId);
2044
+ let mergeMessage = "";
2045
+ let mergedNotes = { ...current.notes };
2046
+ let mergedTrust = current.trustLevel;
2047
+ let orphanExternalIds = [];
2048
+ if (orphan && orphan.id !== command.friendId) {
2049
+ // Merge orphan's notes (target's notes take priority)
2050
+ mergedNotes = { ...orphan.notes, ...current.notes };
2051
+ // Keep higher trust level
2052
+ mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
2053
+ // Collect orphan's other externalIds (excluding the one being linked)
2054
+ orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
2055
+ await store.delete(orphan.id);
2056
+ mergeMessage = ` (merged orphan ${orphan.id})`;
2057
+ }
2058
+ await store.put(command.friendId, {
2059
+ ...current,
2060
+ externalIds: [...newExternalIds, ...orphanExternalIds],
2061
+ notes: mergedNotes,
2062
+ trustLevel: mergedTrust,
2063
+ updatedAt: now,
2064
+ });
2065
+ return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
2066
+ }
2067
+ // command.kind === "friend.unlink"
2068
+ const current = await store.get(command.friendId);
2069
+ if (!current)
2070
+ return `friend not found: ${command.friendId}`;
2071
+ const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
2072
+ if (idx === -1)
2073
+ return `identity not linked: ${command.provider}:${command.externalId}`;
2074
+ const now = new Date().toISOString();
2075
+ const filtered = current.externalIds.filter((_, i) => i !== idx);
2076
+ await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
2077
+ return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
2078
+ }
2079
+ function executeReminderCommand(command, taskMod) {
2080
+ try {
2081
+ const created = taskMod.createTask({
2082
+ title: command.title,
2083
+ type: command.cadence ? "ongoing" : "one-shot",
2084
+ category: command.category ?? "reminder",
2085
+ body: command.body,
2086
+ scheduledAt: command.scheduledAt,
2087
+ cadence: command.cadence,
2088
+ requester: command.requester,
2089
+ });
2090
+ return `created: ${created}`;
2091
+ }
2092
+ catch (error) {
2093
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
2094
+ }
2095
+ }
2096
+ /* v8 ignore start -- repo resolution for ouro dev: repoPath branch tested via daemon-cli-dev; clone requires real git/npm @preserve */
2097
+ function getDevConfigPath() {
2098
+ return path.join((0, ouro_version_manager_1.getOuroCliHome)(), "dev-config.json");
2099
+ }
2100
+ function readPersistedDevPath() {
2101
+ try {
2102
+ const data = JSON.parse(fs.readFileSync(getDevConfigPath(), "utf-8"));
2103
+ return typeof data.repoPath === "string" ? data.repoPath : null;
2104
+ }
2105
+ catch {
2106
+ return null;
2107
+ }
2108
+ }
2109
+ function persistDevPath(repoPath) {
2110
+ try {
2111
+ fs.mkdirSync(path.dirname(getDevConfigPath()), { recursive: true });
2112
+ fs.writeFileSync(getDevConfigPath(), JSON.stringify({ repoPath }, null, 2));
2113
+ }
2114
+ catch { /* best effort */ }
2115
+ }
2116
+ function resolveDevRepoCwd(command, checkExists, deps) {
2117
+ // 1. Explicit --repo-path: use it and persist for next time
2118
+ if (command.repoPath) {
2119
+ const resolved = path.resolve(command.repoPath);
2120
+ persistDevPath(resolved);
2121
+ return resolved;
2122
+ }
2123
+ // 2. Clone request
2124
+ if (command.clone)
2125
+ return resolveClonePath(command, checkExists, deps);
2126
+ // 3. If test/internal deps provide getRepoCwd, use it directly
2127
+ if (deps.getRepoCwd) {
2128
+ return deps.getRepoCwd();
2129
+ }
2130
+ // 4. Check CWD — if we're inside a harness repo, use it
2131
+ const cwd = process.cwd();
2132
+ if (checkExists(path.join(cwd, ".git")) && checkExists(path.join(cwd, "src", "heart", "daemon"))) {
2133
+ persistDevPath(cwd);
2134
+ return cwd;
2135
+ }
2136
+ // 5. Read persisted path from last --repo-path
2137
+ const persisted = readPersistedDevPath();
2138
+ if (persisted && checkExists(path.join(persisted, ".git"))) {
2139
+ deps.writeStdout(`using remembered dev path: ${persisted}`);
2140
+ return persisted;
2141
+ }
2142
+ // 6. Fall back to getRepoRoot (works when running from installed binary → resolves to npm package)
2143
+ return (0, identity_1.getRepoRoot)();
2144
+ }
2145
+ /* v8 ignore stop */
2146
+ /* v8 ignore start -- clone/build: requires real git clone + npm install on disk @preserve */
2147
+ function resolveClonePath(command, checkExists, deps) {
2148
+ const cloneTarget = command.clonePath
2149
+ ? path.resolve(command.clonePath)
2150
+ : path.join(os.homedir(), "Projects", "ouroboros");
2151
+ if (!checkExists(path.join(cloneTarget, ".git"))) {
2152
+ deps.writeStdout(`cloning ouroboros to ${cloneTarget}...`);
2153
+ try {
2154
+ (0, child_process_1.execSync)(`git clone ${identity_1.HARNESS_CANONICAL_REPO_URL} "${cloneTarget}"`, { stdio: "inherit" });
2155
+ }
2156
+ catch {
2157
+ throw new Error(`clone failed. check your network and try again, or clone manually and use --repo-path.`);
2158
+ }
2159
+ }
2160
+ else {
2161
+ deps.writeStdout(`repo already exists at ${cloneTarget}, pulling latest...`);
2162
+ try {
2163
+ (0, child_process_1.execSync)("git pull --ff-only", { cwd: cloneTarget, stdio: "inherit" });
2164
+ }
2165
+ catch {
2166
+ deps.writeStdout("pull failed (may have local changes). continuing with existing code.");
2167
+ }
2168
+ }
2169
+ deps.writeStdout("building...");
2170
+ try {
2171
+ (0, child_process_1.execSync)("npm install && npm run build", { cwd: cloneTarget, stdio: "inherit" });
2172
+ }
2173
+ catch {
2174
+ throw new Error(`build failed in ${cloneTarget}. check the output above.`);
2175
+ }
2176
+ return cloneTarget;
2177
+ }
2178
+ /* v8 ignore stop */
992
2179
  async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
993
- if (args.includes("--help") || args.includes("-h")) {
2180
+ if (args.length === 1 && (args[0] === "--help" || args[0] === "-h")) {
994
2181
  const text = usage();
995
2182
  deps.writeStdout(text);
996
2183
  return text;
@@ -1017,10 +2204,10 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1017
2204
  }
1018
2205
  if (args.length === 0) {
1019
2206
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : defaultListDiscoveredAgents());
1020
- if (discovered.length === 0 && deps.runAdoptionSpecialist) {
2207
+ if (discovered.length === 0 && deps.runSerpentGuide) {
1021
2208
  // System setup first — ouro command, subagents, UTI — before the interactive specialist
1022
2209
  await performSystemSetup(deps);
1023
- const hatchlingName = await deps.runAdoptionSpecialist();
2210
+ const hatchlingName = await deps.runSerpentGuide();
1024
2211
  if (!hatchlingName) {
1025
2212
  return "";
1026
2213
  }
@@ -1070,28 +2257,1068 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1070
2257
  meta: { kind: command.kind },
1071
2258
  });
1072
2259
  if (command.kind === "daemon.up") {
2260
+ // ── dev mode cleanup: delete dev-config.json so the wrapper stops dispatching to dev repo ──
2261
+ /* v8 ignore start -- dev-config cleanup: requires real filesystem state @preserve */
2262
+ try {
2263
+ const devConfigPath = getDevConfigPath();
2264
+ if (fs.existsSync(devConfigPath)) {
2265
+ fs.unlinkSync(devConfigPath);
2266
+ }
2267
+ }
2268
+ catch { /* best effort */ }
2269
+ /* v8 ignore stop */
2270
+ // ── dev mode delegation: ouro up from a dev repo delegates to installed binary ──
2271
+ // Only runs when detectMode is explicitly injected (via createDefaultOuroCliDeps or tests)
2272
+ if (deps.detectMode) {
2273
+ const runtimeMode = deps.detectMode();
2274
+ if (runtimeMode === "dev") {
2275
+ /* v8 ignore next -- defensive: getInstalledBinaryPath always injected in tests @preserve */
2276
+ const installedBinary = deps.getInstalledBinaryPath ? deps.getInstalledBinaryPath() : null;
2277
+ if (installedBinary) {
2278
+ deps.writeStdout("delegating to installed ouro...");
2279
+ /* v8 ignore next 3 -- defensive: execInstalledBinary always injected; missing branch unreachable @preserve */
2280
+ if (deps.execInstalledBinary) {
2281
+ deps.execInstalledBinary(installedBinary, args);
2282
+ }
2283
+ /* v8 ignore next 2 -- unreachable after exec replaces process @preserve */
2284
+ return "";
2285
+ }
2286
+ const message = "no installed version found. run: npx @ouro.bot/cli@alpha";
2287
+ deps.writeStdout(message);
2288
+ return message;
2289
+ }
2290
+ }
2291
+ const linkedVersionBeforeUp = deps.getCurrentCliVersion?.() ?? null;
2292
+ // ── versioned CLI update check ──
2293
+ if (deps.checkForCliUpdate) {
2294
+ let pendingReExec = false;
2295
+ try {
2296
+ const updateResult = await deps.checkForCliUpdate();
2297
+ if (updateResult.available && updateResult.latestVersion) {
2298
+ /* v8 ignore next -- fallback: getCurrentCliVersion always injected in tests @preserve */
2299
+ const currentVersion = linkedVersionBeforeUp ?? "unknown";
2300
+ await deps.installCliVersion(updateResult.latestVersion);
2301
+ deps.activateCliVersion(updateResult.latestVersion);
2302
+ deps.writeStdout(`ouro updated to ${updateResult.latestVersion} (was ${currentVersion})`);
2303
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(currentVersion, updateResult.latestVersion);
2304
+ /* v8 ignore next -- buildChangelogCommand is non-null when an actual newer version is installed @preserve */
2305
+ if (changelogCommand) {
2306
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
2307
+ }
2308
+ pendingReExec = true;
2309
+ }
2310
+ /* v8 ignore start -- update check error: tested via daemon-cli-update-flow.test.ts @preserve */
2311
+ }
2312
+ catch (error) {
2313
+ (0, runtime_1.emitNervesEvent)({
2314
+ level: "warn",
2315
+ component: "daemon",
2316
+ event: "daemon.cli_update_check_error",
2317
+ message: "CLI update check failed",
2318
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
2319
+ });
2320
+ }
2321
+ /* v8 ignore stop */
2322
+ if (pendingReExec) {
2323
+ deps.reExecFromNewVersion(args);
2324
+ }
2325
+ }
1073
2326
  await performSystemSetup(deps);
2327
+ const linkedVersionAfterSetup = deps.getCurrentCliVersion?.() ?? null;
2328
+ const runtimeVersion = (0, bundle_manifest_1.getPackageVersion)();
2329
+ if (linkedVersionBeforeUp && linkedVersionBeforeUp !== runtimeVersion && linkedVersionAfterSetup === runtimeVersion) {
2330
+ deps.writeStdout(`ouro updated to ${runtimeVersion} (was ${linkedVersionBeforeUp})`);
2331
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(linkedVersionBeforeUp, runtimeVersion);
2332
+ if (changelogCommand) {
2333
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
2334
+ }
2335
+ }
2336
+ // Run update hooks before starting daemon so user sees the output
2337
+ (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
2338
+ (0, update_hooks_1.registerUpdateHook)(agent_config_v2_1.agentConfigV2Hook);
2339
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
2340
+ const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
2341
+ // Snapshot the previous CLI version from the first bundle-meta before
2342
+ // hooks overwrite it. This detects when npx downloaded a newer CLI.
2343
+ const previousCliVersion = readFirstBundleMetaVersion(bundlesRoot);
2344
+ const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
2345
+ // Notify about CLI binary update (npx downloaded a new version).
2346
+ // Skip when the symlink already points to the running version — that
2347
+ // means path 1 (checkForCliUpdate + reExecFromNewVersion) already
2348
+ // printed the update message before re-exec.
2349
+ /* v8 ignore start -- CLI update detection: tested via daemon-cli-version-detect.test.ts @preserve */
2350
+ if (previousCliVersion && previousCliVersion !== currentVersion && linkedVersionBeforeUp !== currentVersion) {
2351
+ deps.writeStdout(`ouro updated to ${currentVersion} (was ${previousCliVersion})`);
2352
+ const changelogCommand = (0, ouro_version_manager_1.buildChangelogCommand)(previousCliVersion, currentVersion);
2353
+ /* v8 ignore next -- buildChangelogCommand is non-null when previous/current runtime versions differ @preserve */
2354
+ if (changelogCommand) {
2355
+ deps.writeStdout(`review changes with: ${changelogCommand}`);
2356
+ }
2357
+ }
2358
+ /* v8 ignore stop */
2359
+ if (updateSummary.updated.length > 0) {
2360
+ const agents = updateSummary.updated.map((e) => e.agent);
2361
+ const from = updateSummary.updated[0].from;
2362
+ const to = updateSummary.updated[0].to;
2363
+ const fromStr = from ? ` (was ${from})` : "";
2364
+ const count = agents.length;
2365
+ deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
2366
+ }
1074
2367
  const daemonResult = await ensureDaemonRunning(deps);
1075
2368
  deps.writeStdout(daemonResult.message);
2369
+ // Persist boot startup AFTER daemon is running — bootstrap is safe now
2370
+ // because the daemon socket exists, so launchd's KeepAlive registers
2371
+ // for crash recovery without starting a competing process.
2372
+ if (deps.ensureDaemonBootPersistence) {
2373
+ try {
2374
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
2375
+ }
2376
+ catch (error) {
2377
+ (0, runtime_1.emitNervesEvent)({
2378
+ level: "warn",
2379
+ component: "daemon",
2380
+ event: "daemon.system_setup_launchd_error",
2381
+ message: "failed to persist daemon boot startup",
2382
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
2383
+ });
2384
+ }
2385
+ }
1076
2386
  return daemonResult.message;
1077
2387
  }
2388
+ if (command.kind === "daemon.dev") {
2389
+ /* v8 ignore next -- defensive: existsSync always injected in tests @preserve */
2390
+ const checkExists = deps.existsSync ?? fs.existsSync;
2391
+ /* v8 ignore next -- repo resolution dispatched to v8-ignored helper @preserve */
2392
+ let repoCwd = resolveDevRepoCwd(command, checkExists, deps);
2393
+ let entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2394
+ if (!checkExists(entryPath) || !checkExists(path.join(repoCwd, ".git"))) {
2395
+ if (command.repoPath) {
2396
+ // Explicit --repo-path didn't have a valid repo — error
2397
+ const message = `no harness repo found at ${repoCwd}. run npm run build first.`;
2398
+ deps.writeStdout(message);
2399
+ return message;
2400
+ }
2401
+ /* v8 ignore start -- auto-clone: interactive prompt + existing repo discovery + real git/npm @preserve */
2402
+ const defaultClonePath = path.join(os.homedir(), "Projects", "ouroboros");
2403
+ if (checkExists(path.join(defaultClonePath, ".git"))) {
2404
+ deps.writeStdout(`found existing repo at ${defaultClonePath}`);
2405
+ try {
2406
+ repoCwd = resolveClonePath({ clonePath: defaultClonePath }, checkExists, deps);
2407
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2408
+ }
2409
+ catch (err) {
2410
+ const message = err instanceof Error ? err.message : String(err);
2411
+ deps.writeStdout(message);
2412
+ return message;
2413
+ }
2414
+ }
2415
+ else if (deps.promptInput) {
2416
+ deps.writeStdout("no harness repo found.");
2417
+ const answer = await deps.promptInput(`already have a checkout? enter its path, or press enter to clone to ${defaultClonePath}: `);
2418
+ const cloneTarget = answer.trim() || defaultClonePath;
2419
+ try {
2420
+ repoCwd = resolveClonePath({ clonePath: cloneTarget }, checkExists, deps);
2421
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2422
+ }
2423
+ catch (err) {
2424
+ const message = err instanceof Error ? err.message : String(err);
2425
+ deps.writeStdout(message);
2426
+ return message;
2427
+ }
2428
+ }
2429
+ else {
2430
+ const message = `no harness repo found. run: ouro dev --repo-path /path/to/ouroboros`;
2431
+ deps.writeStdout(message);
2432
+ return message;
2433
+ }
2434
+ /* v8 ignore stop */
2435
+ }
2436
+ // Auto-build: always rebuild in dev mode so dist/ matches source
2437
+ /* v8 ignore start -- dev auto-build: execSync in repo cwd, tested manually @preserve */
2438
+ deps.writeStdout(`building from ${repoCwd}...`);
2439
+ try {
2440
+ (0, child_process_1.execSync)("npm run build", { cwd: repoCwd, stdio: "inherit" });
2441
+ entryPath = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2442
+ }
2443
+ catch {
2444
+ const message = `build failed in ${repoCwd}. fix compilation errors and retry.`;
2445
+ deps.writeStdout(message);
2446
+ return message;
2447
+ }
2448
+ /* v8 ignore stop */
2449
+ /* v8 ignore start -- defensive: ensureDaemonBootPersistence always injected in tests @preserve */
2450
+ if (deps.ensureDaemonBootPersistence) {
2451
+ try {
2452
+ await Promise.resolve(deps.ensureDaemonBootPersistence(deps.socketPath));
2453
+ /* v8 ignore next -- defensive: boot persistence error should not block dev mode @preserve */
2454
+ }
2455
+ catch (error) {
2456
+ (0, runtime_1.emitNervesEvent)({
2457
+ level: "warn",
2458
+ component: "daemon",
2459
+ event: "daemon.dev_boot_persistence_error",
2460
+ message: "failed to persist daemon boot startup in dev mode",
2461
+ meta: { error: error instanceof Error ? error.message : String(error), socketPath: deps.socketPath },
2462
+ });
2463
+ }
2464
+ /* v8 ignore stop */
2465
+ }
2466
+ // Disable launchd KeepAlive before killing — prevents the installed daemon from respawning
2467
+ /* v8 ignore start -- dev launchd disable: requires real launchctl + plist on disk @preserve */
2468
+ const launchdDevDeps = {
2469
+ exec: (cmd) => { (0, child_process_1.execSync)(cmd); },
2470
+ existsFile: (p) => fs.existsSync(p),
2471
+ removeFile: (p) => { try {
2472
+ fs.unlinkSync(p);
2473
+ }
2474
+ catch { /* best effort */ } },
2475
+ homeDir: os.homedir(),
2476
+ userUid: process.getuid?.() ?? 0,
2477
+ };
2478
+ if ((0, launchd_1.isDaemonInstalled)(launchdDevDeps)) {
2479
+ (0, launchd_1.uninstallLaunchAgent)(launchdDevDeps);
2480
+ deps.writeStdout("disabled launchd auto-restart for dev mode");
2481
+ }
2482
+ /* v8 ignore stop */
2483
+ // Always force-restart in dev mode — you rebuilt, you want this code running
2484
+ /* v8 ignore start -- dev force-restart: socket alive/stop/spawn tested via integration; tests inject mocks @preserve */
2485
+ const alive = await deps.checkSocketAlive(deps.socketPath);
2486
+ if (alive) {
2487
+ try {
2488
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
2489
+ }
2490
+ catch { /* already stopping */ }
2491
+ }
2492
+ deps.cleanupStaleSocket(deps.socketPath);
2493
+ const devEntry = path.join(repoCwd, "dist", "heart", "daemon", "daemon-entry.js");
2494
+ const startDevDaemon = deps.startDaemonProcess === defaultStartDaemonProcess
2495
+ ? async (sp) => {
2496
+ const child = (0, child_process_1.spawn)("node", [devEntry, "--socket", sp], { detached: true, stdio: "ignore" });
2497
+ child.unref();
2498
+ return { pid: child.pid ?? null };
2499
+ }
2500
+ : deps.startDaemonProcess;
2501
+ /* v8 ignore stop */
2502
+ const started = await startDevDaemon(deps.socketPath);
2503
+ /* v8 ignore next -- defensive: pid is null only when spawn fails silently @preserve */
2504
+ const message = `daemon running in dev mode from ${repoCwd} (pid ${started.pid ?? "unknown"})\nrun 'ouro up' to return to production mode`;
2505
+ deps.writeStdout(message);
2506
+ return message;
2507
+ }
2508
+ // ── rollback command (local, no daemon socket needed for symlinks) ──
2509
+ /* v8 ignore start -- rollback/versions: tested via daemon-cli-rollback/versions tests @preserve */
2510
+ if (command.kind === "rollback") {
2511
+ const currentVersion = deps.getCurrentCliVersion?.() ?? "unknown";
2512
+ if (command.version) {
2513
+ // Rollback to a specific version
2514
+ const installed = deps.listCliVersions?.() ?? [];
2515
+ if (!installed.includes(command.version)) {
2516
+ try {
2517
+ await deps.installCliVersion(command.version);
2518
+ }
2519
+ catch (error) {
2520
+ const message = `failed to install version ${command.version}: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
2521
+ deps.writeStdout(message);
2522
+ return message;
2523
+ }
2524
+ }
2525
+ deps.activateCliVersion(command.version);
2526
+ }
2527
+ else {
2528
+ // Rollback to previous version
2529
+ const previousVersion = deps.getPreviousCliVersion?.();
2530
+ if (!previousVersion) {
2531
+ const message = "no previous version to roll back to";
2532
+ deps.writeStdout(message);
2533
+ return message;
2534
+ }
2535
+ deps.activateCliVersion(previousVersion);
2536
+ command = { ...command, version: previousVersion };
2537
+ }
2538
+ // Stop daemon (non-fatal if not running)
2539
+ try {
2540
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
2541
+ }
2542
+ catch {
2543
+ // Daemon may not be running — that's fine
2544
+ }
2545
+ const message = `rolled back to ${command.version} (was ${currentVersion})`;
2546
+ deps.writeStdout(message);
2547
+ return message;
2548
+ }
2549
+ // ── versions command (local install list + published update truth, no daemon socket needed) ──
2550
+ if (command.kind === "versions") {
2551
+ const versions = deps.listCliVersions?.() ?? [];
2552
+ const current = deps.getCurrentCliVersion?.();
2553
+ const previous = deps.getPreviousCliVersion?.();
2554
+ const localSection = versions.length === 0
2555
+ ? "no versions installed"
2556
+ : versions.map((v) => {
2557
+ let line = v;
2558
+ if (v === current)
2559
+ line += " * current";
2560
+ if (v === previous)
2561
+ line += " (previous)";
2562
+ return line;
2563
+ }).join("\n");
2564
+ const sections = [localSection];
2565
+ if (deps.checkForCliUpdate) {
2566
+ try {
2567
+ const updateResult = await deps.checkForCliUpdate();
2568
+ if (updateResult.latestVersion) {
2569
+ sections.push(`published alpha: ${updateResult.latestVersion} (${updateResult.available ? "update available" : "up to date"})`);
2570
+ }
2571
+ else if (updateResult.error) {
2572
+ sections.push(`published alpha: unavailable (${updateResult.error})`);
2573
+ }
2574
+ }
2575
+ catch (err) {
2576
+ const reason = err instanceof Error ? err.message : String(err);
2577
+ sections.push(`published alpha: unavailable (${reason})`);
2578
+ }
2579
+ }
2580
+ const message = sections.join("\n\n");
2581
+ deps.writeStdout(message);
2582
+ return message;
2583
+ }
2584
+ /* v8 ignore stop */
1078
2585
  if (command.kind === "daemon.logs" && deps.tailLogs) {
1079
2586
  deps.tailLogs();
1080
2587
  return "";
1081
2588
  }
1082
- if (command.kind === "friend.link") {
1083
- const linker = deps.linkFriendIdentity ?? defaultLinkFriendIdentity;
1084
- const message = await linker(command);
2589
+ if (command.kind === "outlook") {
2590
+ let status;
2591
+ try {
2592
+ status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
2593
+ /* v8 ignore start — error path: daemon not running */
2594
+ }
2595
+ catch {
2596
+ const message = "daemon unavailable — start with `ouro up` first";
2597
+ deps.writeStdout(message);
2598
+ return message;
2599
+ }
2600
+ /* v8 ignore stop */
2601
+ const payload = parseStatusPayload(status.data);
2602
+ /* v8 ignore start -- ?? branch: outlookUrl always present in test fixtures */
2603
+ const outlookUrl = payload?.overview.outlookUrl ?? "unavailable";
2604
+ /* v8 ignore stop */
2605
+ if (!command.json) {
2606
+ deps.writeStdout(outlookUrl);
2607
+ return outlookUrl;
2608
+ }
2609
+ /* v8 ignore start — error path: outlook URL not available */
2610
+ if (outlookUrl === "unavailable") {
2611
+ deps.writeStdout(outlookUrl);
2612
+ return outlookUrl;
2613
+ }
2614
+ /* v8 ignore stop */
2615
+ /* v8 ignore start -- ?? branch: tests always inject fetchImpl */
2616
+ const fetchImpl = deps.fetchImpl ?? fetch;
2617
+ /* v8 ignore stop */
2618
+ const response = await fetchImpl(`${outlookUrl}/api/machine`);
2619
+ const data = await response.json();
2620
+ const text = JSON.stringify(data, null, 2);
2621
+ deps.writeStdout(text);
2622
+ return text;
2623
+ }
2624
+ // ── hook: handle Claude Code lifecycle hooks ──
2625
+ /* v8 ignore start -- hook handler: reads real stdin, sends to real daemon @preserve */
2626
+ if (command.kind === "hook") {
2627
+ let stdinData = "";
2628
+ try {
2629
+ stdinData = require("fs").readFileSync(0, "utf-8");
2630
+ }
2631
+ catch { /* no stdin */ }
2632
+ let event = {};
2633
+ try {
2634
+ event = JSON.parse(stdinData);
2635
+ }
2636
+ catch { /* malformed */ }
2637
+ const eventType = command.event;
2638
+ const sessionId = event.session_id ?? "unknown";
2639
+ const cwd = event.cwd ?? "";
2640
+ // Build notification content based on event type
2641
+ let content;
2642
+ if (eventType === "session-start") {
2643
+ const model = event.model ?? "";
2644
+ const source = event.source ?? "";
2645
+ content = `[Claude Code session started: ${sessionId}, cwd: ${cwd}${model ? `, model: ${model}` : ""}${source ? `, source: ${source}` : ""}]`;
2646
+ }
2647
+ else if (eventType === "stop") {
2648
+ const lastMsg = event.last_assistant_message ?? "";
2649
+ content = `[Claude Code session ended: ${sessionId}${lastMsg ? `, last: ${lastMsg.slice(0, 200)}` : ""}]`;
2650
+ }
2651
+ else if (eventType === "post-tool-use") {
2652
+ const toolName = event.tool_name ?? "";
2653
+ content = `[Claude Code used ${toolName} in session ${sessionId}]`;
2654
+ }
2655
+ else {
2656
+ content = `[Claude Code hook: ${eventType} in session ${sessionId}]`;
2657
+ }
2658
+ // Send to the specific agent configured for this hook
2659
+ try {
2660
+ await deps.sendCommand(deps.socketPath, { kind: "message.send", from: `claude-code:${sessionId}`, to: command.agent, content }).catch(() => { });
2661
+ await deps.sendCommand(deps.socketPath, { kind: "inner.wake", agent: command.agent }).catch(() => { });
2662
+ }
2663
+ catch { /* daemon not running — silent */ }
2664
+ // Output for Claude Code hook system
2665
+ deps.writeStdout(JSON.stringify({ continue: true }));
2666
+ return JSON.stringify({ continue: true });
2667
+ }
2668
+ /* v8 ignore stop */
2669
+ // ── setup: configure dev tool integration ──
2670
+ if (command.kind === "setup") {
2671
+ const { tool, agent: setupAgent } = command;
2672
+ const sourceRoot = (0, identity_1.getRepoRoot)();
2673
+ const runtimeMode = (0, runtime_mode_1.detectRuntimeMode)(sourceRoot);
2674
+ const mcpServeCommand = runtimeMode === "dev"
2675
+ ? `node ${path.join(sourceRoot, "dist", "heart", "daemon", "ouro-bot-entry.js")} mcp-serve --agent ${setupAgent}`
2676
+ : `ouro mcp-serve --agent ${setupAgent}`;
2677
+ if (tool === "claude-code") {
2678
+ // 1. Register MCP server with Claude Code
2679
+ const mcpAddCmd = `claude mcp add ouro-${setupAgent} -s user -- ${mcpServeCommand}`;
2680
+ (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
2681
+ // 2. Write hooks config to ~/.claude/settings.json
2682
+ const settingsPath = path.join(os.homedir(), ".claude", "settings.json");
2683
+ let settings = {};
2684
+ if (fs.existsSync(settingsPath)) {
2685
+ try {
2686
+ settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8"));
2687
+ }
2688
+ catch { /* start fresh */ }
2689
+ }
2690
+ // Use `ouro hook <event>` — resolves the right code based on dev vs installed mode.
2691
+ // Bare `ouro` works because ouro is on PATH via ~/.ouro-cli/bin/.
2692
+ settings.hooks = {
2693
+ ...(settings.hooks ?? {}),
2694
+ SessionStart: [{ hooks: [{ type: "command", command: `ouro hook session-start --agent ${setupAgent}`, timeout: 5 }] }],
2695
+ Stop: [{ hooks: [{ type: "command", command: `ouro hook stop --agent ${setupAgent}`, timeout: 5 }] }],
2696
+ PostToolUse: [{ matcher: "Bash|Edit|Write", hooks: [{ type: "command", command: `ouro hook post-tool-use --agent ${setupAgent}`, timeout: 5 }] }],
2697
+ };
2698
+ fs.mkdirSync(path.dirname(settingsPath), { recursive: true });
2699
+ fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2));
2700
+ (0, runtime_1.emitNervesEvent)({
2701
+ component: "daemon",
2702
+ event: "daemon.setup_complete",
2703
+ message: "dev tool setup complete",
2704
+ meta: { tool, agent: setupAgent, runtimeMode },
2705
+ });
2706
+ // 3. Write conversation formatting instructions to ~/.claude/CLAUDE.md
2707
+ const claudeMdPath = path.join(os.homedir(), ".claude", "CLAUDE.md");
2708
+ 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`;
2709
+ let existingClaudeMd = "";
2710
+ if (fs.existsSync(claudeMdPath)) {
2711
+ existingClaudeMd = fs.readFileSync(claudeMdPath, "utf-8");
2712
+ }
2713
+ if (!existingClaudeMd.includes("Agent conversations (ouro)")) {
2714
+ fs.writeFileSync(claudeMdPath, existingClaudeMd + agentInstructions);
2715
+ }
2716
+ const message = `setup complete: claude-code + ${setupAgent}\n MCP server registered\n hooks configured\n conversation formatting instructions added`;
2717
+ deps.writeStdout(message);
2718
+ return message;
2719
+ }
2720
+ else {
2721
+ // tool === "codex" (parseSetupCommand validates tool, so this is the only remaining option)
2722
+ const mcpAddCmd = `codex mcp add ouro-${setupAgent} -- ${mcpServeCommand}`;
2723
+ (0, child_process_1.execSync)(mcpAddCmd, { stdio: "pipe" });
2724
+ (0, runtime_1.emitNervesEvent)({
2725
+ component: "daemon",
2726
+ event: "daemon.setup_complete",
2727
+ message: "dev tool setup complete",
2728
+ meta: { tool, agent: setupAgent, runtimeMode },
2729
+ });
2730
+ const message = `setup complete: codex + ${setupAgent}\n MCP server registered`;
2731
+ deps.writeStdout(message);
2732
+ return message;
2733
+ }
2734
+ }
2735
+ /* v8 ignore start — mcp-serve block binds to process.stdin/stdout; tested via mcp-server unit tests */
2736
+ // ── mcp-serve: start MCP server in-process on stdin/stdout ──
2737
+ if (command.kind === "mcp-serve") {
2738
+ const { createMcpServer } = await Promise.resolve().then(() => __importStar(require("./mcp-server")));
2739
+ const friendId = command.friendId ?? `local-${os.userInfo().username}`;
2740
+ const mcpSocketPath = command.socketOverride ?? deps.socketPath;
2741
+ const server = createMcpServer({
2742
+ agent: command.agent,
2743
+ friendId,
2744
+ socketPath: mcpSocketPath,
2745
+ stdin: process.stdin,
2746
+ stdout: process.stdout,
2747
+ });
2748
+ server.start();
2749
+ (0, runtime_1.emitNervesEvent)({
2750
+ component: "daemon",
2751
+ event: "daemon.mcp_serve_started",
2752
+ message: "MCP server started via CLI",
2753
+ meta: { agent: command.agent, friendId },
2754
+ });
2755
+ // Keep process alive until stdin closes
2756
+ await new Promise((resolve) => {
2757
+ process.stdin.on("end", () => {
2758
+ server.stop();
2759
+ resolve();
2760
+ });
2761
+ });
2762
+ return "";
2763
+ }
2764
+ /* v8 ignore stop */
2765
+ // ── mcp subcommands (routed through daemon socket) ──
2766
+ if (command.kind === "mcp.list" || command.kind === "mcp.call") {
2767
+ const daemonCommand = toDaemonCommand(command);
2768
+ let response;
2769
+ try {
2770
+ response = await deps.sendCommand(deps.socketPath, daemonCommand);
2771
+ }
2772
+ catch {
2773
+ const message = "daemon unavailable — start with `ouro up` first";
2774
+ deps.writeStdout(message);
2775
+ return message;
2776
+ }
2777
+ if (!response.ok) {
2778
+ const message = response.error ?? "unknown error";
2779
+ deps.writeStdout(message);
2780
+ return message;
2781
+ }
2782
+ const message = formatMcpResponse(command, response);
2783
+ deps.writeStdout(message);
2784
+ return message;
2785
+ }
2786
+ // ── task subcommands (local, no daemon socket needed) ──
2787
+ if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
2788
+ command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
2789
+ command.kind === "task.sessions" || command.kind === "task.fix") {
2790
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2791
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
2792
+ /* v8 ignore stop */
2793
+ const message = executeTaskCommand(command, taskMod);
2794
+ deps.writeStdout(message);
2795
+ return message;
2796
+ }
2797
+ // ── reminder subcommands (local, no daemon socket needed) ──
2798
+ if (command.kind === "reminder.create") {
2799
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2800
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
2801
+ /* v8 ignore stop */
2802
+ const message = executeReminderCommand(command, taskMod);
2803
+ deps.writeStdout(message);
2804
+ return message;
2805
+ }
2806
+ // ── habit subcommands (local, no daemon socket needed) ──
2807
+ if (command.kind === "habit.list" || command.kind === "habit.create") {
2808
+ const { parseHabitFile, renderHabitFile } = await Promise.resolve().then(() => __importStar(require("./habit-parser")));
2809
+ /* v8 ignore start -- production default: uses real bundle root @preserve */
2810
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
2811
+ const bundleRoot = deps.agentBundleRoot ?? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
2812
+ /* v8 ignore stop */
2813
+ const habitsDir = path.join(bundleRoot, "habits");
2814
+ if (command.kind === "habit.list") {
2815
+ let files;
2816
+ try {
2817
+ files = fs.readdirSync(habitsDir).filter((f) => f.endsWith(".md") && f !== "README.md");
2818
+ }
2819
+ catch {
2820
+ const message = "no habits found";
2821
+ deps.writeStdout(message);
2822
+ return message;
2823
+ }
2824
+ if (files.length === 0) {
2825
+ const message = "no habits found";
2826
+ deps.writeStdout(message);
2827
+ return message;
2828
+ }
2829
+ const lines = [];
2830
+ for (const file of files) {
2831
+ const content = fs.readFileSync(path.join(habitsDir, file), "utf-8");
2832
+ const habit = parseHabitFile(content, path.join(habitsDir, file));
2833
+ const lastRunStr = habit.lastRun ?? "never";
2834
+ lines.push(`${habit.name} cadence=${habit.cadence ?? "none"} status=${habit.status} lastRun=${lastRunStr}`);
2835
+ }
2836
+ const message = lines.join("\n");
2837
+ deps.writeStdout(message);
2838
+ return message;
2839
+ }
2840
+ // habit.create
2841
+ const filePath = path.join(habitsDir, `${command.name}.md`);
2842
+ if (fs.existsSync(filePath)) {
2843
+ const message = `error: habit '${command.name}' already exists`;
2844
+ deps.writeStdout(message);
2845
+ return message;
2846
+ }
2847
+ fs.mkdirSync(habitsDir, { recursive: true });
2848
+ const now = new Date().toISOString();
2849
+ const content = renderHabitFile({
2850
+ title: command.name,
2851
+ cadence: command.cadence ?? "null",
2852
+ status: "active",
2853
+ lastRun: now,
2854
+ created: now,
2855
+ }, `Habit: ${command.name}`);
2856
+ fs.writeFileSync(filePath, content, "utf-8");
2857
+ const message = `created: ${filePath}`;
2858
+ deps.writeStdout(message);
2859
+ return message;
2860
+ }
2861
+ // ── friend subcommands (local, no daemon socket needed) ──
2862
+ if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
2863
+ command.kind === "friend.update" || command.kind === "friend.link" || command.kind === "friend.unlink") {
2864
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
2865
+ let store = deps.friendStore;
2866
+ if (!store) {
2867
+ // Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
2868
+ const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
2869
+ const friendsDir = agentName
2870
+ ? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
2871
+ : path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
2872
+ store = new store_file_1.FileFriendStore(friendsDir);
2873
+ }
2874
+ /* v8 ignore stop */
2875
+ const message = await executeFriendCommand(command, store);
2876
+ deps.writeStdout(message);
2877
+ return message;
2878
+ }
2879
+ // ── auth (local, no daemon socket needed) ──
2880
+ if (command.kind === "auth.run") {
2881
+ const provider = command.provider ?? (0, auth_flow_1.readAgentConfigForAgent)(command.agent).config.humanFacing.provider;
2882
+ /* v8 ignore next -- tests always inject runAuthFlow; default is for production @preserve */
2883
+ const authRunner = deps.runAuthFlow ?? auth_flow_1.runRuntimeAuthFlow;
2884
+ const result = await authRunner({
2885
+ agentName: command.agent,
2886
+ provider,
2887
+ promptInput: deps.promptInput,
2888
+ });
2889
+ // Behavior: ouro auth stores credentials only — does NOT switch provider.
2890
+ // Use `ouro auth switch` to change the active provider.
2891
+ deps.writeStdout(result.message);
2892
+ // Verify the credentials actually work by pinging the provider
2893
+ /* v8 ignore start -- integration: real API ping after auth @preserve */
2894
+ try {
2895
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2896
+ const status = await verifyProviderCredentials(provider, secrets.providers);
2897
+ deps.writeStdout(`${provider}: ${status}`);
2898
+ }
2899
+ catch {
2900
+ // Verification failure is non-blocking — credentials were saved regardless
2901
+ }
2902
+ /* v8 ignore stop */
2903
+ return result.message;
2904
+ }
2905
+ // ── auth verify (local, no daemon socket needed) ──
2906
+ /* v8 ignore start -- auth verify/switch: tested in daemon-cli.test.ts but v8 traces differ in CI @preserve */
2907
+ if (command.kind === "auth.verify") {
2908
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2909
+ const providers = secrets.providers;
2910
+ if (command.provider) {
2911
+ const status = await verifyProviderCredentials(command.provider, providers);
2912
+ const message = `${command.provider}: ${status}`;
2913
+ deps.writeStdout(message);
2914
+ return message;
2915
+ }
2916
+ const lines = [];
2917
+ for (const p of Object.keys(providers)) {
2918
+ const status = await verifyProviderCredentials(p, providers);
2919
+ lines.push(`${p}: ${status}`);
2920
+ }
2921
+ const message = lines.join("\n");
2922
+ deps.writeStdout(message);
2923
+ return message;
2924
+ }
2925
+ // ── auth switch (local, no daemon socket needed) ──
2926
+ if (command.kind === "auth.switch") {
2927
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2928
+ const providerSecrets = secrets.providers[command.provider];
2929
+ if (!providerSecrets || !hasStoredCredentials(command.provider, providerSecrets)) {
2930
+ const message = `no credentials stored for ${command.provider}. Run \`ouro auth --agent ${command.agent} --provider ${command.provider}\` first.`;
2931
+ deps.writeStdout(message);
2932
+ return message;
2933
+ }
2934
+ // Verify credentials actually work before switching
2935
+ const status = await verifyProviderCredentials(command.provider, secrets.providers);
2936
+ if (!status.startsWith("ok")) {
2937
+ const message = `${command.provider}: ${status}. fix credentials with \`ouro auth --agent ${command.agent} --provider ${command.provider}\` before switching.`;
2938
+ deps.writeStdout(message);
2939
+ return message;
2940
+ }
2941
+ if (command.facing) {
2942
+ (0, auth_flow_1.writeAgentProviderSelection)(command.agent, command.facing, command.provider);
2943
+ }
2944
+ else {
2945
+ (0, auth_flow_1.writeAgentProviderSelection)(command.agent, "human", command.provider);
2946
+ (0, auth_flow_1.writeAgentProviderSelection)(command.agent, "agent", command.provider);
2947
+ }
2948
+ const message = `switched ${command.agent} to ${command.provider} (verified working)`;
2949
+ deps.writeStdout(message);
2950
+ return message;
2951
+ }
2952
+ /* v8 ignore stop */
2953
+ // ── config models (local, no daemon socket needed) ──
2954
+ /* v8 ignore start -- config models: tested via daemon-cli.test.ts @preserve */
2955
+ if (command.kind === "config.models") {
2956
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
2957
+ const provider = config.humanFacing.provider;
2958
+ if (provider !== "github-copilot") {
2959
+ const message = `model listing not available for ${provider} — check provider documentation.`;
2960
+ deps.writeStdout(message);
2961
+ return message;
2962
+ }
2963
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2964
+ const ghConfig = secrets.providers["github-copilot"];
2965
+ if (!ghConfig.githubToken || !ghConfig.baseUrl) {
2966
+ throw new Error(`github-copilot credentials not configured. Run \`ouro auth --agent ${command.agent} --provider github-copilot\` first.`);
2967
+ }
2968
+ const fetchFn = deps.fetchImpl ?? fetch;
2969
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
2970
+ if (models.length === 0) {
2971
+ const message = "no models found";
2972
+ deps.writeStdout(message);
2973
+ return message;
2974
+ }
2975
+ const lines = ["available models:"];
2976
+ for (const m of models) {
2977
+ const caps = m.capabilities?.length ? ` (${m.capabilities.join(", ")})` : "";
2978
+ lines.push(` ${m.id}${caps}`);
2979
+ }
2980
+ const message = lines.join("\n");
2981
+ deps.writeStdout(message);
2982
+ return message;
2983
+ }
2984
+ /* v8 ignore stop */
2985
+ // ── config model (local, no daemon socket needed) ──
2986
+ /* v8 ignore start -- config model: tested via daemon-cli.test.ts @preserve */
2987
+ if (command.kind === "config.model") {
2988
+ const facing = command.facing ?? "human";
2989
+ // Validate model availability for github-copilot before writing
2990
+ const { config } = (0, auth_flow_1.readAgentConfigForAgent)(command.agent);
2991
+ const facingConfig = facing === "human" ? config.humanFacing : config.agentFacing;
2992
+ if (facingConfig.provider === "github-copilot") {
2993
+ const { secrets } = (0, auth_flow_1.loadAgentSecrets)(command.agent);
2994
+ const ghConfig = secrets.providers["github-copilot"];
2995
+ if (ghConfig.githubToken && ghConfig.baseUrl) {
2996
+ const fetchFn = deps.fetchImpl ?? fetch;
2997
+ try {
2998
+ const models = await listGithubCopilotModels(ghConfig.baseUrl, ghConfig.githubToken, fetchFn);
2999
+ const available = models.map((m) => m.id);
3000
+ if (available.length > 0 && !available.includes(command.modelName)) {
3001
+ const message = `model '${command.modelName}' not found. available models:\n${available.map((id) => ` ${id}`).join("\n")}`;
3002
+ deps.writeStdout(message);
3003
+ return message;
3004
+ }
3005
+ }
3006
+ catch {
3007
+ // Catalog validation failed — fall through to ping test
3008
+ }
3009
+ // Ping test: verify the model actually works before switching
3010
+ const pingResult = await pingGithubCopilotModel(ghConfig.baseUrl, ghConfig.githubToken, command.modelName, fetchFn);
3011
+ if (!pingResult.ok) {
3012
+ const message = `model '${command.modelName}' ping failed: ${pingResult.error}\nrun \`ouro config models --agent ${command.agent}\` to see available models.`;
3013
+ deps.writeStdout(message);
3014
+ return message;
3015
+ }
3016
+ }
3017
+ }
3018
+ const { provider, previousModel } = (0, auth_flow_1.writeAgentModel)(command.agent, facing, command.modelName);
3019
+ const message = previousModel
3020
+ ? `updated ${command.agent} model on ${provider}: ${previousModel} → ${command.modelName}`
3021
+ : `set ${command.agent} model on ${provider}: ${command.modelName}`;
1085
3022
  deps.writeStdout(message);
1086
3023
  return message;
1087
3024
  }
3025
+ /* v8 ignore stop */
3026
+ // ── whoami (local, no daemon socket needed) ──
3027
+ if (command.kind === "whoami") {
3028
+ if (command.agent) {
3029
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
3030
+ const message = [
3031
+ `agent: ${command.agent}`,
3032
+ `home: ${agentRoot}`,
3033
+ `bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
3034
+ ].join("\n");
3035
+ deps.writeStdout(message);
3036
+ return message;
3037
+ }
3038
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
3039
+ try {
3040
+ const info = deps.whoamiInfo
3041
+ ? deps.whoamiInfo()
3042
+ : {
3043
+ agentName: (0, identity_1.getAgentName)(),
3044
+ homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
3045
+ bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
3046
+ };
3047
+ const message = [
3048
+ `agent: ${info.agentName}`,
3049
+ `home: ${info.homePath}`,
3050
+ `bones: ${info.bonesVersion}`,
3051
+ ].join("\n");
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
+ /* v8 ignore stop */
3061
+ }
3062
+ // ── changelog (local, no daemon socket needed) ──
3063
+ if (command.kind === "changelog") {
3064
+ try {
3065
+ const changelogPath = deps.getChangelogPath
3066
+ ? deps.getChangelogPath()
3067
+ : (0, bundle_manifest_1.getChangelogPath)();
3068
+ const raw = fs.readFileSync(changelogPath, "utf-8");
3069
+ const parsed = JSON.parse(raw);
3070
+ const entries = Array.isArray(parsed) ? parsed : (parsed.versions ?? []);
3071
+ let filtered = entries;
3072
+ if (command.from) {
3073
+ const fromVersion = command.from;
3074
+ filtered = entries.filter((e) => semver.valid(e.version) && semver.gt(e.version, fromVersion));
3075
+ }
3076
+ if (filtered.length === 0) {
3077
+ const message = "no changelog entries found.";
3078
+ deps.writeStdout(message);
3079
+ return message;
3080
+ }
3081
+ const lines = [];
3082
+ for (const entry of filtered) {
3083
+ lines.push(`## ${entry.version}${entry.date ? ` (${entry.date})` : ""}`);
3084
+ if (entry.changes) {
3085
+ for (const change of entry.changes) {
3086
+ lines.push(`- ${change}`);
3087
+ }
3088
+ }
3089
+ lines.push("");
3090
+ }
3091
+ const message = lines.join("\n").trim();
3092
+ deps.writeStdout(message);
3093
+ return message;
3094
+ }
3095
+ catch {
3096
+ const message = "no changelog entries found.";
3097
+ deps.writeStdout(message);
3098
+ return message;
3099
+ }
3100
+ }
3101
+ // ── thoughts (local, no daemon socket needed) ──
3102
+ if (command.kind === "thoughts") {
3103
+ try {
3104
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
3105
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`);
3106
+ const sessionFilePath = (0, thoughts_1.getInnerDialogSessionPath)(agentRoot);
3107
+ if (command.json) {
3108
+ try {
3109
+ const raw = fs.readFileSync(sessionFilePath, "utf-8");
3110
+ deps.writeStdout(raw);
3111
+ return raw;
3112
+ }
3113
+ catch {
3114
+ const message = "no inner dialog session found";
3115
+ deps.writeStdout(message);
3116
+ return message;
3117
+ }
3118
+ }
3119
+ const turns = (0, thoughts_1.parseInnerDialogSession)(sessionFilePath);
3120
+ const message = (0, thoughts_1.formatThoughtTurns)(turns, command.last ?? 10);
3121
+ deps.writeStdout(message);
3122
+ if (command.follow) {
3123
+ deps.writeStdout("\n\n--- following (ctrl+c to stop) ---\n");
3124
+ /* v8 ignore start -- callback tested via followThoughts unit tests @preserve */
3125
+ const stop = (0, thoughts_1.followThoughts)(sessionFilePath, (formatted) => {
3126
+ deps.writeStdout("\n" + formatted);
3127
+ });
3128
+ /* v8 ignore stop */
3129
+ // Block until process exit; cleanup watcher on SIGINT/SIGTERM
3130
+ return new Promise((resolve) => {
3131
+ const cleanup = () => { stop(); resolve(message); };
3132
+ process.once("SIGINT", cleanup);
3133
+ process.once("SIGTERM", cleanup);
3134
+ });
3135
+ }
3136
+ return message;
3137
+ }
3138
+ catch {
3139
+ const message = "error: no agent context — use --agent <name> to specify";
3140
+ deps.writeStdout(message);
3141
+ return message;
3142
+ }
3143
+ }
3144
+ // ── attention queue (local, no daemon socket needed) ──
3145
+ /* v8 ignore start -- CLI attention handler: requires real obligation store on disk @preserve */
3146
+ if (command.kind === "attention.list" || command.kind === "attention.show" || command.kind === "attention.history") {
3147
+ try {
3148
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
3149
+ const { listActiveObligations, readObligation } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
3150
+ if (command.kind === "attention.list") {
3151
+ const obligations = listActiveObligations(agentName);
3152
+ if (obligations.length === 0) {
3153
+ const message = "nothing held — attention queue is empty";
3154
+ deps.writeStdout(message);
3155
+ return message;
3156
+ }
3157
+ 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})`);
3158
+ const message = lines.join("\n");
3159
+ deps.writeStdout(message);
3160
+ return message;
3161
+ }
3162
+ if (command.kind === "attention.show") {
3163
+ const obligation = readObligation(agentName, command.id);
3164
+ if (!obligation) {
3165
+ const message = `no obligation found with id ${command.id}`;
3166
+ deps.writeStdout(message);
3167
+ return message;
3168
+ }
3169
+ const message = JSON.stringify(obligation, null, 2);
3170
+ deps.writeStdout(message);
3171
+ return message;
3172
+ }
3173
+ // attention.history: show returned obligations
3174
+ const { getObligationsDir } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
3175
+ const obligationsDir = getObligationsDir(agentName);
3176
+ let entries = [];
3177
+ try {
3178
+ entries = fs.readdirSync(obligationsDir);
3179
+ }
3180
+ catch { /* empty */ }
3181
+ const returned = entries
3182
+ .filter((e) => e.endsWith(".json"))
3183
+ .map((e) => { try {
3184
+ return JSON.parse(fs.readFileSync(path.join(obligationsDir, e), "utf-8"));
3185
+ }
3186
+ catch {
3187
+ return null;
3188
+ } })
3189
+ .filter((o) => o?.status === "returned")
3190
+ .sort((a, b) => (b.returnedAt ?? 0) - (a.returnedAt ?? 0))
3191
+ .slice(0, 20);
3192
+ if (returned.length === 0) {
3193
+ const message = "no surfacing history yet";
3194
+ deps.writeStdout(message);
3195
+ return message;
3196
+ }
3197
+ const lines = returned.map((o) => {
3198
+ const when = o.returnedAt ? new Date(o.returnedAt).toISOString() : "unknown";
3199
+ return `[${o.id}] → ${o.origin.friendId} via ${o.returnTarget ?? "unknown"} at ${when}`;
3200
+ });
3201
+ const message = lines.join("\n");
3202
+ deps.writeStdout(message);
3203
+ return message;
3204
+ }
3205
+ catch {
3206
+ const message = "error: no agent context — use --agent <name> to specify";
3207
+ deps.writeStdout(message);
3208
+ return message;
3209
+ }
3210
+ }
3211
+ /* v8 ignore stop */
3212
+ // ── inner dialog status (local, no daemon socket needed) ──
3213
+ /* v8 ignore start -- inner status handler: requires real agent state on disk @preserve */
3214
+ if (command.kind === "inner.status") {
3215
+ try {
3216
+ const agentName = command.agent ?? (0, identity_1.getAgentName)();
3217
+ const agentRoot = (0, identity_1.getAgentRoot)(agentName);
3218
+ const { buildInnerStatusOutput } = await Promise.resolve().then(() => __importStar(require("./inner-status")));
3219
+ const { sessionPath: getSessionPath } = await Promise.resolve().then(() => __importStar(require("../config")));
3220
+ const { parseCadenceToMs: parseCadenceMs, DEFAULT_CADENCE_MS } = await Promise.resolve().then(() => __importStar(require("./cadence")));
3221
+ const { parseFrontmatter } = await Promise.resolve().then(() => __importStar(require("../../repertoire/tasks/parser")));
3222
+ const { listActiveObligations } = await Promise.resolve().then(() => __importStar(require("../../mind/obligations")));
3223
+ // Read runtime state
3224
+ const innerSessionPath = getSessionPath("inner-dialog", "inner", "session");
3225
+ const runtimeJsonPath = path.join(path.dirname(innerSessionPath), "runtime.json");
3226
+ let runtimeState = null;
3227
+ try {
3228
+ const raw = fs.readFileSync(runtimeJsonPath, "utf-8");
3229
+ runtimeState = JSON.parse(raw);
3230
+ }
3231
+ catch { /* missing or corrupt — will show "unknown" */ }
3232
+ // Read journal files
3233
+ const journalDir = path.join(agentRoot, "journal");
3234
+ let journalFiles = [];
3235
+ try {
3236
+ const entries = fs.readdirSync(journalDir, { withFileTypes: true });
3237
+ journalFiles = entries
3238
+ .filter((e) => e.isFile() && !e.name.startsWith("."))
3239
+ .map((e) => {
3240
+ const stat = fs.statSync(path.join(journalDir, e.name));
3241
+ return { name: e.name, mtimeMs: stat.mtimeMs };
3242
+ });
3243
+ }
3244
+ catch { /* missing dir — will show (empty) */ }
3245
+ // Read heartbeat cadence
3246
+ let heartbeat = null;
3247
+ try {
3248
+ const habitsDir = path.join(agentRoot, "habits");
3249
+ const heartbeatPath = path.join(habitsDir, "heartbeat.md");
3250
+ let cadenceMs = DEFAULT_CADENCE_MS;
3251
+ if (fs.existsSync(heartbeatPath)) {
3252
+ const content = fs.readFileSync(heartbeatPath, "utf-8");
3253
+ const lines = content.split(/\r?\n/);
3254
+ if (lines[0]?.trim() === "---") {
3255
+ const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
3256
+ if (closing !== -1) {
3257
+ const rawFrontmatter = lines.slice(1, closing).join("\n");
3258
+ const frontmatter = parseFrontmatter(rawFrontmatter);
3259
+ const parsed = parseCadenceMs(frontmatter.cadence);
3260
+ if (parsed !== null)
3261
+ cadenceMs = parsed;
3262
+ }
3263
+ }
3264
+ }
3265
+ let lastCompletedAt = null;
3266
+ if (runtimeState?.lastCompletedAt) {
3267
+ const ms = new Date(runtimeState.lastCompletedAt).getTime();
3268
+ if (!Number.isNaN(ms))
3269
+ lastCompletedAt = ms;
3270
+ }
3271
+ heartbeat = { cadenceMs, lastCompletedAt };
3272
+ }
3273
+ catch { /* no habits — heartbeat unknown */ }
3274
+ // Attention count
3275
+ const activeObligations = listActiveObligations(agentName);
3276
+ const message = buildInnerStatusOutput({
3277
+ agentName,
3278
+ runtimeState,
3279
+ journalFiles,
3280
+ heartbeat,
3281
+ attentionCount: activeObligations.length,
3282
+ now: Date.now(),
3283
+ });
3284
+ deps.writeStdout(message);
3285
+ return message;
3286
+ }
3287
+ catch {
3288
+ const message = "error: no agent context — use --agent <name> to specify";
3289
+ deps.writeStdout(message);
3290
+ return message;
3291
+ }
3292
+ }
3293
+ /* v8 ignore stop */
3294
+ // ── session list (local, no daemon socket needed) ──
3295
+ if (command.kind === "session.list") {
3296
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
3297
+ const scanner = deps.scanSessions ?? (async () => []);
3298
+ /* v8 ignore stop */
3299
+ const sessions = await scanner();
3300
+ if (sessions.length === 0) {
3301
+ const message = "no active sessions";
3302
+ deps.writeStdout(message);
3303
+ return message;
3304
+ }
3305
+ const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
3306
+ const message = lines.join("\n");
3307
+ deps.writeStdout(message);
3308
+ return message;
3309
+ }
3310
+ if (command.kind === "chat.connect" && deps.startChat) {
3311
+ await ensureDaemonRunning(deps);
3312
+ await deps.startChat(command.agent);
3313
+ return "";
3314
+ }
1088
3315
  if (command.kind === "hatch.start") {
1089
- // Route through adoption specialist when no explicit hatch args were provided
3316
+ // Route through serpent guide when no explicit hatch args were provided
1090
3317
  const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
1091
- if (deps.runAdoptionSpecialist && !hasExplicitHatchArgs) {
3318
+ if (deps.runSerpentGuide && !hasExplicitHatchArgs) {
1092
3319
  // System setup first — ouro command, subagents, UTI — before the interactive specialist
1093
3320
  await performSystemSetup(deps);
1094
- const hatchlingName = await deps.runAdoptionSpecialist();
3321
+ const hatchlingName = await deps.runSerpentGuide();
1095
3322
  if (!hatchlingName) {
1096
3323
  return "";
1097
3324
  }
@@ -1133,7 +3360,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
1133
3360
  return message;
1134
3361
  }
1135
3362
  if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
1136
- const message = daemonUnavailableStatusOutput(deps.socketPath);
3363
+ const message = daemonUnavailableStatusOutput(deps.socketPath, deps.healthFilePath);
1137
3364
  deps.writeStdout(message);
1138
3365
  return message;
1139
3366
  }