@ouro.bot/cli 0.1.0-alpha.236 → 0.1.0-alpha.238

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
@@ -2,10 +2,12 @@
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
4
  {
5
- "version": "0.1.0-alpha.236",
5
+ "version": "0.1.0-alpha.238",
6
6
  "changes": [
7
- "fix(engine): provider-aware retry classification — retries now use both generic isTransientError AND provider classifyError",
8
- "feat(engine): retry instrumentation — engine.provider_retry / engine.provider_retry_skip events with full diagnostics"
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."
9
11
  ]
10
12
  },
11
13
  {
@@ -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;
@@ -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");
@@ -67,6 +67,8 @@ const DISPATCH_EXEMPT_PATTERNS = [
67
67
  "repertoire/api-client",
68
68
  "repertoire/github-client",
69
69
  "mind/embedding-provider",
70
+ // Commerce utility module: error classes and pure helpers (no independent side effects).
71
+ "repertoire/commerce-errors",
70
72
  ];
71
73
  function isDispatchExempt(filePath) {
72
74
  return DISPATCH_EXEMPT_PATTERNS.some((pattern) => filePath.includes(pattern));
@@ -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 --
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ /**
3
+ * Commerce-specific error types and helpers.
4
+ *
5
+ * These errors carry structured `code` and `meta` fields for nerves event
6
+ * compatibility and provide patterns for common commerce failure modes:
7
+ * retry, price change detection, and partial failure reporting.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.ProfileError = exports.BookingError = exports.PaymentError = exports.CommerceError = void 0;
11
+ exports.retryOnce = retryOnce;
12
+ exports.priceChangeGuard = priceChangeGuard;
13
+ exports.partialFailureReport = partialFailureReport;
14
+ // ---------------------------------------------------------------------------
15
+ // Error types
16
+ // ---------------------------------------------------------------------------
17
+ class CommerceError extends Error {
18
+ code;
19
+ meta;
20
+ constructor(message, code, meta = {}) {
21
+ super(message);
22
+ this.name = "CommerceError";
23
+ this.code = code;
24
+ this.meta = meta;
25
+ }
26
+ }
27
+ exports.CommerceError = CommerceError;
28
+ class PaymentError extends CommerceError {
29
+ constructor(message, code, meta = {}) {
30
+ super(message, code, meta);
31
+ this.name = "PaymentError";
32
+ }
33
+ }
34
+ exports.PaymentError = PaymentError;
35
+ class BookingError extends CommerceError {
36
+ constructor(message, code, meta = {}) {
37
+ super(message, code, meta);
38
+ this.name = "BookingError";
39
+ }
40
+ }
41
+ exports.BookingError = BookingError;
42
+ class ProfileError extends CommerceError {
43
+ constructor(message, code, meta = {}) {
44
+ super(message, code, meta);
45
+ this.name = "ProfileError";
46
+ }
47
+ }
48
+ exports.ProfileError = ProfileError;
49
+ // ---------------------------------------------------------------------------
50
+ // Transient error detection
51
+ // ---------------------------------------------------------------------------
52
+ const TRANSIENT_CODES = new Set(["COMMERCE_TRANSIENT", "COMMERCE_TIMEOUT", "COMMERCE_NETWORK"]);
53
+ function isTransient(err) {
54
+ return err instanceof CommerceError && TRANSIENT_CODES.has(err.code);
55
+ }
56
+ // ---------------------------------------------------------------------------
57
+ // Helpers
58
+ // ---------------------------------------------------------------------------
59
+ /**
60
+ * Retry a function once on transient failure, then throw.
61
+ * Non-transient errors are thrown immediately without retry.
62
+ */
63
+ async function retryOnce(fn) {
64
+ try {
65
+ return await fn();
66
+ }
67
+ catch (err) {
68
+ if (!isTransient(err))
69
+ throw err;
70
+ // One retry
71
+ return fn();
72
+ }
73
+ }
74
+ /**
75
+ * Throw PaymentError if the actual price differs from the approved price
76
+ * by more than 5%. Used to protect against price changes between search
77
+ * and booking.
78
+ */
79
+ function priceChangeGuard(approved, actual) {
80
+ if (approved === 0 && actual === 0)
81
+ return;
82
+ if (approved === 0) {
83
+ throw new PaymentError(`price changed from $0 to $${actual} — cannot verify percentage change`, "PAYMENT_PRICE_CHANGED", { approved, actual });
84
+ }
85
+ const delta = Math.abs(actual - approved) / approved;
86
+ if (delta > 0.05) {
87
+ throw new PaymentError(`price changed by ${(delta * 100).toFixed(1)}% (approved: $${approved}, actual: $${actual})`, "PAYMENT_PRICE_CHANGED", { approved, actual, deltaPercent: delta * 100 });
88
+ }
89
+ }
90
+ /**
91
+ * Format a human-readable status report for multi-service booking attempts.
92
+ * Each entry has a service name, status, and optional error message.
93
+ */
94
+ function partialFailureReport(results) {
95
+ if (results.length === 0) {
96
+ return "no services attempted.";
97
+ }
98
+ const lines = results.map((r) => {
99
+ const status = r.status === "success" ? "success" : "failed";
100
+ const detail = r.error ? ` — ${r.error}` : "";
101
+ return ` ${r.service}: ${status}${detail}`;
102
+ });
103
+ const succeeded = results.filter((r) => r.status === "success").length;
104
+ const failed = results.length - succeeded;
105
+ return [
106
+ `booking status: ${succeeded} succeeded, ${failed} failed`,
107
+ ...lines,
108
+ ].join("\n");
109
+ }