@ouro.bot/cli 0.1.0-alpha.4 → 0.1.0-alpha.40

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 (82) hide show
  1. package/AdoptionSpecialist.ouro/agent.json +70 -9
  2. package/AdoptionSpecialist.ouro/psyche/SOUL.md +5 -2
  3. package/AdoptionSpecialist.ouro/psyche/identities/monty.md +2 -2
  4. package/README.md +117 -188
  5. package/assets/ouroboros.png +0 -0
  6. package/changelog.json +161 -0
  7. package/dist/heart/config.js +81 -8
  8. package/dist/heart/core.js +78 -45
  9. package/dist/heart/daemon/agent-discovery.js +81 -0
  10. package/dist/heart/daemon/daemon-cli.js +987 -77
  11. package/dist/heart/daemon/daemon-entry.js +14 -5
  12. package/dist/heart/daemon/daemon-runtime-sync.js +90 -0
  13. package/dist/heart/daemon/daemon.js +177 -9
  14. package/dist/heart/daemon/hatch-animation.js +35 -0
  15. package/dist/heart/daemon/hatch-flow.js +4 -20
  16. package/dist/heart/daemon/hooks/bundle-meta.js +92 -0
  17. package/dist/heart/daemon/launchd.js +134 -0
  18. package/dist/heart/daemon/ouro-bot-entry.js +0 -0
  19. package/dist/heart/daemon/ouro-bot-global-installer.js +128 -0
  20. package/dist/heart/daemon/ouro-entry.js +0 -0
  21. package/dist/heart/daemon/ouro-path-installer.js +178 -0
  22. package/dist/heart/daemon/ouro-uti.js +11 -2
  23. package/dist/heart/daemon/process-manager.js +1 -1
  24. package/dist/heart/daemon/run-hooks.js +37 -0
  25. package/dist/heart/daemon/runtime-metadata.js +118 -0
  26. package/dist/heart/daemon/sense-manager.js +266 -0
  27. package/dist/heart/daemon/specialist-orchestrator.js +129 -0
  28. package/dist/heart/daemon/specialist-prompt.js +99 -0
  29. package/dist/heart/daemon/specialist-tools.js +283 -0
  30. package/dist/heart/daemon/staged-restart.js +114 -0
  31. package/dist/heart/daemon/subagent-installer.js +10 -1
  32. package/dist/heart/daemon/update-checker.js +103 -0
  33. package/dist/heart/daemon/update-hooks.js +138 -0
  34. package/dist/heart/daemon/wrapper-publish-guard.js +86 -0
  35. package/dist/heart/identity.js +96 -4
  36. package/dist/heart/kicks.js +1 -19
  37. package/dist/heart/providers/anthropic.js +16 -2
  38. package/dist/heart/sense-truth.js +61 -0
  39. package/dist/heart/streaming.js +96 -21
  40. package/dist/mind/bundle-manifest.js +70 -0
  41. package/dist/mind/context.js +7 -7
  42. package/dist/mind/first-impressions.js +2 -1
  43. package/dist/mind/friends/channel.js +43 -0
  44. package/dist/mind/friends/store-file.js +19 -0
  45. package/dist/mind/friends/types.js +9 -1
  46. package/dist/mind/pending.js +10 -2
  47. package/dist/mind/phrases.js +1 -0
  48. package/dist/mind/prompt.js +222 -7
  49. package/dist/mind/token-estimate.js +8 -12
  50. package/dist/nerves/cli-logging.js +15 -2
  51. package/dist/repertoire/ado-client.js +4 -2
  52. package/dist/repertoire/coding/feedback.js +134 -0
  53. package/dist/repertoire/coding/index.js +4 -1
  54. package/dist/repertoire/coding/manager.js +62 -4
  55. package/dist/repertoire/coding/spawner.js +3 -3
  56. package/dist/repertoire/coding/tools.js +41 -2
  57. package/dist/repertoire/data/ado-endpoints.json +188 -0
  58. package/dist/repertoire/tasks/index.js +2 -9
  59. package/dist/repertoire/tasks/transitions.js +1 -2
  60. package/dist/repertoire/tools-base.js +202 -219
  61. package/dist/repertoire/tools-bluebubbles.js +93 -0
  62. package/dist/repertoire/tools-teams.js +58 -25
  63. package/dist/repertoire/tools.js +55 -35
  64. package/dist/senses/bluebubbles-client.js +434 -0
  65. package/dist/senses/bluebubbles-entry.js +11 -0
  66. package/dist/senses/bluebubbles-media.js +338 -0
  67. package/dist/senses/bluebubbles-model.js +261 -0
  68. package/dist/senses/bluebubbles-mutation-log.js +74 -0
  69. package/dist/senses/bluebubbles-session-cleanup.js +72 -0
  70. package/dist/senses/bluebubbles.js +832 -0
  71. package/dist/senses/cli.js +327 -138
  72. package/dist/senses/debug-activity.js +127 -0
  73. package/dist/senses/inner-dialog.js +99 -54
  74. package/dist/senses/pipeline.js +124 -0
  75. package/dist/senses/teams.js +427 -112
  76. package/dist/senses/trust-gate.js +112 -2
  77. package/package.json +14 -3
  78. package/subagents/README.md +40 -53
  79. package/subagents/work-doer.md +26 -24
  80. package/subagents/work-merger.md +24 -30
  81. package/subagents/work-planner.md +34 -25
  82. package/dist/inner-worker-entry.js +0 -4
@@ -35,9 +35,11 @@ var __importStar = (this && this.__importStar) || (function () {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.ensureDaemonRunning = ensureDaemonRunning;
37
37
  exports.parseOuroCommand = parseOuroCommand;
38
+ exports.discoverExistingCredentials = discoverExistingCredentials;
38
39
  exports.createDefaultOuroCliDeps = createDefaultOuroCliDeps;
39
40
  exports.runOuroCli = runOuroCli;
40
41
  const child_process_1 = require("child_process");
42
+ const crypto_1 = require("crypto");
41
43
  const fs = __importStar(require("fs"));
42
44
  const net = __importStar(require("net"));
43
45
  const os = __importStar(require("os"));
@@ -47,15 +49,175 @@ const runtime_1 = require("../../nerves/runtime");
47
49
  const store_file_1 = require("../../mind/friends/store-file");
48
50
  const types_1 = require("../../mind/friends/types");
49
51
  const ouro_uti_1 = require("./ouro-uti");
52
+ const ouro_path_installer_1 = require("./ouro-path-installer");
50
53
  const subagent_installer_1 = require("./subagent-installer");
51
54
  const hatch_flow_1 = require("./hatch-flow");
55
+ const specialist_orchestrator_1 = require("./specialist-orchestrator");
56
+ const specialist_prompt_1 = require("./specialist-prompt");
57
+ const specialist_tools_1 = require("./specialist-tools");
58
+ const runtime_metadata_1 = require("./runtime-metadata");
59
+ const daemon_runtime_sync_1 = require("./daemon-runtime-sync");
60
+ const agent_discovery_1 = require("./agent-discovery");
61
+ const update_hooks_1 = require("./update-hooks");
62
+ const bundle_meta_1 = require("./hooks/bundle-meta");
63
+ const bundle_manifest_1 = require("../../mind/bundle-manifest");
64
+ const tasks_1 = require("../../repertoire/tasks");
65
+ const ouro_bot_global_installer_1 = require("./ouro-bot-global-installer");
66
+ function stringField(value) {
67
+ return typeof value === "string" ? value : null;
68
+ }
69
+ function numberField(value) {
70
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
71
+ }
72
+ function booleanField(value) {
73
+ return typeof value === "boolean" ? value : null;
74
+ }
75
+ function parseStatusPayload(data) {
76
+ if (!data || typeof data !== "object" || Array.isArray(data))
77
+ return null;
78
+ const raw = data;
79
+ const overview = raw.overview;
80
+ const senses = raw.senses;
81
+ const workers = raw.workers;
82
+ if (!overview || typeof overview !== "object" || Array.isArray(overview))
83
+ return null;
84
+ if (!Array.isArray(senses) || !Array.isArray(workers))
85
+ return null;
86
+ const parsedOverview = {
87
+ daemon: stringField(overview.daemon) ?? "unknown",
88
+ health: stringField(overview.health) ?? "unknown",
89
+ socketPath: stringField(overview.socketPath) ?? "unknown",
90
+ version: stringField(overview.version) ?? "unknown",
91
+ lastUpdated: stringField(overview.lastUpdated) ?? "unknown",
92
+ workerCount: numberField(overview.workerCount) ?? 0,
93
+ senseCount: numberField(overview.senseCount) ?? 0,
94
+ };
95
+ const parsedSenses = senses.map((entry) => {
96
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
97
+ return null;
98
+ const row = entry;
99
+ const agent = stringField(row.agent);
100
+ const sense = stringField(row.sense);
101
+ const status = stringField(row.status);
102
+ const detail = stringField(row.detail);
103
+ const enabled = booleanField(row.enabled);
104
+ if (!agent || !sense || !status || detail === null || enabled === null)
105
+ return null;
106
+ return {
107
+ agent,
108
+ sense,
109
+ label: stringField(row.label) ?? undefined,
110
+ enabled,
111
+ status,
112
+ detail,
113
+ };
114
+ });
115
+ const parsedWorkers = workers.map((entry) => {
116
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
117
+ return null;
118
+ const row = entry;
119
+ const agent = stringField(row.agent);
120
+ const worker = stringField(row.worker);
121
+ const status = stringField(row.status);
122
+ const restartCount = numberField(row.restartCount);
123
+ const hasPid = Object.prototype.hasOwnProperty.call(row, "pid");
124
+ const pid = row.pid === null ? null : numberField(row.pid);
125
+ const pidInvalid = !hasPid || (row.pid !== null && pid === null);
126
+ if (!agent || !worker || !status || restartCount === null || pidInvalid)
127
+ return null;
128
+ return {
129
+ agent,
130
+ worker,
131
+ status,
132
+ pid,
133
+ restartCount,
134
+ };
135
+ });
136
+ if (parsedSenses.some((row) => row === null) || parsedWorkers.some((row) => row === null))
137
+ return null;
138
+ return {
139
+ overview: parsedOverview,
140
+ senses: parsedSenses,
141
+ workers: parsedWorkers,
142
+ };
143
+ }
144
+ function humanizeSenseName(sense, label) {
145
+ if (label)
146
+ return label;
147
+ if (sense === "cli")
148
+ return "CLI";
149
+ if (sense === "bluebubbles")
150
+ return "BlueBubbles";
151
+ if (sense === "teams")
152
+ return "Teams";
153
+ return sense;
154
+ }
155
+ function formatTable(headers, rows) {
156
+ const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
157
+ const renderRow = (row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
158
+ const divider = `|-${widths.map((width) => "-".repeat(width)).join("-|-")}-|`;
159
+ return [
160
+ renderRow(headers),
161
+ divider,
162
+ ...rows.map(renderRow),
163
+ ].join("\n");
164
+ }
165
+ function formatDaemonStatusOutput(response, fallback) {
166
+ const payload = parseStatusPayload(response.data);
167
+ if (!payload)
168
+ return fallback;
169
+ const overviewRows = [
170
+ ["Daemon", payload.overview.daemon],
171
+ ["Socket", payload.overview.socketPath],
172
+ ["Version", payload.overview.version],
173
+ ["Last Updated", payload.overview.lastUpdated],
174
+ ["Workers", String(payload.overview.workerCount)],
175
+ ["Senses", String(payload.overview.senseCount)],
176
+ ["Health", payload.overview.health],
177
+ ];
178
+ const senseRows = payload.senses.map((row) => [
179
+ row.agent,
180
+ humanizeSenseName(row.sense, row.label),
181
+ row.enabled ? "ON" : "OFF",
182
+ row.status,
183
+ row.detail,
184
+ ]);
185
+ const workerRows = payload.workers.map((row) => [
186
+ row.agent,
187
+ row.worker,
188
+ row.status,
189
+ row.pid === null ? "n/a" : String(row.pid),
190
+ String(row.restartCount),
191
+ ]);
192
+ return [
193
+ "Overview",
194
+ formatTable(["Item", "Value"], overviewRows),
195
+ "",
196
+ "Senses",
197
+ formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
198
+ "",
199
+ "Workers",
200
+ formatTable(["Agent", "Worker", "State", "PID", "Restarts"], workerRows),
201
+ ].join("\n");
202
+ }
52
203
  async function ensureDaemonRunning(deps) {
53
204
  const alive = await deps.checkSocketAlive(deps.socketPath);
54
205
  if (alive) {
55
- return {
56
- alreadyRunning: true,
57
- message: `daemon already running (${deps.socketPath})`,
58
- };
206
+ const localRuntime = (0, runtime_metadata_1.getRuntimeMetadata)();
207
+ return (0, daemon_runtime_sync_1.ensureCurrentDaemonRuntime)({
208
+ socketPath: deps.socketPath,
209
+ localVersion: localRuntime.version,
210
+ fetchRunningVersion: async () => {
211
+ const status = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
212
+ const payload = parseStatusPayload(status.data);
213
+ return payload?.overview.version ?? "unknown";
214
+ },
215
+ stopDaemon: async () => {
216
+ await deps.sendCommand(deps.socketPath, { kind: "daemon.stop" });
217
+ },
218
+ cleanupStaleSocket: deps.cleanupStaleSocket,
219
+ startDaemonProcess: deps.startDaemonProcess,
220
+ });
59
221
  }
60
222
  deps.cleanupStaleSocket(deps.socketPath);
61
223
  const started = await deps.startDaemonProcess(deps.socketPath);
@@ -64,17 +226,79 @@ async function ensureDaemonRunning(deps) {
64
226
  message: `daemon started (pid ${started.pid ?? "unknown"})`,
65
227
  };
66
228
  }
229
+ /**
230
+ * Extract `--agent <name>` from an args array, returning the agent name and
231
+ * the remaining args with the flag pair removed.
232
+ */
233
+ function extractAgentFlag(args) {
234
+ const idx = args.indexOf("--agent");
235
+ if (idx === -1 || idx + 1 >= args.length)
236
+ return { rest: args };
237
+ const agent = args[idx + 1];
238
+ const rest = [...args.slice(0, idx), ...args.slice(idx + 2)];
239
+ return { agent, rest };
240
+ }
67
241
  function usage() {
68
242
  return [
69
243
  "Usage:",
70
244
  " ouro [up]",
71
- " ouro stop|status|logs|hatch",
245
+ " ouro stop|down|status|logs|hatch",
246
+ " ouro -v|--version",
72
247
  " ouro chat <agent>",
73
248
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
74
249
  " ouro poke <agent> --task <task-id>",
75
250
  " ouro link <agent> --friend <id> --provider <provider> --external-id <external-id>",
251
+ " ouro task board [<status>] [--agent <name>]",
252
+ " ouro task create <title> [--type <type>] [--agent <name>]",
253
+ " ouro task update <id> <status> [--agent <name>]",
254
+ " ouro task show <id> [--agent <name>]",
255
+ " ouro task actionable|deps|sessions [--agent <name>]",
256
+ " ouro reminder create <title> --body <body> [--at <iso>] [--cadence <interval>] [--category <category>] [--agent <name>]",
257
+ " ouro friend list [--agent <name>]",
258
+ " ouro friend show <id> [--agent <name>]",
259
+ " ouro friend create --name <name> [--trust <level>] [--agent <name>]",
260
+ " ouro friend link <agent> --friend <id> --provider <p> --external-id <eid>",
261
+ " ouro friend unlink <agent> --friend <id> --provider <p> --external-id <eid>",
262
+ " ouro whoami [--agent <name>]",
263
+ " ouro session list [--agent <name>]",
76
264
  ].join("\n");
77
265
  }
266
+ function formatVersionOutput() {
267
+ return (0, runtime_metadata_1.getRuntimeMetadata)().version;
268
+ }
269
+ function buildStoppedStatusPayload(socketPath) {
270
+ const metadata = (0, runtime_metadata_1.getRuntimeMetadata)();
271
+ return {
272
+ overview: {
273
+ daemon: "stopped",
274
+ health: "warn",
275
+ socketPath,
276
+ version: metadata.version,
277
+ lastUpdated: metadata.lastUpdated,
278
+ workerCount: 0,
279
+ senseCount: 0,
280
+ },
281
+ senses: [],
282
+ workers: [],
283
+ };
284
+ }
285
+ function daemonUnavailableStatusOutput(socketPath) {
286
+ return [
287
+ formatDaemonStatusOutput({
288
+ ok: true,
289
+ summary: "daemon not running",
290
+ data: buildStoppedStatusPayload(socketPath),
291
+ }, "daemon not running"),
292
+ "",
293
+ "daemon not running; run `ouro up`",
294
+ ].join("\n");
295
+ }
296
+ function isDaemonUnavailableError(error) {
297
+ const code = typeof error === "object" && error !== null && "code" in error
298
+ ? String(error.code ?? "")
299
+ : "";
300
+ return code === "ENOENT" || code === "ECONNREFUSED";
301
+ }
78
302
  function parseMessageCommand(args) {
79
303
  let to;
80
304
  let sessionId;
@@ -126,7 +350,7 @@ function parsePokeCommand(args) {
126
350
  throw new Error(`Usage\n${usage()}`);
127
351
  return { kind: "task.poke", agent, taskId };
128
352
  }
129
- function parseLinkCommand(args) {
353
+ function parseLinkCommand(args, kind = "friend.link") {
130
354
  const agent = args[0];
131
355
  if (!agent)
132
356
  throw new Error(`Usage\n${usage()}`);
@@ -158,7 +382,7 @@ function parseLinkCommand(args) {
158
382
  throw new Error(`Unknown identity provider '${providerRaw}'. Use aad|local|teams-conversation.`);
159
383
  }
160
384
  return {
161
- kind: "friend.link",
385
+ kind,
162
386
  agent,
163
387
  friendId,
164
388
  provider: providerRaw,
@@ -235,13 +459,157 @@ function parseHatchCommand(args) {
235
459
  migrationPath,
236
460
  };
237
461
  }
462
+ function parseTaskCommand(args) {
463
+ const { agent, rest: cleaned } = extractAgentFlag(args);
464
+ const [sub, ...rest] = cleaned;
465
+ if (!sub)
466
+ throw new Error(`Usage\n${usage()}`);
467
+ if (sub === "board") {
468
+ const status = rest[0];
469
+ return status
470
+ ? { kind: "task.board", status, ...(agent ? { agent } : {}) }
471
+ : { kind: "task.board", ...(agent ? { agent } : {}) };
472
+ }
473
+ if (sub === "create") {
474
+ const title = rest[0];
475
+ if (!title)
476
+ throw new Error(`Usage\n${usage()}`);
477
+ let type;
478
+ for (let i = 1; i < rest.length; i++) {
479
+ if (rest[i] === "--type" && rest[i + 1]) {
480
+ type = rest[i + 1];
481
+ i += 1;
482
+ }
483
+ }
484
+ return type
485
+ ? { kind: "task.create", title, type, ...(agent ? { agent } : {}) }
486
+ : { kind: "task.create", title, ...(agent ? { agent } : {}) };
487
+ }
488
+ if (sub === "update") {
489
+ const id = rest[0];
490
+ const status = rest[1];
491
+ if (!id || !status)
492
+ throw new Error(`Usage\n${usage()}`);
493
+ return { kind: "task.update", id, status, ...(agent ? { agent } : {}) };
494
+ }
495
+ if (sub === "show") {
496
+ const id = rest[0];
497
+ if (!id)
498
+ throw new Error(`Usage\n${usage()}`);
499
+ return { kind: "task.show", id, ...(agent ? { agent } : {}) };
500
+ }
501
+ if (sub === "actionable")
502
+ return { kind: "task.actionable", ...(agent ? { agent } : {}) };
503
+ if (sub === "deps")
504
+ return { kind: "task.deps", ...(agent ? { agent } : {}) };
505
+ if (sub === "sessions")
506
+ return { kind: "task.sessions", ...(agent ? { agent } : {}) };
507
+ throw new Error(`Usage\n${usage()}`);
508
+ }
509
+ function parseReminderCommand(args) {
510
+ const { agent, rest: cleaned } = extractAgentFlag(args);
511
+ const [sub, ...rest] = cleaned;
512
+ if (!sub)
513
+ throw new Error(`Usage\n${usage()}`);
514
+ if (sub === "create") {
515
+ const title = rest[0];
516
+ if (!title)
517
+ throw new Error(`Usage\n${usage()}`);
518
+ let body;
519
+ let scheduledAt;
520
+ let cadence;
521
+ let category;
522
+ for (let i = 1; i < rest.length; i++) {
523
+ if (rest[i] === "--body" && rest[i + 1]) {
524
+ body = rest[i + 1];
525
+ i += 1;
526
+ }
527
+ else if (rest[i] === "--at" && rest[i + 1]) {
528
+ scheduledAt = rest[i + 1];
529
+ i += 1;
530
+ }
531
+ else if (rest[i] === "--cadence" && rest[i + 1]) {
532
+ cadence = rest[i + 1];
533
+ i += 1;
534
+ }
535
+ else if (rest[i] === "--category" && rest[i + 1]) {
536
+ category = rest[i + 1];
537
+ i += 1;
538
+ }
539
+ }
540
+ if (!body)
541
+ throw new Error(`Usage\n${usage()}`);
542
+ if (!scheduledAt && !cadence)
543
+ throw new Error(`Usage\n${usage()}`);
544
+ return {
545
+ kind: "reminder.create",
546
+ title,
547
+ body,
548
+ ...(scheduledAt ? { scheduledAt } : {}),
549
+ ...(cadence ? { cadence } : {}),
550
+ ...(category ? { category } : {}),
551
+ ...(agent ? { agent } : {}),
552
+ };
553
+ }
554
+ throw new Error(`Usage\n${usage()}`);
555
+ }
556
+ function parseSessionCommand(args) {
557
+ const { agent, rest: cleaned } = extractAgentFlag(args);
558
+ const [sub] = cleaned;
559
+ if (!sub)
560
+ throw new Error(`Usage\n${usage()}`);
561
+ if (sub === "list")
562
+ return { kind: "session.list", ...(agent ? { agent } : {}) };
563
+ throw new Error(`Usage\n${usage()}`);
564
+ }
565
+ function parseFriendCommand(args) {
566
+ const { agent, rest: cleaned } = extractAgentFlag(args);
567
+ const [sub, ...rest] = cleaned;
568
+ if (!sub)
569
+ throw new Error(`Usage\n${usage()}`);
570
+ if (sub === "list")
571
+ return { kind: "friend.list", ...(agent ? { agent } : {}) };
572
+ if (sub === "show") {
573
+ const friendId = rest[0];
574
+ if (!friendId)
575
+ throw new Error(`Usage\n${usage()}`);
576
+ return { kind: "friend.show", friendId, ...(agent ? { agent } : {}) };
577
+ }
578
+ if (sub === "create") {
579
+ let name;
580
+ let trustLevel;
581
+ for (let i = 0; i < rest.length; i++) {
582
+ if (rest[i] === "--name" && rest[i + 1]) {
583
+ name = rest[i + 1];
584
+ i += 1;
585
+ }
586
+ else if (rest[i] === "--trust" && rest[i + 1]) {
587
+ trustLevel = rest[i + 1];
588
+ i += 1;
589
+ }
590
+ }
591
+ if (!name)
592
+ throw new Error(`Usage\n${usage()}`);
593
+ return {
594
+ kind: "friend.create",
595
+ name,
596
+ ...(trustLevel ? { trustLevel } : {}),
597
+ ...(agent ? { agent } : {}),
598
+ };
599
+ }
600
+ if (sub === "link")
601
+ return parseLinkCommand(rest, "friend.link");
602
+ if (sub === "unlink")
603
+ return parseLinkCommand(rest, "friend.unlink");
604
+ throw new Error(`Usage\n${usage()}`);
605
+ }
238
606
  function parseOuroCommand(args) {
239
607
  const [head, second] = args;
240
608
  if (!head)
241
609
  return { kind: "daemon.up" };
242
610
  if (head === "up")
243
611
  return { kind: "daemon.up" };
244
- if (head === "stop")
612
+ if (head === "stop" || head === "down")
245
613
  return { kind: "daemon.stop" };
246
614
  if (head === "status")
247
615
  return { kind: "daemon.status" };
@@ -249,6 +617,18 @@ function parseOuroCommand(args) {
249
617
  return { kind: "daemon.logs" };
250
618
  if (head === "hatch")
251
619
  return parseHatchCommand(args.slice(1));
620
+ if (head === "task")
621
+ return parseTaskCommand(args.slice(1));
622
+ if (head === "reminder")
623
+ return parseReminderCommand(args.slice(1));
624
+ if (head === "friend")
625
+ return parseFriendCommand(args.slice(1));
626
+ if (head === "whoami") {
627
+ const { agent } = extractAgentFlag(args.slice(1));
628
+ return { kind: "whoami", ...(agent ? { agent } : {}) };
629
+ }
630
+ if (head === "session")
631
+ return parseSessionCommand(args.slice(1));
252
632
  if (head === "chat") {
253
633
  if (!second)
254
634
  throw new Error(`Usage\n${usage()}`);
@@ -401,62 +781,253 @@ async function defaultPromptInput(question) {
401
781
  }
402
782
  }
403
783
  function defaultListDiscoveredAgents() {
404
- const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
784
+ return (0, agent_discovery_1.listEnabledBundleAgents)({
785
+ bundlesRoot: (0, identity_1.getAgentBundlesRoot)(),
786
+ readdirSync: fs.readdirSync,
787
+ readFileSync: fs.readFileSync,
788
+ });
789
+ }
790
+ function discoverExistingCredentials(secretsRoot) {
791
+ const found = [];
405
792
  let entries;
406
793
  try {
407
- entries = fs.readdirSync(bundlesRoot, { withFileTypes: true });
794
+ entries = fs.readdirSync(secretsRoot, { withFileTypes: true });
408
795
  }
409
796
  catch {
410
- return [];
797
+ return found;
411
798
  }
412
- const discovered = [];
413
799
  for (const entry of entries) {
414
- if (!entry.isDirectory() || !entry.name.endsWith(".ouro"))
800
+ if (!entry.isDirectory())
415
801
  continue;
416
- const agentName = entry.name.slice(0, -5);
417
- const configPath = path.join(bundlesRoot, entry.name, "agent.json");
418
- let enabled = true;
802
+ const secretsPath = path.join(secretsRoot, entry.name, "secrets.json");
803
+ let raw;
419
804
  try {
420
- const raw = fs.readFileSync(configPath, "utf-8");
421
- const parsed = JSON.parse(raw);
422
- if (typeof parsed.enabled === "boolean") {
423
- enabled = parsed.enabled;
424
- }
805
+ raw = fs.readFileSync(secretsPath, "utf-8");
425
806
  }
426
807
  catch {
427
808
  continue;
428
809
  }
429
- if (enabled) {
430
- discovered.push(agentName);
810
+ let parsed;
811
+ try {
812
+ parsed = JSON.parse(raw);
813
+ }
814
+ catch {
815
+ continue;
816
+ }
817
+ if (!parsed.providers)
818
+ continue;
819
+ for (const [provName, provConfig] of Object.entries(parsed.providers)) {
820
+ if (provName === "anthropic" && provConfig.setupToken) {
821
+ found.push({ agentName: entry.name, provider: "anthropic", credentials: { setupToken: provConfig.setupToken }, providerConfig: { ...provConfig } });
822
+ }
823
+ else if (provName === "openai-codex" && provConfig.oauthAccessToken) {
824
+ found.push({ agentName: entry.name, provider: "openai-codex", credentials: { oauthAccessToken: provConfig.oauthAccessToken }, providerConfig: { ...provConfig } });
825
+ }
826
+ else if (provName === "minimax" && provConfig.apiKey) {
827
+ found.push({ agentName: entry.name, provider: "minimax", credentials: { apiKey: provConfig.apiKey }, providerConfig: { ...provConfig } });
828
+ }
829
+ else if (provName === "azure" && provConfig.apiKey && provConfig.endpoint && provConfig.deployment) {
830
+ found.push({ agentName: entry.name, provider: "azure", credentials: { apiKey: provConfig.apiKey, endpoint: provConfig.endpoint, deployment: provConfig.deployment }, providerConfig: { ...provConfig } });
831
+ }
431
832
  }
432
833
  }
433
- return discovered.sort((left, right) => left.localeCompare(right));
834
+ // Deduplicate by provider+credential value (keep first seen)
835
+ const seen = new Set();
836
+ return found.filter((cred) => {
837
+ const key = `${cred.provider}:${JSON.stringify(cred.credentials)}`;
838
+ if (seen.has(key))
839
+ return false;
840
+ seen.add(key);
841
+ return true;
842
+ });
434
843
  }
435
- async function defaultLinkFriendIdentity(command) {
436
- const friendStore = new store_file_1.FileFriendStore(path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`, "friends"));
437
- const current = await friendStore.get(command.friendId);
438
- if (!current) {
439
- return `friend not found: ${command.friendId}`;
844
+ /* v8 ignore start -- integration: interactive terminal specialist session @preserve */
845
+ async function defaultRunAdoptionSpecialist() {
846
+ const { runCliSession } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
847
+ const { patchRuntimeConfig } = await Promise.resolve().then(() => __importStar(require("../config")));
848
+ const { setAgentName, setAgentConfigOverride } = await Promise.resolve().then(() => __importStar(require("../identity")));
849
+ const readlinePromises = await Promise.resolve().then(() => __importStar(require("readline/promises")));
850
+ const crypto = await Promise.resolve().then(() => __importStar(require("crypto")));
851
+ // Phase 1: cold CLI — collect provider/credentials with a simple readline
852
+ const coldRl = readlinePromises.createInterface({ input: process.stdin, output: process.stdout });
853
+ const coldPrompt = async (q) => {
854
+ const answer = await coldRl.question(q);
855
+ return answer.trim();
856
+ };
857
+ let providerRaw;
858
+ let credentials = {};
859
+ let providerConfig = {};
860
+ const tempDir = path.join(os.tmpdir(), `ouro-hatch-${crypto.randomUUID()}`);
861
+ try {
862
+ const secretsRoot = path.join(os.homedir(), ".agentsecrets");
863
+ const discovered = discoverExistingCredentials(secretsRoot);
864
+ const existingBundleCount = (0, specialist_orchestrator_1.listExistingBundles)((0, identity_1.getAgentBundlesRoot)()).length;
865
+ const hatchVerb = existingBundleCount > 0 ? "let's hatch a new agent." : "let's hatch your first agent.";
866
+ // Default models per provider (used when entering new credentials)
867
+ const defaultModels = {
868
+ anthropic: "claude-opus-4-6",
869
+ minimax: "MiniMax-Text-01",
870
+ "openai-codex": "gpt-5.4",
871
+ azure: "",
872
+ };
873
+ if (discovered.length > 0) {
874
+ process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
875
+ process.stdout.write("i found existing API credentials:\n\n");
876
+ const unique = [...new Map(discovered.map((d) => [`${d.provider}`, d])).values()];
877
+ for (let i = 0; i < unique.length; i++) {
878
+ const model = unique[i].providerConfig.model || unique[i].providerConfig.deployment || "";
879
+ const modelLabel = model ? `, ${model}` : "";
880
+ process.stdout.write(` ${i + 1}. ${unique[i].provider}${modelLabel} (from ${unique[i].agentName})\n`);
881
+ }
882
+ process.stdout.write("\n");
883
+ const choice = await coldPrompt("use one of these? enter number, or 'new' for a different key: ");
884
+ const idx = parseInt(choice, 10) - 1;
885
+ if (idx >= 0 && idx < unique.length) {
886
+ providerRaw = unique[idx].provider;
887
+ credentials = unique[idx].credentials;
888
+ providerConfig = unique[idx].providerConfig;
889
+ }
890
+ else {
891
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
892
+ if (!isAgentProvider(pRaw)) {
893
+ process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
894
+ coldRl.close();
895
+ return null;
896
+ }
897
+ providerRaw = pRaw;
898
+ providerConfig = { model: defaultModels[providerRaw] };
899
+ if (providerRaw === "anthropic")
900
+ credentials.setupToken = await coldPrompt("API key: ");
901
+ if (providerRaw === "openai-codex")
902
+ credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
903
+ if (providerRaw === "minimax")
904
+ credentials.apiKey = await coldPrompt("API key: ");
905
+ if (providerRaw === "azure") {
906
+ credentials.apiKey = await coldPrompt("API key: ");
907
+ credentials.endpoint = await coldPrompt("endpoint: ");
908
+ credentials.deployment = await coldPrompt("deployment: ");
909
+ }
910
+ }
911
+ }
912
+ else {
913
+ process.stdout.write(`\n\ud83d\udc0d welcome to ouroboros! ${hatchVerb}\n`);
914
+ process.stdout.write("i need an API key to power our conversation.\n\n");
915
+ const pRaw = await coldPrompt("provider (anthropic/azure/minimax/openai-codex): ");
916
+ if (!isAgentProvider(pRaw)) {
917
+ process.stdout.write("unknown provider. run `ouro hatch` to try again.\n");
918
+ coldRl.close();
919
+ return null;
920
+ }
921
+ providerRaw = pRaw;
922
+ providerConfig = { model: defaultModels[providerRaw] };
923
+ if (providerRaw === "anthropic")
924
+ credentials.setupToken = await coldPrompt("API key: ");
925
+ if (providerRaw === "openai-codex")
926
+ credentials.oauthAccessToken = await coldPrompt("OAuth token: ");
927
+ if (providerRaw === "minimax")
928
+ credentials.apiKey = await coldPrompt("API key: ");
929
+ if (providerRaw === "azure") {
930
+ credentials.apiKey = await coldPrompt("API key: ");
931
+ credentials.endpoint = await coldPrompt("endpoint: ");
932
+ credentials.deployment = await coldPrompt("deployment: ");
933
+ }
934
+ }
935
+ coldRl.close();
936
+ process.stdout.write("\n");
937
+ // Phase 2: configure runtime for adoption specialist
938
+ const bundleSourceDir = path.resolve(__dirname, "..", "..", "..", "AdoptionSpecialist.ouro");
939
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
940
+ const secretsRoot2 = path.join(os.homedir(), ".agentsecrets");
941
+ // Suppress non-critical log noise during adoption (no secrets.json, etc.)
942
+ const { setRuntimeLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
943
+ const { createLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves")));
944
+ setRuntimeLogger(createLogger({ level: "error" }));
945
+ // Configure runtime: set agent identity + config override so runAgent
946
+ // doesn't try to read from ~/AgentBundles/AdoptionSpecialist.ouro/
947
+ setAgentName("AdoptionSpecialist");
948
+ // Build specialist system prompt
949
+ const soulText = (0, specialist_orchestrator_1.loadSoulText)(bundleSourceDir);
950
+ const identitiesDir = path.join(bundleSourceDir, "psyche", "identities");
951
+ const identity = (0, specialist_orchestrator_1.pickRandomIdentity)(identitiesDir);
952
+ // Load identity-specific spinner phrases (falls back to DEFAULT_AGENT_PHRASES)
953
+ const { loadIdentityPhrases } = await Promise.resolve().then(() => __importStar(require("./specialist-orchestrator")));
954
+ const phrases = loadIdentityPhrases(bundleSourceDir, identity.fileName);
955
+ setAgentConfigOverride({
956
+ version: 1,
957
+ enabled: true,
958
+ provider: providerRaw,
959
+ phrases,
960
+ });
961
+ patchRuntimeConfig({
962
+ providers: {
963
+ [providerRaw]: { ...providerConfig, ...credentials },
964
+ },
965
+ });
966
+ const existingBundles = (0, specialist_orchestrator_1.listExistingBundles)(bundlesRoot);
967
+ const systemPrompt = (0, specialist_prompt_1.buildSpecialistSystemPrompt)(soulText, identity.content, existingBundles, {
968
+ tempDir,
969
+ provider: providerRaw,
970
+ });
971
+ // Build specialist tools
972
+ const specialistTools = (0, specialist_tools_1.getSpecialistTools)();
973
+ const specialistExecTool = (0, specialist_tools_1.createSpecialistExecTool)({
974
+ tempDir,
975
+ credentials,
976
+ provider: providerRaw,
977
+ bundlesRoot,
978
+ secretsRoot: secretsRoot2,
979
+ animationWriter: (text) => process.stdout.write(text),
980
+ });
981
+ // Run the adoption specialist session via runCliSession
982
+ const result = await runCliSession({
983
+ agentName: "AdoptionSpecialist",
984
+ tools: specialistTools,
985
+ execTool: specialistExecTool,
986
+ exitOnToolCall: "complete_adoption",
987
+ autoFirstTurn: true,
988
+ banner: false,
989
+ disableCommands: true,
990
+ skipSystemPromptRefresh: true,
991
+ messages: [
992
+ { role: "system", content: systemPrompt },
993
+ { role: "user", content: "hi" },
994
+ ],
995
+ });
996
+ if (result.exitReason === "tool_exit" && result.toolResult) {
997
+ const parsed = typeof result.toolResult === "string" ? JSON.parse(result.toolResult) : result.toolResult;
998
+ if (parsed.success && parsed.agentName) {
999
+ return parsed.agentName;
1000
+ }
1001
+ }
1002
+ return null;
440
1003
  }
441
- const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
442
- if (alreadyLinked) {
443
- return `identity already linked: ${command.provider}:${command.externalId}`;
1004
+ catch (err) {
1005
+ process.stderr.write(`\nouro adoption error: ${err instanceof Error ? err.stack ?? err.message : String(err)}\n`);
1006
+ coldRl.close();
1007
+ return null;
1008
+ }
1009
+ finally {
1010
+ // Clear specialist config/identity so the hatched agent gets its own
1011
+ setAgentConfigOverride(null);
1012
+ const { resetProviderRuntime } = await Promise.resolve().then(() => __importStar(require("../core")));
1013
+ resetProviderRuntime();
1014
+ const { resetConfigCache } = await Promise.resolve().then(() => __importStar(require("../config")));
1015
+ resetConfigCache();
1016
+ // Restore default logging
1017
+ const { setRuntimeLogger: restoreLogger } = await Promise.resolve().then(() => __importStar(require("../../nerves/runtime")));
1018
+ restoreLogger(null);
1019
+ // Clean up temp dir if it still exists
1020
+ try {
1021
+ if (fs.existsSync(tempDir)) {
1022
+ fs.rmSync(tempDir, { recursive: true, force: true });
1023
+ }
1024
+ }
1025
+ catch {
1026
+ // Best effort cleanup
1027
+ }
444
1028
  }
445
- const now = new Date().toISOString();
446
- await friendStore.put(command.friendId, {
447
- ...current,
448
- externalIds: [
449
- ...current.externalIds,
450
- {
451
- provider: command.provider,
452
- externalId: command.externalId,
453
- linkedAt: now,
454
- },
455
- ],
456
- updatedAt: now,
457
- });
458
- return `linked ${command.provider}:${command.externalId} to ${command.friendId}`;
459
1029
  }
1030
+ /* v8 ignore stop */
460
1031
  function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
461
1032
  return {
462
1033
  socketPath,
@@ -467,11 +1038,13 @@ function createDefaultOuroCliDeps(socketPath = "/tmp/ouroboros-daemon.sock") {
467
1038
  cleanupStaleSocket: defaultCleanupStaleSocket,
468
1039
  fallbackPendingMessage: defaultFallbackPendingMessage,
469
1040
  installSubagents: defaultInstallSubagents,
470
- linkFriendIdentity: defaultLinkFriendIdentity,
471
1041
  listDiscoveredAgents: defaultListDiscoveredAgents,
472
1042
  runHatchFlow: hatch_flow_1.runHatchFlow,
473
1043
  promptInput: defaultPromptInput,
1044
+ runAdoptionSpecialist: defaultRunAdoptionSpecialist,
474
1045
  registerOuroBundleType: ouro_uti_1.registerOuroBundleUti,
1046
+ installOuroCommand: ouro_path_installer_1.installOuroCommand,
1047
+ syncGlobalOuroBotWrapper: ouro_bot_global_installer_1.syncGlobalOuroBotWrapper,
475
1048
  /* v8 ignore next 3 -- integration: launches interactive CLI session @preserve */
476
1049
  startChat: async (agentName) => {
477
1050
  const { main } = await Promise.resolve().then(() => __importStar(require("../../senses/cli")));
@@ -533,12 +1106,235 @@ async function registerOuroBundleTypeNonBlocking(deps) {
533
1106
  });
534
1107
  }
535
1108
  }
1109
+ async function performSystemSetup(deps) {
1110
+ // Install ouro command to PATH (non-blocking)
1111
+ if (deps.installOuroCommand) {
1112
+ try {
1113
+ deps.installOuroCommand();
1114
+ }
1115
+ catch (error) {
1116
+ (0, runtime_1.emitNervesEvent)({
1117
+ level: "warn",
1118
+ component: "daemon",
1119
+ event: "daemon.system_setup_ouro_cmd_error",
1120
+ message: "failed to install ouro command to PATH",
1121
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1122
+ });
1123
+ }
1124
+ }
1125
+ if (deps.syncGlobalOuroBotWrapper) {
1126
+ try {
1127
+ await Promise.resolve(deps.syncGlobalOuroBotWrapper());
1128
+ }
1129
+ catch (error) {
1130
+ (0, runtime_1.emitNervesEvent)({
1131
+ level: "warn",
1132
+ component: "daemon",
1133
+ event: "daemon.system_setup_ouro_bot_wrapper_error",
1134
+ message: "failed to sync global ouro.bot wrapper",
1135
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1136
+ });
1137
+ }
1138
+ }
1139
+ // Install subagents (claude/codex skills)
1140
+ try {
1141
+ await deps.installSubagents();
1142
+ }
1143
+ catch (error) {
1144
+ (0, runtime_1.emitNervesEvent)({
1145
+ level: "warn",
1146
+ component: "daemon",
1147
+ event: "daemon.subagent_install_error",
1148
+ message: "subagent auto-install failed",
1149
+ meta: { error: error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error) },
1150
+ });
1151
+ }
1152
+ // Register .ouro bundle type (UTI on macOS)
1153
+ await registerOuroBundleTypeNonBlocking(deps);
1154
+ }
1155
+ function executeTaskCommand(command, taskMod) {
1156
+ if (command.kind === "task.board") {
1157
+ if (command.status) {
1158
+ const lines = taskMod.boardStatus(command.status);
1159
+ return lines.length > 0 ? lines.join("\n") : "no tasks in that status";
1160
+ }
1161
+ const board = taskMod.getBoard();
1162
+ return board.full || board.compact || "no tasks found";
1163
+ }
1164
+ if (command.kind === "task.create") {
1165
+ try {
1166
+ const created = taskMod.createTask({
1167
+ title: command.title,
1168
+ type: command.type ?? "one-shot",
1169
+ category: "general",
1170
+ body: "",
1171
+ });
1172
+ return `created: ${created}`;
1173
+ }
1174
+ catch (error) {
1175
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1176
+ }
1177
+ }
1178
+ if (command.kind === "task.update") {
1179
+ const result = taskMod.updateStatus(command.id, command.status);
1180
+ if (!result.ok) {
1181
+ return `error: ${result.reason ?? "status update failed"}`;
1182
+ }
1183
+ const archivedSuffix = result.archived && result.archived.length > 0
1184
+ ? ` | archived: ${result.archived.join(", ")}`
1185
+ : "";
1186
+ return `updated: ${command.id} -> ${result.to}${archivedSuffix}`;
1187
+ }
1188
+ if (command.kind === "task.show") {
1189
+ const task = taskMod.getTask(command.id);
1190
+ if (!task)
1191
+ return `task not found: ${command.id}`;
1192
+ return [
1193
+ `title: ${task.title}`,
1194
+ `type: ${task.type}`,
1195
+ `status: ${task.status}`,
1196
+ `category: ${task.category}`,
1197
+ `created: ${task.created}`,
1198
+ `updated: ${task.updated}`,
1199
+ `path: ${task.path}`,
1200
+ task.body ? `\n${task.body}` : "",
1201
+ ].filter(Boolean).join("\n");
1202
+ }
1203
+ if (command.kind === "task.actionable") {
1204
+ const lines = taskMod.boardAction();
1205
+ return lines.length > 0 ? lines.join("\n") : "no action required";
1206
+ }
1207
+ if (command.kind === "task.deps") {
1208
+ const lines = taskMod.boardDeps();
1209
+ return lines.length > 0 ? lines.join("\n") : "no unresolved dependencies";
1210
+ }
1211
+ // command.kind === "task.sessions"
1212
+ const lines = taskMod.boardSessions();
1213
+ return lines.length > 0 ? lines.join("\n") : "no active sessions";
1214
+ }
1215
+ const TRUST_RANK = { family: 4, friend: 3, acquaintance: 2, stranger: 1 };
1216
+ /* v8 ignore start -- defensive: ?? fallbacks are unreachable when inputs are valid TrustLevel values @preserve */
1217
+ function higherTrust(a, b) {
1218
+ const rankA = TRUST_RANK[a ?? "stranger"] ?? 1;
1219
+ const rankB = TRUST_RANK[b ?? "stranger"] ?? 1;
1220
+ return rankA >= rankB ? (a ?? "stranger") : (b ?? "stranger");
1221
+ }
1222
+ /* v8 ignore stop */
1223
+ async function executeFriendCommand(command, store) {
1224
+ if (command.kind === "friend.list") {
1225
+ const listAll = store.listAll;
1226
+ if (!listAll)
1227
+ return "friend store does not support listing";
1228
+ const friends = await listAll.call(store);
1229
+ if (friends.length === 0)
1230
+ return "no friends found";
1231
+ const lines = friends.map((f) => {
1232
+ const trust = f.trustLevel ?? "unknown";
1233
+ return `${f.id} ${f.name} ${trust}`;
1234
+ });
1235
+ return lines.join("\n");
1236
+ }
1237
+ if (command.kind === "friend.show") {
1238
+ const record = await store.get(command.friendId);
1239
+ if (!record)
1240
+ return `friend not found: ${command.friendId}`;
1241
+ return JSON.stringify(record, null, 2);
1242
+ }
1243
+ if (command.kind === "friend.create") {
1244
+ const now = new Date().toISOString();
1245
+ const id = (0, crypto_1.randomUUID)();
1246
+ const trustLevel = (command.trustLevel ?? "acquaintance");
1247
+ await store.put(id, {
1248
+ id,
1249
+ name: command.name,
1250
+ trustLevel,
1251
+ externalIds: [],
1252
+ tenantMemberships: [],
1253
+ toolPreferences: {},
1254
+ notes: {},
1255
+ totalTokens: 0,
1256
+ createdAt: now,
1257
+ updatedAt: now,
1258
+ schemaVersion: 1,
1259
+ });
1260
+ return `created: ${id} (${command.name}, ${trustLevel})`;
1261
+ }
1262
+ if (command.kind === "friend.link") {
1263
+ const current = await store.get(command.friendId);
1264
+ if (!current)
1265
+ return `friend not found: ${command.friendId}`;
1266
+ const alreadyLinked = current.externalIds.some((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
1267
+ if (alreadyLinked)
1268
+ return `identity already linked: ${command.provider}:${command.externalId}`;
1269
+ const now = new Date().toISOString();
1270
+ const newExternalIds = [
1271
+ ...current.externalIds,
1272
+ { provider: command.provider, externalId: command.externalId, linkedAt: now },
1273
+ ];
1274
+ // Orphan cleanup: check if another friend has this externalId
1275
+ const orphan = await store.findByExternalId(command.provider, command.externalId);
1276
+ let mergeMessage = "";
1277
+ let mergedNotes = { ...current.notes };
1278
+ let mergedTrust = current.trustLevel;
1279
+ let orphanExternalIds = [];
1280
+ if (orphan && orphan.id !== command.friendId) {
1281
+ // Merge orphan's notes (target's notes take priority)
1282
+ mergedNotes = { ...orphan.notes, ...current.notes };
1283
+ // Keep higher trust level
1284
+ mergedTrust = higherTrust(current.trustLevel, orphan.trustLevel);
1285
+ // Collect orphan's other externalIds (excluding the one being linked)
1286
+ orphanExternalIds = orphan.externalIds.filter((ext) => !(ext.provider === command.provider && ext.externalId === command.externalId));
1287
+ await store.delete(orphan.id);
1288
+ mergeMessage = ` (merged orphan ${orphan.id})`;
1289
+ }
1290
+ await store.put(command.friendId, {
1291
+ ...current,
1292
+ externalIds: [...newExternalIds, ...orphanExternalIds],
1293
+ notes: mergedNotes,
1294
+ trustLevel: mergedTrust,
1295
+ updatedAt: now,
1296
+ });
1297
+ return `linked ${command.provider}:${command.externalId} to ${command.friendId}${mergeMessage}`;
1298
+ }
1299
+ // command.kind === "friend.unlink"
1300
+ const current = await store.get(command.friendId);
1301
+ if (!current)
1302
+ return `friend not found: ${command.friendId}`;
1303
+ const idx = current.externalIds.findIndex((ext) => ext.provider === command.provider && ext.externalId === command.externalId);
1304
+ if (idx === -1)
1305
+ return `identity not linked: ${command.provider}:${command.externalId}`;
1306
+ const now = new Date().toISOString();
1307
+ const filtered = current.externalIds.filter((_, i) => i !== idx);
1308
+ await store.put(command.friendId, { ...current, externalIds: filtered, updatedAt: now });
1309
+ return `unlinked ${command.provider}:${command.externalId} from ${command.friendId}`;
1310
+ }
1311
+ function executeReminderCommand(command, taskMod) {
1312
+ try {
1313
+ const created = taskMod.createTask({
1314
+ title: command.title,
1315
+ type: command.cadence ? "habit" : "one-shot",
1316
+ category: command.category ?? "reminder",
1317
+ body: command.body,
1318
+ scheduledAt: command.scheduledAt,
1319
+ cadence: command.cadence,
1320
+ });
1321
+ return `created: ${created}`;
1322
+ }
1323
+ catch (error) {
1324
+ return `error: ${error instanceof Error ? error.message : /* v8 ignore next -- defensive: non-Error catch branch @preserve */ String(error)}`;
1325
+ }
1326
+ }
536
1327
  async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
537
1328
  if (args.includes("--help") || args.includes("-h")) {
538
1329
  const text = usage();
539
1330
  deps.writeStdout(text);
540
1331
  return text;
541
1332
  }
1333
+ if (args.length === 1 && (args[0] === "-v" || args[0] === "--version")) {
1334
+ const text = formatVersionOutput();
1335
+ deps.writeStdout(text);
1336
+ return text;
1337
+ }
542
1338
  let command;
543
1339
  try {
544
1340
  command = parseOuroCommand(args);
@@ -556,7 +1352,20 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
556
1352
  }
557
1353
  if (args.length === 0) {
558
1354
  const discovered = await Promise.resolve(deps.listDiscoveredAgents ? deps.listDiscoveredAgents() : defaultListDiscoveredAgents());
559
- if (discovered.length === 0) {
1355
+ if (discovered.length === 0 && deps.runAdoptionSpecialist) {
1356
+ // System setup first — ouro command, subagents, UTI — before the interactive specialist
1357
+ await performSystemSetup(deps);
1358
+ const hatchlingName = await deps.runAdoptionSpecialist();
1359
+ if (!hatchlingName) {
1360
+ return "";
1361
+ }
1362
+ await ensureDaemonRunning(deps);
1363
+ if (deps.startChat) {
1364
+ await deps.startChat(hatchlingName);
1365
+ }
1366
+ return "";
1367
+ }
1368
+ else if (discovered.length === 0) {
560
1369
  command = { kind: "hatch.start" };
561
1370
  }
562
1371
  else if (discovered.length === 1) {
@@ -596,19 +1405,20 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
596
1405
  meta: { kind: command.kind },
597
1406
  });
598
1407
  if (command.kind === "daemon.up") {
599
- try {
600
- await deps.installSubagents();
1408
+ await performSystemSetup(deps);
1409
+ // Run update hooks before starting daemon so user sees the output
1410
+ (0, update_hooks_1.registerUpdateHook)(bundle_meta_1.bundleMetaHook);
1411
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
1412
+ const currentVersion = (0, bundle_manifest_1.getPackageVersion)();
1413
+ const updateSummary = await (0, update_hooks_1.applyPendingUpdates)(bundlesRoot, currentVersion);
1414
+ if (updateSummary.updated.length > 0) {
1415
+ const agents = updateSummary.updated.map((e) => e.agent);
1416
+ const from = updateSummary.updated[0].from;
1417
+ const to = updateSummary.updated[0].to;
1418
+ const fromStr = from ? ` (was ${from})` : "";
1419
+ const count = agents.length;
1420
+ deps.writeStdout(`updated ${count} agent${count === 1 ? "" : "s"} to runtime ${to}${fromStr}`);
601
1421
  }
602
- catch (error) {
603
- (0, runtime_1.emitNervesEvent)({
604
- level: "warn",
605
- component: "daemon",
606
- event: "daemon.subagent_install_error",
607
- message: "subagent auto-install failed",
608
- meta: { error: error instanceof Error ? error.message : String(error) },
609
- });
610
- }
611
- await registerOuroBundleTypeNonBlocking(deps);
612
1422
  const daemonResult = await ensureDaemonRunning(deps);
613
1423
  deps.writeStdout(daemonResult.message);
614
1424
  return daemonResult.message;
@@ -617,13 +1427,112 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
617
1427
  deps.tailLogs();
618
1428
  return "";
619
1429
  }
620
- if (command.kind === "friend.link") {
621
- const linker = deps.linkFriendIdentity ?? defaultLinkFriendIdentity;
622
- const message = await linker(command);
1430
+ // ── task subcommands (local, no daemon socket needed) ──
1431
+ if (command.kind === "task.board" || command.kind === "task.create" || command.kind === "task.update" ||
1432
+ command.kind === "task.show" || command.kind === "task.actionable" || command.kind === "task.deps" ||
1433
+ command.kind === "task.sessions") {
1434
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1435
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
1436
+ /* v8 ignore stop */
1437
+ const message = executeTaskCommand(command, taskMod);
1438
+ deps.writeStdout(message);
1439
+ return message;
1440
+ }
1441
+ // ── reminder subcommands (local, no daemon socket needed) ──
1442
+ if (command.kind === "reminder.create") {
1443
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1444
+ const taskMod = deps.taskModule ?? (0, tasks_1.getTaskModule)();
1445
+ /* v8 ignore stop */
1446
+ const message = executeReminderCommand(command, taskMod);
1447
+ deps.writeStdout(message);
1448
+ return message;
1449
+ }
1450
+ // ── friend subcommands (local, no daemon socket needed) ──
1451
+ if (command.kind === "friend.list" || command.kind === "friend.show" || command.kind === "friend.create" ||
1452
+ command.kind === "friend.link" || command.kind === "friend.unlink") {
1453
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1454
+ let store = deps.friendStore;
1455
+ if (!store) {
1456
+ // Derive agent-scoped friends dir from --agent flag or link/unlink's agent field
1457
+ const agentName = ("agent" in command && command.agent) ? command.agent : undefined;
1458
+ const friendsDir = agentName
1459
+ ? path.join((0, identity_1.getAgentBundlesRoot)(), `${agentName}.ouro`, "friends")
1460
+ : path.join((0, identity_1.getAgentBundlesRoot)(), "friends");
1461
+ store = new store_file_1.FileFriendStore(friendsDir);
1462
+ }
1463
+ /* v8 ignore stop */
1464
+ const message = await executeFriendCommand(command, store);
1465
+ deps.writeStdout(message);
1466
+ return message;
1467
+ }
1468
+ // ── whoami (local, no daemon socket needed) ──
1469
+ if (command.kind === "whoami") {
1470
+ if (command.agent) {
1471
+ const agentRoot = path.join((0, identity_1.getAgentBundlesRoot)(), `${command.agent}.ouro`);
1472
+ const message = [
1473
+ `agent: ${command.agent}`,
1474
+ `home: ${agentRoot}`,
1475
+ `bones: ${(0, runtime_metadata_1.getRuntimeMetadata)().version}`,
1476
+ ].join("\n");
1477
+ deps.writeStdout(message);
1478
+ return message;
1479
+ }
1480
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1481
+ try {
1482
+ const info = deps.whoamiInfo
1483
+ ? deps.whoamiInfo()
1484
+ : {
1485
+ agentName: (0, identity_1.getAgentName)(),
1486
+ homePath: path.join((0, identity_1.getAgentBundlesRoot)(), `${(0, identity_1.getAgentName)()}.ouro`),
1487
+ bonesVersion: (0, runtime_metadata_1.getRuntimeMetadata)().version,
1488
+ };
1489
+ const message = [
1490
+ `agent: ${info.agentName}`,
1491
+ `home: ${info.homePath}`,
1492
+ `bones: ${info.bonesVersion}`,
1493
+ ].join("\n");
1494
+ deps.writeStdout(message);
1495
+ return message;
1496
+ }
1497
+ catch {
1498
+ const message = "error: no agent context — use --agent <name> to specify";
1499
+ deps.writeStdout(message);
1500
+ return message;
1501
+ }
1502
+ /* v8 ignore stop */
1503
+ }
1504
+ // ── session list (local, no daemon socket needed) ──
1505
+ if (command.kind === "session.list") {
1506
+ /* v8 ignore start -- production default: requires full identity setup @preserve */
1507
+ const scanner = deps.scanSessions ?? (async () => []);
1508
+ /* v8 ignore stop */
1509
+ const sessions = await scanner();
1510
+ if (sessions.length === 0) {
1511
+ const message = "no active sessions";
1512
+ deps.writeStdout(message);
1513
+ return message;
1514
+ }
1515
+ const lines = sessions.map((s) => `${s.friendId} ${s.friendName} ${s.channel} ${s.lastActivity}`);
1516
+ const message = lines.join("\n");
623
1517
  deps.writeStdout(message);
624
1518
  return message;
625
1519
  }
626
1520
  if (command.kind === "hatch.start") {
1521
+ // Route through adoption specialist when no explicit hatch args were provided
1522
+ const hasExplicitHatchArgs = !!(command.agentName || command.humanName || command.provider || command.credentials);
1523
+ if (deps.runAdoptionSpecialist && !hasExplicitHatchArgs) {
1524
+ // System setup first — ouro command, subagents, UTI — before the interactive specialist
1525
+ await performSystemSetup(deps);
1526
+ const hatchlingName = await deps.runAdoptionSpecialist();
1527
+ if (!hatchlingName) {
1528
+ return "";
1529
+ }
1530
+ await ensureDaemonRunning(deps);
1531
+ if (deps.startChat) {
1532
+ await deps.startChat(hatchlingName);
1533
+ }
1534
+ return "";
1535
+ }
627
1536
  const hatchRunner = deps.runHatchFlow;
628
1537
  if (!hatchRunner) {
629
1538
  const response = await deps.sendCommand(deps.socketPath, { kind: "hatch.start" });
@@ -633,19 +1542,7 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
633
1542
  }
634
1543
  const hatchInput = await resolveHatchInput(command, deps);
635
1544
  const result = await hatchRunner(hatchInput);
636
- try {
637
- await deps.installSubagents();
638
- }
639
- catch (error) {
640
- (0, runtime_1.emitNervesEvent)({
641
- level: "warn",
642
- component: "daemon",
643
- event: "daemon.subagent_install_error",
644
- message: "subagent auto-install failed",
645
- meta: { error: error instanceof Error ? error.message : String(error) },
646
- });
647
- }
648
- await registerOuroBundleTypeNonBlocking(deps);
1545
+ await performSystemSetup(deps);
649
1546
  const daemonResult = await ensureDaemonRunning(deps);
650
1547
  if (deps.startChat) {
651
1548
  await deps.startChat(hatchInput.agentName);
@@ -667,9 +1564,22 @@ async function runOuroCli(args, deps = createDefaultOuroCliDeps()) {
667
1564
  deps.writeStdout(message);
668
1565
  return message;
669
1566
  }
1567
+ if (command.kind === "daemon.status" && isDaemonUnavailableError(error)) {
1568
+ const message = daemonUnavailableStatusOutput(deps.socketPath);
1569
+ deps.writeStdout(message);
1570
+ return message;
1571
+ }
1572
+ if (command.kind === "daemon.stop" && isDaemonUnavailableError(error)) {
1573
+ const message = "daemon not running";
1574
+ deps.writeStdout(message);
1575
+ return message;
1576
+ }
670
1577
  throw error;
671
1578
  }
672
- const message = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
1579
+ const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
1580
+ const message = command.kind === "daemon.status"
1581
+ ? formatDaemonStatusOutput(response, fallbackMessage)
1582
+ : fallbackMessage;
673
1583
  deps.writeStdout(message);
674
1584
  return message;
675
1585
  }