@ouro.bot/cli 0.1.0-alpha.235 → 0.1.0-alpha.237

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,15 @@
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.237",
6
+ "changes": [
7
+ "feat: pretty `ouro status` \u2014 ANSI colored output with box-drawing header, status dots, grouped senses/workers by agent. Added git sync info to overview.",
8
+ "fix: socket half-close \u2014 sendDaemonCommand now calls client.end() after writing, preventing intermittent connection hangs.",
9
+ "refactor: config tiers \u2014 replaced numeric T1/T2/T3 with `self` (agent-configurable) and `managed` (harness-only). All config keys are now agent-writable except `version` and `enabled`. mcpServers promoted to self.",
10
+ "refactor: removed confirmation system \u2014 deleted propose_config tool, confirmationRequired/confirmationAlwaysRequired/onConfirmAction from core, all tool definitions, and Teams sense. Was only wired up on Teams, silently failed everywhere else. -1,361 lines."
11
+ ]
12
+ },
4
13
  {
5
14
  "version": "0.1.0-alpha.235",
6
15
  "changes": [
@@ -82,11 +82,11 @@ function validateObject(requiredFields) {
82
82
  };
83
83
  }
84
84
  const registryData = [
85
- // --- Tier 3: Operator-only ---
85
+ // --- Managed: harness-only ---
86
86
  {
87
87
  path: "version",
88
- tier: 3,
89
- description: "Agent config schema version. Must be an integer >= 1.",
88
+ tier: "managed",
89
+ description: "Agent config schema version. Managed by the harness. Must be an integer >= 1.",
90
90
  default: 2,
91
91
  effects: "Controls which config migrations apply on load. Changing incorrectly can corrupt config.",
92
92
  topics: ["schema", "migration"],
@@ -94,25 +94,25 @@ const registryData = [
94
94
  },
95
95
  {
96
96
  path: "enabled",
97
- tier: 3,
98
- description: "Whether the agent is enabled. When false, the agent refuses to start.",
97
+ tier: "managed",
98
+ description: "Whether the agent is enabled. Managed by the harness. When false, the agent refuses to start.",
99
99
  default: true,
100
100
  effects: "Disables all agent functionality when set to false.",
101
101
  topics: ["lifecycle", "activation"],
102
102
  validate: validateBoolean,
103
103
  },
104
+ // --- Self: agent-configurable ---
104
105
  {
105
106
  path: "mcpServers",
106
- tier: 3,
107
- description: "MCP server configurations. Maps server name to command, args, and env.",
107
+ tier: "self",
108
+ description: "MCP server configurations. Maps server name to command, args, and env. The agent can add or remove MCP servers.",
108
109
  default: undefined,
109
- effects: "Adds or removes MCP tool servers. Incorrect config can expose the agent to untrusted tool sources.",
110
+ effects: "Adds or removes MCP tool servers.",
110
111
  topics: ["tools", "mcp", "servers", "extensions"],
111
112
  },
112
- // --- Tier 2: Proposal ---
113
113
  {
114
114
  path: "humanFacing.provider",
115
- tier: 2,
115
+ tier: "self",
116
116
  description: "Provider for human-facing interactions (CLI, Teams, BlueBubbles).",
117
117
  default: "anthropic",
118
118
  effects: "Changes the LLM provider used for all human-facing conversations. Affects quality, latency, and cost.",
@@ -121,7 +121,7 @@ const registryData = [
121
121
  },
122
122
  {
123
123
  path: "humanFacing.model",
124
- tier: 2,
124
+ tier: "self",
125
125
  description: "Model name for human-facing interactions.",
126
126
  default: "claude-opus-4-6",
127
127
  effects: "Changes the specific model used for human-facing conversations.",
@@ -130,7 +130,7 @@ const registryData = [
130
130
  },
131
131
  {
132
132
  path: "agentFacing.provider",
133
- tier: 2,
133
+ tier: "self",
134
134
  description: "Provider for agent-facing interactions (inner dialog, delegation).",
135
135
  default: "anthropic",
136
136
  effects: "Changes the LLM provider used for inner dialog and agent-to-agent communication.",
@@ -139,7 +139,7 @@ const registryData = [
139
139
  },
140
140
  {
141
141
  path: "agentFacing.model",
142
- tier: 2,
142
+ tier: "self",
143
143
  description: "Model name for agent-facing interactions.",
144
144
  default: "claude-opus-4-6",
145
145
  effects: "Changes the specific model used for inner dialog and agent-to-agent communication.",
@@ -148,7 +148,7 @@ const registryData = [
148
148
  },
149
149
  {
150
150
  path: "context.maxTokens",
151
- tier: 2,
151
+ tier: "self",
152
152
  description: "Maximum context window size in tokens.",
153
153
  default: 80000,
154
154
  effects: "Larger values allow more context but increase cost and latency. Must match model capability.",
@@ -157,7 +157,7 @@ const registryData = [
157
157
  },
158
158
  {
159
159
  path: "senses.cli",
160
- tier: 2,
160
+ tier: "self",
161
161
  description: "CLI sense configuration. Controls whether the CLI interface is enabled.",
162
162
  default: { enabled: true },
163
163
  effects: "Enables or disables the CLI (terminal) interaction channel.",
@@ -166,7 +166,7 @@ const registryData = [
166
166
  },
167
167
  {
168
168
  path: "senses.teams",
169
- tier: 2,
169
+ tier: "self",
170
170
  description: "Teams sense configuration. Controls whether the Teams interface is enabled.",
171
171
  default: { enabled: false },
172
172
  effects: "Enables or disables the Microsoft Teams interaction channel.",
@@ -175,7 +175,7 @@ const registryData = [
175
175
  },
176
176
  {
177
177
  path: "senses.bluebubbles",
178
- tier: 2,
178
+ tier: "self",
179
179
  description: "BlueBubbles sense configuration. Controls whether the iMessage interface is enabled.",
180
180
  default: { enabled: false },
181
181
  effects: "Enables or disables the BlueBubbles (iMessage) interaction channel.",
@@ -184,7 +184,7 @@ const registryData = [
184
184
  },
185
185
  {
186
186
  path: "sync.enabled",
187
- tier: 2,
187
+ tier: "self",
188
188
  description: "Whether git-based bundle sync is enabled.",
189
189
  default: false,
190
190
  effects: "Enables automatic synchronization of agent state via git. Requires sync.remote to be configured.",
@@ -193,17 +193,16 @@ const registryData = [
193
193
  },
194
194
  {
195
195
  path: "sync.remote",
196
- tier: 2,
196
+ tier: "self",
197
197
  description: "Git remote name used for bundle sync.",
198
198
  default: "origin",
199
199
  effects: "Controls which git remote is used when sync is enabled.",
200
200
  topics: ["sync", "git", "remote"],
201
201
  validate: validateString,
202
202
  },
203
- // --- Tier 1: Self-service ---
204
203
  {
205
204
  path: "context.contextMargin",
206
- tier: 1,
205
+ tier: "self",
207
206
  description: "Percentage of context window reserved as margin before compaction triggers.",
208
207
  default: 20,
209
208
  effects: "Higher values trigger compaction earlier, preserving more headroom. Lower values use more context.",
@@ -212,7 +211,7 @@ const registryData = [
212
211
  },
213
212
  {
214
213
  path: "phrases.thinking",
215
- tier: 1,
214
+ tier: "self",
216
215
  description: "Array of phrases displayed while the agent is thinking.",
217
216
  default: ["working"],
218
217
  effects: "Changes the thinking indicator text shown to users. Purely cosmetic.",
@@ -221,7 +220,7 @@ const registryData = [
221
220
  },
222
221
  {
223
222
  path: "phrases.tool",
224
- tier: 1,
223
+ tier: "self",
225
224
  description: "Array of phrases displayed while the agent is running a tool.",
226
225
  default: ["running tool"],
227
226
  effects: "Changes the tool-use indicator text shown to users. Purely cosmetic.",
@@ -230,7 +229,7 @@ const registryData = [
230
229
  },
231
230
  {
232
231
  path: "phrases.followup",
233
- tier: 1,
232
+ tier: "self",
234
233
  description: "Array of phrases displayed during follow-up processing.",
235
234
  default: ["processing"],
236
235
  effects: "Changes the follow-up indicator text shown to users. Purely cosmetic.",
@@ -239,7 +238,7 @@ const registryData = [
239
238
  },
240
239
  {
241
240
  path: "shell.defaultTimeout",
242
- tier: 1,
241
+ tier: "self",
243
242
  description: "Default timeout in milliseconds for shell command execution.",
244
243
  default: undefined,
245
244
  effects: "Controls how long shell commands run before timing out. Undefined uses system default.",
@@ -248,7 +247,7 @@ const registryData = [
248
247
  },
249
248
  {
250
249
  path: "logging.level",
251
- tier: 1,
250
+ tier: "self",
252
251
  description: "Minimum log level: debug, info, warn, or error.",
253
252
  default: undefined,
254
253
  effects: "Controls verbosity of runtime logging. Lower levels produce more output.",
@@ -257,7 +256,7 @@ const registryData = [
257
256
  },
258
257
  {
259
258
  path: "logging.sinks",
260
- tier: 1,
259
+ tier: "self",
261
260
  description: "Array of log sink types: 'terminal' and/or 'ndjson'.",
262
261
  default: undefined,
263
262
  effects: "Controls where log output is directed. Terminal shows in console, ndjson writes structured logs.",
@@ -905,23 +905,6 @@ async function runAgent(messages, callbacks, channel, signal, options) {
905
905
  providerRuntime.appendToolOutput(tc.id, rejection);
906
906
  continue;
907
907
  }
908
- // Confirmation check for mutate tools.
909
- // confirmationAlwaysRequired tools cannot be bypassed by skipConfirmation.
910
- const needsConfirmation = (0, tools_1.isConfirmationRequired)(tc.name) && (!options?.skipConfirmation || (0, tools_1.isConfirmationAlwaysRequired)(tc.name));
911
- if (needsConfirmation) {
912
- let decision = "denied";
913
- if (callbacks.onConfirmAction) {
914
- decision = await callbacks.onConfirmAction(tc.name, args);
915
- }
916
- if (decision !== "confirmed") {
917
- const cancelled = "Action cancelled by user.";
918
- callbacks.onToolStart(tc.name, args);
919
- callbacks.onToolEnd(tc.name, argSummary, false);
920
- messages.push({ role: "tool", tool_call_id: tc.id, content: cancelled });
921
- providerRuntime.appendToolOutput(tc.id, cancelled);
922
- continue;
923
- }
924
- }
925
908
  callbacks.onToolStart(tc.name, args);
926
909
  let toolResult;
927
910
  let success;
@@ -959,20 +942,48 @@ async function runAgent(messages, callbacks, channel, signal, options) {
959
942
  callbacks.onError(new Error("context trimmed, retrying..."), "transient");
960
943
  continue;
961
944
  }
962
- // Transient errors: retry with exponential backoff
963
- if (isTransientError(e) && retryCount < MAX_RETRIES) {
945
+ // Transient errors: retry with exponential backoff.
946
+ // Two-layer detection: generic isTransientError (HTTP codes, network patterns)
947
+ // PLUS provider-specific classifyError (catches SDK-wrapped errors that
948
+ // generic detection might miss).
949
+ const errorForClassification = e instanceof Error ? e : /* v8 ignore next -- defensive @preserve */ new Error(String(e));
950
+ let providerClassification;
951
+ try {
952
+ providerClassification = providerRuntime.classifyError(errorForClassification);
953
+ }
954
+ catch {
955
+ /* v8 ignore next -- defensive: classifyError should not throw @preserve */
956
+ providerClassification = "unknown";
957
+ }
958
+ const retryableByClassification = providerClassification === "rate-limit"
959
+ || providerClassification === "server-error"
960
+ || providerClassification === "network-error";
961
+ const retryableByGeneric = isTransientError(e);
962
+ const shouldRetry = (retryableByGeneric || retryableByClassification) && retryCount < MAX_RETRIES;
963
+ (0, runtime_1.emitNervesEvent)({
964
+ level: shouldRetry ? "info" : "warn",
965
+ event: shouldRetry ? "engine.provider_retry" : "engine.provider_retry_skip",
966
+ component: "engine",
967
+ message: shouldRetry
968
+ ? `provider error is retryable (attempt ${retryCount + 1}/${MAX_RETRIES})`
969
+ : `provider error is not retryable or retries exhausted`,
970
+ meta: {
971
+ provider: providerRuntime.id,
972
+ model: providerRuntime.model,
973
+ retryCount,
974
+ maxRetries: MAX_RETRIES,
975
+ retryableByGeneric,
976
+ retryableByClassification,
977
+ providerClassification,
978
+ errorMessage: errorForClassification.message.slice(0, 200),
979
+ httpStatus: e.status ?? null,
980
+ },
981
+ });
982
+ if (shouldRetry) {
964
983
  retryCount++;
965
984
  const delay = RETRY_BASE_MS * Math.pow(2, retryCount - 1);
966
- let classification;
967
- try {
968
- classification = providerRuntime.classifyError(e instanceof Error ? e : /* v8 ignore next -- defensive: errors are always Error instances @preserve */ new Error(String(e)));
969
- }
970
- catch {
971
- /* v8 ignore next -- defensive: classifyError should not throw @preserve */
972
- classification = classifyTransientError(e);
973
- }
974
985
  /* v8 ignore next -- defensive: all classifications have labels @preserve */
975
- const cause = TRANSIENT_RETRY_LABELS[classification] ?? classification;
986
+ const cause = TRANSIENT_RETRY_LABELS[providerClassification] ?? providerClassification;
976
987
  callbacks.onError(new Error(`${cause}, retrying in ${delay / 1000}s (${retryCount}/${MAX_RETRIES})...`), "transient");
977
988
  // Wait with abort support
978
989
  const aborted = await new Promise((resolve) => {
@@ -995,14 +1006,8 @@ async function runAgent(messages, callbacks, channel, signal, options) {
995
1006
  providerRuntime.resetTurnState(messages);
996
1007
  continue;
997
1008
  }
998
- terminalError = e instanceof Error ? e : new Error(String(e));
999
- try {
1000
- terminalErrorClassification = providerRuntime.classifyError(terminalError);
1001
- }
1002
- catch {
1003
- /* v8 ignore next -- defensive: classifyError should not throw @preserve */
1004
- terminalErrorClassification = "unknown";
1005
- }
1009
+ terminalError = errorForClassification;
1010
+ terminalErrorClassification = providerClassification;
1006
1011
  /* v8 ignore start — auth-failure guidance: tested via provider error classification tests @preserve */
1007
1012
  if (terminalErrorClassification === "auth-failure") {
1008
1013
  const agentName = (0, identity_2.getAgentName)();
@@ -89,6 +89,8 @@ function parseStatusPayload(data) {
89
89
  senseCount: numberField(overview.senseCount) ?? 0,
90
90
  entryPath: stringField(overview.entryPath) ?? "unknown",
91
91
  mode: stringField(overview.mode) ?? "unknown",
92
+ syncEnabled: booleanField(overview.syncEnabled) ?? false,
93
+ syncRemote: stringField(overview.syncRemote) ?? "origin",
92
94
  };
93
95
  const parsedSenses = senses.map((entry) => {
94
96
  if (!entry || typeof entry !== "object" || Array.isArray(entry))
@@ -141,6 +143,44 @@ function parseStatusPayload(data) {
141
143
  workers: parsedWorkers,
142
144
  };
143
145
  }
146
+ // ── ANSI color helpers (private) ──
147
+ const RESET = "\x1b[0m";
148
+ const BOLD = "\x1b[1m";
149
+ const DIM = "\x1b[2m";
150
+ const TEAL = "\x1b[38;2;78;201;176m";
151
+ const GREEN = "\x1b[38;2;46;204;64m";
152
+ const RED = "\x1b[38;2;231;76;60m";
153
+ const YELLOW = "\x1b[38;2;230;190;50m";
154
+ /* v8 ignore start -- cosmetic ANSI wrappers @preserve */
155
+ function teal(text) { return `${TEAL}${text}${RESET}`; }
156
+ function green(text) { return `${GREEN}${text}${RESET}`; }
157
+ function red(text) { return `${RED}${text}${RESET}`; }
158
+ function yellow(text) { return `${YELLOW}${text}${RESET}`; }
159
+ function bold(text) { return `${BOLD}${text}${RESET}`; }
160
+ function dim(text) { return `${DIM}${text}${RESET}`; }
161
+ /* v8 ignore stop */
162
+ /* v8 ignore start -- cosmetic display: status dot color mapping tested visually @preserve */
163
+ function statusDot(status) {
164
+ switch (status) {
165
+ case "running":
166
+ case "ok":
167
+ case "interactive":
168
+ case "enabled":
169
+ return green("●");
170
+ case "crashed":
171
+ case "warn":
172
+ case "error":
173
+ return red("●");
174
+ case "needs_config":
175
+ return yellow("●");
176
+ case "disabled":
177
+ case "stopped":
178
+ return dim("○");
179
+ default:
180
+ return dim("●");
181
+ }
182
+ }
183
+ /* v8 ignore stop */
144
184
  // ── Formatters ──
145
185
  function humanizeSenseName(sense, label) {
146
186
  if (label)
@@ -153,6 +193,7 @@ function humanizeSenseName(sense, label) {
153
193
  return "Teams";
154
194
  return sense;
155
195
  }
196
+ /* v8 ignore start -- utility formatter; retained for non-status table output @preserve */
156
197
  function formatTable(headers, rows) {
157
198
  const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => row[index].length)));
158
199
  const renderRow = (row) => `| ${row.map((cell, index) => (index === row.length - 1
@@ -165,56 +206,96 @@ function formatTable(headers, rows) {
165
206
  ...rows.map(renderRow),
166
207
  ].join("\n");
167
208
  }
209
+ /* v8 ignore stop */
168
210
  function formatDaemonStatusOutput(response, fallback) {
169
211
  const payload = parseStatusPayload(response.data);
170
212
  if (!payload)
171
213
  return fallback;
172
- const overviewRows = [
173
- ["Daemon", payload.overview.daemon],
174
- ["Socket", payload.overview.socketPath],
175
- ["Version", payload.overview.version],
176
- ["Last Updated", payload.overview.lastUpdated],
177
- ["Outlook", payload.overview.outlookUrl],
178
- ["Entry Path", payload.overview.entryPath],
179
- ["Mode", payload.overview.mode],
180
- ["Workers", String(payload.overview.workerCount)],
181
- ["Senses", String(payload.overview.senseCount)],
182
- ["Health", payload.overview.health],
183
- ];
184
- const senseRows = payload.senses.map((row) => [
185
- row.agent,
186
- humanizeSenseName(row.sense, row.label),
187
- row.enabled ? "ON" : "OFF",
188
- row.status,
189
- row.detail,
190
- ]);
191
- const workerRows = payload.workers.map((row) => {
192
- /* v8 ignore start — exit info branches tested via daemon-crash-context; v8 misreports conditional chains @preserve */
193
- let exitInfo = "n/a";
194
- if (row.lastExitCode !== null)
195
- exitInfo = `code=${row.lastExitCode}`;
196
- if (row.lastSignal !== null)
197
- exitInfo = row.lastExitCode !== null ? `code=${row.lastExitCode} sig=${row.lastSignal}` : `sig=${row.lastSignal}`;
198
- /* v8 ignore stop */
199
- return [
200
- row.agent,
201
- row.worker,
202
- row.status,
203
- row.pid === null ? "n/a" : String(row.pid),
204
- String(row.restartCount),
205
- exitInfo,
206
- ];
207
- });
208
- return [
209
- "Overview",
210
- formatTable(["Item", "Value"], overviewRows),
211
- "",
212
- "Senses",
213
- formatTable(["Agent", "Sense", "Enabled", "State", "Detail"], senseRows),
214
- "",
215
- "Workers",
216
- formatTable(["Agent", "Worker", "State", "PID", "Restarts", "Last Exit"], workerRows),
217
- ].join("\n");
214
+ const ov = payload.overview;
215
+ const lines = [];
216
+ // ── Header banner ──
217
+ const modeTag = ov.mode === "dev" ? "dev" : "";
218
+ const daemonStatus = ov.daemon === "running" ? green("●") + " " + bold("running") : red("●") + " " + bold(ov.daemon);
219
+ const modeStr = modeTag ? ` ${dim(`(${modeTag})`)}` : "";
220
+ const bannerContent = ` ${bold(ov.version)} ${daemonStatus}${modeStr} `;
221
+ // Measure raw content width (strip ANSI for width calc)
222
+ const rawBanner = bannerContent.replace(/\x1b\[[0-9;]*m/g, "");
223
+ const bannerWidth = Math.max(rawBanner.length, 42);
224
+ const titleLabel = " ouroboros daemon ";
225
+ const topRightPad = Math.max(0, bannerWidth - titleLabel.length - 2);
226
+ lines.push(` ${teal("╭─")}${teal(titleLabel)}${teal("─".repeat(topRightPad))}${teal("╮")}`);
227
+ lines.push(` ${teal("│")}${bannerContent}${" ".repeat(Math.max(0, bannerWidth - rawBanner.length))}${teal("│")}`);
228
+ lines.push(` ${teal("╰")}${teal("─".repeat(bannerWidth))}${teal("╯")}`);
229
+ lines.push("");
230
+ // ── Key-value overview ──
231
+ const kvLine = (label, value) => ` ${teal(label.padEnd(11))} ${value}`;
232
+ lines.push(kvLine("Socket", ov.socketPath));
233
+ lines.push(kvLine("Outlook", ov.outlookUrl));
234
+ lines.push(kvLine("Health", `${statusDot(ov.health)} ${ov.health}`));
235
+ /* v8 ignore next -- cosmetic: sync-enabled branch requires agent with sync configured @preserve */
236
+ const syncLabel = ov.syncEnabled ? `${green("●")} enabled (remote: ${ov.syncRemote})` : `${dim("○")} disabled`;
237
+ lines.push(kvLine("Git Sync", syncLabel));
238
+ lines.push(kvLine("Updated", ov.lastUpdated));
239
+ lines.push("");
240
+ // ── Senses ──
241
+ if (payload.senses.length > 0) {
242
+ lines.push(` ${teal("──")} ${bold("Senses")} ${teal("─".repeat(37))}`);
243
+ // Group senses by agent
244
+ const sensesByAgent = new Map();
245
+ for (const row of payload.senses) {
246
+ const list = sensesByAgent.get(row.agent) ?? [];
247
+ list.push(row);
248
+ sensesByAgent.set(row.agent, list);
249
+ }
250
+ // Calculate column widths for alignment
251
+ const allSenseNames = payload.senses.map((r) => humanizeSenseName(r.sense, r.label));
252
+ const nameWidth = Math.max(12, ...allSenseNames.map((n) => n.length));
253
+ const allStatuses = payload.senses.map((r) => r.status);
254
+ const statusWidth = Math.max(10, ...allStatuses.map((s) => s.length));
255
+ for (const [agent, rows] of sensesByAgent) {
256
+ lines.push(` ${bold(agent)}`);
257
+ for (const row of rows) {
258
+ const name = humanizeSenseName(row.sense, row.label).padEnd(nameWidth);
259
+ const dot = row.enabled ? statusDot(row.status) : dim("○");
260
+ const statusText = (row.enabled ? row.status : "disabled").padEnd(statusWidth);
261
+ lines.push(` ${name} ${dot} ${statusText} ${dim(row.detail)}`);
262
+ }
263
+ }
264
+ lines.push("");
265
+ }
266
+ // ── Workers ──
267
+ if (payload.workers.length > 0) {
268
+ lines.push(` ${teal("──")} ${bold("Workers")} ${teal("─".repeat(36))}`);
269
+ // Group workers by agent
270
+ const workersByAgent = new Map();
271
+ for (const row of payload.workers) {
272
+ const list = workersByAgent.get(row.agent) ?? [];
273
+ list.push(row);
274
+ workersByAgent.set(row.agent, list);
275
+ }
276
+ const allWorkerNames = payload.workers.map((r) => r.worker);
277
+ const workerNameWidth = Math.max(12, ...allWorkerNames.map((n) => n.length));
278
+ for (const [agent, rows] of workersByAgent) {
279
+ lines.push(` ${bold(agent)}`);
280
+ for (const row of rows) {
281
+ const name = row.worker.padEnd(workerNameWidth);
282
+ const dot = statusDot(row.status);
283
+ const pidStr = row.pid !== null ? `pid ${row.pid}` : "";
284
+ const restartStr = `restarts: ${row.restartCount}`;
285
+ /* v8 ignore start — exit info branches tested via daemon-crash-context; v8 misreports conditional chains @preserve */
286
+ let exitStr = "";
287
+ if (row.lastExitCode !== null)
288
+ exitStr = `exit=${row.lastExitCode}`;
289
+ if (row.lastSignal !== null)
290
+ exitStr = row.lastExitCode !== null ? `exit=${row.lastExitCode} sig=${row.lastSignal}` : `sig=${row.lastSignal}`;
291
+ /* v8 ignore stop */
292
+ const details = [pidStr, restartStr, exitStr].filter(Boolean).join(" ");
293
+ lines.push(` ${name} ${dot} ${row.status.padEnd(10)} ${dim(details)}`);
294
+ }
295
+ }
296
+ lines.push("");
297
+ }
298
+ return lines.join("\n");
218
299
  }
219
300
  function formatVersionOutput() {
220
301
  const version = (0, runtime_metadata_1.getRuntimeMetadata)().version;
@@ -240,6 +321,8 @@ function buildStoppedStatusPayload(socketPath) {
240
321
  senseCount: 0,
241
322
  entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
242
323
  mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
324
+ syncEnabled: false,
325
+ syncRemote: "origin",
243
326
  },
244
327
  senses: [],
245
328
  workers: [],
@@ -42,6 +42,7 @@ const net = __importStar(require("net"));
42
42
  const os = __importStar(require("os"));
43
43
  const path = __importStar(require("path"));
44
44
  const identity_1 = require("../identity");
45
+ const config_1 = require("../config");
45
46
  const runtime_1 = require("../../nerves/runtime");
46
47
  const runtime_metadata_1 = require("./runtime-metadata");
47
48
  const runtime_mode_1 = require("./runtime-mode");
@@ -233,6 +234,7 @@ class OuroDaemon {
233
234
  const workers = buildWorkerRows(snapshots);
234
235
  const senses = this.senseManager?.listSenseRows() ?? [];
235
236
  const repoRoot = (0, identity_1.getRepoRoot)();
237
+ const syncConfig = (0, config_1.getSyncConfig)();
236
238
  return {
237
239
  overview: {
238
240
  daemon: "running",
@@ -244,6 +246,8 @@ class OuroDaemon {
244
246
  senseCount: senses.length,
245
247
  entryPath: path.join(repoRoot, "dist", "heart", "daemon", "daemon-entry.js"),
246
248
  mode: (0, runtime_mode_1.detectRuntimeMode)(repoRoot),
249
+ syncEnabled: syncConfig.enabled,
250
+ syncRemote: syncConfig.remote,
247
251
  },
248
252
  workers,
249
253
  senses,
@@ -53,6 +53,7 @@ function sendDaemonCommand(socketPath, command) {
53
53
  let raw = "";
54
54
  client.on("connect", () => {
55
55
  client.write(JSON.stringify(command) + "\n");
56
+ client.end();
56
57
  });
57
58
  client.on("data", (chunk) => {
58
59
  raw += chunk.toString("utf-8");
@@ -318,7 +318,6 @@ exports.adoSemanticToolDefinitions = [
318
318
  return result;
319
319
  },
320
320
  integration: "ado",
321
- confirmationRequired: true,
322
321
  summaryKeys: ["organization", "project", "title"],
323
322
  },
324
323
  // -- ado_create_issue --
@@ -360,7 +359,6 @@ exports.adoSemanticToolDefinitions = [
360
359
  return result;
361
360
  },
362
361
  integration: "ado",
363
- confirmationRequired: true,
364
362
  summaryKeys: ["organization", "project", "title"],
365
363
  },
366
364
  // -- ado_move_items --
@@ -415,7 +413,6 @@ exports.adoSemanticToolDefinitions = [
415
413
  return JSON.stringify({ moved, errors, organization: adoCtx.organization, project: adoCtx.project });
416
414
  },
417
415
  integration: "ado",
418
- confirmationRequired: true,
419
416
  summaryKeys: ["organization", "project", "workItemIds"],
420
417
  },
421
418
  // -- ado_restructure_backlog --
@@ -474,7 +471,6 @@ exports.adoSemanticToolDefinitions = [
474
471
  return JSON.stringify({ results, organization: adoCtx.organization, project: adoCtx.project });
475
472
  },
476
473
  integration: "ado",
477
- confirmationRequired: true,
478
474
  summaryKeys: ["organization", "project"],
479
475
  },
480
476
  // -- ado_validate_structure --
@@ -676,7 +672,6 @@ exports.adoSemanticToolDefinitions = [
676
672
  return JSON.stringify({ results, organization: adoCtx.organization, project: adoCtx.project });
677
673
  },
678
674
  integration: "ado",
679
- confirmationRequired: true,
680
675
  summaryKeys: ["organization", "project"],
681
676
  },
682
677
  // -- ado_detect_orphans --
@@ -118,7 +118,7 @@ exports.configToolDefinitions = [
118
118
  type: "function",
119
119
  function: {
120
120
  name: "update_config",
121
- description: "Update a Tier 1 (self-service) agent configuration value immediately. For Tier 2 keys that require operator approval, use propose_config instead. Tier 3 (operator-only) keys are refused.",
121
+ description: "Update an agent configuration value. Managed keys (version, enabled) cannot be modified.",
122
122
  parameters: {
123
123
  type: "object",
124
124
  properties: {
@@ -152,15 +152,10 @@ exports.configToolDefinitions = [
152
152
  return `Error: validation failed for "${a.path}": ${validationError}`;
153
153
  }
154
154
  }
155
- // Tier 3: refuse
156
- if (entry.tier === 3) {
157
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused T3 change to ${a.path}`, meta: { path: a.path, tier: 3 } });
158
- return `Refused: "${a.path}" is an operator-only (Tier 3) key. ${entry.description} Only the operator can change this value directly in agent.json.`;
159
- }
160
- // Tier 2: refuse — must use propose_config
161
- if (entry.tier === 2) {
162
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused T2 change to ${a.path} — use propose_config`, meta: { path: a.path, tier: 2 } });
163
- return `Refused: "${a.path}" is a Tier 2 (proposal) key that requires operator approval. Use propose_config instead of update_config to propose this change.`;
155
+ // Managed: refuse
156
+ if (entry.tier === "managed") {
157
+ (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.update_config_refused", message: `refused managed change to ${a.path}`, meta: { path: a.path, tier: "managed" } });
158
+ return `This key is managed by the harness and cannot be modified directly.`;
164
159
  }
165
160
  const agentRoot = (0, identity_1.getAgentRoot)();
166
161
  const configPath = path.join(agentRoot, "agent.json");
@@ -172,7 +167,7 @@ exports.configToolDefinitions = [
172
167
  /* v8 ignore next -- defensive: agent.json read failure in update_config @preserve */
173
168
  return `Error: failed to read agent.json at ${configPath}`;
174
169
  }
175
- // Apply T1 change immediately
170
+ // Apply change immediately
176
171
  const parts = entry.path.split(".");
177
172
  let target = rawConfig;
178
173
  for (let i = 0; i < parts.length - 1; i++) {
@@ -187,80 +182,4 @@ exports.configToolDefinitions = [
187
182
  return `Success: "${a.path}" updated to ${JSON.stringify(parsedValue)}. Change applied immediately.`;
188
183
  },
189
184
  },
190
- {
191
- tool: {
192
- type: "function",
193
- function: {
194
- name: "propose_config",
195
- description: "Propose a change to a Tier 2 (proposal) agent configuration value. Requires operator approval before the change is applied. For Tier 1 self-service keys, use update_config instead.",
196
- parameters: {
197
- type: "object",
198
- properties: {
199
- path: { type: "string", description: "Config key in dot-notation (e.g., 'context.maxTokens', 'humanFacing.model')" },
200
- value: { type: "string", description: "New value as JSON (e.g., '120000', '\"azure\"', '{\"enabled\": true}')" },
201
- },
202
- required: ["path", "value"],
203
- },
204
- },
205
- },
206
- confirmationRequired: true,
207
- confirmationAlwaysRequired: true,
208
- handler: (a) => {
209
- const entry = (0, config_registry_1.getRegistryEntry)(a.path);
210
- if (!entry) {
211
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `unknown config path: ${a.path}`, meta: { path: a.path } });
212
- return `Error: unknown config path "${a.path}". Use read_config to see available paths.`;
213
- }
214
- let parsedValue;
215
- try {
216
- parsedValue = JSON.parse(a.value);
217
- }
218
- catch {
219
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `invalid JSON value for ${a.path}`, meta: { path: a.path, value: a.value } });
220
- return `Error: invalid JSON value. Provide value as valid JSON (e.g., 120000, "azure", {"enabled": true}).`;
221
- }
222
- // Validate value against registry entry's validator
223
- /* v8 ignore next -- all current entries have validators; guard kept for future entries without one @preserve */
224
- if (entry.validate) {
225
- const validationError = entry.validate(parsedValue);
226
- if (validationError) {
227
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_error", message: `validation failed for ${a.path}: ${validationError}`, meta: { path: a.path, value: a.value, validationError } });
228
- return `Error: validation failed for "${a.path}": ${validationError}`;
229
- }
230
- }
231
- // Tier 3: refuse
232
- if (entry.tier === 3) {
233
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_refused", message: `refused T3 change to ${a.path}`, meta: { path: a.path, tier: 3 } });
234
- return `Refused: "${a.path}" is an operator-only (Tier 3) key. ${entry.description} Only the operator can change this value directly in agent.json.`;
235
- }
236
- // Tier 1: refuse — use update_config for self-service keys
237
- if (entry.tier === 1) {
238
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_refused", message: `refused T1 change to ${a.path} — use update_config`, meta: { path: a.path, tier: 1 } });
239
- return `Refused: "${a.path}" is a Tier 1 (self-service) key. Use update_config instead — no operator approval needed.`;
240
- }
241
- const agentRoot = (0, identity_1.getAgentRoot)();
242
- const configPath = path.join(agentRoot, "agent.json");
243
- let rawConfig;
244
- try {
245
- rawConfig = JSON.parse(fs.readFileSync(configPath, "utf-8"));
246
- }
247
- catch {
248
- /* v8 ignore next -- defensive: agent.json read failure in propose_config @preserve */
249
- return `Error: failed to read agent.json at ${configPath}`;
250
- }
251
- // Apply T2 change (operator confirmation already obtained via confirmationAlwaysRequired)
252
- const parts = entry.path.split(".");
253
- let target = rawConfig;
254
- for (let i = 0; i < parts.length - 1; i++) {
255
- if (!(parts[i] in target) || typeof target[parts[i]] !== "object" || target[parts[i]] === null) {
256
- target[parts[i]] = {};
257
- }
258
- target = target[parts[i]];
259
- }
260
- target[parts[parts.length - 1]] = parsedValue;
261
- fs.writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n", "utf-8");
262
- (0, runtime_1.emitNervesEvent)({ component: "repertoire", event: "repertoire.propose_config_applied", message: `applied approved config change to ${a.path}`, meta: { path: a.path, tier: entry.tier, value: parsedValue } });
263
- return `Success: "${a.path}" updated to ${JSON.stringify(parsedValue)}. Change approved and applied.`;
264
- },
265
- },
266
185
  ];
@@ -42,7 +42,6 @@ exports.githubToolDefinitions = [
42
42
  return (0, github_client_1.githubRequest)(ctx.githubToken, "POST", `/repos/${owner}/${repo}/issues`, JSON.stringify(payload));
43
43
  },
44
44
  integration: "github",
45
- confirmationRequired: true,
46
45
  summaryKeys: ["title"],
47
46
  },
48
47
  ];
@@ -64,7 +64,6 @@ exports.teamsToolDefinitions = [
64
64
  return (0, graph_client_1.graphRequest)(ctx.graphToken, args.method, args.path, args.body);
65
65
  },
66
66
  integration: "graph",
67
- confirmationRequired: true,
68
67
  summaryKeys: ["method", "path"],
69
68
  },
70
69
  // -- Generic ADO tools --
@@ -126,7 +125,6 @@ exports.teamsToolDefinitions = [
126
125
  return (0, ado_client_1.adoRequest)(ctx.adoToken, args.method, args.organization, args.path, args.body, args.host);
127
126
  },
128
127
  integration: "ado",
129
- confirmationRequired: true,
130
128
  summaryKeys: ["method", "organization", "path"],
131
129
  },
132
130
  // -- Convenience aliases (backward compat) --
@@ -237,7 +235,6 @@ exports.teamsToolDefinitions = [
237
235
  }
238
236
  },
239
237
  /* v8 ignore stop */
240
- confirmationRequired: true,
241
238
  summaryKeys: ["user_name", "user_id"],
242
239
  },
243
240
  // -- Documentation tools --
@@ -3,8 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.surfaceToolDef = exports.restTool = exports.ponderTool = exports.observeTool = exports.settleTool = exports.tools = void 0;
4
4
  exports.resetMcpDefinitions = resetMcpDefinitions;
5
5
  exports.getToolsForChannel = getToolsForChannel;
6
- exports.isConfirmationRequired = isConfirmationRequired;
7
- exports.isConfirmationAlwaysRequired = isConfirmationAlwaysRequired;
8
6
  exports.execTool = execTool;
9
7
  exports.summarizeArgs = summarizeArgs;
10
8
  exports.buildToolResultSummary = buildToolResultSummary;
@@ -39,7 +37,7 @@ Object.defineProperty(exports, "surfaceToolDef", { enumerable: true, get: functi
39
37
  // All tool definitions in a single registry
40
38
  const allDefinitions = [...tools_base_1.baseToolDefinitions, ...tools_bluebubbles_1.bluebubblesToolDefinitions, ...tools_teams_1.teamsToolDefinitions, ...ado_semantic_1.adoSemanticToolDefinitions, ...tools_github_1.githubToolDefinitions, tools_surface_1.surfaceToolDefinition];
41
39
  // MCP tool definitions — populated each time getToolsForChannel() is called with an mcpManager.
42
- // Kept separate from allDefinitions so execTool/isConfirmation* can find them.
40
+ // Kept separate from allDefinitions so execTool can find them.
43
41
  let mcpDefinitions = [];
44
42
  /** Exported for testing — reset the MCP definitions cache. */
45
43
  function resetMcpDefinitions() {
@@ -120,18 +118,6 @@ function findDefinition(toolName) {
120
118
  return allDefinitions.find((d) => d.tool.function.name === toolName)
121
119
  ?? mcpDefinitions.find((d) => d.tool.function.name === toolName);
122
120
  }
123
- // Check whether a tool requires user confirmation before execution.
124
- // Reads from ToolDefinition.confirmationRequired instead of a separate Set.
125
- function isConfirmationRequired(toolName) {
126
- const def = findDefinition(toolName);
127
- return def?.confirmationRequired === true;
128
- }
129
- // Check whether a tool's confirmation is non-bypassable (cannot be skipped by skipConfirmation).
130
- // Used for T2 config proposals that must always require operator approval.
131
- function isConfirmationAlwaysRequired(toolName) {
132
- const def = findDefinition(toolName);
133
- return def?.confirmationAlwaysRequired === true;
134
- }
135
121
  function normalizeGuardArgs(_name, args) {
136
122
  return args;
137
123
  }
@@ -693,7 +693,6 @@ async function runInnerDialogTurn(options) {
693
693
  runAgentOptions: {
694
694
  traceId,
695
695
  toolChoiceRequired: true,
696
- skipConfirmation: true,
697
696
  mcpManager,
698
697
  toolContext: {
699
698
  signin: async () => undefined,
@@ -41,7 +41,6 @@ exports.sanitizeFeedbackComment = sanitizeFeedbackComment;
41
41
  exports.buildFeedbackSyntheticText = buildFeedbackSyntheticText;
42
42
  exports.buildWelcomeCard = buildWelcomeCard;
43
43
  exports.createTeamsCallbacks = createTeamsCallbacks;
44
- exports.resolvePendingConfirmation = resolvePendingConfirmation;
45
44
  exports.withConversationLock = withConversationLock;
46
45
  exports.handleTeamsMessage = handleTeamsMessage;
47
46
  exports.sendProactiveTeamsMessageToSession = sendProactiveTeamsMessageToSession;
@@ -479,24 +478,6 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
479
478
  safeSend(msg);
480
479
  }
481
480
  },
482
- onConfirmAction: options?.conversationId
483
- ? async (name, args) => {
484
- const convId = options.conversationId;
485
- const argsDesc = Object.entries(args).map(([k, v]) => `${k}: ${v}`).join(", ");
486
- safeUpdate(`Confirm action: ${name} (${argsDesc}) -- reply "yes" to confirm or "no" to cancel`);
487
- return new Promise((resolve) => {
488
- _pendingConfirmations.set(convId, resolve);
489
- // Auto-deny after 2 minutes to prevent indefinite blocking
490
- // (e.g. when the stream dies and the user never sees the prompt).
491
- setTimeout(() => {
492
- if (_pendingConfirmations.has(convId)) {
493
- _pendingConfirmations.delete(convId);
494
- resolve("denied");
495
- }
496
- }, 120_000);
497
- });
498
- }
499
- : undefined,
500
481
  flush: async () => {
501
482
  stopFlushTimer();
502
483
  stopPhraseRotation();
@@ -534,26 +515,6 @@ function createTeamsCallbacks(stream, controller, sendMessage, options) {
534
515
  },
535
516
  };
536
517
  }
537
- // Per-conversation pending confirmation resolvers.
538
- // When a mutate tool needs confirmation, the resolver is stored here.
539
- // The next message from the same conversation resolves it.
540
- const _pendingConfirmations = new Map();
541
- // Confirmation response words (case-insensitive)
542
- const CONFIRM_WORDS = new Set(["yes", "confirm", "go", "y", "ok", "approve", "proceed"]);
543
- function resolvePendingConfirmation(convId, text) {
544
- const resolver = _pendingConfirmations.get(convId);
545
- if (!resolver)
546
- return false;
547
- _pendingConfirmations.delete(convId);
548
- const word = text.trim().toLowerCase();
549
- if (CONFIRM_WORDS.has(word)) {
550
- resolver("confirmed");
551
- }
552
- else {
553
- resolver("denied");
554
- }
555
- return true;
556
- }
557
518
  const _turnCoordinator = (0, turn_coordinator_1.createTurnCoordinator)();
558
519
  function teamsTurnKey(conversationId) {
559
520
  return `teams:${conversationId}`;
@@ -659,8 +620,6 @@ async function handleTeamsMessage(text, stream, conversationId, teamsContext, se
659
620
  },
660
621
  ...(reactionOverrides?.isReactionSignal ? { isReactionSignal: true } : {}),
661
622
  };
662
- if (channelConfig.skipConfirmation)
663
- agentOptions.skipConfirmation = true;
664
623
  // ── Call shared pipeline ──────────────────────────────────────────
665
624
  // Capture terminal errors — failover message replaces the error card if it triggers
666
625
  let capturedTerminalError = null;
@@ -961,13 +920,6 @@ function registerBotHandlers(app, label) {
961
920
  const userId = activity.from?.id || "";
962
921
  const channelId = activity.channelId || "msteams";
963
922
  (0, runtime_1.emitNervesEvent)({ level: "info", event: "channel.message_received", component: "channels", message: `[${label}] incoming teams message`, meta: { userId: userId.slice(0, 12), conversationId: convId.slice(0, 20) } });
964
- // Resolve pending confirmations IMMEDIATELY — before token fetches or
965
- // the conversation lock. The original message holds the lock while
966
- // awaiting confirmation, so acquiring it here would deadlock. Token
967
- // fetches are also unnecessary (and slow) for a simple yes/no reply.
968
- if (resolvePendingConfirmation(convId, text)) {
969
- return;
970
- }
971
923
  const commandRegistry = createTeamsCommandRegistry();
972
924
  const parsedSlashCommand = (0, commands_1.parseSlashCommand)(text);
973
925
  if (parsedSlashCommand) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ouro.bot/cli",
3
- "version": "0.1.0-alpha.235",
3
+ "version": "0.1.0-alpha.237",
4
4
  "main": "dist/heart/daemon/ouro-entry.js",
5
5
  "bin": {
6
6
  "cli": "dist/heart/daemon/ouro-bot-entry.js",