@ouro.bot/cli 0.1.0-alpha.443 → 0.1.0-alpha.445

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.445",
6
+ "changes": [
7
+ "`ouro up` no longer smears old lines across the screen when the live frame shrinks. The boot checklist and daemon-start polling now share one overwrite primitive, so long waits keep breathing without duplicating the masthead or leaving stale rows behind.",
8
+ "`ouro status` is back to being a compact runtime cockpit instead of a generic wizard board. The TTY command now routes through the dense status deck again, so operators get the fast, scannable view they actually need.",
9
+ "The boot surface itself is lighter and truer: the masthead only uses the wide wordmark when it really fits, the checklist stays in plan order while a middle step is active, and the current-work panel now reads like a real boot sequencer instead of a stack of `Overview` cards."
10
+ ]
11
+ },
12
+ {
13
+ "version": "0.1.0-alpha.444",
14
+ "changes": [
15
+ "`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.",
16
+ "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.",
17
+ "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."
18
+ ]
19
+ },
4
20
  {
5
21
  "version": "0.1.0-alpha.443",
6
22
  "changes": [
@@ -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;
@@ -629,7 +655,7 @@ async function ensureDaemonRunning(deps, options = {}) {
629
655
  // The daemon writes structured events to daemon.ndjson in the first
630
656
  // agent bundle's state/daemon/logs/ directory. Read the last line to
631
657
  // surface what it's currently doing (e.g., "starting auto-start agents").
632
- const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
658
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
633
659
  if (!fs.existsSync(bundlesRoot))
634
660
  return null;
635
661
  const agents = fs.readdirSync(bundlesRoot).filter((d) => d.endsWith(".ouro"));
@@ -706,9 +732,27 @@ async function ensureDaemonRunning(deps, options = {}) {
706
732
  startDaemonProcess: deps.startDaemonProcess,
707
733
  checkSocketAlive: deps.checkSocketAlive,
708
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
+ },
709
746
  });
710
- if (!runtimeResult.verifyStartupStatus) {
711
- 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
+ };
712
756
  }
713
757
  const stability = await (0, startup_tui_1.pollDaemonStartup)({
714
758
  sendCommand: deps.sendCommand,
@@ -729,8 +773,12 @@ async function ensureDaemonRunning(deps, options = {}) {
729
773
  render: !deps.reportDaemonStartupPhase,
730
774
  });
731
775
  return {
776
+ ok: true,
732
777
  alreadyRunning: runtimeResult.alreadyRunning,
733
778
  message: runtimeResult.message,
779
+ verifyStartupStatus: runtimeResult.verifyStartupStatus,
780
+ startedPid: runtimeResult.startedPid,
781
+ startupFailureReason: null,
734
782
  stability,
735
783
  };
736
784
  }
@@ -750,6 +798,8 @@ async function ensureDaemonRunning(deps, options = {}) {
750
798
  const startupFailure = await waitForDaemonStartup(deps, {
751
799
  bootStartedAtMs,
752
800
  pid: lastPid,
801
+ serviceLabel: "new background service",
802
+ readLatestDaemonEvent: readLatestDaemonStartupEvent,
753
803
  });
754
804
  if (!startupFailure) {
755
805
  const stability = await (0, startup_tui_1.pollDaemonStartup)({
@@ -771,8 +821,10 @@ async function ensureDaemonRunning(deps, options = {}) {
771
821
  render: !deps.reportDaemonStartupPhase,
772
822
  });
773
823
  return {
824
+ ok: true,
774
825
  alreadyRunning: false,
775
826
  message: `daemon started (pid ${lastPid ?? "unknown"})`,
827
+ startupFailureReason: null,
776
828
  stability,
777
829
  };
778
830
  }
@@ -783,8 +835,10 @@ async function ensureDaemonRunning(deps, options = {}) {
783
835
  deps.reportDaemonStartupPhase?.("background service startup went sideways once; trying one more time");
784
836
  }
785
837
  return {
838
+ ok: false,
786
839
  alreadyRunning: false,
787
840
  message: formatDaemonStartupFailureMessage(lastPid, lastFailure, deps),
841
+ startupFailureReason: lastFailure.reason,
788
842
  };
789
843
  }
790
844
  function hasStartupHealthMonitor(deps) {
@@ -809,14 +863,14 @@ function hasFreshCurrentBootHealthSignal(deps, bootStartedAtMs, pid) {
809
863
  }
810
864
  function formatDaemonStartupFailureMessage(pid, failure, deps) {
811
865
  const lines = [
812
- `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}`,
813
867
  ];
814
868
  const recentLogLines = deps.readRecentDaemonLogLines?.(DEFAULT_DAEMON_STARTUP_LOG_LINES) ?? [];
815
869
  if (recentLogLines.length > 0) {
816
870
  lines.push("recent daemon logs:");
817
871
  lines.push(...recentLogLines.map((line) => ` ${line}`));
818
872
  }
819
- 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.");
820
874
  return lines.join("\n");
821
875
  }
822
876
  async function waitForDaemonStartup(deps, options) {
@@ -830,13 +884,17 @@ async function waitForDaemonStartup(deps, options) {
830
884
  let stableSinceMs = null;
831
885
  let sawSocket = false;
832
886
  if (!useHealthMonitor) {
833
- const verified = await verifyDaemonAlive(deps.checkSocketAlive, deps.socketPath, timeoutMs, pollIntervalMs, sleep, now);
834
- return verified
835
- ? null
836
- : {
837
- reason: `daemon failed to respond within ${Math.ceil(timeoutMs / 1000)}s`,
838
- retryable: false,
839
- };
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
+ };
840
898
  }
841
899
  while (now() < deadline) {
842
900
  await sleep(pollIntervalMs);
@@ -844,40 +902,37 @@ async function waitForDaemonStartup(deps, options) {
844
902
  if (!aliveNow) {
845
903
  if (sawSocket) {
846
904
  return {
847
- reason: "daemon socket disappeared during startup",
905
+ reason: `${options.serviceLabel} answered once and then disappeared during startup`,
848
906
  retryable: true,
849
907
  };
850
908
  }
909
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
910
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "waiting", latestEvent));
851
911
  continue;
852
912
  }
853
913
  if (!sawSocket) {
854
914
  sawSocket = true;
855
915
  stableSinceMs = now();
856
- deps.reportDaemonStartupPhase?.("verifying daemon health");
916
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
917
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "answering", latestEvent));
857
918
  }
858
919
  if (!hasFreshCurrentBootHealthSignal(deps, options.bootStartedAtMs, options.pid)) {
920
+ const latestEvent = options.readLatestDaemonEvent?.() ?? null;
921
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "answering", latestEvent));
859
922
  continue;
860
923
  }
924
+ deps.reportDaemonStartupPhase?.(formatDaemonStartupProgressLine(options.serviceLabel, "stabilizing"));
861
925
  if (stableSinceMs !== null && now() - stableSinceMs >= stabilityWindowMs) {
862
926
  return null;
863
927
  }
864
928
  }
865
929
  return {
866
930
  reason: sawSocket
867
- ? "daemon did not publish fresh health for the current boot attempt"
868
- : `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`,
869
933
  retryable: sawSocket,
870
934
  };
871
935
  }
872
- async function verifyDaemonAlive(checkSocketAlive, socketPath, maxWaitMs = 10_000, pollIntervalMs = 500, sleep = defaultSleep, now = Date.now) {
873
- const deadline = now() + maxWaitMs;
874
- while (now() < deadline) {
875
- await sleep(pollIntervalMs);
876
- if (await checkSocketAlive(socketPath))
877
- return true;
878
- }
879
- return false;
880
- }
881
936
  function defaultSleep(ms) {
882
937
  return new Promise((resolve) => setTimeout(resolve, ms));
883
938
  }
@@ -4144,22 +4199,19 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4144
4199
  if (command.noRepair) {
4145
4200
  writeProviderRepairSummary(deps, "Provider checks need attention", preflightProviderDegraded);
4146
4201
  const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
4147
- deps.writeStdout(message);
4148
- return message;
4202
+ return returnCliFailure(deps, message);
4149
4203
  }
4150
4204
  const repairResult = await runReadinessRepairForDegraded(preflightProviderDegraded, deps);
4151
4205
  if (!repairResult.repairsAttempted) {
4152
4206
  writeProviderRepairSummary(deps, "Provider checks still need attention", repairResult.remainingDegraded);
4153
4207
  const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
4154
- deps.writeStdout(message);
4155
- return message;
4208
+ return returnCliFailure(deps, message);
4156
4209
  }
4157
4210
  const remainingDegraded = repairResult.remainingDegraded;
4158
4211
  if (remainingDegraded.length > 0) {
4159
4212
  writeProviderRepairSummary(deps, "Still needs attention", remainingDegraded);
4160
4213
  const message = "daemon not started: provider checks still need repair.";
4161
- deps.writeStdout(message);
4162
- return message;
4214
+ return returnCliFailure(deps, message);
4163
4215
  }
4164
4216
  deps.writeStdout("All set. Provider checks recovered after repair.");
4165
4217
  }
@@ -4172,12 +4224,11 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4172
4224
  progress.announceStep?.(label);
4173
4225
  },
4174
4226
  }, { initialAlive: daemonAliveBeforeStart });
4175
- if (daemonResult.verifyStartupStatus === false) {
4227
+ if (!daemonResult.ok) {
4176
4228
  ;
4177
- progress.announceStep?.("replacement daemon did not answer in time");
4229
+ progress.failPhase?.("starting daemon", summarizeDaemonStartupFailure(daemonResult));
4178
4230
  progress.end();
4179
- deps.writeStdout(daemonResult.message);
4180
- return daemonResult.message;
4231
+ return returnCliFailure(deps, daemonResult.message);
4181
4232
  }
4182
4233
  progress.completePhase("starting daemon", daemonProgressSummary(daemonResult));
4183
4234
  if (!providerChecksAlreadyRun || daemonResult.alreadyRunning) {
@@ -4193,8 +4244,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
4193
4244
  progress.failPhase?.("final daemon check", finalDaemonCheck.summary);
4194
4245
  progress.end();
4195
4246
  const message = finalDaemonCheck.message ?? "background service stopped before boot finished";
4196
- deps.writeStdout(message);
4197
- return message;
4247
+ return returnCliFailure(deps, message);
4198
4248
  }
4199
4249
  progress.completePhase("final daemon check", finalDaemonCheck.summary);
4200
4250
  progress.end();
@@ -5706,17 +5756,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5706
5756
  }
5707
5757
  const fallbackMessage = response.summary ?? response.message ?? (response.ok ? "ok" : `error: ${response.error ?? "unknown error"}`);
5708
5758
  const message = command.kind === "daemon.status"
5709
- ? (() => {
5710
- const payload = (0, cli_render_1.parseStatusPayload)(response.data);
5711
- if (payload && ttyBoardEnabled(deps)) {
5712
- return (0, human_command_screens_1.renderHouseStatusScreen)({
5713
- payload,
5714
- isTTY: true,
5715
- columns: deps.stdoutColumns ?? process.stdout.columns,
5716
- }).trimEnd();
5717
- }
5718
- return (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage);
5719
- })()
5759
+ ? (0, cli_render_1.formatDaemonStatusOutput)(response, fallbackMessage)
5720
5760
  : fallbackMessage;
5721
5761
  deps.writeStdout(message);
5722
5762
  return message;
@@ -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",
@@ -7,7 +7,6 @@ exports.renderAgentPickerScreen = renderAgentPickerScreen;
7
7
  exports.resolveNamedAgentSelection = resolveNamedAgentSelection;
8
8
  exports.renderHumanReadinessBoard = renderHumanReadinessBoard;
9
9
  exports.renderHumanCommandBoard = renderHumanCommandBoard;
10
- exports.renderHouseStatusScreen = renderHouseStatusScreen;
11
10
  const runtime_1 = require("../../nerves/runtime");
12
11
  const terminal_ui_1 = require("./terminal-ui");
13
12
  function renderScreenEvent(screen) {
@@ -155,82 +154,3 @@ function renderHumanCommandBoard(options) {
155
154
  prompt: options.prompt,
156
155
  });
157
156
  }
158
- function renderHouseStatusScreen(options) {
159
- renderScreenEvent("house-status");
160
- const sections = [
161
- {
162
- title: "Runtime",
163
- lines: [
164
- `Daemon: ${options.payload.overview.daemon}`,
165
- `Health: ${options.payload.overview.health}`,
166
- `Outlook: ${options.payload.overview.outlookUrl}`,
167
- `Updated: ${options.payload.overview.lastUpdated}`,
168
- ],
169
- },
170
- ];
171
- if (options.payload.agents.length > 0) {
172
- sections.push({
173
- title: "Agents",
174
- lines: options.payload.agents.map((agent) => `${agent.name} — ${agent.enabled ? "enabled" : "disabled"}`),
175
- });
176
- }
177
- if (options.payload.providers.length > 0) {
178
- sections.push({
179
- title: "Providers",
180
- lines: options.payload.providers.map((provider) => {
181
- const detail = [provider.readiness, provider.detail, provider.source, provider.credential].filter(Boolean).join("; ");
182
- return `${provider.agent} ${provider.lane} — ${provider.provider} / ${provider.model}${detail ? ` — ${detail}` : ""}`;
183
- }),
184
- });
185
- }
186
- if (options.payload.senses.length > 0) {
187
- sections.push({
188
- title: "Senses",
189
- lines: options.payload.senses.map((sense) => {
190
- const status = sense.enabled ? sense.status : "disabled";
191
- return `${sense.agent} — ${sense.label ?? sense.sense} — ${status}${sense.detail ? ` — ${sense.detail}` : ""}`;
192
- }),
193
- });
194
- }
195
- if (options.payload.workers.length > 0) {
196
- sections.push({
197
- title: "Workers",
198
- lines: options.payload.workers.map((worker) => {
199
- const details = [`restarts: ${worker.restartCount}`];
200
- if (worker.pid !== null)
201
- details.unshift(`pid ${worker.pid}`);
202
- if (worker.lastExitCode !== null)
203
- details.push(`exit=${worker.lastExitCode}`);
204
- if (worker.lastSignal !== null)
205
- details.push(`signal=${worker.lastSignal}`);
206
- if (worker.errorReason)
207
- details.push(`error: ${worker.errorReason}`);
208
- if (worker.fixHint)
209
- details.push(`fix: ${worker.fixHint}`);
210
- return `${worker.agent} — ${worker.worker} — ${worker.status} — ${details.join("; ")}`;
211
- }),
212
- });
213
- }
214
- if (options.payload.sync.length > 0) {
215
- sections.push({
216
- title: "Git sync",
217
- lines: options.payload.sync.map((row) => {
218
- if (!row.enabled)
219
- return `${row.agent} — disabled`;
220
- if (row.gitInitialized === false)
221
- return `${row.agent} — needs git init`;
222
- if (row.remoteUrl)
223
- return `${row.agent} — ${row.remote} -> ${row.remoteUrl}`;
224
- return `${row.agent} — local only`;
225
- }),
226
- });
227
- }
228
- return renderHumanCommandBoard({
229
- title: "Ouro status",
230
- subtitle: "Current runtime status for this machine.",
231
- summary: "What is running, what is stopped, and what needs attention.",
232
- isTTY: options.isTTY,
233
- columns: options.columns,
234
- sections,
235
- });
236
- }
@@ -17,6 +17,7 @@ exports.renderWaitingForDaemon = renderWaitingForDaemon;
17
17
  exports.pollDaemonStartup = pollDaemonStartup;
18
18
  const cli_render_1 = require("./cli-render");
19
19
  const runtime_1 = require("../../nerves/runtime");
20
+ const terminal_ui_1 = require("./terminal-ui");
20
21
  // ── Constants ──
21
22
  const SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏";
22
23
  const STABILITY_THRESHOLD_MS = 5_000;
@@ -248,16 +249,7 @@ function colorStatus(status) {
248
249
  return `${statusColor}${status}${RESET}`;
249
250
  }
250
251
  function renderStartupLines(lines, prevLineCount, isTTY) {
251
- if (!isTTY)
252
- return lines.join("\n") + "\n";
253
- let output = "";
254
- if (prevLineCount > 0) {
255
- output += `\x1b[${prevLineCount}A`;
256
- }
257
- for (const line of lines) {
258
- output += `\x1b[2K${line}\n`;
259
- }
260
- return output;
252
+ return (0, terminal_ui_1.renderOverwriteFrame)(lines, prevLineCount, isTTY);
261
253
  }
262
254
  /* v8 ignore start -- process liveness check: uses real process.kill(0), tested via deployment @preserve */
263
255
  function defaultIsProcessAlive(pid) {
@@ -4,6 +4,7 @@ exports.stripAnsi = stripAnsi;
4
4
  exports.visibleLength = visibleLength;
5
5
  exports.padAnsi = padAnsi;
6
6
  exports.wrapPlain = wrapPlain;
7
+ exports.renderOverwriteFrame = renderOverwriteFrame;
7
8
  exports.renderOuroMasthead = renderOuroMasthead;
8
9
  exports.formatActionActorLabel = formatActionActorLabel;
9
10
  exports.renderTerminalBoard = renderTerminalBoard;
@@ -16,6 +17,7 @@ const SCALE = "\x1b[38;2;45;148;71m";
16
17
  const GLOW = "\x1b[38;2;74;227;108m";
17
18
  const BONE = "\x1b[38;2;237;242;238m";
18
19
  const MIST = "\x1b[38;2;154;174;159m";
20
+ const ALERT = "\x1b[38;2;255;106;106m";
19
21
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
20
22
  const MASTHEAD_WORD = "OUROBOROS";
21
23
  const CLASSIC_WORDMARK_GLYPHS = {
@@ -71,6 +73,25 @@ function boardWidth(columns) {
71
73
  const requested = columns ?? 88;
72
74
  return Math.max(58, Math.min(requested, 96));
73
75
  }
76
+ function renderOverwriteFrame(lines, prevLineCount, isTTY) {
77
+ if (!isTTY)
78
+ return `${lines.join("\n")}\n`;
79
+ let output = "";
80
+ if (prevLineCount > 0) {
81
+ output += `\x1b[${prevLineCount}A`;
82
+ }
83
+ for (const line of lines) {
84
+ output += `\x1b[2K${line}\n`;
85
+ }
86
+ const extraLineCount = Math.max(0, prevLineCount - lines.length);
87
+ for (let i = 0; i < extraLineCount; i++) {
88
+ output += "\x1b[2K\n";
89
+ }
90
+ if (extraLineCount > 0) {
91
+ output += `\x1b[${extraLineCount}A`;
92
+ }
93
+ return output;
94
+ }
74
95
  function renderPanelTTY(title, lines, width) {
75
96
  const innerWidth = Math.max(8, width - 4);
76
97
  const topPrefix = `╭─ ${title} `;
@@ -94,15 +115,18 @@ function renderPanelPlain(title, lines) {
94
115
  ];
95
116
  }
96
117
  function mastheadArt(columns) {
97
- if ((columns ?? 88) >= 74) {
98
- const rows = Array.from({ length: 5 }, () => []);
99
- for (const letter of MASTHEAD_WORD.split("")) {
100
- const glyph = CLASSIC_WORDMARK_GLYPHS[letter];
101
- for (const [index, line] of glyph.entries()) {
102
- rows[index].push(line);
103
- }
118
+ const rows = Array.from({ length: 5 }, () => []);
119
+ for (const letter of MASTHEAD_WORD.split("")) {
120
+ const glyph = CLASSIC_WORDMARK_GLYPHS[letter];
121
+ for (const [index, line] of glyph.entries()) {
122
+ rows[index].push(line);
104
123
  }
105
- return rows.map((row) => row.join(" "));
124
+ }
125
+ const classicWordmark = rows.map((row) => row.join(" "));
126
+ const availableColumns = columns ?? 88;
127
+ const classicWidth = Math.max(...classicWordmark.map((line) => line.length));
128
+ if (availableColumns >= classicWidth) {
129
+ return classicWordmark;
106
130
  }
107
131
  return [MASTHEAD_WORD];
108
132
  }
@@ -182,7 +206,58 @@ function formatOperationStep(step) {
182
206
  const detail = step.detail ? ` — ${step.detail}` : "";
183
207
  return `${marker} ${step.label}${detail}`;
184
208
  }
209
+ function operationMarkerTone(status) {
210
+ switch (status) {
211
+ case "done":
212
+ return GLOW;
213
+ case "active":
214
+ return BONE;
215
+ case "failed":
216
+ return ALERT;
217
+ case "pending":
218
+ default:
219
+ return MIST;
220
+ }
221
+ }
222
+ function renderOperationStepTTY(step) {
223
+ const marker = step.status === "done"
224
+ ? "✓"
225
+ : step.status === "failed"
226
+ ? "✗"
227
+ : step.status === "active"
228
+ ? "→"
229
+ : "○";
230
+ const label = color(step.label, step.status === "pending" ? MIST : BONE, step.status !== "pending");
231
+ const detail = step.detail ? ` ${color(`— ${step.detail}`, MIST)}` : "";
232
+ return `${color(marker, operationMarkerTone(step.status), true)} ${label}${detail}`;
233
+ }
234
+ function renderOperationSectionTTY(title, lines, width) {
235
+ const rule = "─".repeat(Math.max(8, width - title.length - 3));
236
+ return [
237
+ `${color("─ ", CANOPY)}${color(title, BONE, true)} ${color(rule, CANOPY)}`,
238
+ ...lines.map((line) => ` ${line}`),
239
+ ];
240
+ }
241
+ function renderOperationSectionPlain(title, lines) {
242
+ return [
243
+ title,
244
+ ...lines.map((line) => ` ${plainLine(line)}`),
245
+ ];
246
+ }
185
247
  function renderTerminalOperation(options) {
248
+ if (!options.suppressEvent) {
249
+ (0, runtime_1.emitNervesEvent)({
250
+ component: "daemon",
251
+ event: "daemon.terminal_operation_rendered",
252
+ message: "rendered terminal operation surface",
253
+ meta: {
254
+ title: options.title,
255
+ steps: options.steps?.length ?? 0,
256
+ hasCurrentStep: !!options.currentStep,
257
+ tty: options.isTTY,
258
+ },
259
+ });
260
+ }
186
261
  const steps = options.steps ?? [];
187
262
  const currentLines = options.currentStep
188
263
  ? [
@@ -191,25 +266,34 @@ function renderTerminalOperation(options) {
191
266
  ]
192
267
  : ["Standing by."];
193
268
  const progressLines = steps.length > 0
194
- ? steps.map((step) => formatOperationStep(step))
269
+ ? options.isTTY
270
+ ? steps.map((step) => renderOperationStepTTY(step))
271
+ : steps.map((step) => formatOperationStep(step))
195
272
  : ["No active steps yet."];
196
- return renderTerminalBoard({
273
+ const width = boardWidth(options.columns);
274
+ const blocks = [];
275
+ blocks.push(renderOuroMasthead({
197
276
  isTTY: options.isTTY,
198
- columns: options.columns,
199
- masthead: options.masthead,
200
- title: options.title,
201
- summary: options.summary,
202
- sections: [
203
- {
204
- title: options.currentTitle ?? "Right now",
205
- lines: currentLines,
206
- },
207
- {
208
- title: options.stepsTitle ?? "Progress",
209
- lines: progressLines,
210
- },
211
- ],
212
- prompt: options.prompt,
213
- suppressEvent: options.suppressEvent,
214
- });
277
+ columns: width,
278
+ subtitle: options.masthead?.subtitle,
279
+ }).trimEnd());
280
+ const introLines = [
281
+ options.isTTY ? color(options.title, BONE, true) : options.title,
282
+ ...(options.summary
283
+ ? wrapPlain(options.summary, Math.max(20, width - 2)).map((line) => options.isTTY ? color(line, MIST) : line)
284
+ : []),
285
+ ];
286
+ blocks.push(introLines.join("\n"));
287
+ const renderedSteps = options.isTTY
288
+ ? renderOperationSectionTTY(options.stepsTitle ?? "Checklist", progressLines, width)
289
+ : renderOperationSectionPlain(options.stepsTitle ?? "Checklist", progressLines);
290
+ const renderedCurrent = options.isTTY
291
+ ? renderOperationSectionTTY(options.currentTitle ?? "Current work", currentLines.map((line, index) => index === 0 ? color(line, BONE, true) : color(line, MIST)), width)
292
+ : renderOperationSectionPlain(options.currentTitle ?? "Current work", currentLines);
293
+ blocks.push(renderedSteps.join("\n"));
294
+ blocks.push(renderedCurrent.join("\n"));
295
+ if (options.prompt) {
296
+ blocks.push(options.isTTY ? color(options.prompt, BONE, true) : options.prompt);
297
+ }
298
+ return `${blocks.join("\n\n")}\n`;
215
299
  }
@@ -219,19 +219,7 @@ class UpProgress {
219
219
  return "";
220
220
  }
221
221
  const lines = this.renderLines(now);
222
- let output = "";
223
- if (this.prevLineCount > 0) {
224
- output += `\x1b[${this.prevLineCount}A`;
225
- }
226
- for (const line of lines) {
227
- output += `\x1b[2K${line}\n`;
228
- }
229
- // Clear any leftover lines from previous render that are no longer needed
230
- if (lines.length < this.prevLineCount) {
231
- for (let i = 0; i < this.prevLineCount - lines.length; i++) {
232
- output += `\x1b[2K\n`;
233
- }
234
- }
222
+ const output = (0, terminal_ui_1.renderOverwriteFrame)(lines, this.prevLineCount, true);
235
223
  this.prevLineCount = lines.length;
236
224
  return output;
237
225
  }
@@ -301,14 +289,8 @@ class UpProgress {
301
289
  }
302
290
  renderUpScreen(now) {
303
291
  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
- });
292
+ const completedByLabel = new Map(this.completed.map((phase) => [phase.label, phase]));
293
+ const steps = [];
312
294
  let currentStepLabel = this.completed.some((phase) => phase.status === "failure")
313
295
  ? "Boot paused."
314
296
  : this.completed.length > 0
@@ -322,19 +304,44 @@ class UpProgress {
322
304
  const spinner = SPINNER_FRAMES[frameIndex];
323
305
  currentStepLabel = `${spinner} ${this.renderUpStepLabel(this.currentPhase.label)} (${elapsedSec}s)`;
324
306
  currentStepDetails = splitDetailLines(this.currentPhase.detail);
325
- steps.push({
326
- label: this.renderUpStepLabel(this.currentPhase.label),
327
- status: "active",
328
- });
329
- seenLabels.add(this.currentPhase.label);
330
307
  }
331
308
  for (const label of this.upPhasePlan) {
332
- if (!seenLabels.has(label)) {
309
+ seenLabels.add(label);
310
+ const completedPhase = completedByLabel.get(label);
311
+ if (completedPhase) {
312
+ steps.push({
313
+ label: this.renderUpStepLabel(label),
314
+ status: completedPhase.status === "failure" ? "failed" : "done",
315
+ detail: completedPhase.detail,
316
+ });
317
+ continue;
318
+ }
319
+ if (this.currentPhase?.label === label) {
333
320
  steps.push({
334
321
  label: this.renderUpStepLabel(label),
335
- status: "pending",
322
+ status: "active",
336
323
  });
324
+ continue;
337
325
  }
326
+ steps.push({
327
+ label: this.renderUpStepLabel(label),
328
+ status: "pending",
329
+ });
330
+ }
331
+ for (const phase of this.completed) {
332
+ if (!seenLabels.has(phase.label)) {
333
+ steps.push({
334
+ label: this.renderUpStepLabel(phase.label),
335
+ status: phase.status === "failure" ? "failed" : "done",
336
+ detail: phase.detail,
337
+ });
338
+ }
339
+ }
340
+ if (this.currentPhase && !seenLabels.has(this.currentPhase.label)) {
341
+ steps.push({
342
+ label: this.renderUpStepLabel(this.currentPhase.label),
343
+ status: "active",
344
+ });
338
345
  }
339
346
  return (0, terminal_ui_1.renderTerminalOperation)({
340
347
  isTTY: true,
@@ -342,8 +349,8 @@ class UpProgress {
342
349
  masthead: {
343
350
  subtitle: "Booting the local agent runtime.",
344
351
  },
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.",
352
+ title: "Starting Ouro",
353
+ summary: "Ouro is bringing the local agent runtime online and will stop here if anything needs attention.",
347
354
  currentStep: {
348
355
  label: currentStepLabel,
349
356
  detailLines: currentStepDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.443",
3
+ "version": "0.1.0-alpha.445",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",