@ouro.bot/cli 0.1.0-alpha.442 → 0.1.0-alpha.444

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.
package/changelog.json CHANGED
@@ -1,6 +1,22 @@
1
1
  {
2
2
  "_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
3
3
  "versions": [
4
+ {
5
+ "version": "0.1.0-alpha.444",
6
+ "changes": [
7
+ "`ouro up` now treats daemon startup as one truthful boot story across fresh starts and daemon replacement: both paths wait on the same current-boot readiness check, keep narrating real progress while the background service warms up, and stop calling startup done just because one quick socket probe answered.",
8
+ "Slow real-world boots now get more patience and better visibility. The default startup timeout was extended to 60 seconds, the boot checklist keeps printing live startup detail instead of stalling on a blinking cursor, and replacement boot failures now land as a failed `starting daemon` step with plain-language diagnosis.",
9
+ "Startup failures now behave like failures to the shell as well as to the human: `ouro up` sets a non-zero exit code when daemon boot does not finish, and `ouro logs` is wired into the installed CLI again so the recovery path it points to actually opens the live daemon/agent log tail."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.443",
14
+ "changes": [
15
+ "`ouro up` now renders as a real boot checklist instead of a loose progress wall: the screen shows the full startup path up front, marks what is running now, and keeps pending steps visible so humans can see where Ouro is in the bring-up flow.",
16
+ "Provider checks during startup now narrate the providers each selected lane is actually using, translate noisy vault chatter into plain language, and keep the live-check copy truthful instead of implying that every configured provider was checked.",
17
+ "`ouro up` no longer reports success just because the daemon answered once during startup. Before handing control back, Ouro now performs one final daemon-status handoff check and fails with a clear diagnosis if the background service stopped before boot actually finished."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.442",
6
22
  "changes": [
@@ -318,6 +318,33 @@ function failedPingResult(agentName, lane, provider, model, result) {
318
318
  function credentialRecordForLane(pool, provider) {
319
319
  return pool.providers[provider];
320
320
  }
321
+ function laneAudienceLabel(lane) {
322
+ return lane === "outward" ? "chat" : "inner dialog";
323
+ }
324
+ function bindingLabel(binding) {
325
+ return `${binding.provider} / ${binding.model}`;
326
+ }
327
+ function selectedProviderPlan(agentName, state) {
328
+ return [
329
+ `${agentName}: checking the providers this agent uses right now`,
330
+ ...["outward", "inner"].map((lane) => `- ${laneAudienceLabel(lane)}: ${bindingLabel(state.lanes[lane])}`),
331
+ ].join("\n");
332
+ }
333
+ function mapVaultRefreshProgress(agentName, onProgress) {
334
+ return (message) => {
335
+ if (message.startsWith("reading vault items for ")) {
336
+ onProgress(`${agentName}: opening saved provider credentials in the vault`);
337
+ return;
338
+ }
339
+ if (message === "parsing provider credentials...") {
340
+ onProgress(`${agentName}: organizing saved provider credentials`);
341
+ }
342
+ };
343
+ }
344
+ function providerPingSubject(agentName, lanes) {
345
+ const laneList = lanes.map((lane) => laneAudienceLabel(lane)).join(" + ");
346
+ return `${agentName} (${laneList})`;
347
+ }
321
348
  /**
322
349
  * Structural validation only. Live provider credential validation belongs to
323
350
  * checkAgentConfigWithProviderHealth(), which reads the agent vault and pings.
@@ -352,8 +379,9 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
352
379
  return stateResult.result;
353
380
  if (stateResult.disabled)
354
381
  return { ok: true };
382
+ deps.onProgress?.(selectedProviderPlan(agentName, stateResult.state));
355
383
  const ping = deps.pingProvider ?? (await Promise.resolve().then(() => __importStar(require("../provider-ping")))).pingProvider;
356
- const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, deps.onProgress ? { onProgress: deps.onProgress } : undefined);
384
+ const poolResult = await (0, provider_credentials_1.refreshProviderCredentialPool)(agentName, deps.onProgress ? { onProgress: mapVaultRefreshProgress(agentName, deps.onProgress) } : undefined);
357
385
  const pingGroups = new Map();
358
386
  const lanes = ["outward", "inner"];
359
387
  for (const lane of lanes) {
@@ -390,7 +418,11 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
390
418
  const result = await ping(group.provider, providerCredentialConfig(group.record), {
391
419
  model: group.model,
392
420
  ...(deps.onProgress
393
- ? (0, provider_ping_progress_1.createProviderPingProgressReporter)({ provider: group.provider, model: group.model }, deps.onProgress)
421
+ ? (0, provider_ping_progress_1.createProviderPingProgressReporter)({
422
+ provider: group.provider,
423
+ model: group.model,
424
+ subject: providerPingSubject(agentName, group.lanes),
425
+ }, deps.onProgress)
394
426
  : {}),
395
427
  });
396
428
  return { group, result };
@@ -514,11 +514,16 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
514
514
  sendCommand: socket_client_1.sendDaemonCommand,
515
515
  startDaemonProcess: defaultStartDaemonProcess,
516
516
  writeStdout: defaultWriteStdout,
517
+ setExitCode: (code) => {
518
+ const current = typeof process.exitCode === "number" ? process.exitCode : 0;
519
+ process.exitCode = Math.max(current, code);
520
+ },
517
521
  writeRaw: defaultWriteRaw,
518
522
  isTTY: process.stdout.isTTY === true,
519
523
  checkSocketAlive: socket_client_1.checkDaemonSocketAlive,
520
524
  cleanupStaleSocket: defaultCleanupStaleSocket,
521
525
  fallbackPendingMessage: defaultFallbackPendingMessage,
526
+ tailLogs: (options) => (0, log_tailer_1.tailLogs)(options),
522
527
  healthFilePath: (0, daemon_health_1.getDefaultHealthPath)(),
523
528
  readHealthState: daemon_health_1.readHealth,
524
529
  readHealthUpdatedAt: defaultReadHealthUpdatedAt,
@@ -528,7 +533,7 @@ function createDefaultOuroCliDeps(socketPath = socket_client_1.DEFAULT_DAEMON_SO
528
533
  updateCheckTimeoutMs: update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS,
529
534
  startupPollIntervalMs: 250,
530
535
  startupStabilityWindowMs: 1_500,
531
- startupTimeoutMs: 10_000,
536
+ startupTimeoutMs: 60_000,
532
537
  startupRetryLimit: 1,
533
538
  listDiscoveredAgents: defaultListDiscoveredAgents,
534
539
  runHatchFlow: hatch_flow_1.runHatchFlow,
@@ -39,6 +39,7 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  };
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
+ exports.summarizeDaemonStartupFailure = summarizeDaemonStartupFailure;
42
43
  exports.mergeStartupStability = mergeStartupStability;
43
44
  exports.ensureDaemonRunning = ensureDaemonRunning;
44
45
  exports.listGithubCopilotModels = listGithubCopilotModels;
@@ -100,7 +101,7 @@ const agent_discovery_1 = require("./agent-discovery");
100
101
  const connect_bay_1 = require("./connect-bay");
101
102
  const runtime_capability_check_1 = require("../runtime-capability-check");
102
103
  // ── ensureDaemonRunning ──
103
- const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 10_000;
104
+ const DEFAULT_DAEMON_STARTUP_TIMEOUT_MS = 60_000;
104
105
  const DEFAULT_DAEMON_STARTUP_POLL_INTERVAL_MS = 500;
105
106
  const DEFAULT_DAEMON_STARTUP_STABILITY_WINDOW_MS = 1_500;
106
107
  const DEFAULT_DAEMON_STARTUP_RETRY_LIMIT = 1;
@@ -122,6 +123,31 @@ function summarizeCliUpdateCheckStatus(error, timedOut = false) {
122
123
  }
123
124
  return "skipped; update check unavailable";
124
125
  }
126
+ function returnCliFailure(deps, message, exitCode = 1) {
127
+ deps.setExitCode?.(exitCode);
128
+ deps.writeStdout(message);
129
+ return message;
130
+ }
131
+ function summarizeDaemonStartupFailure(result) {
132
+ if (result.startupFailureReason && result.startupFailureReason.trim().length > 0) {
133
+ return result.startupFailureReason;
134
+ }
135
+ const firstLine = result.message.split(/\r?\n/, 1)[0]?.trim();
136
+ return firstLine && firstLine.length > 0
137
+ ? firstLine
138
+ : "background service failed to finish booting";
139
+ }
140
+ function formatDaemonStartupProgressLine(serviceLabel, status, latestEvent) {
141
+ const base = status === "waiting"
142
+ ? `waiting for the ${serviceLabel} to answer`
143
+ : status === "answering"
144
+ ? `${serviceLabel} answered\n- waiting for this boot to publish its ready signal`
145
+ : `${serviceLabel} answered\n- ready signal received; making sure it holds`;
146
+ const trimmedEvent = latestEvent?.trim();
147
+ return trimmedEvent && status !== "stabilizing"
148
+ ? `${base}\n- latest daemon event: ${trimmedEvent}`
149
+ : base;
150
+ }
125
151
  async function runCliUpdateCheckWithTimeout(checkForCliUpdate, timeoutMs = update_checker_1.CLI_UPDATE_CHECK_TIMEOUT_MS) {
126
152
  return await new Promise((resolve, reject) => {
127
153
  let settled = false;
@@ -160,7 +186,6 @@ async function checkAgentProviders(deps, agentsOverride, onProgress) {
160
186
  const degraded = [];
161
187
  for (const agent of [...new Set(agents)]) {
162
188
  try {
163
- onProgress?.(`${agent}: checking providers...`);
164
189
  const result = await checkAgentProviderHealth(agent, bundlesRoot, deps, onProgress);
165
190
  if (result.ok)
166
191
  continue;
@@ -358,9 +383,14 @@ function writeProviderRepairSummary(deps, title, degraded) {
358
383
  }
359
384
  function providerRepairCountSummary(count) {
360
385
  if (count === 0)
361
- return "ok";
386
+ return "selected providers answered live checks";
362
387
  return `${count} ${count === 1 ? "needs" : "need"} attention`;
363
388
  }
389
+ function bootPhasePlan(daemonAlive) {
390
+ return daemonAlive
391
+ ? ["update check", "system setup", "starting daemon", "provider checks", "final daemon check"]
392
+ : ["update check", "system setup", "provider checks", "starting daemon", "final daemon check"];
393
+ }
364
394
  function createHumanCommandProgress(deps, commandName) {
365
395
  return new up_progress_1.CommandProgress({
366
396
  write: deps.writeRaw ?? deps.writeStdout,
@@ -387,8 +417,67 @@ function daemonProgressSummary(result) {
387
417
  if (result.alreadyRunning)
388
418
  return "already running";
389
419
  if (result.message.includes("replaced"))
390
- return "replacement ready";
391
- return "ready";
420
+ return "replacement answered";
421
+ return "background service answered";
422
+ }
423
+ function finalDaemonFailureMessage(deps, reason) {
424
+ const lines = [`background service stopped before boot finished: ${reason}`];
425
+ const recentLogLines = deps.readRecentDaemonLogLines?.(DEFAULT_DAEMON_STARTUP_LOG_LINES) ?? [];
426
+ if (recentLogLines.length > 0) {
427
+ lines.push("recent daemon logs:");
428
+ lines.push(...recentLogLines.map((line) => ` ${line}`));
429
+ }
430
+ lines.push("Run `ouro up` again or `ouro doctor` for a deeper diagnosis.");
431
+ return lines.join("\n");
432
+ }
433
+ async function verifyDaemonReadyForHandoff(deps) {
434
+ const socketAlive = await deps.checkSocketAlive(deps.socketPath);
435
+ if (!socketAlive) {
436
+ return {
437
+ ok: false,
438
+ summary: "background service stopped",
439
+ message: finalDaemonFailureMessage(deps, "the daemon socket is no longer answering"),
440
+ };
441
+ }
442
+ try {
443
+ const response = await deps.sendCommand(deps.socketPath, { kind: "daemon.status" });
444
+ if (!response.ok) {
445
+ const reason = response.error ?? response.message ?? "daemon status did not answer cleanly";
446
+ return {
447
+ ok: false,
448
+ summary: "daemon status did not answer",
449
+ message: finalDaemonFailureMessage(deps, reason),
450
+ };
451
+ }
452
+ const payload = (0, cli_render_1.parseStatusPayload)(response.data);
453
+ if (!payload) {
454
+ return {
455
+ ok: true,
456
+ summary: "daemon answered",
457
+ };
458
+ }
459
+ if (payload.overview.daemon !== "running") {
460
+ return {
461
+ ok: false,
462
+ summary: `daemon reported ${payload.overview.daemon}`,
463
+ message: finalDaemonFailureMessage(deps, `the daemon reported state ${payload.overview.daemon}`),
464
+ };
465
+ }
466
+ const workerCount = payload.workers.length;
467
+ return {
468
+ ok: true,
469
+ summary: workerCount === 0
470
+ ? "daemon answered"
471
+ : `${workerCount} worker${workerCount === 1 ? "" : "s"} still answering`,
472
+ };
473
+ }
474
+ catch (error) {
475
+ return {
476
+ ok: false,
477
+ summary: "daemon status did not answer",
478
+ message: finalDaemonFailureMessage(deps, error instanceof Error ? error.message : String(error)),
479
+ };
480
+ }
392
481
  }
393
482
  async function reportPostRepairProviderHealth(deps, repairedAgents, onProgress) {
394
483
  const remainingDegraded = await checkAgentProviders(deps, repairedAgents, onProgress);
@@ -566,7 +655,7 @@ async function ensureDaemonRunning(deps, options = {}) {
566
655
  // The daemon writes structured events to daemon.ndjson in the first
567
656
  // agent bundle's state/daemon/logs/ directory. Read the last line to
568
657
  // surface what it's currently doing (e.g., "starting auto-start agents").
569
- const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
658
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
570
659
  if (!fs.existsSync(bundlesRoot))
571
660
  return null;
572
661
  const agents = fs.readdirSync(bundlesRoot).filter((d) => d.endsWith(".ouro"));
@@ -643,9 +732,27 @@ async function ensureDaemonRunning(deps, options = {}) {
643
732
  startDaemonProcess: deps.startDaemonProcess,
644
733
  checkSocketAlive: deps.checkSocketAlive,
645
734
  onProgress: deps.reportDaemonStartupPhase,
735
+ waitForDaemonStartup: async ({ pid }) => {
736
+ const startupFailure = await waitForDaemonStartup(deps, {
737
+ bootStartedAtMs: (deps.now ?? Date.now)(),
738
+ pid,
739
+ serviceLabel: "replacement background service",
740
+ readLatestDaemonEvent: readLatestDaemonStartupEvent,
741
+ });
742
+ return startupFailure
743
+ ? { ok: false, reason: startupFailure.reason }
744
+ : { ok: true };
745
+ },
646
746
  });
647
- if (!runtimeResult.verifyStartupStatus) {
648
- return runtimeResult;
747
+ if (!runtimeResult.ok) {
748
+ return {
749
+ ok: false,
750
+ alreadyRunning: runtimeResult.alreadyRunning,
751
+ message: runtimeResult.message,
752
+ verifyStartupStatus: runtimeResult.verifyStartupStatus,
753
+ startedPid: runtimeResult.startedPid,
754
+ startupFailureReason: runtimeResult.startupFailureReason ?? null,
755
+ };
649
756
  }
650
757
  const stability = await (0, startup_tui_1.pollDaemonStartup)({
651
758
  sendCommand: deps.sendCommand,
@@ -666,8 +773,12 @@ async function ensureDaemonRunning(deps, options = {}) {
666
773
  render: !deps.reportDaemonStartupPhase,
667
774
  });
668
775
  return {
776
+ ok: true,
669
777
  alreadyRunning: runtimeResult.alreadyRunning,
670
778
  message: runtimeResult.message,
779
+ verifyStartupStatus: runtimeResult.verifyStartupStatus,
780
+ startedPid: runtimeResult.startedPid,
781
+ startupFailureReason: null,
671
782
  stability,
672
783
  };
673
784
  }
@@ -687,6 +798,8 @@ async function ensureDaemonRunning(deps, options = {}) {
687
798
  const startupFailure = await waitForDaemonStartup(deps, {
688
799
  bootStartedAtMs,
689
800
  pid: lastPid,
801
+ serviceLabel: "new background service",
802
+ readLatestDaemonEvent: readLatestDaemonStartupEvent,
690
803
  });
691
804
  if (!startupFailure) {
692
805
  const stability = await (0, startup_tui_1.pollDaemonStartup)({
@@ -708,8 +821,10 @@ async function ensureDaemonRunning(deps, options = {}) {
708
821
  render: !deps.reportDaemonStartupPhase,
709
822
  });
710
823
  return {
824
+ ok: true,
711
825
  alreadyRunning: false,
712
826
  message: `daemon started (pid ${lastPid ?? "unknown"})`,
827
+ startupFailureReason: null,
713
828
  stability,
714
829
  };
715
830
  }
@@ -720,8 +835,10 @@ async function ensureDaemonRunning(deps, options = {}) {
720
835
  deps.reportDaemonStartupPhase?.("background service startup went sideways once; trying one more time");
721
836
  }
722
837
  return {
838
+ ok: false,
723
839
  alreadyRunning: false,
724
840
  message: formatDaemonStartupFailureMessage(lastPid, lastFailure, deps),
841
+ startupFailureReason: lastFailure.reason,
725
842
  };
726
843
  }
727
844
  function hasStartupHealthMonitor(deps) {
@@ -746,14 +863,14 @@ function hasFreshCurrentBootHealthSignal(deps, bootStartedAtMs, pid) {
746
863
  }
747
864
  function formatDaemonStartupFailureMessage(pid, failure, deps) {
748
865
  const lines = [
749
- `daemon spawned (pid ${pid ?? "unknown"}) but failed to stabilize: ${failure.reason}`,
866
+ `background service started (pid ${pid ?? "unknown"}) but did not finish booting: ${failure.reason}`,
750
867
  ];
751
868
  const recentLogLines = deps.readRecentDaemonLogLines?.(DEFAULT_DAEMON_STARTUP_LOG_LINES) ?? [];
752
869
  if (recentLogLines.length > 0) {
753
870
  lines.push("recent daemon logs:");
754
871
  lines.push(...recentLogLines.map((line) => ` ${line}`));
755
872
  }
756
- lines.push("fix hint for daemon: check daemon logs or run `ouro doctor`");
873
+ lines.push("Run `ouro logs` to watch live startup logs or `ouro doctor` for a deeper diagnosis.");
757
874
  return lines.join("\n");
758
875
  }
759
876
  async function waitForDaemonStartup(deps, options) {
@@ -767,13 +884,17 @@ async function waitForDaemonStartup(deps, options) {
767
884
  let stableSinceMs = null;
768
885
  let sawSocket = false;
769
886
  if (!useHealthMonitor) {
770
- const verified = await verifyDaemonAlive(deps.checkSocketAlive, deps.socketPath, timeoutMs, pollIntervalMs, sleep, now);
771
- return verified
772
- ? null
773
- : {
774
- reason: `daemon failed to respond within ${Math.ceil(timeoutMs / 1000)}s`,
775
- retryable: false,
776
- };
887
+ while (now() < deadline) {
888
+ await sleep(pollIntervalMs);
889
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
890
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "waiting", latestEvent));
891
+ if (await deps.checkSocketAlive(deps.socketPath))
892
+ return null;
893
+ }
894
+ return {
895
+ reason: `${options.serviceLabel} did not answer within ${Math.ceil(timeoutMs / 1000)}s`,
896
+ retryable: false,
897
+ };
777
898
  }
778
899
  while (now() < deadline) {
779
900
  await sleep(pollIntervalMs);
@@ -781,40 +902,37 @@ async function waitForDaemonStartup(deps, options) {
781
902
  if (!aliveNow) {
782
903
  if (sawSocket) {
783
904
  return {
784
- reason: "daemon socket disappeared during startup",
905
+ reason: `${options.serviceLabel} answered once and then disappeared during startup`,
785
906
  retryable: true,
786
907
  };
787
908
  }
909
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
910
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "waiting", latestEvent));
788
911
  continue;
789
912
  }
790
913
  if (!sawSocket) {
791
914
  sawSocket = true;
792
915
  stableSinceMs = now();
793
- deps.reportDaemonStartupPhase?.("verifying daemon health");
916
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
917
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "answering", latestEvent));
794
918
  }
795
919
  if (!hasFreshCurrentBootHealthSignal(deps, options.bootStartedAtMs, options.pid)) {
920
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
921
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "answering", latestEvent));
796
922
  continue;
797
923
  }
924
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "stabilizing"));
798
925
  if (stableSinceMs !== null && now() - stableSinceMs >= stabilityWindowMs) {
799
926
  return null;
800
927
  }
801
928
  }
802
929
  return {
803
930
  reason: sawSocket
804
- ? "daemon did not publish fresh health for the current boot attempt"
805
- : `daemon failed to respond within ${Math.ceil(timeoutMs / 1000)}s`,
931
+ ? `${options.serviceLabel} answered but this boot never published a ready signal within ${Math.ceil(timeoutMs / 1000)}s`
932
+ : `${options.serviceLabel} did not answer within ${Math.ceil(timeoutMs / 1000)}s`,
806
933
  retryable: sawSocket,
807
934
  };
808
935
  }
809
- async function verifyDaemonAlive(checkSocketAlive, socketPath, maxWaitMs = 10_000, pollIntervalMs = 500, sleep = defaultSleep, now = Date.now) {
810
- const deadline = now() + maxWaitMs;
811
- while (now() < deadline) {
812
- await sleep(pollIntervalMs);
813
- if (await checkSocketAlive(socketPath))
814
- return true;
815
- }
816
- return false;
817
- }
818
936
  function defaultSleep(ms) {
819
937
  return new Promise((resolve) => setTimeout(resolve, ms));
820
938
  }
@@ -3939,6 +4057,8 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
3939
4057
  now: deps.now ?? (() => Date.now()),
3940
4058
  autoRender: true,
3941
4059
  });
4060
+ const daemonAliveAtBootStart = await deps.checkSocketAlive(deps.socketPath);
4061
+ progress.setPhasePlan?.(bootPhasePlan(daemonAliveAtBootStart));
3942
4062
  // ── versioned CLI update check ──
3943
4063
  if (deps.checkForCliUpdate) {
3944
4064
  progress.startPhase("update check");
@@ -4067,6 +4187,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4067
4187
  promptInput: deps.promptInput,
4068
4188
  });
4069
4189
  const daemonAliveBeforeStart = await deps.checkSocketAlive(deps.socketPath);
4190
+ progress.setPhasePlan?.(bootPhasePlan(daemonAliveBeforeStart));
4070
4191
  let providerChecksAlreadyRun = false;
4071
4192
  if (!daemonAliveBeforeStart) {
4072
4193
  progress.startPhase("provider checks");
@@ -4078,22 +4199,19 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4078
4199
  if (command.noRepair) {
4079
4200
  writeProviderRepairSummary(deps, "Provider checks need attention", preflightProviderDegraded);
4080
4201
  const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
4081
- deps.writeStdout(message);
4082
- return message;
4202
+ return returnCliFailure(deps, message);
4083
4203
  }
4084
4204
  const repairResult = await runReadinessRepairForDegraded(preflightProviderDegraded, deps);
4085
4205
  if (!repairResult.repairsAttempted) {
4086
4206
  writeProviderRepairSummary(deps, "Provider checks still need attention", repairResult.remainingDegraded);
4087
4207
  const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
4088
- deps.writeStdout(message);
4089
- return message;
4208
+ return returnCliFailure(deps, message);
4090
4209
  }
4091
4210
  const remainingDegraded = repairResult.remainingDegraded;
4092
4211
  if (remainingDegraded.length > 0) {
4093
4212
  writeProviderRepairSummary(deps, "Still needs attention", remainingDegraded);
4094
4213
  const message = "daemon not started: provider checks still need repair.";
4095
- deps.writeStdout(message);
4096
- return message;
4214
+ return returnCliFailure(deps, message);
4097
4215
  }
4098
4216
  deps.writeStdout("All set. Provider checks recovered after repair.");
4099
4217
  }
@@ -4106,12 +4224,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4106
4224
  progress.announceStep?.(label);
4107
4225
  },
4108
4226
  }, { initialAlive: daemonAliveBeforeStart });
4109
- if (daemonResult.verifyStartupStatus === false) {
4227
+ if (!daemonResult.ok) {
4110
4228
  ;
4111
- progress.announceStep?.("replacement daemon did not answer in time");
4229
+ progress.failPhase?.("starting daemon", summarizeDaemonStartupFailure(daemonResult));
4112
4230
  progress.end();
4113
- deps.writeStdout(daemonResult.message);
4114
- return daemonResult.message;
4231
+ return returnCliFailure(deps, daemonResult.message);
4115
4232
  }
4116
4233
  progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
4117
4234
  if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
@@ -4120,6 +4237,16 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4120
4237
  daemonResult.stability = mergeStartupStability(daemonResult.stability, providerDegraded);
4121
4238
  progress.completePhase("provider checks", providerRepairCountSummary(providerDegraded.length));
4122
4239
  }
4240
+ progress.startPhase("final daemon check");
4241
+ const finalDaemonCheck = await verifyDaemonReadyForHandoff(deps);
4242
+ if (!finalDaemonCheck.ok) {
4243
+ ;
4244
+ progress.failPhase?.("final daemon check", finalDaemonCheck.summary);
4245
+ progress.end();
4246
+ const message = finalDaemonCheck.message ?? "background service stopped before boot finished";
4247
+ return returnCliFailure(deps, message);
4248
+ }
4249
+ progress.completePhase("final daemon check", finalDaemonCheck.summary);
4123
4250
  progress.end();
4124
4251
  // Interactive repair for degraded agents (Unit 5) — skipped by --no-repair (Unit 6)
4125
4252
  if (daemonResult.stability?.degraded && daemonResult.stability.degraded.length > 0) {
@@ -14,7 +14,7 @@
14
14
  * ouro-entry.ts, ouro-bot-wrapper.ts) continue to work unchanged.
15
15
  */
16
16
  Object.defineProperty(exports, "__esModule", { value: true });
17
- exports.readFirstBundleMetaVersion = exports.createDefaultOuroCliDeps = exports.pingGithubCopilotModel = exports.listGithubCopilotModels = exports.ensureDaemonRunning = exports.runOuroCli = exports.parseOuroCommand = void 0;
17
+ exports.readFirstBundleMetaVersion = exports.createDefaultOuroCliDeps = exports.pingGithubCopilotModel = exports.summarizeDaemonStartupFailure = exports.listGithubCopilotModels = exports.ensureDaemonRunning = exports.runOuroCli = exports.parseOuroCommand = void 0;
18
18
  // ── Parsing ──
19
19
  var cli_parse_1 = require("./cli-parse");
20
20
  Object.defineProperty(exports, "parseOuroCommand", { enumerable: true, get: function () { return cli_parse_1.parseOuroCommand; } });
@@ -23,6 +23,7 @@ var cli_exec_1 = require("./cli-exec");
23
23
  Object.defineProperty(exports, "runOuroCli", { enumerable: true, get: function () { return cli_exec_1.runOuroCli; } });
24
24
  Object.defineProperty(exports, "ensureDaemonRunning", { enumerable: true, get: function () { return cli_exec_1.ensureDaemonRunning; } });
25
25
  Object.defineProperty(exports, "listGithubCopilotModels", { enumerable: true, get: function () { return cli_exec_1.listGithubCopilotModels; } });
26
+ Object.defineProperty(exports, "summarizeDaemonStartupFailure", { enumerable: true, get: function () { return cli_exec_1.summarizeDaemonStartupFailure; } });
26
27
  var provider_ping_1 = require("../provider-ping");
27
28
  Object.defineProperty(exports, "pingGithubCopilotModel", { enumerable: true, get: function () { return provider_ping_1.pingGithubCopilotModel; } });
28
29
  // ── Defaults ──
@@ -111,10 +111,14 @@ async function ensureCurrentDaemonRuntime(deps) {
111
111
  catch (error) {
112
112
  const reason = formatErrorReason(error);
113
113
  result = {
114
+ ok: false,
114
115
  alreadyRunning: true,
115
116
  message: includesVersionDrift
116
117
  ? `daemon already running (${deps.socketPath}; could not replace the older background service ${runningVersion} -> ${deps.localVersion}: ${reason})`
117
118
  : `daemon already running (${deps.socketPath}; could not replace the older background service after runtime drift ${publicDriftSummary}: ${reason})`,
119
+ startupFailureReason: includesVersionDrift
120
+ ? "could not replace the older background service"
121
+ : "could not replace the older background service after runtime drift",
118
122
  };
119
123
  (0, runtime_1.emitNervesEvent)({
120
124
  level: "warn",
@@ -144,16 +148,23 @@ async function ensureCurrentDaemonRuntime(deps) {
144
148
  deps.onProgress?.("starting the replacement background service");
145
149
  const started = await deps.startDaemonProcess(deps.socketPath);
146
150
  const pid = started.pid ?? "unknown";
147
- const verified = await verifyDaemonStarted(deps);
151
+ const startupCheck = deps.waitForDaemonStartup
152
+ ? await deps.waitForDaemonStartup({ pid: started.pid ?? null })
153
+ : { ok: await verifyDaemonStarted(deps) };
154
+ const verified = startupCheck.ok;
148
155
  /* v8 ignore next -- daemon liveness failure: requires real daemon crash timing @preserve */
149
- const suffix = verified ? "" : "\nreplacement daemon did not answer in time; check logs with `ouro logs` or run `ouro doctor`.";
156
+ const suffix = verified
157
+ ? ""
158
+ : `\n${startupCheck.reason ?? "replacement background service did not answer in time"}; check logs with \`ouro logs\` or run \`ouro doctor\`.`;
150
159
  result = {
160
+ ok: verified,
151
161
  alreadyRunning: false,
152
162
  message: includesVersionDrift
153
163
  ? `replaced an older background service ${runningVersion} -> ${deps.localVersion} (pid ${pid})${suffix}`
154
164
  : `replaced an older background service after runtime drift: ${publicDriftSummary} (pid ${pid})${suffix}`,
155
165
  verifyStartupStatus: verified,
156
166
  startedPid: started.pid ?? null,
167
+ startupFailureReason: verified ? null : (startupCheck.reason ?? "replacement background service did not answer in time"),
157
168
  };
158
169
  (0, runtime_1.emitNervesEvent)({
159
170
  component: "daemon",
@@ -180,6 +191,7 @@ async function ensureCurrentDaemonRuntime(deps) {
180
191
  }
181
192
  if (!isKnownVersion(localRuntime.version) || !isKnownVersion(runningVersion)) {
182
193
  result = {
194
+ ok: true,
183
195
  alreadyRunning: true,
184
196
  message: `daemon already running (${deps.socketPath}; unable to verify version)`,
185
197
  };
@@ -208,6 +220,7 @@ async function ensureCurrentDaemonRuntime(deps) {
208
220
  catch (error) {
209
221
  const reason = formatErrorReason(error);
210
222
  const result = {
223
+ ok: true,
211
224
  alreadyRunning: true,
212
225
  message: `daemon already running (${deps.socketPath}; unable to verify version: ${reason})`,
213
226
  };
@@ -230,10 +243,12 @@ async function ensureCurrentDaemonRuntime(deps) {
230
243
  return result;
231
244
  }
232
245
  const result = {
246
+ ok: true,
233
247
  alreadyRunning: true,
234
248
  message: `daemon already running (${deps.socketPath})`,
235
249
  verifyStartupStatus: true,
236
250
  startedPid: null,
251
+ startupFailureReason: null,
237
252
  };
238
253
  (0, runtime_1.emitNervesEvent)({
239
254
  component: "daemon",
@@ -33,11 +33,16 @@ function providerRetryTiming(delayMs) {
33
33
  return `in ${rounded}`;
34
34
  }
35
35
  function formatProviderAttemptProgress(context, attempt, maxAttempts) {
36
- return `checking ${formatProviderPingLabel(context)} (attempt ${attempt} of ${maxAttempts})...`;
36
+ const prefix = context.subject ? `${context.subject}: ` : "";
37
+ return `${prefix}checking ${formatProviderPingLabel(context)} (attempt ${attempt} of ${maxAttempts})...`;
37
38
  }
38
- function formatProviderRetryProgress(record, maxAttempts) {
39
+ function formatProviderRetryProgress(context, record, maxAttempts) {
39
40
  const nextAttempt = Math.min(record.attempt + 1, maxAttempts);
40
- return `${formatProviderPingLabel(record)}: ${providerRetryReason(record)}; retrying ${providerRetryTiming(record.delayMs)} (attempt ${nextAttempt} of ${maxAttempts})`;
41
+ const retryDetail = `${providerRetryReason(record)}; retrying ${providerRetryTiming(record.delayMs)} (attempt ${nextAttempt} of ${maxAttempts})`;
42
+ if (context.subject) {
43
+ return `${context.subject}: ${retryDetail} while checking ${formatProviderPingLabel(context)}`;
44
+ }
45
+ return `${formatProviderPingLabel(record)}: ${retryDetail}`;
41
46
  }
42
47
  function createProviderPingProgressReporter(context, onProgress) {
43
48
  return {
@@ -70,7 +75,7 @@ function createProviderPingProgressReporter(context, onProgress) {
70
75
  classification: record.classification ?? "unknown",
71
76
  },
72
77
  });
73
- onProgress(formatProviderRetryProgress(record, maxAttempts));
78
+ onProgress(formatProviderRetryProgress(context, record, maxAttempts));
74
79
  },
75
80
  };
76
81
  }
@@ -201,11 +201,11 @@ function renderTerminalOperation(options) {
201
201
  summary: options.summary,
202
202
  sections: [
203
203
  {
204
- title: "Right now",
204
+ title: options.currentTitle ?? "Right now",
205
205
  lines: currentLines,
206
206
  },
207
207
  {
208
- title: "Progress",
208
+ title: options.stepsTitle ?? "Progress",
209
209
  lines: progressLines,
210
210
  },
211
211
  ],
@@ -22,6 +22,22 @@ const BOLD = "\x1b[1m";
22
22
  const DIM = "\x1b[2m";
23
23
  const GREEN = "\x1b[38;2;46;204;64m";
24
24
  const RED = "\x1b[38;2;255;106;106m";
25
+ const BASE_UP_PHASE_PLAN = [
26
+ "update check",
27
+ "system setup",
28
+ "provider checks",
29
+ "starting daemon",
30
+ "final daemon check",
31
+ ];
32
+ const FRIENDLY_UP_PHASE_LABELS = {
33
+ "update check": "Check for updates",
34
+ "system setup": "Prepare this machine",
35
+ "agent updates": "Update installed agents",
36
+ "bundle cleanup": "Clean up stale bundles",
37
+ "provider checks": "Check the providers your agents use right now",
38
+ "starting daemon": "Start the background service",
39
+ "final daemon check": "Confirm the background service stayed up",
40
+ };
25
41
  function splitDetailLines(detail) {
26
42
  if (!detail)
27
43
  return [];
@@ -45,6 +61,7 @@ class UpProgress {
45
61
  completed = [];
46
62
  currentPhase = null;
47
63
  currentDetail = null;
64
+ upPhasePlan = BASE_UP_PHASE_PLAN;
48
65
  prevLineCount = 0;
49
66
  ended = false;
50
67
  renderTimer = null;
@@ -96,6 +113,13 @@ class UpProgress {
96
113
  return;
97
114
  this.write(` ${label}\n`);
98
115
  }
116
+ setPhasePlan(labels) {
117
+ const nextPlan = [...new Set(labels.map((label) => label.trim()).filter((label) => label.length > 0))];
118
+ this.upPhasePlan = nextPlan.length > 0 ? nextPlan : BASE_UP_PHASE_PLAN;
119
+ if (this.isTTY && this.eventScope === "up") {
120
+ this.flushRender();
121
+ }
122
+ }
99
123
  /**
100
124
  * Update the sub-step detail on the current spinner phase. Rendered as
101
125
  * "label (Xs) -- detail" in TTY mode. In non-TTY mode, writes changed
@@ -272,39 +296,61 @@ class UpProgress {
272
296
  }
273
297
  return lines;
274
298
  }
299
+ renderUpStepLabel(label) {
300
+ return FRIENDLY_UP_PHASE_LABELS[label] ?? label;
301
+ }
275
302
  renderUpScreen(now) {
276
- const steps = this.completed.map((phase) => ({
277
- label: phase.label,
278
- status: phase.status === "failure" ? "failed" : "done",
279
- detail: phase.detail,
280
- }));
281
- let currentStepLabel = "Standing by.";
303
+ const seenLabels = new Set();
304
+ const steps = this.completed.map((phase) => {
305
+ seenLabels.add(phase.label);
306
+ return {
307
+ label: this.renderUpStepLabel(phase.label),
308
+ status: phase.status === "failure" ? "failed" : "done",
309
+ detail: phase.detail,
310
+ };
311
+ });
312
+ let currentStepLabel = this.completed.some((phase) => phase.status === "failure")
313
+ ? "Boot paused."
314
+ : this.completed.length > 0
315
+ ? "Boot checklist complete."
316
+ : "Waiting to begin.";
282
317
  let currentStepDetails = [];
283
318
  if (this.currentPhase) {
284
319
  const elapsed = now - this.currentPhase.startedAt;
285
320
  const elapsedSec = (elapsed / 1000).toFixed(1);
286
321
  const frameIndex = Math.floor(elapsed / 80) % SPINNER_FRAMES.length;
287
322
  const spinner = SPINNER_FRAMES[frameIndex];
288
- currentStepLabel = `${spinner} ${this.currentPhase.label} (${elapsedSec}s)`;
323
+ currentStepLabel = `${spinner} ${this.renderUpStepLabel(this.currentPhase.label)} (${elapsedSec}s)`;
289
324
  currentStepDetails = splitDetailLines(this.currentPhase.detail);
290
325
  steps.push({
291
- label: this.currentPhase.label,
326
+ label: this.renderUpStepLabel(this.currentPhase.label),
292
327
  status: "active",
293
328
  });
329
+ seenLabels.add(this.currentPhase.label);
330
+ }
331
+ for (const label of this.upPhasePlan) {
332
+ if (!seenLabels.has(label)) {
333
+ steps.push({
334
+ label: this.renderUpStepLabel(label),
335
+ status: "pending",
336
+ });
337
+ }
294
338
  }
295
339
  return (0, terminal_ui_1.renderTerminalOperation)({
296
340
  isTTY: true,
297
341
  columns: this.columns,
298
342
  masthead: {
299
- subtitle: "Starting the local agent runtime.",
343
+ subtitle: "Booting the local agent runtime.",
300
344
  },
301
- title: "Starting Ouro",
302
- summary: "Ouro is starting the background runtime, checking credentials, and surfacing anything that needs attention before chat begins.",
345
+ title: "Ouro boot checklist",
346
+ summary: "Ouro will check for updates, prepare this machine, verify the providers your agents use right now, start the background service, and make sure it stays up.",
303
347
  currentStep: {
304
348
  label: currentStepLabel,
305
349
  detailLines: currentStepDetails,
306
350
  },
307
351
  steps,
352
+ currentTitle: "Doing now",
353
+ stepsTitle: "Boot checklist",
308
354
  suppressEvent: true,
309
355
  }).trimEnd().split("\n");
310
356
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.442",
3
+ "version": "0.1.0-alpha.444",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",