@ouro.bot/cli 0.1.0-alpha.692 → 0.1.0-alpha.694

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,19 @@
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.694",
6
+ "changes": [
7
+ "Align Workbench setup with runtime MCP injection and stale bundle cleanup, including prompt/turn stale-state coverage."
8
+ ]
9
+ },
10
+ {
11
+ "version": "0.1.0-alpha.693",
12
+ "changes": [
13
+ "Load vault-backed provider credentials before manual context-loss Sentinel refreshes so a fresh CLI process cannot report false not-loaded provider warnings.",
14
+ "Have doctor lifecycle checks read the machine-local daemon log alongside bundle-local logs so stale historical bundle logs cannot mask live daemon activity."
15
+ ]
16
+ },
4
17
  {
5
18
  "version": "0.1.0-alpha.692",
6
19
  "changes": [
@@ -203,9 +203,9 @@ const registryData = [
203
203
  {
204
204
  path: "senses.workbench",
205
205
  tier: "self",
206
- description: "Workbench sense configuration. Controls whether the native terminal-agent control room is enabled for this agent.",
206
+ description: "Legacy Workbench sense marker. Runtime-injected Workbench tools do not require this bundle entry.",
207
207
  default: { enabled: false },
208
- effects: "Enables the local Ouro Workbench sense. Workbench MCP registration remains in mcpServers.ouro_workbench.",
208
+ effects: "Kept only for old bundles; modern Workbench launches the boss with --workbench-mcp and injects tools per turn.",
209
209
  topics: ["senses", "workbench", "mcp", "terminal", "channels", "interface"],
210
210
  validate: validateObject({ enabled: validateBoolean }),
211
211
  },
@@ -2649,7 +2649,6 @@ function enableAgentSense(agent, sense, deps) {
2649
2649
  mail: senses.mail ?? { enabled: false },
2650
2650
  voice: senses.voice ?? { enabled: false },
2651
2651
  a2a: senses.a2a ?? { enabled: false },
2652
- workbench: senses.workbench ?? { enabled: false },
2653
2652
  [sense]: { ...existing, enabled: true },
2654
2653
  };
2655
2654
  fs.writeFileSync(configPath, `${JSON.stringify(raw, null, 2)}\n`, "utf-8");
@@ -2661,17 +2660,32 @@ function connectMenuIsTTY(deps) {
2661
2660
  function readConnectBaySenseFlags(agent, deps) {
2662
2661
  const configPath = path.join(providerCliAgentRoot({ agent }, deps), "agent.json");
2663
2662
  const parsed = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2664
- const workbenchCommand = parsed.mcpServers?.ouro_workbench?.command;
2663
+ const senses = parsed.senses && typeof parsed.senses === "object" && !Array.isArray(parsed.senses)
2664
+ ? parsed.senses
2665
+ : undefined;
2666
+ const mcpServers = parsed.mcpServers && typeof parsed.mcpServers === "object" && !Array.isArray(parsed.mcpServers)
2667
+ ? parsed.mcpServers
2668
+ : undefined;
2669
+ const workbenchSensePresent = senses
2670
+ ? Object.prototype.hasOwnProperty.call(senses, "workbench")
2671
+ : false;
2672
+ const workbenchMcpPresent = mcpServers
2673
+ ? Object.prototype.hasOwnProperty.call(mcpServers, "ouro_workbench")
2674
+ : false;
2675
+ const workbenchCommand = mcpServers?.ouro_workbench?.command;
2676
+ const workbenchEnabled = senses?.workbench?.enabled === true;
2677
+ const workbenchMcpCommand = typeof workbenchCommand === "string" && workbenchCommand.trim().length > 0
2678
+ ? workbenchCommand
2679
+ : null;
2665
2680
  return {
2666
- teamsEnabled: parsed.senses?.teams?.enabled === true,
2667
- blueBubblesEnabled: parsed.senses?.bluebubbles?.enabled === true,
2668
- mailEnabled: parsed.senses?.mail?.enabled === true,
2669
- voiceEnabled: parsed.senses?.voice?.enabled === true,
2670
- a2aEnabled: parsed.senses?.a2a?.enabled === true,
2671
- workbenchEnabled: parsed.senses?.workbench?.enabled === true,
2672
- workbenchMcpCommand: typeof workbenchCommand === "string" && workbenchCommand.trim().length > 0
2673
- ? workbenchCommand
2674
- : null,
2681
+ teamsEnabled: senses?.teams?.enabled === true,
2682
+ blueBubblesEnabled: senses?.bluebubbles?.enabled === true,
2683
+ mailEnabled: senses?.mail?.enabled === true,
2684
+ voiceEnabled: senses?.voice?.enabled === true,
2685
+ a2aEnabled: senses?.a2a?.enabled === true,
2686
+ workbenchEnabled,
2687
+ workbenchMcpCommand,
2688
+ workbenchHasStaleBundleEntry: workbenchSensePresent || workbenchMcpPresent,
2675
2689
  };
2676
2690
  }
2677
2691
  function defaultWorkbenchMcpCandidates(deps) {
@@ -2713,21 +2727,30 @@ function resolveWorkbenchRuntimeMcp(flag, deps) {
2713
2727
  return null;
2714
2728
  return { ouro_workbench: { command, args: [] } };
2715
2729
  }
2716
- function writeWorkbenchMcpRegistration(agent, executablePath, deps) {
2730
+ function removeWorkbenchMcpRegistration(agent, deps) {
2717
2731
  const { configPath } = (0, auth_flow_1.readAgentConfigForAgent)(agent, deps.bundlesRoot);
2718
- enableAgentSense(agent, "workbench", deps);
2719
2732
  const nextRaw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
2733
+ let changed = false;
2720
2734
  const mcpServers = nextRaw.mcpServers && typeof nextRaw.mcpServers === "object" && !Array.isArray(nextRaw.mcpServers)
2721
2735
  ? nextRaw.mcpServers
2722
- : {};
2723
- nextRaw.mcpServers = {
2724
- ...mcpServers,
2725
- ouro_workbench: {
2726
- command: executablePath,
2727
- args: [],
2728
- },
2729
- };
2730
- fs.writeFileSync(configPath, `${JSON.stringify(nextRaw, null, 2)}\n`, "utf-8");
2736
+ : null;
2737
+ if (mcpServers && "ouro_workbench" in mcpServers) {
2738
+ delete mcpServers.ouro_workbench;
2739
+ nextRaw.mcpServers = mcpServers;
2740
+ changed = true;
2741
+ }
2742
+ const senses = nextRaw.senses && typeof nextRaw.senses === "object" && !Array.isArray(nextRaw.senses)
2743
+ ? nextRaw.senses
2744
+ : null;
2745
+ if (senses && "workbench" in senses) {
2746
+ delete senses.workbench;
2747
+ nextRaw.senses = senses;
2748
+ changed = true;
2749
+ }
2750
+ if (changed) {
2751
+ fs.writeFileSync(configPath, `${JSON.stringify(nextRaw, null, 2)}\n`, "utf-8");
2752
+ }
2753
+ return changed;
2731
2754
  }
2732
2755
  async function buildConnectMenu(agent, deps, onProgress) {
2733
2756
  const bundlesRoot = path.dirname(providerCliAgentRoot({ agent }, deps));
@@ -2756,7 +2779,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
2756
2779
  const runtimeConfig = await (0, runtime_credentials_1.refreshRuntimeCredentialConfig)(agent, { preserveCachedOnFailure: true });
2757
2780
  onProgress?.("loading this machine's settings");
2758
2781
  const machineRuntime = await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(deps), { preserveCachedOnFailure: true });
2759
- const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled, a2aEnabled, workbenchEnabled, workbenchMcpCommand, } = readConnectBaySenseFlags(agent, deps);
2782
+ const { teamsEnabled, blueBubblesEnabled, mailEnabled, voiceEnabled, a2aEnabled, workbenchMcpCommand, workbenchHasStaleBundleEntry, } = readConnectBaySenseFlags(agent, deps);
2760
2783
  const perplexityApiKey = runtimeConfig.ok
2761
2784
  ? readRuntimeConfigString(runtimeConfig.config, "integrations.perplexityApiKey")
2762
2785
  : null;
@@ -2913,31 +2936,18 @@ async function buildConnectMenu(agent, deps, onProgress) {
2913
2936
  : runtimeConfigReadStatus(runtimeConfig);
2914
2937
  const a2aStatus = a2aEnabled ? "ready" : "missing";
2915
2938
  const installedWorkbenchMcp = findInstalledWorkbenchMcp(deps, workbenchMcpCommand);
2916
- const configuredWorkbenchMcpExists = cliPathExists(deps, workbenchMcpCommand);
2917
- const workbenchStatus = workbenchEnabled && configuredWorkbenchMcpExists
2918
- ? "ready"
2919
- : workbenchEnabled || workbenchMcpCommand
2920
- ? "needs attention"
2921
- : installedWorkbenchMcp
2922
- ? "not attached"
2923
- : "missing";
2939
+ const workbenchStatus = installedWorkbenchMcp
2940
+ ? workbenchHasStaleBundleEntry ? "needs attention" : "ready"
2941
+ : "missing";
2924
2942
  const workbenchDetailLines = [
2925
- workbenchEnabled ? "senses.workbench.enabled = true" : "senses.workbench.enabled is not enabled",
2926
- workbenchMcpCommand
2927
- ? configuredWorkbenchMcpExists
2928
- ? `MCP command registered: ${workbenchMcpCommand}`
2929
- : `registered MCP command is missing: ${workbenchMcpCommand}`
2930
- : "mcpServers.ouro_workbench is not registered",
2931
2943
  installedWorkbenchMcp
2932
2944
  ? `OuroWorkbenchMCP found: ${installedWorkbenchMcp}`
2933
2945
  : "OuroWorkbenchMCP not found in ~/Applications or /Applications",
2934
- // Runtime injection is the boss's normal path: the Workbench app launches
2935
- // its boss with `ouro mcp-serve --workbench-mcp`, which injects the
2936
- // ouro_workbench MCP per-turn without any agent.json entry. A blank/“not
2937
- // registered” bundle state above is expected for a runtime-only boss; this
2938
- // `connect workbench` command only writes a persistent entry as an opt-in.
2946
+ workbenchHasStaleBundleEntry
2947
+ ? "stale bundle entry detected: remove senses.workbench and mcpServers.ouro_workbench; Workbench tools are injected at runtime instead"
2948
+ : "bundle state: clean; no senses.workbench or mcpServers.ouro_workbench entry required",
2939
2949
  installedWorkbenchMcp
2940
- ? "boss runtime injection: when the Workbench app launches this agent it injects ouro_workbench per-turn; an agent.json entry is not required (this command writes one as an opt-in escape hatch)."
2950
+ ? "boss runtime injection: when the Workbench app launches this agent it injects ouro_workbench per-turn; an agent.json entry is not required."
2941
2951
  : "boss runtime injection: install Ouro Workbench.app so the app can inject ouro_workbench per-turn when it launches this agent as boss (no agent.json entry required).",
2942
2952
  ];
2943
2953
  const entries = [
@@ -3073,7 +3083,7 @@ async function buildConnectMenu(agent, deps, onProgress) {
3073
3083
  name: "Ouro Workbench",
3074
3084
  section: "This machine",
3075
3085
  status: workbenchStatus,
3076
- description: "Native terminal-agent control room and local MCP sense.",
3086
+ description: "Native terminal-agent control room with runtime-injected MCP tools.",
3077
3087
  detailLines: workbenchDetailLines,
3078
3088
  nextAction: (0, connect_bay_1.connectEntryNeedsAttention)({
3079
3089
  option: "9",
@@ -4695,24 +4705,31 @@ async function executeConnectA2A(agent, deps) {
4695
4705
  return message;
4696
4706
  }
4697
4707
  async function executeConnectWorkbench(agent, deps) {
4698
- const { workbenchMcpCommand } = readConnectBaySenseFlags(agent, deps);
4708
+ const { workbenchMcpCommand, workbenchHasStaleBundleEntry } = readConnectBaySenseFlags(agent, deps);
4709
+ const changed = removeWorkbenchMcpRegistration(agent, deps);
4710
+ const syncSummary = changed ? pushAgentBundleAfterCliMutation(agent, deps) : "";
4699
4711
  const executablePath = findInstalledWorkbenchMcp(deps, workbenchMcpCommand);
4700
4712
  if (!executablePath) {
4701
4713
  throw new Error([
4702
4714
  `Ouro Workbench is not installed for ${agent} on this machine.`,
4715
+ changed
4716
+ ? "agent.json: removed stale senses.workbench / mcpServers.ouro_workbench entries"
4717
+ : "agent.json: no Workbench entries needed or written",
4718
+ ...(syncSummary ? [syncSummary] : []),
4703
4719
  "Expected the MCP executable at one of:",
4704
4720
  ...defaultWorkbenchMcpCandidates(deps).map((candidate) => ` ${candidate}`),
4705
4721
  "Install or copy Ouro Workbench.app there, then run:",
4706
4722
  ` ouro connect workbench --agent ${agent}`,
4707
4723
  ].join("\n"));
4708
4724
  }
4709
- writeWorkbenchMcpRegistration(agent, executablePath, deps);
4710
- const syncSummary = pushAgentBundleAfterCliMutation(agent, deps);
4711
4725
  const message = [
4712
- `Workbench connected for ${agent}`,
4713
- "agent.json: senses.workbench.enabled = true",
4714
- `agent.json: mcpServers.ouro_workbench.command = ${executablePath}`,
4715
- "Workbench is a local machine sense; provider secrets stay in the agent vault.",
4726
+ `Workbench runtime injection ready for ${agent}`,
4727
+ `OuroWorkbenchMCP found: ${executablePath}`,
4728
+ workbenchHasStaleBundleEntry
4729
+ ? "agent.json: removed stale senses.workbench / mcpServers.ouro_workbench entries"
4730
+ : "agent.json: no Workbench entries needed or written",
4731
+ `boss launch path: ouro mcp-serve --agent ${agent} --workbench-mcp ${executablePath}`,
4732
+ "Workbench tools are injected per served turn; provider secrets stay in the agent vault.",
4716
4733
  ...(syncSummary ? [syncSummary] : []),
4717
4734
  ].join("\n");
4718
4735
  deps.writeStdout(message);
@@ -7647,6 +7664,9 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7647
7664
  throw new Error("work sentinel requires --agent <name>");
7648
7665
  const bundlesRoot = deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)();
7649
7666
  const agentRoot = deps.agentBundleRoot ?? path.join(bundlesRoot, `${command.agent}.ouro`);
7667
+ if (command.kind === "work.sentinel.refresh") {
7668
+ await (0, provider_credentials_1.refreshProviderCredentialPool)(command.agent, { preserveCachedOnFailure: true });
7669
+ }
7650
7670
  const sentinel = command.kind === "work.sentinel.refresh"
7651
7671
  ? await refreshContextLossSentinel(command.agent, agentRoot, { trigger: "manual_cli", homeDir: deps.homeDir })
7652
7672
  : readContextLossSentinelView(agentRoot, { limit: 20 });
@@ -7885,6 +7905,7 @@ async function runOuroCli(args, deps = (0, cli_defaults_1.createDefaultOuroCliDe
7885
7905
  fetchImpl: deps.fetchImpl ?? fetch,
7886
7906
  socketPath: deps.socketPath,
7887
7907
  bundlesRoot: deps.bundlesRoot ?? (0, identity_1.getAgentBundlesRoot)(),
7908
+ daemonLogsDir: path.join((0, ouro_version_manager_1.getOuroCliHome)(deps.homeDir), "daemon", "logs"),
7888
7909
  homedir: os.homedir(),
7889
7910
  envPath: process.env.PATH ?? "",
7890
7911
  platform: process.platform,
@@ -402,7 +402,7 @@ const SUBCOMMAND_HELP = {
402
402
  example: "ouro connect a2a --agent <agent>",
403
403
  },
404
404
  "connect workbench": {
405
- description: "Attach native Ouro Workbench on this machine and register its MCP sense",
405
+ description: "Verify native Ouro Workbench runtime injection and clean stale bundle entries",
406
406
  usage: "ouro connect workbench [--agent <name>]",
407
407
  example: "ouro connect workbench --agent <agent>",
408
408
  },
@@ -783,83 +783,106 @@ function computeSummary(categories) {
783
783
  * to answer the operator's question after the daemon has gone silent: "did
784
784
  * it crash? when did it last do anything? did it just upgrade?"
785
785
  *
786
- * Reads daemon.ndjson from the first available agent bundle (one daemon
787
- * serves all agents, so any agent's bundle has the shared log).
786
+ * Reads the machine-local daemon.ndjson when available, plus any
787
+ * bundle-local copies left by older runtimes. The newest parseable event
788
+ * is the liveness signal; stale bundle logs cannot mask a live daemon.
788
789
  */
789
790
  function checkLifecycle(deps) {
790
791
  const checks = [];
791
792
  const HOUR_MS = 60 * 60 * 1000;
792
793
  const STALE_THRESHOLD_MS = 5 * 60 * 1000;
793
- const cutoff = Date.now() - HOUR_MS;
794
+ const now = Date.now();
795
+ const cutoff = now - HOUR_MS;
796
+ const logPaths = [];
797
+ const seenLogPaths = new Set();
798
+ const addLogPath = (candidate) => {
799
+ if (seenLogPaths.has(candidate) || !deps.existsSync(candidate))
800
+ return;
801
+ seenLogPaths.add(candidate);
802
+ logPaths.push(candidate);
803
+ };
804
+ if (deps.daemonLogsDir) {
805
+ addLogPath(`${deps.daemonLogsDir}/daemon.ndjson`);
806
+ }
794
807
  const agents = discoverAgents(deps);
795
- let logPath = null;
796
808
  for (const agentDir of agents) {
797
- const candidate = `${deps.bundlesRoot}/${agentDir}/state/daemon/logs/daemon.ndjson`;
798
- if (deps.existsSync(candidate)) {
799
- logPath = candidate;
800
- break;
801
- }
809
+ addLogPath(`${deps.bundlesRoot}/${agentDir}/state/daemon/logs/daemon.ndjson`);
802
810
  }
803
- if (!logPath) {
804
- checks.push({ label: "daemon log readable", status: "warn", detail: "no daemon.ndjson found in any agent bundle" });
811
+ if (logPaths.length === 0) {
812
+ checks.push({ label: "daemon log readable", status: "warn", detail: "no daemon.ndjson found in daemon or agent bundle logs" });
805
813
  return { name: "Lifecycle", checks };
806
814
  }
807
815
  let lastTs = null;
808
816
  let lastEvent = null;
817
+ let lastEventTimeMs = Number.NEGATIVE_INFINITY;
809
818
  let startCount = 0;
810
819
  let installCount = 0;
811
820
  let installVersions = [];
812
821
  let processErrors = [];
813
822
  let lastEntryAgeMs = Number.POSITIVE_INFINITY;
814
- try {
815
- // Read the whole log via deps.readFileSync, then take the tail. For a
816
- // chatty daemon this can be a few MB; we only inspect the last 5000
817
- // lines which is enough for the last hour of activity. If the file is
818
- // small (typical case), reading it all is cheap.
819
- const raw = deps.readFileSync(logPath);
820
- const allLines = raw.split("\n").filter((l) => l.trim());
821
- const usable = allLines.length > 5000 ? allLines.slice(-5000) : allLines;
822
- for (const line of usable) {
823
- let parsed;
824
- try {
825
- parsed = JSON.parse(line);
826
- }
827
- catch {
828
- continue;
829
- }
830
- const ts = typeof parsed.ts === "string" ? parsed.ts : null;
831
- const event = typeof parsed.event === "string" ? parsed.event : null;
832
- if (!ts || !event)
833
- continue;
834
- const tsMs = Date.parse(ts);
835
- if (Number.isNaN(tsMs))
836
- continue;
837
- lastTs = ts;
838
- lastEvent = event;
839
- lastEntryAgeMs = Math.min(lastEntryAgeMs, Date.now() - tsMs);
840
- if (tsMs < cutoff)
841
- continue;
842
- if (event === "daemon.daemon_started")
843
- startCount++;
844
- if (event === "daemon.cli_version_install_end") {
845
- installCount++;
846
- const meta = parsed.meta;
847
- const ver = typeof meta?.version === "string" ? meta.version : null;
848
- if (ver)
849
- installVersions.push(ver);
850
- }
851
- if (event === "daemon.agent_process_error") {
852
- const meta = parsed.meta;
853
- const reason = typeof meta?.reason === "string" ? meta.reason : "unknown";
854
- const agent = typeof meta?.agent === "string" ? meta.agent : "unknown";
855
- processErrors.push(`${agent}: ${reason}`);
823
+ let readCount = 0;
824
+ const readErrors = [];
825
+ for (const logPath of logPaths) {
826
+ try {
827
+ // Read the whole log via deps.readFileSync, then take the tail. For a
828
+ // chatty daemon this can be a few MB; we only inspect the last 5000
829
+ // lines which is enough for the last hour of activity. If the file is
830
+ // small (typical case), reading it all is cheap.
831
+ const raw = deps.readFileSync(logPath);
832
+ readCount++;
833
+ const allLines = raw.split("\n").filter((l) => l.trim());
834
+ const usable = allLines.length > 5000 ? allLines.slice(-5000) : allLines;
835
+ for (const line of usable) {
836
+ let parsed;
837
+ try {
838
+ parsed = JSON.parse(line);
839
+ }
840
+ catch {
841
+ continue;
842
+ }
843
+ const ts = typeof parsed.ts === "string" ? parsed.ts : null;
844
+ const event = typeof parsed.event === "string" ? parsed.event : null;
845
+ if (!ts || !event)
846
+ continue;
847
+ const tsMs = Date.parse(ts);
848
+ if (Number.isNaN(tsMs))
849
+ continue;
850
+ if (tsMs > lastEventTimeMs) {
851
+ lastEventTimeMs = tsMs;
852
+ lastTs = ts;
853
+ lastEvent = event;
854
+ lastEntryAgeMs = Math.max(0, now - tsMs);
855
+ }
856
+ if (tsMs < cutoff)
857
+ continue;
858
+ if (event === "daemon.daemon_started")
859
+ startCount++;
860
+ if (event === "daemon.cli_version_install_end") {
861
+ installCount++;
862
+ const meta = parsed.meta;
863
+ const ver = typeof meta?.version === "string" ? meta.version : null;
864
+ if (ver)
865
+ installVersions.push(ver);
866
+ }
867
+ if (event === "daemon.agent_process_error") {
868
+ const meta = parsed.meta;
869
+ const reason = typeof meta?.reason === "string" ? meta.reason : "unknown";
870
+ const agent = typeof meta?.agent === "string" ? meta.agent : "unknown";
871
+ processErrors.push(`${agent}: ${reason}`);
872
+ }
856
873
  }
857
874
  }
875
+ catch (error) {
876
+ readErrors.push(`${logPath}: ${error instanceof Error ? error.message : /* v8 ignore next -- non-Error throw is unreachable from deps.readFileSync (always Error) @preserve */ String(error)}`);
877
+ }
858
878
  }
859
- catch (error) {
860
- checks.push({ label: "daemon log readable", status: "fail", detail: `read failed: ${error instanceof Error ? error.message : /* v8 ignore next -- non-Error throw is unreachable from deps.readFileSync (always Error) @preserve */ String(error)}` });
879
+ if (readCount === 0) {
880
+ checks.push({ label: "daemon log readable", status: "fail", detail: `read failed: ${readErrors.join("; ")}` });
861
881
  return { name: "Lifecycle", checks };
862
882
  }
883
+ if (readErrors.length > 0) {
884
+ checks.push({ label: "daemon log readable", status: "warn", detail: `some daemon logs were unreadable: ${readErrors.join("; ")}` });
885
+ }
863
886
  if (lastTs === null) {
864
887
  checks.push({ label: "recent daemon activity", status: "warn", detail: "no parseable events in tail of daemon.ndjson" });
865
888
  }
@@ -50,6 +50,8 @@ const DEFAULT_BLUEBUBBLES_PORT = 18790;
50
50
  const DEFAULT_BLUEBUBBLES_WEBHOOK_PATH = "/bluebubbles-webhook";
51
51
  const BLUEBUBBLES_RUNTIME_FRESHNESS_WINDOW_MS = 90_000;
52
52
  const SENSE_CONFIG_RETRY_DELAYS_MS = [1_000, 2_000, 5_000, 10_000, 30_000, 60_000];
53
+ const WORKBENCH_RUNTIME_INJECTION_DETAIL = "runtime-injected by Workbench app; no agent.json entry required";
54
+ const WORKBENCH_STALE_BUNDLE_DETAIL = "stale agent.json Workbench entry; run 'ouro connect workbench' to clean it and use runtime injection";
53
55
  function defaultSenses() {
54
56
  return {
55
57
  cli: { ...identity_1.DEFAULT_AGENT_SENSES.cli },
@@ -78,12 +80,26 @@ function readAgentSenses(agentJsonPath) {
78
80
  reason: error instanceof Error ? error.message : String(error),
79
81
  },
80
82
  });
81
- return defaults;
83
+ return { senses: defaults, workbenchHasStaleBundleEntry: false };
82
84
  }
83
85
  const rawSenses = parsed.senses;
84
86
  if (!rawSenses || typeof rawSenses !== "object" || Array.isArray(rawSenses)) {
85
- return defaults;
86
- }
87
+ const rawMcpServers = parsed.mcpServers;
88
+ const workbenchHasStaleBundleEntry = !!rawMcpServers
89
+ && typeof rawMcpServers === "object"
90
+ && !Array.isArray(rawMcpServers)
91
+ && Object.prototype.hasOwnProperty.call(rawMcpServers, "ouro_workbench");
92
+ if (workbenchHasStaleBundleEntry)
93
+ defaults.workbench = { enabled: true };
94
+ return { senses: defaults, workbenchHasStaleBundleEntry };
95
+ }
96
+ const workbenchSensePresent = Object.prototype.hasOwnProperty.call(rawSenses, "workbench");
97
+ const rawMcpServers = parsed.mcpServers;
98
+ const workbenchMcpPresent = !!rawMcpServers
99
+ && typeof rawMcpServers === "object"
100
+ && !Array.isArray(rawMcpServers)
101
+ && Object.prototype.hasOwnProperty.call(rawMcpServers, "ouro_workbench");
102
+ const workbenchHasStaleBundleEntry = workbenchSensePresent || workbenchMcpPresent;
87
103
  for (const sense of ["cli", "teams", "bluebubbles", "mail", "voice", "a2a", "workbench"]) {
88
104
  const rawSense = rawSenses[sense];
89
105
  if (!rawSense || typeof rawSense !== "object" || Array.isArray(rawSense)) {
@@ -94,7 +110,10 @@ function readAgentSenses(agentJsonPath) {
94
110
  defaults[sense] = { enabled };
95
111
  }
96
112
  }
97
- return defaults;
113
+ if (workbenchHasStaleBundleEntry) {
114
+ defaults.workbench = { enabled: true };
115
+ }
116
+ return { senses: defaults, workbenchHasStaleBundleEntry };
98
117
  }
99
118
  function textField(record, key) {
100
119
  const value = record?.[key];
@@ -141,7 +160,7 @@ function runtimeConfigUnavailableDetail(agent, runtimeConfig) {
141
160
  return `missing vault ${itemName} (${agent})`;
142
161
  return `vault ${itemName} unavailable (${compactRuntimeConfigError(agent, runtimeConfig.error)})`;
143
162
  }
144
- function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntimeConfig = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent)) {
163
+ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntimeConfig = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent), workbenchHasStaleBundleEntry = false) {
145
164
  const base = {
146
165
  cli: { configured: true, detail: "local interactive terminal" },
147
166
  teams: { configured: false, detail: "not enabled in agent.json" },
@@ -304,10 +323,10 @@ function senseFactsFromRuntimeConfig(agent, senses, runtimeConfig, machineRuntim
304
323
  detail: publicUrl ? `${publicUrl}${endpointPath}` : `:${port} ${endpointPath}`,
305
324
  };
306
325
  }
307
- if (senses.workbench.enabled) {
326
+ if (workbenchHasStaleBundleEntry || senses.workbench.enabled) {
308
327
  base.workbench = {
309
- configured: true,
310
- detail: "native Workbench local control room; MCP registration is stored in agent.json",
328
+ configured: false,
329
+ detail: WORKBENCH_STALE_BUNDLE_DETAIL,
311
330
  };
312
331
  }
313
332
  return base;
@@ -328,7 +347,7 @@ function senseRepairHint(agent, sense) {
328
347
  }
329
348
  /* v8 ignore next -- Workbench is deliberately not daemon-managed, so getSenseInventory never asks the daemon manager for a repair hint @preserve */
330
349
  if (sense === "workbench") {
331
- return `Agent-runnable: run 'ouro connect workbench --agent ${agent}' to enable senses.workbench.enabled and mcpServers.ouro_workbench in agent.json.`;
350
+ return `Agent-runnable: install Ouro Workbench.app, then launch the boss through Workbench or run 'ouro mcp-serve --agent ${agent} --workbench-mcp'; 'ouro connect workbench --agent ${agent}' only cleans stale bundle entries.`;
332
351
  }
333
352
  return `Run 'ouro connect bluebubbles --agent ${agent}' to attach BlueBubbles on this machine; then run 'ouro up' again.`;
334
353
  }
@@ -527,9 +546,9 @@ class DaemonSenseManager {
527
546
  const bundlesRoot = options.bundlesRoot ?? path.join(os.homedir(), "AgentBundles");
528
547
  this.bundlesRoot = bundlesRoot;
529
548
  this.contexts = new Map(options.agents.map((agent) => {
530
- const senses = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
531
- const facts = senseFactsFromRuntimeConfig(agent, senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent));
532
- return [agent, { senses, facts }];
549
+ const { senses, workbenchHasStaleBundleEntry } = readAgentSenses(path.join(bundlesRoot, `${agent}.ouro`, "agent.json"));
550
+ const facts = senseFactsFromRuntimeConfig(agent, senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent), workbenchHasStaleBundleEntry);
551
+ return [agent, { senses, workbenchHasStaleBundleEntry, facts }];
533
552
  }));
534
553
  const managedSenseAgents = [...this.contexts.entries()].flatMap(([agent, context]) => {
535
554
  return ["teams", "bluebubbles", "mail", "voice", "a2a"]
@@ -553,7 +572,7 @@ class DaemonSenseManager {
553
572
  const context = this.contexts.get(parsed.agent);
554
573
  if (!context)
555
574
  return { ok: true };
556
- context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(parsed.agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent));
575
+ context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(parsed.agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent), context.workbenchHasStaleBundleEntry);
557
576
  const fact = context.facts[parsed.sense];
558
577
  if (fact.configured)
559
578
  return { ok: true };
@@ -641,7 +660,7 @@ class DaemonSenseManager {
641
660
  /* v8 ignore next -- defensive: config refreshes are only scheduled for known agent contexts @preserve */
642
661
  if (!context)
643
662
  return;
644
- context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed);
663
+ context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, refreshed, machineRefreshed, context.workbenchHasStaleBundleEntry);
645
664
  if (!context.facts[parsed.sense].configured) {
646
665
  if (this.shouldRetryConfigRefresh(parsed.sense, refreshed, machineRefreshed)) {
647
666
  retryAfterMs = this.nextConfigRetryDelayMs(name);
@@ -696,7 +715,7 @@ class DaemonSenseManager {
696
715
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(agent, currentMachineId(), { preserveCachedOnFailure: true })
697
716
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
698
717
  /* v8 ignore stop */
699
- context.facts = senseFactsFromRuntimeConfig(agent, context.senses, runtimeConfig, machineRuntimeConfig);
718
+ context.facts = senseFactsFromRuntimeConfig(agent, context.senses, runtimeConfig, machineRuntimeConfig, context.workbenchHasStaleBundleEntry);
700
719
  });
701
720
  await Promise.all(refreshes);
702
721
  }
@@ -751,7 +770,7 @@ class DaemonSenseManager {
751
770
  for (const [agent, context] of this.contexts.entries()) {
752
771
  const runtimeConfig = (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent);
753
772
  const machineRuntimeConfig = (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent);
754
- context.facts = senseFactsFromRuntimeConfig(agent, context.senses, runtimeConfig, machineRuntimeConfig);
773
+ context.facts = senseFactsFromRuntimeConfig(agent, context.senses, runtimeConfig, machineRuntimeConfig, context.workbenchHasStaleBundleEntry);
755
774
  if (!context.senses.bluebubbles.enabled || !context.facts.bluebubbles.configured || !machineRuntimeConfig.ok) {
756
775
  continue;
757
776
  }
@@ -805,7 +824,7 @@ class DaemonSenseManager {
805
824
  const machineRuntimeConfig = needsMachineConfig
806
825
  ? await (0, runtime_credentials_1.refreshMachineRuntimeCredentialConfig)(parsed.agent, currentMachineId(), { preserveCachedOnFailure: true })
807
826
  : (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(parsed.agent);
808
- context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, runtimeConfig, machineRuntimeConfig);
827
+ context.facts = senseFactsFromRuntimeConfig(parsed.agent, context.senses, runtimeConfig, machineRuntimeConfig, context.workbenchHasStaleBundleEntry);
809
828
  this.processManager.resetAgentFailureState?.(managedName);
810
829
  await this.processManager.startAgent?.(managedName);
811
830
  return this.listSenseRows().find((row) => row.agent === parsed.agent && row.sense === parsed.sense) ?? null;
@@ -821,7 +840,7 @@ class DaemonSenseManager {
821
840
  runtime.set(parsed.agent, current);
822
841
  }
823
842
  const rows = [...this.contexts.entries()].flatMap(([agent, context]) => {
824
- context.facts = senseFactsFromRuntimeConfig(agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent));
843
+ context.facts = senseFactsFromRuntimeConfig(agent, context.senses, (0, runtime_credentials_1.readRuntimeCredentialConfig)(agent), (0, runtime_credentials_1.readMachineRuntimeCredentialConfig)(agent), context.workbenchHasStaleBundleEntry);
825
844
  const blueBubblesRuntimeFacts = readBlueBubblesRuntimeFacts(agent, this.bundlesRoot, runtime.get(agent)?.bluebubbles);
826
845
  const blueBubblesConfigured = context.facts.bluebubbles.configured;
827
846
  const runtimeInfo = {
@@ -871,7 +890,9 @@ class DaemonSenseManager {
871
890
  ?? context.facts[entry.sense].detail
872
891
  : context.facts[entry.sense].detail
873
892
  : context.facts[entry.sense].detail
874
- : "not enabled in agent.json",
893
+ : entry.sense === "workbench"
894
+ ? WORKBENCH_RUNTIME_INJECTION_DETAIL
895
+ : "not enabled in agent.json",
875
896
  ...(entry.sense === "bluebubbles" && blueBubblesConfigured ? {
876
897
  proofMethod: blueBubblesRuntimeFacts.proofMethod,
877
898
  lastProofAt: blueBubblesRuntimeFacts.lastProofAt,
@@ -30,6 +30,9 @@ function resolveStatus(enabled, daemonManaged, runtimeInfo) {
30
30
  return "disabled";
31
31
  }
32
32
  if (!daemonManaged) {
33
+ if (runtimeInfo?.configured === false) {
34
+ return "needs_config";
35
+ }
33
36
  return "interactive";
34
37
  }
35
38
  if (runtimeInfo?.runtime === "error") {
@@ -131,6 +131,28 @@ function hasOpenAIRealtimeVoiceConfig(options) {
131
131
  function recordOrUndefined(value) {
132
132
  return !!value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
133
133
  }
134
+ function rawWorkbenchBundleEntryPresent() {
135
+ try {
136
+ const raw = JSON.parse(fs.readFileSync(path.join((0, identity_1.getAgentRoot)(), "agent.json"), "utf-8"));
137
+ const senses = recordOrUndefined(raw.senses);
138
+ const mcpServers = recordOrUndefined(raw.mcpServers);
139
+ const hasWorkbenchSense = senses
140
+ ? Object.prototype.hasOwnProperty.call(senses, "workbench")
141
+ : false;
142
+ const hasWorkbenchMcp = mcpServers
143
+ ? Object.prototype.hasOwnProperty.call(mcpServers, "ouro_workbench")
144
+ : false;
145
+ return hasWorkbenchSense || hasWorkbenchMcp;
146
+ }
147
+ catch {
148
+ return null;
149
+ }
150
+ }
151
+ function hasStaleWorkbenchBundleEntry(config) {
152
+ return rawWorkbenchBundleEntryPresent()
153
+ ?? (config.senses?.workbench?.enabled === true
154
+ || Object.prototype.hasOwnProperty.call(config.mcpServers ?? {}, "ouro_workbench"));
155
+ }
134
156
  function readSenseStatusLines() {
135
157
  const config = (0, identity_1.loadAgentConfig)();
136
158
  const configuredSenses = config.senses ?? {};
@@ -175,6 +197,7 @@ function readSenseStatusLines() {
175
197
  && hasTextField(voice, "whisperCliPath")
176
198
  && hasTextField(voice, "whisperModelPath");
177
199
  const privateKeys = mailroom?.privateKeys;
200
+ const workbenchHasStaleBundleEntry = hasStaleWorkbenchBundleEntry(config);
178
201
  const configured = {
179
202
  cli: true,
180
203
  teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
@@ -187,8 +210,7 @@ function readSenseStatusLines() {
187
210
  ? openAIRealtimeVoiceReady
188
211
  : cascadeVoiceReady,
189
212
  a2a: true,
190
- workbench: typeof config.mcpServers?.ouro_workbench?.command === "string"
191
- && config.mcpServers.ouro_workbench.command.trim().length > 0,
213
+ workbench: false,
192
214
  };
193
215
  const rows = [
194
216
  { label: "CLI", status: "interactive" },
@@ -216,13 +238,13 @@ function readSenseStatusLines() {
216
238
  label: "Workbench",
217
239
  // Workbench can be injected at runtime (the Workbench app spawns
218
240
  // `ouro mcp-serve --workbench-mcp` for its boss) with NO agent.json entry.
219
- // A disabled/needs_config row here does NOT mean the tools are absent —
241
+ // A disabled/stale row here does NOT mean the tools are absent —
220
242
  // the authoritative signal is whether `workbench_*` tools are in the
221
243
  // toolset this turn. The annotation prevents the agent from misreading the
222
244
  // row as "blocked".
223
- status: !senses.workbench.enabled
224
- ? "disabled (runtime-injected when launched by Workbench app)"
225
- : configured.workbench ? "ready" : "needs_config",
245
+ status: workbenchHasStaleBundleEntry
246
+ ? "stale_bundle_entry (runtime-injected when launched by Workbench app; run ouro connect workbench to clean agent.json)"
247
+ : "disabled (runtime-injected when launched by Workbench app)",
226
248
  },
227
249
  ];
228
250
  return rows.map((row) => `- ${row.label}: ${row.status}`);
@@ -435,6 +435,28 @@ function hasTextField(record, key) {
435
435
  function recordOrUndefined(value) {
436
436
  return !!value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
437
437
  }
438
+ function rawWorkbenchBundleEntryPresent() {
439
+ try {
440
+ const raw = JSON.parse(fs.readFileSync(path.join((0, identity_1.getAgentRoot)(), "agent.json"), "utf-8"));
441
+ const senses = recordOrUndefined(raw.senses);
442
+ const mcpServers = recordOrUndefined(raw.mcpServers);
443
+ const hasWorkbenchSense = senses
444
+ ? Object.prototype.hasOwnProperty.call(senses, "workbench")
445
+ : false;
446
+ const hasWorkbenchMcp = mcpServers
447
+ ? Object.prototype.hasOwnProperty.call(mcpServers, "ouro_workbench")
448
+ : false;
449
+ return hasWorkbenchSense || hasWorkbenchMcp;
450
+ }
451
+ catch {
452
+ return null;
453
+ }
454
+ }
455
+ function hasStaleWorkbenchBundleEntry(config) {
456
+ return rawWorkbenchBundleEntryPresent()
457
+ ?? (config.senses?.workbench?.enabled === true
458
+ || Object.prototype.hasOwnProperty.call(config.mcpServers ?? {}, "ouro_workbench"));
459
+ }
438
460
  function localSenseStatusLines() {
439
461
  const config = (0, identity_1.loadAgentConfig)();
440
462
  const configuredSenses = config.senses ?? {};
@@ -460,6 +482,7 @@ function localSenseStatusLines() {
460
482
  const portableVoice = recordOrUndefined(runtimePayload.voice) ?? recordOrUndefined(payload.voice);
461
483
  const integrations = recordOrUndefined(runtimePayload.integrations) ?? recordOrUndefined(payload.integrations);
462
484
  const privateKeys = mailroom?.privateKeys;
485
+ const workbenchHasStaleBundleEntry = hasStaleWorkbenchBundleEntry(config);
463
486
  const configured = {
464
487
  cli: true,
465
488
  teams: hasTextField(teams, "clientId") && hasTextField(teams, "clientSecret") && hasTextField(teams, "tenantId"),
@@ -470,8 +493,7 @@ function localSenseStatusLines() {
470
493
  && hasTextField(voice, "whisperCliPath")
471
494
  && hasTextField(voice, "whisperModelPath"),
472
495
  a2a: true,
473
- workbench: typeof config.mcpServers?.ouro_workbench?.command === "string"
474
- && config.mcpServers.ouro_workbench.command.trim().length > 0,
496
+ workbench: false,
475
497
  };
476
498
  const rows = [
477
499
  { label: "CLI", status: "interactive" },
@@ -499,13 +521,13 @@ function localSenseStatusLines() {
499
521
  label: "Workbench",
500
522
  // Workbench can be injected at runtime (the Workbench app spawns
501
523
  // `ouro mcp-serve --workbench-mcp` for its boss) with NO agent.json entry.
502
- // A disabled/needs_config row here does NOT mean the tools are absent —
524
+ // A disabled/stale row here does NOT mean the tools are absent —
503
525
  // the authoritative signal is whether `workbench_*` tools are in the
504
526
  // toolset this turn. The annotation prevents the agent from misreading the
505
527
  // row as "blocked".
506
- status: !senses.workbench.enabled
507
- ? "disabled (runtime-injected when launched by Workbench app)"
508
- : configured.workbench ? "ready" : "needs_config",
528
+ status: workbenchHasStaleBundleEntry
529
+ ? "stale_bundle_entry (runtime-injected when launched by Workbench app; run ouro connect workbench to clean agent.json)"
530
+ : "disabled (runtime-injected when launched by Workbench app)",
509
531
  },
510
532
  ];
511
533
  return rows.map((row) => `- ${row.label}: ${row.status}`);
@@ -525,7 +547,7 @@ function senseRuntimeGuidance(channel, preReadStatusLines) {
525
547
  lines.push("teams setup truth: run `ouro connect teams --agent <agent>` from the connect bay; it stores Teams runtime/config fields and enables `senses.teams.enabled`.");
526
548
  lines.push("bluebubbles setup truth: run `ouro connect bluebubbles --agent <agent>` from the connect bay; it stores this machine's BlueBubbles URL/password/listener config in the agent vault machine runtime item.");
527
549
  lines.push("a2a setup truth: run `ouro connect a2a --agent <agent>` to enable the A2A sense, `ouro a2a card --agent <agent> --base-url <public-url>` to publish an agent card, and `ouro a2a onboard --agent <agent> --card-url <peer-card-url>` to add a peer as an agent friend. A2A uses the existing friend trust model, not a separate trust registry.");
528
- lines.push("workbench setup truth: Ouro Workbench is the local machine sense for terminal/TUI agents. When the Workbench app launches me as its boss it injects the `ouro_workbench` MCP into my turn at runtime (it spawns `ouro mcp-serve --agent <me> --workbench-mcp`), so I receive the tools for the served session without any `agent.json` `mcpServers.ouro_workbench` entry — nothing is written to the bundle, so this stays path-free and cross-machine clean. The authoritative signal that Workbench is active is simply that the `workbench_*` tools are present in my toolset this turn; the sense table or `agent.json` may still read as disabled/needs_config because no bundle entry exists, and that is expected under runtime injection — I do NOT treat that as blocked or as a trust-level problem. I observe and queue auditable Workbench actions through `workbench_status`, `workbench_sense`, `workbench_transcript_tail`, `workbench_search_transcripts`, `workbench_recovery_drill`, and `workbench_request_action`; raw provider secrets remain in the agent vault, and Apple notarization is unrelated to local use. The explicit `ouro connect workbench --agent <me>` command still writes a persistent bundle entry as an opt-in escape hatch, but the boss does not need it.");
550
+ lines.push("workbench setup truth: Ouro Workbench is the local machine sense for terminal/TUI agents. When the Workbench app launches me as its boss it injects the `ouro_workbench` MCP into my turn at runtime (it spawns `ouro mcp-serve --agent <me> --workbench-mcp`), so I receive the tools for the served session without any `agent.json` `mcpServers.ouro_workbench` entry — nothing is written to the bundle, so this stays path-free and cross-machine clean. The authoritative signal that Workbench is active is simply that the `workbench_*` tools are present in my toolset this turn; the sense table may read as disabled when no bundle entry exists, or as `stale_bundle_entry` when legacy bundle config needs cleanup, and neither state proves the runtime-injected tools are absent — I do NOT treat that as blocked or as a trust-level problem. I observe and queue auditable Workbench actions through `workbench_status`, `workbench_sense`, `workbench_transcript_tail`, `workbench_search_transcripts`, `workbench_recovery_drill`, and `workbench_request_action`; raw provider secrets remain in the agent vault, and Apple notarization is unrelated to local use. The explicit `ouro connect workbench --agent <me>` command verifies the installed MCP binary and removes stale Workbench bundle entries; it does not enable `senses.workbench` or write `mcpServers.ouro_workbench`.");
529
551
  lines.push("mail setup AX: if a human asks me to set up email, I do not hand them a terminal checklist. I guide the flow end-to-end: name the current phase, run agent-runnable commands myself with shell/tools when available, ask the human only for human-required facts or browser actions, wait for their reply, verify the result, then continue.");
530
552
  lines.push("mail setup hard rule: never tell the human to run `ouro account ensure`, `ouro connect mail`, `ouro mail import-mbox`, `ouro status`, or `ouro doctor` for setup. Say what I am about to run, run it myself, and report the result. If my current surface cannot run shell/tools, I ask for a tool-capable Ouro setup session or companion to continue; I do not offload CLI operation to the human.");
531
553
  lines.push("mail setup truth: Agent Mail uses Mailroom, not HEY OAuth/IMAP. For the full work substrate account, the agent-runnable command is `ouro account ensure --agent <agent> --owner-email <email> --source hey`; use `ouro connect mail --agent <agent> --owner-email <email> --source hey` for mail-only repair/provisioning, or `--no-delegated-source` for native-only mail. The detailed runbook is `docs/agent-mail-setup.md`.");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.692",
3
+ "version": "0.1.0-alpha.694",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",