@ouro.bot/cli 0.1.0-alpha.515 → 0.1.0-alpha.517

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,28 @@
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.517",
6
+ "changes": [
7
+ "Layer 4 of the harness-hardening sequence (PR 2 of 4 in 1→4→2→3 from `docs/planning/2026-04-28-1900-planning-harness-hardening-and-repairguide.md`). Detects per-lane drift between each agent's intent (committed `agent.json`) and the observed binding on this machine (`state/providers.json`), surfaces the drift through the existing `EffectiveProviderReadiness.reason: \"provider-model-changed\"` vocabulary, and emits a copy-pasteable `ouro use` repair proposal. Read-only: the PR never mutates `state/providers.json` and never invokes the `ouro use` CLI surface.",
8
+ "New module `src/heart/daemon/drift-detection.ts`. `detectProviderBindingDrift(input)` is a pure intent-vs-observed comparator — it tolerates legacy `humanFacing`/`agentFacing` AND new `outward`/`inner` keys in `agent.json` with a 'new key wins, fall back to legacy' precedence rule (the rename is in flight; mixed `agent.json` files must work). `loadDriftInputsForAgent(bundlesRoot, agentName)` is the I/O wrapper that reads both files off disk, mapping missing/invalid `state/providers.json` to a `null` providerState (the comparator interprets `null` as 'no observation, nothing to drift against' — fresh-install case).",
9
+ "`checkAgentConfigWithProviderHealth` gains an additive optional `driftFindings: DriftFinding[]` field on `ConfigCheckResult`. Drift detection runs once after state setup so findings ride along with both success and failure return paths. Drift is advisory and never flips `ok` to false. Non-breaking: 7 existing tests using strict `toEqual({ok:true})` were loosened to `toMatchObject({ok:true})` to accept the additive field — no behavior change.",
10
+ "`computeDaemonRollup` (Layer 1) gains an optional `driftDetected: boolean`. When true, `healthy` → `partial` (same downgrade rule as `bootstrapDegraded`). `degraded` and `safe-mode` rollups are unaffected — drift never escalates past `partial` and never un-downgrades. `daemon-entry.ts` probes each enabled agent for drift before computing the rollup; a single agent's read failure is best-effort and does not block the scan.",
11
+ "`buildInnerStatusOutput` gains an optional `driftFindings` field and renders a `drift advisory` section per finding (lane, intent vs observed, copy-pasteable `ouro use`). `cli-exec.ts` adds `collectAgentDriftAdvisories` + `writeDriftAdvisorySummary` helpers; wired into the `--no-repair` boot path (preflight provider-degraded, post-startup degraded, AND the all-clear path) and the `inner.status` command. Operators see drift advisories without running `ouro inner status` per agent.",
12
+ "Daemon-wide drift visibility (post-review fix from ouroboros): `DaemonHealthState` gains a required `drift: DriftFinding[]` field, populated by `buildDaemonHealthState` from the per-agent drift probe. `renderRollupStatusLine`'s `partial` branch now distinguishes three sub-cases — agents-unhealthy-only, drift-only, and both — so a drift-induced `partial` rollup carries a clear cause rather than the misleading 'some agents unhealthy' copy. `readHealth` tolerates legacy cached health files missing the field (defaults to `[]`).",
13
+ "9874 tests pass. 100% coverage on all new and changed source files. Typecheck clean. Lint clean. Layer 3 (RepairGuide) consumes the `driftFindings` array surfaced here — Layer 3 is where drift findings cease to be advisory and become actionable. Layer 2 (sync probe) is independent of this PR."
14
+ ]
15
+ },
16
+ {
17
+ "version": "0.1.0-alpha.516",
18
+ "changes": [
19
+ "Layer 1 of the harness-hardening sequence (1→4→2→3 from `docs/planning/2026-04-28-1900-planning-harness-hardening-and-repairguide.md`). Replaces the daemon-wide rollup at `daemon-entry.ts` (the binary `degraded.length > 0 ? \"degraded\" : \"ok\"` literal) with a five-state vocabulary: `healthy / partial / degraded / safe-mode / down`. A single sick agent no longer tips the whole daemon to `degraded`.",
20
+ "Type structure: `RollupStatus` (4-state, returned by the new pure `computeDaemonRollup` decision function in `daemon-rollup.ts`) and `DaemonStatus = RollupStatus | \"down\"` (full daemon-status; `down` is caller-owned because it represents pre-inventory failure, before the rollup is reachable). Both unions project from a single source-of-truth literal tuple so future widening touches one site.",
21
+ "`renderRollupStatusLine` in `cli-render.ts` uses a compiler-forced `never`-typed exhaustive switch — adding a future state compile-errors at every consumer using the pattern. The `degraded` literal carries three copy variants picked by inspecting cached agent statuses: empty map (fresh install, prompts `ouro hatch`), non-empty + any running agent (legacy stale cache from pre-Layer-1 daemons, prompts `ouro up` refresh), non-empty + zero running (all-failed live-check, prompts `ouro doctor`).",
22
+ "`runtime-readers.ts:readDaemonHealthDeep` parse tightened to use `isDaemonStatus`. `OutlookDaemonHealthDeep.status` widened to `DaemonStatus | \"unknown\"` so legacy serialized strings (`\"running\"`, `\"ok\"`) coerce defensively rather than failing the parse during rollout.",
23
+ "9759 tests pass (508 test files); coverage gate clean. The per-agent live-check loop in `cli-exec.ts` is intentionally untouched — it was already try/catch-isolated; the bug was in how its output rolled up. Subsequent PRs (layers 4, 2, 3) build on this PR's vocabulary."
24
+ ]
25
+ },
4
26
  {
5
27
  "version": "0.1.0-alpha.515",
6
28
  "changes": [
@@ -47,6 +47,7 @@ const provider_credentials_1 = require("../provider-credentials");
47
47
  const vault_unlock_1 = require("../../repertoire/vault-unlock");
48
48
  const readiness_repair_1 = require("./readiness-repair");
49
49
  const provider_ping_progress_1 = require("./provider-ping-progress");
50
+ const drift_detection_1 = require("./drift-detection");
50
51
  function isAgentProvider(value) {
51
52
  return Object.prototype.hasOwnProperty.call(identity_1.PROVIDER_CREDENTIALS, value);
52
53
  }
@@ -387,6 +388,26 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
387
388
  return stateResult.result;
388
389
  if (stateResult.disabled)
389
390
  return { ok: true };
391
+ // Layer 4 drift detection. Runs once per call after state setup so that
392
+ // drift findings ride along regardless of ping outcome — consumers
393
+ // (Layer 4 rollup, Layer 3 RepairGuide) want to see drift even when a
394
+ // live-check is failing for unrelated reasons. Drift detection is pure
395
+ // and never throws on a malformed agent.json: any read error here would
396
+ // indicate the bundle disappeared between state setup and now (a
397
+ // race), which we surface as `[]` rather than a hard failure.
398
+ let driftFindings = [];
399
+ try {
400
+ const inputs = (0, drift_detection_1.loadDriftInputsForAgent)(bundlesRoot, agentName);
401
+ driftFindings = (0, drift_detection_1.detectProviderBindingDrift)({
402
+ agentName,
403
+ agentJson: inputs.agentJson,
404
+ providerState: inputs.providerState,
405
+ });
406
+ }
407
+ catch {
408
+ /* v8 ignore next 1 -- defensive race-window guard: agent.json went away after state setup. Tested via Unit 5 integration when at all. @preserve */
409
+ driftFindings = [];
410
+ }
390
411
  deps.onProgress?.(selectedProviderPlan(agentName, stateResult.state));
391
412
  const ping = deps.pingProvider ?? (await Promise.resolve().then(() => __importStar(require("../provider-ping")))).pingProvider;
392
413
  const shouldRecordReadiness = deps.recordReadiness ?? true;
@@ -402,16 +423,16 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
402
423
  const binding = stateResult.state.lanes[lane];
403
424
  if (!poolResult.ok) {
404
425
  if (poolResult.reason === "missing") {
405
- return missingCredentialResult(agentName, lane, binding.provider, binding.model, poolResult.poolPath);
426
+ return { ...missingCredentialResult(agentName, lane, binding.provider, binding.model, poolResult.poolPath), driftFindings };
406
427
  }
407
- return invalidPoolResult(agentName, lane, binding.provider, binding.model, {
408
- ...poolResult,
409
- reason: poolResult.reason,
410
- });
428
+ return { ...invalidPoolResult(agentName, lane, binding.provider, binding.model, {
429
+ ...poolResult,
430
+ reason: poolResult.reason,
431
+ }), driftFindings };
411
432
  }
412
433
  const record = credentialRecordForLane(poolResult.pool, binding.provider);
413
434
  if (!record) {
414
- return missingCredentialResult(agentName, lane, binding.provider, binding.model, poolResult.poolPath);
435
+ return { ...missingCredentialResult(agentName, lane, binding.provider, binding.model, poolResult.poolPath), driftFindings };
415
436
  }
416
437
  const key = `${binding.provider}\0${binding.model}\0${record.revision}`;
417
438
  const group = pingGroups.get(key);
@@ -475,7 +496,7 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
475
496
  }
476
497
  }
477
498
  if (firstFailure)
478
- return firstFailure;
499
+ return { ...firstFailure, driftFindings };
479
500
  (0, runtime_1.emitNervesEvent)({
480
501
  component: "daemon",
481
502
  event: "daemon.agent_config_valid",
@@ -484,7 +505,8 @@ async function checkAgentConfigWithProviderHealth(agentName, bundlesRoot, deps =
484
505
  agent: agentName,
485
506
  providers: [...new Set([...pingGroups.values()].map((group) => group.provider))],
486
507
  liveProviderCheck: true,
508
+ driftCount: driftFindings.length,
487
509
  },
488
510
  });
489
- return { ok: true };
511
+ return { ok: true, driftFindings };
490
512
  }
@@ -40,6 +40,8 @@ var __importStar = (this && this.__importStar) || (function () {
40
40
  })();
41
41
  Object.defineProperty(exports, "__esModule", { value: true });
42
42
  exports.summarizeDaemonStartupFailure = summarizeDaemonStartupFailure;
43
+ exports.writeDriftAdvisorySummary = writeDriftAdvisorySummary;
44
+ exports.collectAgentDriftAdvisories = collectAgentDriftAdvisories;
43
45
  exports.mergeStartupStability = mergeStartupStability;
44
46
  exports.ensureDaemonRunning = ensureDaemonRunning;
45
47
  exports.listGithubCopilotModels = listGithubCopilotModels;
@@ -94,6 +96,7 @@ const cli_help_1 = require("./cli-help");
94
96
  const cli_render_1 = require("./cli-render");
95
97
  const cli_defaults_1 = require("./cli-defaults");
96
98
  const agent_config_check_1 = require("./agent-config-check");
99
+ const drift_detection_1 = require("./drift-detection");
97
100
  const doctor_1 = require("./doctor");
98
101
  const cli_render_doctor_1 = require("./cli-render-doctor");
99
102
  const interactive_repair_1 = require("./interactive-repair");
@@ -408,6 +411,61 @@ function writeProviderRepairSummary(deps, title, degraded) {
408
411
  const blocks = degraded.map((entry) => (0, readiness_repair_1.renderReadinessIssueNextSteps)(readinessIssueFromDegraded(entry)).join("\n"));
409
412
  deps.writeStdout([title, ...blocks].join("\n\n"));
410
413
  }
414
+ /**
415
+ * Layer 4: render a per-agent drift advisory block to stdout. Called from
416
+ * the `--no-repair` summary path when one or more enabled agents have a
417
+ * mismatch between `agent.json` (intent) and `state/providers.json`
418
+ * (observation). The block surfaces the lane, intent vs observed
419
+ * binding, and the copy-pasteable `ouro use` repair command per finding.
420
+ *
421
+ * Drift is advisory: this helper only PRINTS the advisory; running the
422
+ * repair command is the operator's call (and is Layer 3 RepairGuide's
423
+ * domain when the daemon does it automatically).
424
+ *
425
+ * Exported for direct unit-testing; the production callers are
426
+ * inside this module.
427
+ */
428
+ function writeDriftAdvisorySummary(deps, advisories) {
429
+ if (advisories.length === 0)
430
+ return;
431
+ const lines = ["Drift advisory: agent intent does not match this machine's observed binding"];
432
+ for (const advisory of advisories) {
433
+ lines.push("");
434
+ lines.push(` ${advisory.agent} (${advisory.lane}):`);
435
+ lines.push(` intent: ${advisory.intentProvider}/${advisory.intentModel}`);
436
+ lines.push(` observed: ${advisory.observedProvider}/${advisory.observedModel}`);
437
+ lines.push(` repair: ${advisory.repairCommand}`);
438
+ }
439
+ deps.writeStdout(lines.join("\n"));
440
+ }
441
+ /**
442
+ * Collect drift findings across every enabled agent in the bundles
443
+ * directory. Used by the `--no-repair` summary path so that drift
444
+ * advisories ride along with (or stand alone in place of) the existing
445
+ * provider-repair summary. Errors during a single agent's load (e.g.
446
+ * malformed `agent.json`) are swallowed: drift detection is advisory
447
+ * and must not block the rest of the boot path.
448
+ */
449
+ async function collectAgentDriftAdvisories(deps) {
450
+ const agents = await listCliAgents(deps);
451
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
452
+ const findings = [];
453
+ for (const agent of [...new Set(agents)]) {
454
+ try {
455
+ const inputs = (0, drift_detection_1.loadDriftInputsForAgent)(bundlesRoot, agent);
456
+ const agentFindings = (0, drift_detection_1.detectProviderBindingDrift)({
457
+ agentName: agent,
458
+ agentJson: inputs.agentJson,
459
+ providerState: inputs.providerState,
460
+ });
461
+ findings.push(...agentFindings);
462
+ }
463
+ catch {
464
+ // Best-effort: a per-agent read failure is not blocking.
465
+ }
466
+ }
467
+ return findings;
468
+ }
411
469
  function providerRepairCountSummary(count) {
412
470
  if (count === 0)
413
471
  return "selected providers answered live checks";
@@ -5661,6 +5719,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5661
5719
  progress.end();
5662
5720
  if (command.noRepair) {
5663
5721
  writeProviderRepairSummary(deps, "Provider checks need attention", preflightProviderDegraded);
5722
+ // Layer 4: drift advisories ride along with the provider-repair
5723
+ // summary under --no-repair. Non-blocking; failure to collect
5724
+ // findings (e.g. malformed agent.json on one bundle) is swallowed
5725
+ // by `collectAgentDriftAdvisories` so the rest of the boot path
5726
+ // is unaffected.
5727
+ const driftAdvisories = await collectAgentDriftAdvisories(deps);
5728
+ writeDriftAdvisorySummary(deps, driftAdvisories);
5664
5729
  const message = "daemon not started: provider checks need repair. Run `ouro repair` or rerun `ouro up` to choose a repair path.";
5665
5730
  return returnCliFailure(deps, message);
5666
5731
  }
@@ -5716,12 +5781,19 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5716
5781
  if (command.noRepair) {
5717
5782
  // --no-repair: write degraded summary and skip interactive repair
5718
5783
  writeProviderRepairSummary(deps, "Provider checks need attention", daemonResult.stability.degraded);
5784
+ // Layer 4: drift advisories ride along with the post-startup
5785
+ // degraded summary too — same rationale as the preflight path.
5786
+ const driftAdvisories = await collectAgentDriftAdvisories(deps);
5787
+ writeDriftAdvisorySummary(deps, driftAdvisories);
5719
5788
  (0, runtime_1.emitNervesEvent)({
5720
5789
  level: "warn",
5721
5790
  component: "daemon",
5722
5791
  event: "daemon.no_repair_degraded_summary",
5723
5792
  message: "degraded agents detected with --no-repair, skipping interactive repair",
5724
- meta: { degradedCount: daemonResult.stability.degraded.length },
5793
+ meta: {
5794
+ degradedCount: daemonResult.stability.degraded.length,
5795
+ driftCount: driftAdvisories.length,
5796
+ },
5725
5797
  });
5726
5798
  }
5727
5799
  else {
@@ -5796,6 +5868,13 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
5796
5868
  }
5797
5869
  }
5798
5870
  }
5871
+ else if (command.noRepair) {
5872
+ // Layer 4: no degraded agents to summarize, but --no-repair still
5873
+ // surfaces drift advisories so the operator sees them without
5874
+ // having to run `ouro inner status` per agent.
5875
+ const driftAdvisories = await collectAgentDriftAdvisories(deps);
5876
+ writeDriftAdvisorySummary(deps, driftAdvisories);
5877
+ }
5799
5878
  // Persist boot startup AFTER daemon is running — bootstrap is safe now
5800
5879
  // because the daemon socket exists, so launchd's KeepAlive registers
5801
5880
  // for crash recovery without starting a competing process.
@@ -6903,6 +6982,22 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6903
6982
  catch { /* no habits — heartbeat unknown */ }
6904
6983
  // Attention count
6905
6984
  const activeObligations = listActiveReturnObligations(command.agent);
6985
+ // Layer 4 drift findings for this agent. Best-effort: if agent.json
6986
+ // is unreadable mid-call we just suppress the advisory rather than
6987
+ // failing the inner-status render.
6988
+ let driftFindings = [];
6989
+ try {
6990
+ const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
6991
+ const inputs = (0, drift_detection_1.loadDriftInputsForAgent)(bundlesRoot, command.agent);
6992
+ driftFindings = (0, drift_detection_1.detectProviderBindingDrift)({
6993
+ agentName: command.agent,
6994
+ agentJson: inputs.agentJson,
6995
+ providerState: inputs.providerState,
6996
+ });
6997
+ }
6998
+ catch {
6999
+ driftFindings = [];
7000
+ }
6906
7001
  const message = buildInnerStatusOutput({
6907
7002
  agentName: command.agent,
6908
7003
  runtimeState,
@@ -6910,6 +7005,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
6910
7005
  heartbeat,
6911
7006
  attentionCount: activeObligations.length,
6912
7007
  now: Date.now(),
7008
+ driftFindings,
6913
7009
  });
6914
7010
  deps.writeStdout(message);
6915
7011
  return message;
@@ -45,6 +45,7 @@ exports.formatTable = formatTable;
45
45
  exports.formatDaemonStatusOutput = formatDaemonStatusOutput;
46
46
  exports.formatVersionOutput = formatVersionOutput;
47
47
  exports.buildStoppedStatusPayload = buildStoppedStatusPayload;
48
+ exports.renderRollupStatusLine = renderRollupStatusLine;
48
49
  exports.daemonUnavailableStatusOutput = daemonUnavailableStatusOutput;
49
50
  exports.isDaemonUnavailableError = isDaemonUnavailableError;
50
51
  exports.formatMcpResponse = formatMcpResponse;
@@ -472,6 +473,93 @@ function buildStoppedStatusPayload(socketPath, syncRows = [], agentRows = []) {
472
473
  providers: [],
473
474
  };
474
475
  }
476
+ /**
477
+ * Render the cached daemon-rollup status as a one-line string for the
478
+ * "daemon not running" view. Each `DaemonStatus` literal maps to a
479
+ * label + a brief explanatory copy fragment. The default branch is
480
+ * `never`-typed so future widening of `DaemonStatus` compile-errors
481
+ * here — Layer 1's compiler-forced exhaustiveness contract.
482
+ *
483
+ * The `degraded` literal splits into two copy variants based on the
484
+ * cached health file's `agents` map:
485
+ * - empty map → "no agents configured" (fresh-install copy).
486
+ * - non-empty map → "none ready" (all-agents-failed-live-check copy).
487
+ *
488
+ * The split lives at the render layer (not in the rollup status itself)
489
+ * so the same status can carry distinct UX copy without inflating the
490
+ * type union.
491
+ */
492
+ function renderRollupStatusLine(health) {
493
+ const status = health.status;
494
+ const tail = `(pid ${health.pid}, uptime ${health.uptimeSeconds}s)`;
495
+ /* v8 ignore next -- v8 instruments the switch statement itself as a branch; the never-typed default below is unreachable by construction so v8 cannot observe its branch firing @preserve */
496
+ switch (status) {
497
+ case "healthy":
498
+ return `Last known status: healthy ${tail}`;
499
+ case "partial": {
500
+ // `partial` arises from two independent sources:
501
+ // (a) some enabled agents not in `running` state, or
502
+ // (b) Layer-4 drift between agent.json (intent) and state/providers.json (observed).
503
+ // Either or both can be true. Render mentions both when applicable so
504
+ // the operator isn't misled into thinking agents are unhealthy when the
505
+ // only anomaly is configuration drift.
506
+ const unhealthyCount = Object.values(health.agents).filter((agent) => agent.status !== "running").length;
507
+ const driftCount = (health.drift ?? []).length;
508
+ const parts = [];
509
+ if (unhealthyCount > 0) {
510
+ parts.push(`${unhealthyCount} agent${unhealthyCount === 1 ? "" : "s"} unhealthy`);
511
+ }
512
+ if (driftCount > 0) {
513
+ parts.push(`drift on ${driftCount} agent${driftCount === 1 ? "" : "s"}`);
514
+ }
515
+ // Fallback for legacy cached files: status="partial" with no
516
+ // visible cause (no unhealthy agents AND no drift array). This
517
+ // happens when the file pre-dates Layer 4 and the daemon flagged
518
+ // partial via some signal that's no longer expressible. Prompt
519
+ // refresh via `ouro up` rather than asserting a specific cause.
520
+ const detail = parts.length > 0
521
+ ? ` — ${parts.join("; ")}`
522
+ : " — stale cache, run `ouro up` to refresh";
523
+ return `Last known status: partial${detail} ${tail}`;
524
+ }
525
+ case "degraded": {
526
+ // Three-way copy split based on the cached agents map:
527
+ // - empty map → fresh install / no agents configured.
528
+ // - non-empty + any agent reports "running" → legacy stale cache:
529
+ // pre-Layer-1, status="degraded" meant "any sick component," so a
530
+ // running agent could coexist with a degraded daemon. Post-Layer-1,
531
+ // degraded means "zero serving" — mutually exclusive with a running
532
+ // agent. A live disagreement therefore implies the cache pre-dates
533
+ // the rollup-semantics fix; prompt for `ouro up` to refresh rather
534
+ // than falsely claim "none ready."
535
+ // - non-empty + zero agents reporting "running" → all-failed copy.
536
+ const agentEntries = Object.values(health.agents);
537
+ if (agentEntries.length === 0) {
538
+ return `Last known status: degraded — no agents configured (run \`ouro hatch\` to add one) ${tail}`;
539
+ }
540
+ const anyRunning = agentEntries.some((agent) => agent.status === "running");
541
+ if (anyRunning) {
542
+ return `Last known status: degraded — stale cache, run \`ouro up\` to refresh ${tail}`;
543
+ }
544
+ return `Last known status: degraded — agents configured but none ready (run \`ouro doctor\`) ${tail}`;
545
+ }
546
+ case "safe-mode":
547
+ return `Last known status: safe-mode — crash loop tripped ${tail}`;
548
+ case "down":
549
+ return `Last known status: down ${tail}`;
550
+ /* v8 ignore start -- compiler-forced exhaustiveness: the never-typed default branch is unreachable by construction; if DaemonStatus widens, tsc errors at the assignment before the throw can run @preserve */
551
+ default: {
552
+ // Compiler-forced exhaustiveness. If DaemonStatus grows a new
553
+ // literal, this `never` cast errors at tsc, forcing every
554
+ // consumer to handle it explicitly. NEVER replace this with a
555
+ // permissive `default:` returning a fallback string — that's
556
+ // exactly how the old "ok | degraded" semantics leaked through.
557
+ const _exhaustive = status;
558
+ throw new Error(`unhandled daemon status: ${_exhaustive}`);
559
+ }
560
+ /* v8 ignore stop */
561
+ }
562
+ }
475
563
  function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
476
564
  // Read per-agent sync config and bundle list from disk so the user still
477
565
  // sees them when the daemon is down. Best-effort: any fs error returns []
@@ -515,7 +603,7 @@ function daemonUnavailableStatusOutput(socketPath, healthFilePath) {
515
603
  const resolvedHealthPath = healthFilePath ?? (0, daemon_health_1.getDefaultHealthPath)();
516
604
  const health = (0, daemon_health_1.readHealth)(resolvedHealthPath);
517
605
  if (health) {
518
- lines.push(`Last known status: ${health.status} (pid ${health.pid}, uptime ${health.uptimeSeconds}s)`);
606
+ lines.push(renderRollupStatusLine(health));
519
607
  if (health.safeMode?.active) {
520
608
  lines.push(`SAFE MODE: ${health.safeMode.reason}`);
521
609
  }
@@ -43,6 +43,7 @@ const index_1 = require("../../nerves/index");
43
43
  const message_router_1 = require("./message-router");
44
44
  const health_monitor_1 = require("./health-monitor");
45
45
  const daemon_health_1 = require("./daemon-health");
46
+ const daemon_rollup_1 = require("./daemon-rollup");
46
47
  const task_scheduler_1 = require("./task-scheduler");
47
48
  const runtime_logging_1 = require("./runtime-logging");
48
49
  const sense_manager_1 = require("./sense-manager");
@@ -55,6 +56,7 @@ const os_cron_deps_1 = require("./os-cron-deps");
55
56
  const os_cron_1 = require("./os-cron");
56
57
  const daemon_tombstone_1 = require("./daemon-tombstone");
57
58
  const agent_config_check_1 = require("./agent-config-check");
59
+ const drift_detection_1 = require("./drift-detection");
58
60
  const pulse_1 = require("./pulse");
59
61
  const socket_client_1 = require("./socket-client");
60
62
  const bundle_manifest_1 = require("../../mind/bundle-manifest");
@@ -173,18 +175,75 @@ function buildDaemonHealthState() {
173
175
  since: snapshot.lastCrashAt ?? daemonStartedAt,
174
176
  };
175
177
  });
178
+ // Preserved for backwards-compatible inspection: callers (status
179
+ // command, outlook surface, etc.) may still read this combined list
180
+ // for per-component reasons. The rollup status field above is what
181
+ // changed meaning — the array is still the union of bootstrap +
182
+ // agent-derived degradation entries.
176
183
  const degraded = [
177
184
  ...degradedComponents.map((entry) => ({ ...entry })),
178
185
  ...agentDegradedComponents,
179
186
  ];
187
+ // Layer 4: probe each enabled agent for drift between agent.json
188
+ // (intent) and state/providers.json (observed binding). The result is
189
+ // a single boolean — driftDetected — that downgrades the rollup from
190
+ // healthy to partial when true. Per-finding detail is consumed by
191
+ // render-side surfaces (inner-status, --no-repair summary) and by
192
+ // Layer 3 RepairGuide; the rollup itself only cares about presence.
193
+ // Best-effort: a single agent's read failure is not blocking — the
194
+ // function returns "no drift detected for that agent" and continues.
195
+ const bundlesRoot = (0, identity_1.getAgentBundlesRoot)();
196
+ const drift = [];
197
+ for (const snapshot of snapshots) {
198
+ try {
199
+ const inputs = (0, drift_detection_1.loadDriftInputsForAgent)(bundlesRoot, snapshot.name);
200
+ const findings = (0, drift_detection_1.detectProviderBindingDrift)({
201
+ agentName: snapshot.name,
202
+ agentJson: inputs.agentJson,
203
+ providerState: inputs.providerState,
204
+ });
205
+ if (findings.length > 0) {
206
+ drift.push(...findings);
207
+ }
208
+ }
209
+ catch {
210
+ // best-effort: continue scanning
211
+ }
212
+ }
213
+ const driftDetected = drift.length > 0;
214
+ // Layer 1 rollup: project per-agent snapshots into the minimal
215
+ // AgentRollupInput shape and let computeDaemonRollup decide. The
216
+ // input is "every enabled agent" — managedAgents was filtered via
217
+ // listEnabledBundleAgents at module init, and snapshots only covers
218
+ // agents the process manager was told to manage, so by construction
219
+ // these entries are all enabled. The rollup function is a pure
220
+ // declarative function on the data we hand it.
221
+ //
222
+ // Note: safe-mode is wired as `false` here. Existing crash-loop
223
+ // detection (safe-mode.ts) already runs at the daemon-up boot path
224
+ // (cli-exec.ts), not from inside the daemon process itself. Once
225
+ // the daemon is up and reaching this rollup, safe mode no longer
226
+ // applies — the daemon is by definition past the crash-loop gate.
227
+ // If a future PR moves safe-mode signal into the running daemon,
228
+ // wire it through this third argument.
229
+ const rollupStatus = (0, daemon_rollup_1.computeDaemonRollup)({
230
+ enabledAgents: snapshots.map((snapshot) => ({
231
+ name: snapshot.name,
232
+ status: snapshot.status,
233
+ })),
234
+ bootstrapDegraded: degradedComponents,
235
+ safeMode: false,
236
+ driftDetected,
237
+ });
180
238
  return {
181
- status: degraded.length > 0 ? "degraded" : "ok",
239
+ status: rollupStatus,
182
240
  mode,
183
241
  pid: process.pid,
184
242
  startedAt: daemonStartedAt,
185
243
  uptimeSeconds: Math.floor(process.uptime()),
186
244
  safeMode: null,
187
245
  degraded,
246
+ drift,
188
247
  agents: Object.fromEntries(snapshots.map((snapshot) => [
189
248
  snapshot.name,
190
249
  {
@@ -34,6 +34,8 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.HEALTH_TRACKED_EVENTS = exports.DaemonHealthWriter = void 0;
37
+ exports.isRollupStatus = isRollupStatus;
38
+ exports.isDaemonStatus = isDaemonStatus;
37
39
  exports.getDefaultHealthPath = getDefaultHealthPath;
38
40
  exports.createHealthNervesSink = createHealthNervesSink;
39
41
  exports.readHealth = readHealth;
@@ -41,6 +43,38 @@ const fs = __importStar(require("fs"));
41
43
  const os = __importStar(require("os"));
42
44
  const path = __importStar(require("path"));
43
45
  const runtime_1 = require("../../nerves/runtime");
46
+ /**
47
+ * Daemon-wide rollup vocabulary — locked layer-1 contract.
48
+ *
49
+ * - `RollupStatus` is what `computeDaemonRollup` returns (post-inventory,
50
+ * four-state). The function never returns `"down"` because by the time
51
+ * the rollup is reachable the daemon has already started, opened its
52
+ * socket, and read its agent inventory — pre-inventory failure is the
53
+ * caller's domain.
54
+ * - `DaemonStatus` is what `DaemonHealthState.status` accepts. The caller
55
+ * widens the rollup result with `"down"` along the daemon-entry failure
56
+ * path (e.g. when the daemon process can't read inventory at all).
57
+ *
58
+ * `isRollupStatus` and `isDaemonStatus` are runtime guards used both by
59
+ * `readHealth` (validating cached health files on disk) and by render-side
60
+ * consumers that want to narrow `unknown` JSON into the typed union before
61
+ * branching on it.
62
+ */
63
+ // Single source of truth — the literal lists below are the runtime
64
+ // projection of the type unions. A future literal added to RollupStatus
65
+ // MUST also be added to ROLLUP_STATUS_LITERALS or `satisfies` blows up
66
+ // at tsc. That tightens the Layer 1 contract: producer + consumer +
67
+ // guard all stay in lockstep.
68
+ const ROLLUP_STATUS_LITERALS = ["healthy", "partial", "degraded", "safe-mode"];
69
+ const DAEMON_STATUS_LITERALS = [...ROLLUP_STATUS_LITERALS, "down"];
70
+ const ROLLUP_STATUS_VALUES = new Set(ROLLUP_STATUS_LITERALS);
71
+ const DAEMON_STATUS_VALUES = new Set(DAEMON_STATUS_LITERALS);
72
+ function isRollupStatus(value) {
73
+ return typeof value === "string" && ROLLUP_STATUS_VALUES.has(value);
74
+ }
75
+ function isDaemonStatus(value) {
76
+ return typeof value === "string" && DAEMON_STATUS_VALUES.has(value);
77
+ }
44
78
  class DaemonHealthWriter {
45
79
  healthPath;
46
80
  constructor(healthPath) {
@@ -111,7 +145,7 @@ function readHealth(healthPath) {
111
145
  try {
112
146
  const raw = fs.readFileSync(healthPath, "utf-8");
113
147
  const parsed = JSON.parse(raw);
114
- if (typeof parsed.status !== "string" ||
148
+ if (!isDaemonStatus(parsed.status) ||
115
149
  typeof parsed.mode !== "string" ||
116
150
  typeof parsed.pid !== "number" ||
117
151
  typeof parsed.startedAt !== "string" ||
@@ -123,6 +157,12 @@ function readHealth(healthPath) {
123
157
  parsed.habits === null) {
124
158
  return null;
125
159
  }
160
+ // `drift` is required in DaemonHealthState but absent from cached
161
+ // health files written by pre-Layer-4 daemons. Tolerate that legacy
162
+ // shape by defaulting to []; the rest of the file is still valid.
163
+ const drift = Array.isArray(parsed.drift)
164
+ ? parsed.drift
165
+ : [];
126
166
  return {
127
167
  status: parsed.status,
128
168
  mode: parsed.mode,
@@ -131,6 +171,7 @@ function readHealth(healthPath) {
131
171
  uptimeSeconds: parsed.uptimeSeconds,
132
172
  safeMode: parsed.safeMode,
133
173
  degraded: parsed.degraded,
174
+ drift,
134
175
  agents: parsed.agents,
135
176
  habits: parsed.habits,
136
177
  };
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.computeDaemonRollup = computeDaemonRollup;
4
+ /**
5
+ * Pure rollup decision function — given the post-inventory daemon
6
+ * surface, returns the daemon-wide rollup state per the locked Layer 1
7
+ * vocabulary table:
8
+ *
9
+ * | rollup | when |
10
+ * | ---------- | --------------------------------------------------------- |
11
+ * | healthy | every enabled agent serving + no bootstrap-degraded + no safe-mode |
12
+ * | partial | (≥1 serving + ≥1 not serving) OR (all serving + ≥1 bootstrap-degraded) |
13
+ * | degraded | zero enabled agents serving (fresh install OR all unhealthy) |
14
+ * | safe-mode | `safeMode === true` overrides everything else |
15
+ *
16
+ * The function NEVER returns `"down"`. By the time `computeDaemonRollup`
17
+ * is reachable, the daemon process has started, opened its socket, and
18
+ * read its agent inventory — pre-inventory failure is the caller's
19
+ * domain. `daemon-entry.ts`'s startup-failure path assigns `"down"` to
20
+ * `DaemonHealthState.status` directly without consulting this function.
21
+ */
22
+ function computeDaemonRollup(input) {
23
+ // Safe mode wins, period. Crash-loop detection trumps everything —
24
+ // we want the human to see SAFE MODE, not a noisy partial/degraded.
25
+ if (input.safeMode) {
26
+ return "safe-mode";
27
+ }
28
+ // Count serving agents. "Serving" = "running" worker status.
29
+ // Anything else (crashed/stopped/starting/etc) is not serving.
30
+ let serving = 0;
31
+ let notServing = 0;
32
+ for (const agent of input.enabledAgents) {
33
+ if (agent.status === "running") {
34
+ serving++;
35
+ }
36
+ else {
37
+ notServing++;
38
+ }
39
+ }
40
+ // Zero-serving wins over bootstrap-degraded — we have no working
41
+ // agents to surface a "partially working" story about. This covers
42
+ // both fresh-install (`enabledAgents.length === 0`) and
43
+ // all-failed-live-check (`serving === 0` with `notServing > 0`).
44
+ // Render layer (cli-render.ts) splits the UX copy by inspecting the
45
+ // agents map; the rollup itself doesn't carry the distinction.
46
+ if (serving === 0) {
47
+ return "degraded";
48
+ }
49
+ // From here we have ≥1 serving agent. The remaining choice is
50
+ // healthy vs partial.
51
+ const hasUnhealthyAgent = notServing > 0;
52
+ const hasBootstrapDegraded = input.bootstrapDegraded.length > 0;
53
+ const hasDrift = input.driftDetected === true;
54
+ if (hasUnhealthyAgent || hasBootstrapDegraded || hasDrift) {
55
+ return "partial";
56
+ }
57
+ return "healthy";
58
+ }
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.detectProviderBindingDrift = detectProviderBindingDrift;
37
+ exports.loadDriftInputsForAgent = loadDriftInputsForAgent;
38
+ const fs = __importStar(require("fs"));
39
+ const path = __importStar(require("path"));
40
+ const provider_state_1 = require("../provider-state");
41
+ /**
42
+ * Pull the per-lane intent out of an agent.json view, preferring the new
43
+ * `outward`/`inner` keys over the legacy `humanFacing`/`agentFacing` keys
44
+ * when both are present. Returns `null` if neither set carries a usable
45
+ * binding for this lane (the comparator treats that as "no intent to
46
+ * compare against" and emits no drift for that lane).
47
+ */
48
+ function resolveLaneIntent(agentJson, lane) {
49
+ const newKey = lane === "outward" ? agentJson.outward : agentJson.inner;
50
+ if (newKey && typeof newKey.provider === "string" && typeof newKey.model === "string") {
51
+ return { provider: newKey.provider, model: newKey.model };
52
+ }
53
+ const legacy = lane === "outward" ? agentJson.humanFacing : agentJson.agentFacing;
54
+ if (legacy && typeof legacy.provider === "string" && typeof legacy.model === "string") {
55
+ return { provider: legacy.provider, model: legacy.model };
56
+ }
57
+ return null;
58
+ }
59
+ function buildRepairCommand(agentName, lane, provider, model) {
60
+ return `ouro use --agent ${agentName} --lane ${lane} --provider ${provider} --model ${model}`;
61
+ }
62
+ /**
63
+ * Pure intent-vs-observed comparator. Emits one `DriftFinding` per lane
64
+ * whose intent (agent.json) does not match its observation
65
+ * (state/providers.json).
66
+ *
67
+ * Returns `[]` when:
68
+ * - `providerState === null` (fresh install, nothing to drift against), OR
69
+ * - both lanes match, OR
70
+ * - a lane has no intent in agent.json AND no observation in providerState
71
+ * (deferred to other layers — drift detection is silent on that lane).
72
+ */
73
+ function detectProviderBindingDrift(input) {
74
+ if (input.providerState === null) {
75
+ return [];
76
+ }
77
+ const findings = [];
78
+ const lanes = ["outward", "inner"];
79
+ for (const lane of lanes) {
80
+ const intent = resolveLaneIntent(input.agentJson, lane);
81
+ if (!intent)
82
+ continue;
83
+ const observed = input.providerState.lanes[lane];
84
+ if (intent.provider === observed.provider && intent.model === observed.model) {
85
+ continue;
86
+ }
87
+ findings.push({
88
+ agent: input.agentName,
89
+ lane,
90
+ intentProvider: intent.provider,
91
+ intentModel: intent.model,
92
+ observedProvider: observed.provider,
93
+ observedModel: observed.model,
94
+ reason: "provider-model-changed",
95
+ repairCommand: buildRepairCommand(input.agentName, lane, intent.provider, intent.model),
96
+ });
97
+ }
98
+ return findings;
99
+ }
100
+ function agentRootFor(bundlesRoot, agentName) {
101
+ return path.join(bundlesRoot, `${agentName}.ouro`);
102
+ }
103
+ function readAgentJson(agentJsonPath) {
104
+ let raw;
105
+ try {
106
+ raw = fs.readFileSync(agentJsonPath, "utf-8");
107
+ }
108
+ catch {
109
+ throw new Error(`agent.json not found at ${agentJsonPath}`);
110
+ }
111
+ let parsed;
112
+ try {
113
+ parsed = JSON.parse(raw);
114
+ }
115
+ catch (error) {
116
+ throw new Error(`agent.json at ${agentJsonPath} contains invalid JSON: ${String(error)}`);
117
+ }
118
+ // The drift loader is intentionally permissive: it parses the file as a
119
+ // typed `AgentConfig` view but does not validate every field. The
120
+ // comparator (Unit 1) is the one that decides which intent fields are
121
+ // usable; non-conforming bindings just silently skip drift detection
122
+ // on that lane. Stricter validation belongs to `loadAgentConfig` in
123
+ // identity.ts (which also has side effects we don't want here).
124
+ return parsed;
125
+ }
126
+ /**
127
+ * Loader for the drift comparator. Reads the per-agent `agent.json` and
128
+ * `state/providers.json` off disk, returning typed inputs ready to feed
129
+ * into `detectProviderBindingDrift`.
130
+ *
131
+ * - Throws when `agent.json` is missing or unparseable. The caller decides
132
+ * whether to swallow (drift detection has no opinion on a broken
133
+ * `agent.json` — that's the existing `agent-config-check` flow's job).
134
+ * - Returns `providerState: null` when `state/providers.json` is missing
135
+ * (fresh install) or invalid (the comparator interprets `null` as "no
136
+ * observation, nothing to drift against").
137
+ * - Never writes to disk.
138
+ */
139
+ function loadDriftInputsForAgent(bundlesRoot, agentName) {
140
+ const agentRoot = agentRootFor(bundlesRoot, agentName);
141
+ const agentJsonPath = path.join(agentRoot, "agent.json");
142
+ const agentJson = readAgentJson(agentJsonPath);
143
+ const stateResult = (0, provider_state_1.readProviderState)(agentRoot);
144
+ const providerState = stateResult.ok ? stateResult.state : null;
145
+ return { agentJson, providerState };
146
+ }
@@ -74,6 +74,18 @@ function buildInnerStatusOutput(input) {
74
74
  // Attention
75
75
  const thoughtWord = attentionCount === 1 ? "thought" : "thoughts";
76
76
  lines.push(` attention: ${attentionCount} held ${thoughtWord}`);
77
+ // Layer 4 drift advisory. Renders one line per finding with the
78
+ // lane, intent vs observed binding, and the copy-pasteable repair
79
+ // command. Suppressed entirely when no findings exist (or the field
80
+ // is absent — pre-Layer-4 callers).
81
+ const driftFindings = input.driftFindings ?? [];
82
+ if (driftFindings.length > 0) {
83
+ lines.push(" drift advisory:");
84
+ for (const finding of driftFindings) {
85
+ lines.push(` - ${finding.lane}: intent ${finding.intentProvider}/${finding.intentModel} vs observed ${finding.observedProvider}/${finding.observedModel}`);
86
+ lines.push(` repair: ${finding.repairCommand}`);
87
+ }
88
+ }
77
89
  (0, runtime_1.emitNervesEvent)({
78
90
  component: "daemon",
79
91
  event: "daemon.inner_status_read",
@@ -83,6 +95,7 @@ function buildInnerStatusOutput(input) {
83
95
  status: runtimeState?.status ?? "unknown",
84
96
  journalCount: journalFiles.length,
85
97
  attentionCount,
98
+ driftCount: driftFindings.length,
86
99
  },
87
100
  });
88
101
  return lines.join("\n");
@@ -49,6 +49,7 @@ const runtime_1 = require("../../../nerves/runtime");
49
49
  const habit_parser_1 = require("../../habits/habit-parser");
50
50
  const habit_runtime_state_1 = require("../../habits/habit-runtime-state");
51
51
  const identity_1 = require("../../identity");
52
+ const daemon_health_1 = require("../../daemon/daemon-health");
52
53
  const shared_1 = require("./shared");
53
54
  const agent_machine_1 = require("./agent-machine");
54
55
  const sessions_1 = require("./sessions");
@@ -270,7 +271,12 @@ function readDaemonHealthDeep(healthPath) {
270
271
  const raw = fs.readFileSync(resolvedPath, "utf-8");
271
272
  const health = JSON.parse(raw);
272
273
  return {
273
- status: typeof health.status === "string" ? health.status : "unknown",
274
+ // Layer 1: tighten the parse so only post-Layer-1 vocabulary
275
+ // carries through. Stale cached files that still hold legacy
276
+ // string values like "ok" or "running" — written by an older
277
+ // daemon binary — fall back to "unknown" so downstream Outlook
278
+ // consumers can detect the unparseable case explicitly.
279
+ status: (0, daemon_health_1.isDaemonStatus)(health.status) ? health.status : "unknown",
274
280
  mode: typeof health.mode === "string" ? health.mode : "unknown",
275
281
  pid: typeof health.pid === "number" ? health.pid : 0,
276
282
  startedAt: typeof health.startedAt === "string" ? health.startedAt : "",
@@ -87,6 +87,22 @@ const DISPATCH_EXEMPT_PATTERNS = [
87
87
  // HTTP health probe: pure HTTP utility factory. The HealthMonitor caller
88
88
  // owns observability via daemon.health_result events.
89
89
  "daemon/http-health-probe",
90
+ // Rollup decision function: pure decision tree mapping per-agent
91
+ // snapshots + bootstrap-degraded entries + safe-mode flag to a
92
+ // RollupStatus. No side effects. The caller (daemon-entry.ts
93
+ // buildDaemonHealthState → DaemonHealthWriter) owns observability via
94
+ // daemon.health_written when the rolled-up state is persisted.
95
+ "daemon/daemon-rollup",
96
+ // Drift comparator + thin I/O loader: `detectProviderBindingDrift`
97
+ // is a pure intent-vs-observed comparator with no side effects;
98
+ // `loadDriftInputsForAgent` is a small fs-read wrapper that returns
99
+ // `null` on missing/invalid state rather than emitting. The caller
100
+ // (daemon-entry.ts buildDaemonHealthState's per-agent drift probe)
101
+ // owns observability — drift findings ride along through
102
+ // `daemon.health_written` as part of the rolled-up state, and
103
+ // `agent-config-check.ts` carries `driftFindings` through its
104
+ // existing instrumentation. Same pattern as `daemon-rollup`.
105
+ "daemon/drift-detection",
90
106
  // Attachment helper modules: generic file-path/extension utilities and the
91
107
  // source registry are pure support seams. The orchestrator/adapters that
92
108
  // call them own the observability.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.515",
3
+ "version": "0.1.0-alpha.517",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",