@mono-agent/agent-app 0.4.0 → 0.5.0

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.
Files changed (173) hide show
  1. package/README.md +15 -5
  2. package/dist/adapter-send-tools-main.js +2 -2
  3. package/dist/adapter-send-tools-main.js.map +1 -1
  4. package/dist/adapter-send-tools.d.ts +72 -6
  5. package/dist/adapter-send-tools.d.ts.map +1 -1
  6. package/dist/adapter-send-tools.js +413 -38
  7. package/dist/adapter-send-tools.js.map +1 -1
  8. package/dist/app-config.d.ts +48 -11
  9. package/dist/app-config.d.ts.map +1 -1
  10. package/dist/app-config.js +98 -24
  11. package/dist/app-config.js.map +1 -1
  12. package/dist/app.d.ts +10 -0
  13. package/dist/app.d.ts.map +1 -1
  14. package/dist/app.js +659 -48
  15. package/dist/app.js.map +1 -1
  16. package/dist/artifact-retention.d.ts +32 -0
  17. package/dist/artifact-retention.d.ts.map +1 -0
  18. package/dist/artifact-retention.js +111 -0
  19. package/dist/artifact-retention.js.map +1 -0
  20. package/dist/audit-runs.d.ts +12 -0
  21. package/dist/audit-runs.d.ts.map +1 -0
  22. package/dist/audit-runs.js +68 -0
  23. package/dist/audit-runs.js.map +1 -0
  24. package/dist/backfill.d.ts +3 -7
  25. package/dist/backfill.d.ts.map +1 -1
  26. package/dist/backfill.js +115 -11
  27. package/dist/backfill.js.map +1 -1
  28. package/dist/background.d.ts +8 -1
  29. package/dist/background.d.ts.map +1 -1
  30. package/dist/background.js +202 -4
  31. package/dist/background.js.map +1 -1
  32. package/dist/broadcast-recorder.d.ts +27 -0
  33. package/dist/broadcast-recorder.d.ts.map +1 -0
  34. package/dist/broadcast-recorder.js +82 -0
  35. package/dist/broadcast-recorder.js.map +1 -0
  36. package/dist/channel-config-view.d.ts +28 -0
  37. package/dist/channel-config-view.d.ts.map +1 -0
  38. package/dist/channel-config-view.js +79 -0
  39. package/dist/channel-config-view.js.map +1 -0
  40. package/dist/channel-gate.d.ts +22 -0
  41. package/dist/channel-gate.d.ts.map +1 -0
  42. package/dist/channel-gate.js +35 -0
  43. package/dist/channel-gate.js.map +1 -0
  44. package/dist/channel-plugins.d.ts +39 -0
  45. package/dist/channel-plugins.d.ts.map +1 -0
  46. package/dist/channel-plugins.js +372 -0
  47. package/dist/channel-plugins.js.map +1 -0
  48. package/dist/channels.d.ts +62 -73
  49. package/dist/channels.d.ts.map +1 -1
  50. package/dist/channels.js +833 -126
  51. package/dist/channels.js.map +1 -1
  52. package/dist/cli.d.ts +65 -3
  53. package/dist/cli.d.ts.map +1 -1
  54. package/dist/cli.js +755 -30
  55. package/dist/cli.js.map +1 -1
  56. package/dist/config-reference.d.ts +24 -0
  57. package/dist/config-reference.d.ts.map +1 -0
  58. package/dist/config-reference.js +580 -0
  59. package/dist/config-reference.js.map +1 -0
  60. package/dist/configured-agent.d.ts +104 -0
  61. package/dist/configured-agent.d.ts.map +1 -0
  62. package/dist/configured-agent.js +578 -0
  63. package/dist/configured-agent.js.map +1 -0
  64. package/dist/consumer-contract.d.ts +36 -0
  65. package/dist/consumer-contract.d.ts.map +1 -0
  66. package/dist/consumer-contract.js +290 -0
  67. package/dist/consumer-contract.js.map +1 -0
  68. package/dist/doctor.d.ts +9 -0
  69. package/dist/doctor.d.ts.map +1 -1
  70. package/dist/doctor.js +352 -55
  71. package/dist/doctor.js.map +1 -1
  72. package/dist/index.d.ts +9 -3
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +5 -2
  75. package/dist/index.js.map +1 -1
  76. package/dist/init.d.ts +23 -7
  77. package/dist/init.d.ts.map +1 -1
  78. package/dist/init.js +91 -26
  79. package/dist/init.js.map +1 -1
  80. package/dist/install-skill.d.ts +1 -1
  81. package/dist/install-skill.js +2 -2
  82. package/dist/install-skill.js.map +1 -1
  83. package/dist/interaction-bridge.d.ts +72 -0
  84. package/dist/interaction-bridge.d.ts.map +1 -0
  85. package/dist/interaction-bridge.js +354 -0
  86. package/dist/interaction-bridge.js.map +1 -0
  87. package/dist/memory-command.d.ts +10 -0
  88. package/dist/memory-command.d.ts.map +1 -0
  89. package/dist/memory-command.js +649 -0
  90. package/dist/memory-command.js.map +1 -0
  91. package/dist/memory-recall-main.js +1 -1
  92. package/dist/memory-recall-main.js.map +1 -1
  93. package/dist/memory-recall.d.ts +41 -5
  94. package/dist/memory-recall.d.ts.map +1 -1
  95. package/dist/memory-recall.js +75 -5
  96. package/dist/memory-recall.js.map +1 -1
  97. package/dist/memory-rituals.d.ts +8 -11
  98. package/dist/memory-rituals.d.ts.map +1 -1
  99. package/dist/memory-rituals.js +26 -25
  100. package/dist/memory-rituals.js.map +1 -1
  101. package/dist/metrics.d.ts +13 -0
  102. package/dist/metrics.d.ts.map +1 -0
  103. package/dist/metrics.js +105 -0
  104. package/dist/metrics.js.map +1 -0
  105. package/dist/notify-destinations.d.ts +23 -0
  106. package/dist/notify-destinations.d.ts.map +1 -0
  107. package/dist/notify-destinations.js +79 -0
  108. package/dist/notify-destinations.js.map +1 -0
  109. package/dist/posted-message-index.d.ts +61 -0
  110. package/dist/posted-message-index.d.ts.map +1 -0
  111. package/dist/posted-message-index.js +178 -0
  112. package/dist/posted-message-index.js.map +1 -0
  113. package/dist/proactive-notify.d.ts +32 -0
  114. package/dist/proactive-notify.d.ts.map +1 -0
  115. package/dist/proactive-notify.js +62 -0
  116. package/dist/proactive-notify.js.map +1 -0
  117. package/dist/recipes/base.d.ts +15 -0
  118. package/dist/recipes/base.d.ts.map +1 -0
  119. package/dist/recipes/base.js +51 -0
  120. package/dist/recipes/base.js.map +1 -0
  121. package/dist/recipes/catalog.d.ts +4 -0
  122. package/dist/recipes/catalog.d.ts.map +1 -0
  123. package/dist/recipes/catalog.js +525 -0
  124. package/dist/recipes/catalog.js.map +1 -0
  125. package/dist/recipes/index.d.ts +11 -0
  126. package/dist/recipes/index.d.ts.map +1 -0
  127. package/dist/recipes/index.js +14 -0
  128. package/dist/recipes/index.js.map +1 -0
  129. package/dist/recipes/types.d.ts +70 -0
  130. package/dist/recipes/types.d.ts.map +1 -0
  131. package/dist/recipes/types.js +15 -0
  132. package/dist/recipes/types.js.map +1 -0
  133. package/dist/request-model-override.d.ts +67 -0
  134. package/dist/request-model-override.d.ts.map +1 -0
  135. package/dist/request-model-override.js +96 -0
  136. package/dist/request-model-override.js.map +1 -0
  137. package/dist/runs-health.d.ts +22 -0
  138. package/dist/runs-health.d.ts.map +1 -0
  139. package/dist/runs-health.js +144 -0
  140. package/dist/runs-health.js.map +1 -0
  141. package/dist/seen-conversations.d.ts +22 -0
  142. package/dist/seen-conversations.d.ts.map +1 -0
  143. package/dist/seen-conversations.js +120 -0
  144. package/dist/seen-conversations.js.map +1 -0
  145. package/dist/setup.d.ts +29 -0
  146. package/dist/setup.d.ts.map +1 -0
  147. package/dist/setup.js +97 -0
  148. package/dist/setup.js.map +1 -0
  149. package/dist/trigger-overrides.d.ts +17 -0
  150. package/dist/trigger-overrides.d.ts.map +1 -0
  151. package/dist/trigger-overrides.js +30 -0
  152. package/dist/trigger-overrides.js.map +1 -0
  153. package/dist/tui-command.d.ts +56 -0
  154. package/dist/tui-command.d.ts.map +1 -0
  155. package/dist/tui-command.js +160 -0
  156. package/dist/tui-command.js.map +1 -0
  157. package/dist/ui.d.ts.map +1 -1
  158. package/dist/ui.js +4 -0
  159. package/dist/ui.js.map +1 -1
  160. package/dist/web-command.d.ts +45 -0
  161. package/dist/web-command.d.ts.map +1 -0
  162. package/dist/web-command.js +152 -0
  163. package/dist/web-command.js.map +1 -0
  164. package/package.json +19 -18
  165. package/schema/mono-agent.config.schema.json +1686 -0
  166. package/skills/mono-agent-composer/SKILL.md +34 -9
  167. package/skills/mono-agent-composer/agents/openai.yaml +4 -0
  168. package/skills/mono-agent-composer/references/config-blueprint.md +92 -49
  169. package/skills/mono-agent-composer/references/discovery-questions.md +15 -17
  170. package/skills/mono-agent-composer/references/feature-coverage.md +22 -17
  171. package/skills/mono-agent-composer/references/package-map.md +19 -21
  172. package/skills/mono-agent-composer/references/playbooks.md +267 -0
  173. package/skills/mono-agent-composer/references/validation.md +9 -1
package/dist/cli.js CHANGED
@@ -1,16 +1,29 @@
1
1
  #!/usr/bin/env node
2
+ import { createInterface } from "node:readline/promises";
2
3
  import { stat } from "node:fs/promises";
4
+ import { createRequire } from "node:module";
3
5
  import { basename, resolve } from "node:path";
4
6
  import process from "node:process";
5
7
  import { fileURLToPath } from "node:url";
8
+ import { buildMonoAgentConfigView, findJsonSecretConfigWarnings, findRemovedConfigWarnings, readMonoAgentConfigJson, redactMonoAgentConfig, } from "@mono-agent/config";
9
+ import { listRecordedRuns } from "@mono-agent/observability";
10
+ import { describeSandboxEffectiveState, sandboxEffectiveStateWarning, } from "@mono-agent/runtime-adapter";
6
11
  import { startMonoAgentApp } from "./app.js";
7
- import { phoenixAppBaseUrl } from "./app-config.js";
12
+ import { describeSensitiveDataExportWarning, isAppCoreConfigError, loadAppCoreConfig, phoenixAppBaseUrl, } from "./app-config.js";
13
+ import { runAuditRuns } from "./audit-runs.js";
8
14
  import { runBackfill } from "./backfill.js";
9
15
  import { defaultBackgroundDeps, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
16
+ import { collectChannelConfigViews } from "./channel-config-view.js";
17
+ import { resolveChannelDrivers } from "./channels.js";
18
+ import { findUnknownAppConfigWarnings } from "./config-reference.js";
10
19
  import { validateMonoAgentFolder } from "./doctor.js";
11
- import { initMonoAgentFolder } from "./init.js";
20
+ import { initMonoAgentFolder, isWithChannel } from "./init.js";
12
21
  import { installComposerSkill } from "./install-skill.js";
22
+ import { runMetrics } from "./metrics.js";
23
+ import { findRecipe, RECIPE_CATALOG, recipeIds, resolveRecipeInputs } from "./recipes/index.js";
24
+ import { buildRunsHealthDisplay, RUNS_HEALTH_MAX_RUNS } from "./runs-health.js";
13
25
  import { purgeSessions } from "./sessions.js";
26
+ import { collectSetupOptions } from "./setup.js";
14
27
  import * as ui from "./ui.js";
15
28
  const DEFAULT_LOG_LINES = 200;
16
29
  // Node's maximum setInterval/setTimeout delay (2^31 - 1 ms, ~24.8 days). A
@@ -18,21 +31,29 @@ const DEFAULT_LOG_LINES = 200;
18
31
  // busy-waiting; larger values silently overflow to a 1ms delay.
19
32
  const KEEP_ALIVE_INTERVAL_MS = 2_147_483_647;
20
33
  const BACKGROUND_COMMANDS = ["start", "restart", "stop", "status", "logs"];
21
- const KNOWN_COMMANDS = ["init", "validate", "start", "restart", "stop", "status", "logs", "install-skill", "backfill"];
34
+ const KNOWN_COMMANDS = ["init", "setup", "validate", "doctor", "config", "recipes", "start", "restart", "stop", "status", "logs", "tui", "web", "install-skill", "backfill", "audit-runs", "metrics", "memory"];
22
35
  export function parseCliArgs(argv) {
23
36
  const [command, ...rest] = argv;
24
37
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
25
- return { command: "help", force: false, foreground: false, follow: false, all: false, dryRun: false };
38
+ return { command: "help", positionals: [], force: false, foreground: false, follow: false, all: false, dryRun: false, includeMemory: false };
39
+ }
40
+ if (command === "version" || command === "--version" || command === "-v") {
41
+ return { command: "version", positionals: [], force: false, foreground: false, follow: false, all: false, dryRun: false, includeMemory: false };
26
42
  }
27
43
  if (!KNOWN_COMMANDS.includes(command)) {
28
44
  throw new Error(`Unknown command \`${command}\`. Expected ${KNOWN_COMMANDS.join(", ")}.`);
29
45
  }
30
- const cmd = command;
46
+ // `doctor` is an alias of `validate`; normalize here so every downstream
47
+ // validate path (env-file resolution, --consumer, routing) applies unchanged.
48
+ const cmd = (command === "doctor" ? "validate" : command);
31
49
  const isLogs = cmd === "logs";
32
50
  let configPath;
33
51
  let model;
34
52
  let fallbackModels;
35
53
  let memory;
54
+ let recipe;
55
+ let withChannels;
56
+ const positionals = [];
36
57
  let envFile;
37
58
  let target;
38
59
  let force = false;
@@ -44,6 +65,20 @@ export function parseCliArgs(argv) {
44
65
  let since;
45
66
  let until;
46
67
  let dryRun = false;
68
+ let includeMemory = false;
69
+ let artifactDir;
70
+ let groupBy;
71
+ let consumerPath;
72
+ let agent;
73
+ let conversation;
74
+ let staleAfterMs;
75
+ let json = false;
76
+ let limit;
77
+ let host;
78
+ let port;
79
+ let open;
80
+ let allowNonLoopback;
81
+ let maxRunsPerInstance;
47
82
  for (let i = 0; i < rest.length; i += 1) {
48
83
  const flag = rest[i];
49
84
  switch (flag) {
@@ -65,6 +100,80 @@ export function parseCliArgs(argv) {
65
100
  case "--dry-run":
66
101
  dryRun = true;
67
102
  break;
103
+ case "--include-memory":
104
+ includeMemory = true;
105
+ break;
106
+ case "--artifact-dir":
107
+ artifactDir = requireValue(rest, ++i, flag);
108
+ break;
109
+ case "--artifacts":
110
+ artifactDir = requireValue(rest, ++i, flag);
111
+ break;
112
+ case "--by": {
113
+ const raw = requireValue(rest, ++i, flag);
114
+ if (raw !== "model" && raw !== "channel" && raw !== "failureKind") {
115
+ throw new Error("--by must be model, channel, or failureKind.");
116
+ }
117
+ groupBy = raw;
118
+ break;
119
+ }
120
+ case "--consumer":
121
+ consumerPath = requireValue(rest, ++i, flag);
122
+ break;
123
+ case "--agent":
124
+ agent = requireValue(rest, ++i, flag);
125
+ break;
126
+ case "--conversation":
127
+ conversation = requireValue(rest, ++i, flag);
128
+ break;
129
+ case "--stale-after-ms": {
130
+ const raw = requireValue(rest, ++i, flag);
131
+ const parsed = Number(raw);
132
+ if (!Number.isInteger(parsed) || parsed < 1) {
133
+ throw new Error("--stale-after-ms must be a positive integer.");
134
+ }
135
+ staleAfterMs = parsed;
136
+ break;
137
+ }
138
+ case "--json":
139
+ json = true;
140
+ break;
141
+ case "--limit": {
142
+ const raw = requireValue(rest, ++i, flag);
143
+ const parsed = Number(raw);
144
+ if (!Number.isInteger(parsed) || parsed < 1 || parsed > 100) {
145
+ throw new Error("--limit must be an integer between 1 and 100.");
146
+ }
147
+ limit = parsed;
148
+ break;
149
+ }
150
+ case "--host":
151
+ host = requireValue(rest, ++i, flag);
152
+ break;
153
+ case "--port": {
154
+ const raw = requireValue(rest, ++i, flag);
155
+ const parsed = Number(raw);
156
+ if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65535) {
157
+ throw new Error("--port must be an integer between 0 and 65535.");
158
+ }
159
+ port = parsed;
160
+ break;
161
+ }
162
+ case "--no-open":
163
+ open = false;
164
+ break;
165
+ case "--allow-non-loopback":
166
+ allowNonLoopback = true;
167
+ break;
168
+ case "--max-runs": {
169
+ const raw = requireValue(rest, ++i, flag);
170
+ const parsed = Number(raw);
171
+ if (!Number.isInteger(parsed) || parsed < 1) {
172
+ throw new Error("--max-runs must be a positive integer.");
173
+ }
174
+ maxRunsPerInstance = parsed;
175
+ break;
176
+ }
68
177
  case "--model":
69
178
  model = requireValue(rest, ++i, flag);
70
179
  break;
@@ -82,6 +191,15 @@ export function parseCliArgs(argv) {
82
191
  memory = raw;
83
192
  break;
84
193
  }
194
+ case "--recipe":
195
+ recipe = requireValue(rest, ++i, flag);
196
+ break;
197
+ case "--with":
198
+ withChannels = requireValue(rest, ++i, flag)
199
+ .split(",")
200
+ .map((entry) => entry.trim())
201
+ .filter((entry) => entry.length > 0);
202
+ break;
85
203
  case "--env-file":
86
204
  envFile = requireValue(rest, ++i, flag);
87
205
  break;
@@ -121,15 +239,39 @@ export function parseCliArgs(argv) {
121
239
  break;
122
240
  }
123
241
  default:
124
- throw new Error(`Unknown flag \`${flag}\` for \`mono-agent ${command}\`.`);
242
+ if (flag === undefined) {
243
+ break;
244
+ }
245
+ if (flag.startsWith("--")) {
246
+ throw new Error(`Unknown flag \`${flag}\` for \`mono-agent ${command}\`.`);
247
+ }
248
+ // Non-flag tokens are positional arguments (e.g. `recipes show <id>`).
249
+ positionals.push(flag);
250
+ break;
125
251
  }
126
252
  }
253
+ if (consumerPath !== undefined && cmd !== "validate" && cmd !== "audit-runs") {
254
+ throw new Error("--consumer is only supported for `mono-agent validate` and `mono-agent audit-runs`.");
255
+ }
256
+ if ((host !== undefined || port !== undefined || open !== undefined || allowNonLoopback !== undefined || maxRunsPerInstance !== undefined) &&
257
+ cmd !== "web") {
258
+ throw new Error("--host, --port, --no-open, --allow-non-loopback, and --max-runs are only supported for `mono-agent web`.");
259
+ }
260
+ if (includeMemory && cmd !== "audit-runs" && cmd !== "metrics" && cmd !== "backfill" && cmd !== "web") {
261
+ throw new Error("--include-memory is only supported for `mono-agent audit-runs`, `mono-agent metrics`, `mono-agent backfill`, and `mono-agent web`.");
262
+ }
263
+ if (limit !== undefined && cmd !== "memory") {
264
+ throw new Error("--limit is only supported for `mono-agent memory`.");
265
+ }
127
266
  return {
128
267
  command: cmd,
129
268
  ...(configPath === undefined ? {} : { configPath }),
130
269
  ...(model === undefined ? {} : { model }),
131
270
  ...(fallbackModels === undefined ? {} : { fallbackModels }),
132
271
  ...(memory === undefined ? {} : { memory }),
272
+ ...(recipe === undefined ? {} : { recipe }),
273
+ ...(withChannels === undefined ? {} : { withChannels }),
274
+ positionals,
133
275
  ...(envFile === undefined ? {} : { envFile }),
134
276
  ...(target === undefined ? {} : { target }),
135
277
  force,
@@ -141,6 +283,20 @@ export function parseCliArgs(argv) {
141
283
  ...(since === undefined ? {} : { since }),
142
284
  ...(until === undefined ? {} : { until }),
143
285
  dryRun,
286
+ includeMemory,
287
+ ...(artifactDir === undefined ? {} : { artifactDir }),
288
+ ...(groupBy === undefined ? {} : { groupBy }),
289
+ ...(consumerPath === undefined ? {} : { consumerPath }),
290
+ ...(staleAfterMs === undefined ? {} : { staleAfterMs }),
291
+ ...(json ? { json } : {}),
292
+ ...(limit === undefined ? {} : { limit }),
293
+ ...(agent === undefined ? {} : { agent }),
294
+ ...(conversation === undefined ? {} : { conversation }),
295
+ ...(host === undefined ? {} : { host }),
296
+ ...(port === undefined ? {} : { port }),
297
+ ...(open === undefined ? {} : { open }),
298
+ ...(allowNonLoopback === undefined ? {} : { allowNonLoopback }),
299
+ ...(maxRunsPerInstance === undefined ? {} : { maxRunsPerInstance }),
144
300
  };
145
301
  }
146
302
  /**
@@ -157,6 +313,15 @@ export function loadCliEnvFile(path) {
157
313
  return false;
158
314
  }
159
315
  }
316
+ function resolveValidateContext(args, invocationCwd) {
317
+ const cwd = resolve(invocationCwd, args.consumerPath ?? ".");
318
+ return {
319
+ cwd,
320
+ configPath: resolve(cwd, args.configPath ?? "mono-agent.config.json"),
321
+ envFilePath: resolve(cwd, args.envFile ?? ".env"),
322
+ allowFilesystemWrites: args.consumerPath === undefined,
323
+ };
324
+ }
160
325
  function requireValue(args, index, flag) {
161
326
  const value = args[index];
162
327
  if (value === undefined || value.startsWith("--")) {
@@ -166,15 +331,45 @@ function requireValue(args, index, flag) {
166
331
  }
167
332
  const HELP_COMMANDS = [
168
333
  {
169
- signature: "mono-agent init [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]",
334
+ signature: "mono-agent init [--recipe <id>] [--with <csv>] [--dry-run]\n" +
335
+ " [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]",
170
336
  lines: [
171
337
  "Scaffold mono-agent.config.json, IDENTITY.md, and .mono-agent/ in the",
172
- "current folder. Existing files are never overwritten.",
338
+ "current folder. With --recipe, build from a blueprint (+ .env.example);",
339
+ "--with adds channels, --dry-run previews. Existing files are never overwritten.",
173
340
  ],
174
341
  },
175
342
  {
176
- signature: "mono-agent validate [--config <path>] [--env-file <path>]",
177
- lines: ["Load every config section and report what would run, wait, or fail."],
343
+ signature: "mono-agent setup [--recipe <id>] [--with <csv>] [--dry-run]\n" +
344
+ " [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]",
345
+ lines: [
346
+ "Guided setup when attached to a TTY: choose a recipe, answer non-secret",
347
+ "inputs, select channel add-ons, scaffold, validate, and print the secrets",
348
+ "checklist. In non-TTY contexts it falls back to init-style flags.",
349
+ ],
350
+ },
351
+ {
352
+ signature: "mono-agent recipes list | show <id>",
353
+ lines: [
354
+ "List the executable config blueprints, or show one's generated config,",
355
+ ".env.example, and follow-up checklist.",
356
+ ],
357
+ },
358
+ {
359
+ signature: "mono-agent validate [--recipe <id>] [--consumer <path>] [--config <path>] [--env-file <path>]",
360
+ lines: [
361
+ "Load every config section and report what would run, wait, or fail.",
362
+ "--consumer validates another agent folder read-only, including its .env.",
363
+ "With --recipe, also report whether the recipe's capabilities are live.",
364
+ "`mono-agent doctor` is an alias for this command.",
365
+ ],
366
+ },
367
+ {
368
+ signature: "mono-agent config [--config <path>] [--env-file <path>]",
369
+ lines: [
370
+ "Print the resolved config field-by-field, tagging each value with where",
371
+ "it came from (env / json / default), plus the channel summary. Read-only.",
372
+ ],
178
373
  },
179
374
  {
180
375
  signature: "mono-agent start [--config <path>] [--env-file <path>] [--foreground|-f]",
@@ -205,21 +400,70 @@ const HELP_COMMANDS = [
205
400
  signature: "mono-agent logs [--config <path>] [--follow|-f] [--lines <n>]",
206
401
  lines: ["Print (and optionally follow) the background instance's log files."],
207
402
  },
403
+ {
404
+ signature: "mono-agent tui [--agent <label|sourceId>] [--conversation <id>]",
405
+ lines: [
406
+ "Open the operator console from any directory: live chat with full",
407
+ "thinking/tool/telemetry insight, recorded-run replay, and config view.",
408
+ "Discovers running agents via the trace-source registry; one running",
409
+ "agent connects directly, several open a picker.",
410
+ ],
411
+ },
412
+ {
413
+ signature: "mono-agent web [--host <addr>] [--port <n>] [--no-open] [--allow-non-loopback] [--include-memory] [--max-runs <n>]",
414
+ lines: [
415
+ "Serve the read-only Session Recorder web PWA from any directory: a live",
416
+ "flight-recorder over every agent's runs (prompt, reasoning, tools, cost).",
417
+ "Discovers running agents via the trace-source registry — the same",
418
+ "mechanism as `tui` — and streams new/updated runs in real time.",
419
+ "--include-memory also shows memory-maintenance runs. --max-runs (default",
420
+ "200) bounds the in-memory working set; the UI still pages the full",
421
+ "on-disk history via \"Load older\".",
422
+ ],
423
+ },
208
424
  {
209
425
  signature: "mono-agent install-skill [--target claude|codex|both] [--force]",
210
426
  lines: [
211
427
  "Copy the bundled mono-agent-composer skill into ~/.claude/skills and",
212
- "~/.codex/skills (default: both). Refuses to overwrite without --force.",
428
+ "~/.agents/skills (default: both). Refuses to overwrite without --force.",
213
429
  ],
214
430
  },
215
431
  {
216
432
  signature: "mono-agent backfill (--run <id> | --all) [--since <iso>] [--until <iso>]\n" +
217
- " [--dry-run] [--config <path>] [--env-file <path>]",
433
+ " [--include-memory] [--dry-run] [--config <path>] [--env-file <path>]",
218
434
  lines: [
219
- "Export already-recorded run artifacts to the configured Phoenix exporter",
435
+ "Export already-recorded agent-run artifacts to the configured Phoenix exporter",
220
436
  "with their historical timestamps. Trace ids are deterministic per run, so",
221
437
  "re-running overwrites rather than duplicating. --dry-run maps and",
222
- "serializes without sending.",
438
+ "serializes without sending. --include-memory adds memory-run artifacts",
439
+ "for --all; explicit --run can target a memory run directly.",
440
+ ],
441
+ },
442
+ {
443
+ signature: "mono-agent audit-runs [--artifact-dir <path> | --consumer <path>]\n" +
444
+ " [--include-memory] [--config <path>] [--env-file <path>] [--stale-after-ms <n>] [--json]",
445
+ lines: [
446
+ "Read local agent-run summary artifacts without rewriting them. Reports parse",
447
+ "failures, status and failure-kind histograms, stale running summaries,",
448
+ "and per-failure-kind rates. --include-memory includes memory-run artifacts.",
449
+ ],
450
+ },
451
+ {
452
+ signature: "mono-agent metrics [--artifacts <path>] [--since <iso>] [--until <iso>]\n" +
453
+ " [--include-memory] [--by model|channel|failureKind] [--json] [--config <path>] [--env-file <path>]",
454
+ lines: [
455
+ "Aggregate local agent-run summaries into status rates, failure-kind rates,",
456
+ "duration percentiles, and total/per-run cost. Read-only and offline.",
457
+ "--include-memory includes memory-run artifacts.",
458
+ ],
459
+ },
460
+ {
461
+ signature: "mono-agent memory [stats|today|show <date>|search <query>|top]\n" +
462
+ " [--limit <n>] [--json] [--config <path>] [--env-file <path>]",
463
+ lines: [
464
+ "Preview the configured memory store from an agent folder. Reads the",
465
+ "memory block from mono-agent.config.json, not the standalone memory-bujo",
466
+ "env workflow. Human-first output by default; --json is for scripts.",
223
467
  ],
224
468
  },
225
469
  ];
@@ -252,6 +496,22 @@ export function renderHelp() {
252
496
  out += ui.style.dim(HELP_NOTES);
253
497
  return out;
254
498
  }
499
+ /**
500
+ * The published CLI version, read from this package's own package.json at
501
+ * runtime. `../package.json` resolves the same from both `src/cli.ts` (tests) and
502
+ * the built `dist/cli.js` (one level below the package root in both layouts), and
503
+ * npm always ships package.json in the tarball. Best-effort: a read failure yields
504
+ * "unknown" rather than crashing `--version`.
505
+ */
506
+ export function monoAgentVersion() {
507
+ try {
508
+ const pkg = createRequire(import.meta.url)("../package.json");
509
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
510
+ }
511
+ catch {
512
+ return "unknown";
513
+ }
514
+ }
255
515
  export async function runCli(argv) {
256
516
  let args;
257
517
  try {
@@ -262,15 +522,27 @@ export async function runCli(argv) {
262
522
  process.stdout.write(`\n${renderHelp()}`);
263
523
  return 2;
264
524
  }
265
- loadCliEnvFile(resolve(process.cwd(), args.envFile ?? ".env"));
525
+ const invocationCwd = process.cwd();
526
+ loadCliEnvFile(args.command === "validate"
527
+ ? resolveValidateContext(args, invocationCwd).envFilePath
528
+ : resolve(invocationCwd, args.envFile ?? ".env"));
266
529
  switch (args.command) {
267
530
  case "help":
268
531
  process.stdout.write(renderHelp());
269
532
  return 0;
533
+ case "version":
534
+ process.stdout.write(`mono-agent ${monoAgentVersion()}\n`);
535
+ return 0;
270
536
  case "init":
271
537
  return await runInit(args);
538
+ case "setup":
539
+ return await runSetup(args);
272
540
  case "validate":
273
541
  return await runValidate(args);
542
+ case "config":
543
+ return await runConfig(args);
544
+ case "recipes":
545
+ return runRecipes(args);
274
546
  case "start":
275
547
  return await runStart(args);
276
548
  case "restart":
@@ -278,6 +550,32 @@ export async function runCli(argv) {
278
550
  case "status":
279
551
  case "logs":
280
552
  return await runBackgroundCommand(args, args.command);
553
+ case "tui": {
554
+ // Lazy import: the operator console (and pi-tui) load only on demand.
555
+ const { runTui } = await import("./tui-command.js");
556
+ return await runTui({
557
+ configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
558
+ cwd: process.cwd(),
559
+ env: process.env,
560
+ ...(args.agent === undefined ? {} : { agent: args.agent }),
561
+ ...(args.conversation === undefined ? {} : { conversationId: args.conversation }),
562
+ });
563
+ }
564
+ case "web": {
565
+ // Lazy import: the web server (and express/session-web) load only on demand.
566
+ const { runWeb } = await import("./web-command.js");
567
+ return await runWeb({
568
+ configPath: resolve(process.cwd(), args.configPath ?? "mono-agent.config.json"),
569
+ cwd: process.cwd(),
570
+ env: process.env,
571
+ ...(args.host === undefined ? {} : { host: args.host }),
572
+ ...(args.port === undefined ? {} : { port: args.port }),
573
+ ...(args.open === undefined ? {} : { open: args.open }),
574
+ ...(args.allowNonLoopback === undefined ? {} : { allowNonLoopback: args.allowNonLoopback }),
575
+ includeMemory: args.includeMemory,
576
+ ...(args.maxRunsPerInstance === undefined ? {} : { maxRunsPerInstance: args.maxRunsPerInstance }),
577
+ });
578
+ }
281
579
  case "install-skill":
282
580
  return await runInstallSkill(args);
283
581
  case "backfill":
@@ -288,31 +586,182 @@ export async function runCli(argv) {
288
586
  ...(args.since === undefined ? {} : { since: args.since }),
289
587
  ...(args.until === undefined ? {} : { until: args.until }),
290
588
  dryRun: args.dryRun,
589
+ includeMemory: args.includeMemory,
590
+ });
591
+ case "audit-runs":
592
+ return await runAuditRuns({
593
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
594
+ ...(args.artifactDir === undefined ? {} : { artifactDir: args.artifactDir }),
595
+ ...(args.consumerPath === undefined ? {} : { consumerPath: args.consumerPath }),
596
+ ...(args.staleAfterMs === undefined ? {} : { staleAfterMs: args.staleAfterMs }),
597
+ json: args.json === true,
598
+ includeMemory: args.includeMemory,
599
+ });
600
+ case "metrics":
601
+ return await runMetrics({
602
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
603
+ ...(args.artifactDir === undefined ? {} : { artifactDir: args.artifactDir }),
604
+ ...(args.since === undefined ? {} : { since: args.since }),
605
+ ...(args.until === undefined ? {} : { until: args.until }),
606
+ ...(args.groupBy === undefined ? {} : { groupBy: args.groupBy }),
607
+ json: args.json === true,
608
+ includeMemory: args.includeMemory,
609
+ });
610
+ case "memory": {
611
+ // Lazy import: the memory preview path pulls SQLite/backend clients only on demand.
612
+ const { runMemoryCommand } = await import("./memory-command.js");
613
+ return await runMemoryCommand({
614
+ cwd: process.cwd(),
615
+ env: process.env,
616
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
617
+ positionals: args.positionals,
618
+ json: args.json === true,
619
+ ...(args.limit === undefined ? {} : { limit: args.limit }),
291
620
  });
621
+ }
292
622
  }
293
623
  }
294
624
  async function runInit(args) {
625
+ const recipe = resolveRecipeArg(args);
626
+ if (recipe === "unknown") {
627
+ return 1;
628
+ }
629
+ const withChannels = resolveWithChannels(args);
630
+ if (withChannels === "invalid") {
631
+ return 1;
632
+ }
295
633
  const result = await initMonoAgentFolder({
296
634
  dir: process.cwd(),
297
635
  ...(args.model === undefined ? {} : { model: args.model }),
298
636
  ...(args.fallbackModels === undefined ? {} : { fallbackModels: args.fallbackModels }),
299
637
  ...(args.memory === undefined ? {} : { memory: args.memory }),
638
+ ...(recipe === undefined ? {} : { recipe }),
639
+ ...(withChannels === undefined ? {} : { withChannels }),
640
+ dryRun: args.dryRun,
641
+ });
642
+ printInitResult(result, recipe);
643
+ printNextSteps(result.configPath, recipe);
644
+ return 0;
645
+ }
646
+ async function runSetup(args) {
647
+ if (process.stdin.isTTY !== true) {
648
+ return await runInit(args);
649
+ }
650
+ const recipeArg = resolveRecipeArg(args);
651
+ if (recipeArg === "unknown") {
652
+ return 1;
653
+ }
654
+ const withChannelsArg = resolveWithChannels(args);
655
+ if (withChannelsArg === "invalid") {
656
+ return 1;
657
+ }
658
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
659
+ let collected;
660
+ try {
661
+ collected = await collectSetupOptions({
662
+ prompt: { question: (prompt) => rl.question(prompt) },
663
+ ...(recipeArg === undefined ? {} : { recipe: recipeArg }),
664
+ ...(args.model === undefined ? {} : { model: args.model }),
665
+ ...(args.fallbackModels === undefined ? {} : { fallbackModels: args.fallbackModels }),
666
+ ...(withChannelsArg === undefined ? {} : { withChannels: withChannelsArg }),
667
+ });
668
+ }
669
+ finally {
670
+ rl.close();
671
+ }
672
+ const result = await initMonoAgentFolder({
673
+ dir: process.cwd(),
674
+ ...(collected.fallbackModels.length === 0 ? {} : { fallbackModels: collected.fallbackModels }),
675
+ ...(args.memory === undefined ? {} : { memory: args.memory }),
676
+ recipe: collected.recipe,
677
+ recipeInputs: collected.recipeInputs,
678
+ withChannels: collected.withChannels,
679
+ dryRun: args.dryRun,
300
680
  });
681
+ printInitResult(result, collected.recipe);
682
+ if (!result.dryRun) {
683
+ const report = await validateMonoAgentFolder({
684
+ env: process.env,
685
+ cwd: process.cwd(),
686
+ configPath: result.configPath,
687
+ });
688
+ process.stdout.write("\n" + ui.heading("Validation"));
689
+ for (const section of report.sections) {
690
+ process.stdout.write(formatSection(section));
691
+ }
692
+ process.stdout.write(renderRecipeCompleteness(collected.recipe, report));
693
+ printSecretsChecklist(collected.secrets);
694
+ printNextSteps(result.configPath, collected.recipe);
695
+ return report.ok ? 0 : 1;
696
+ }
697
+ printSecretsChecklist(collected.secrets);
698
+ printNextSteps(result.configPath, collected.recipe);
699
+ return 0;
700
+ }
701
+ function resolveRecipeArg(args) {
702
+ if (args.recipe === undefined) {
703
+ return undefined;
704
+ }
705
+ const recipe = findRecipe(args.recipe);
706
+ if (recipe === undefined) {
707
+ process.stderr.write(ui.errorLine(`Unknown recipe \`${args.recipe}\`.`));
708
+ process.stderr.write(ui.hint("Run `mono-agent recipes list` to see available recipes."));
709
+ return "unknown";
710
+ }
711
+ return recipe;
712
+ }
713
+ function resolveWithChannels(args) {
714
+ if (args.withChannels === undefined) {
715
+ return undefined;
716
+ }
717
+ const invalid = args.withChannels.filter((channel) => !isWithChannel(channel));
718
+ if (invalid.length > 0) {
719
+ process.stderr.write(ui.errorLine(`Unknown --with channel(s): ${invalid.join(", ")}.`));
720
+ process.stderr.write(ui.hint("Valid channels: telegram, slack, webhook, openaiApi, cron."));
721
+ return "invalid";
722
+ }
723
+ return args.withChannels.filter(isWithChannel);
724
+ }
725
+ function printInitResult(result, recipe) {
726
+ if (result.dryRun) {
727
+ process.stdout.write(ui.style.dim("Dry run — nothing was written.\n"));
728
+ }
729
+ const verb = result.dryRun ? "would create" : "created";
301
730
  for (const path of result.created) {
302
- process.stdout.write(`${ui.badge("ok")}${ui.style.green("created")} ${path}\n`);
731
+ process.stdout.write(`${ui.badge("ok")}${ui.style.green(verb.padEnd(12))} ${path}\n`);
303
732
  }
304
733
  for (const path of result.skipped) {
305
- process.stdout.write(ui.style.dim(` kept ${path}`) + "\n");
734
+ process.stdout.write(ui.style.dim(` kept ${path}`) + "\n");
306
735
  }
307
736
  if (result.knowledgeFiles.length > 0) {
308
737
  process.stdout.write(`\nIdentity references existing knowledge: ${ui.style.cyan(result.knowledgeFiles.join(", "))}\n`);
309
738
  }
739
+ if (recipe !== undefined) {
740
+ process.stdout.write(`\n${ui.style.bold("Recipe:")} ${ui.style.cyan(recipe.id)} ${ui.style.dim(`(risk: ${recipe.riskLevel})`)}\n`);
741
+ if (recipe.envExample !== undefined) {
742
+ process.stdout.write(ui.style.dim("Fill the secret placeholders in .env.example, then copy it to .env.\n"));
743
+ }
744
+ }
745
+ }
746
+ function printSecretsChecklist(secrets) {
747
+ process.stdout.write("\n" + ui.heading("Secrets checklist"));
748
+ if (secrets.length === 0) {
749
+ process.stdout.write(ui.style.dim("No secret recipe inputs were prompted. Review .env.example if the recipe created one.\n"));
750
+ return;
751
+ }
752
+ process.stdout.write(ui.style.dim("Copy .env.example to .env, then fill these variables. Secret values were not prompted or written to JSON.\n"));
753
+ for (const secret of secrets) {
754
+ const key = secret.envVar ?? secret.id;
755
+ process.stdout.write(` ${ui.style.bold(key)} ${ui.style.dim(`- ${secret.label}: ${secret.description}`)}\n`);
756
+ }
757
+ }
758
+ function printNextSteps(configPath, recipe) {
759
+ const validateCmd = recipe === undefined ? "mono-agent validate" : `mono-agent validate --recipe ${recipe.id}`;
310
760
  process.stdout.write("\n" +
311
761
  ui.heading("Next steps") +
312
- ` ${ui.style.bold("1.")} Edit ${result.configPath} ${ui.style.dim("(model, channels, skills, memory, sandbox)")}\n` +
313
- ` ${ui.style.bold("2.")} mono-agent validate\n` +
762
+ ` ${ui.style.bold("1.")} Edit ${configPath} ${ui.style.dim("(model, channels, skills, memory, sandbox)")}\n` +
763
+ ` ${ui.style.bold("2.")} ${validateCmd}\n` +
314
764
  ` ${ui.style.bold("3.")} mono-agent start\n`);
315
- return 0;
316
765
  }
317
766
  async function runInstallSkill(args) {
318
767
  let result;
@@ -332,20 +781,219 @@ async function runInstallSkill(args) {
332
781
  return 0;
333
782
  }
334
783
  async function runValidate(args) {
335
- const cwd = process.cwd();
784
+ const context = resolveValidateContext(args, process.cwd());
336
785
  const report = await validateMonoAgentFolder({
337
786
  env: process.env,
338
- cwd,
339
- configPath: resolve(cwd, args.configPath ?? "mono-agent.config.json"),
787
+ cwd: context.cwd,
788
+ configPath: context.configPath,
789
+ allowFilesystemWrites: context.allowFilesystemWrites,
340
790
  });
341
791
  for (const section of report.sections) {
342
792
  process.stdout.write(formatSection(section));
343
793
  }
794
+ if (args.recipe !== undefined) {
795
+ const recipe = findRecipe(args.recipe);
796
+ if (recipe === undefined) {
797
+ process.stderr.write(ui.errorLine(`Unknown recipe \`${args.recipe}\`.`));
798
+ process.stderr.write(ui.hint("Run `mono-agent recipes list` to see available recipes."));
799
+ return 1;
800
+ }
801
+ process.stdout.write(renderRecipeCompleteness(recipe, report));
802
+ }
344
803
  process.stdout.write(report.ok
345
- ? `\n${ui.style.green("✓ Config is ready to start.")}\n`
804
+ ? `\n${ui.style.green("✓ Config is ready to start.")}\n${ui.style.dim("Run `mono-agent config` for the full field-by-field view.")}\n`
346
805
  : `\n${ui.hint("Fix the errors above, then re-run mono-agent validate.")}`);
806
+ process.stdout.write(ui.style.dim("Core sections activate by presence; channels need `enabled: true` — see docs/config (How sections activate).\n"));
347
807
  return report.ok ? 0 : 1;
348
808
  }
809
+ /**
810
+ * Capability-aware recipe check: for each capability the recipe promises, report
811
+ * whether the matching doctor section has reached the expected status. `waiting`
812
+ * stays non-fatal (it never changes the validate exit code) but is surfaced as
813
+ * "selected recipe incomplete" so the operator knows what is left to wire up.
814
+ */
815
+ function renderRecipeCompleteness(recipe, report) {
816
+ let out = "\n" + ui.heading(`Recipe: ${recipe.id}`);
817
+ let incomplete = 0;
818
+ for (const expectation of recipe.validateExpectations) {
819
+ const section = report.sections.find((entry) => entry.id === expectation.sectionId);
820
+ const status = section?.status ?? "missing";
821
+ const met = status === expectation.mustBe;
822
+ if (!met) {
823
+ incomplete += 1;
824
+ }
825
+ const badge = met ? ui.badge("ok") : ui.badge(status === "error" ? "error" : "waiting");
826
+ out += `${badge}${ui.style.bold(expectation.sectionId)} ${ui.style.dim(`(${status}, expected ${expectation.mustBe})`)}\n`;
827
+ if (!met && expectation.note !== undefined) {
828
+ out += ` ${ui.style.dim(expectation.note)}\n`;
829
+ }
830
+ }
831
+ out += incomplete === 0
832
+ ? `${ui.style.green(`✓ Recipe ${recipe.id} is fully configured.`)}\n`
833
+ : ui.style.yellow(`⚠ Selected recipe incomplete: ${incomplete} capability(ies) not yet live.\n`);
834
+ return out;
835
+ }
836
+ function riskColor(risk) {
837
+ if (risk === "high") {
838
+ return ui.style.red(risk);
839
+ }
840
+ if (risk === "medium") {
841
+ return ui.style.yellow(risk);
842
+ }
843
+ return ui.style.green(risk);
844
+ }
845
+ /** `mono-agent recipes list` — one line per recipe. */
846
+ export function renderRecipeList() {
847
+ let out = ui.banner("mono-agent", "recipes") + "\n";
848
+ for (const recipe of RECIPE_CATALOG) {
849
+ out += `${ui.style.bold(ui.style.cyan(recipe.id))} ${ui.style.dim(`[${riskColor(recipe.riskLevel)}]`)}\n`;
850
+ out += ` ${recipe.title}\n`;
851
+ out += ` ${ui.style.dim(recipe.tags.join(", "))}\n`;
852
+ }
853
+ out += "\n" + ui.style.dim("Scaffold one with: mono-agent init --recipe <id> [--dry-run] [--with slack,cron]\n");
854
+ return out;
855
+ }
856
+ /** `mono-agent recipes show <id>` — description, generated JSON, env example, checklist. */
857
+ export function renderRecipeShow(recipe) {
858
+ const inputs = resolveRecipeInputs(recipe);
859
+ let out = ui.banner("mono-agent", `recipe: ${recipe.id}`) + "\n";
860
+ out += `${ui.style.bold(recipe.title)} ${ui.style.dim(`(risk: ${riskColor(recipe.riskLevel)})`)}\n`;
861
+ out += `${recipe.description}\n`;
862
+ if (recipe.playbook !== undefined) {
863
+ out += ui.style.dim(`Playbook: docs/playbooks/${recipe.playbook}\n`);
864
+ }
865
+ out += "\n" + ui.heading("Generated mono-agent.config.json");
866
+ out += JSON.stringify(recipe.config(inputs), null, 2) + "\n";
867
+ const envExample = recipe.envExample?.(inputs);
868
+ if (envExample !== undefined && envExample.trim().length > 0) {
869
+ out += "\n" + ui.heading(".env.example");
870
+ out += envExample.endsWith("\n") ? envExample : envExample + "\n";
871
+ }
872
+ const files = recipe.files?.(inputs) ?? [];
873
+ if (files.length > 0) {
874
+ out += "\n" + ui.heading("Scaffolded files");
875
+ for (const file of files) {
876
+ out += ` ${ui.style.cyan(file.path)}\n`;
877
+ }
878
+ }
879
+ if (recipe.validateExpectations.length > 0) {
880
+ out += "\n" + ui.heading("Follow-up checklist");
881
+ for (const expectation of recipe.validateExpectations) {
882
+ const note = expectation.note === undefined ? "" : ` — ${expectation.note}`;
883
+ out += ` ${ui.style.gray("•")} ${expectation.sectionId} ${ui.style.dim(`must be ${expectation.mustBe}`)}${ui.style.dim(note)}\n`;
884
+ }
885
+ }
886
+ return out;
887
+ }
888
+ /** Dispatch `mono-agent recipes list|show <id>`. */
889
+ function runRecipes(args) {
890
+ const [sub, id] = args.positionals;
891
+ if (sub === undefined || sub === "list") {
892
+ process.stdout.write(renderRecipeList());
893
+ return 0;
894
+ }
895
+ if (sub === "show") {
896
+ if (id === undefined) {
897
+ process.stderr.write(ui.errorLine("Usage: mono-agent recipes show <id>."));
898
+ process.stderr.write(ui.hint(`Available: ${recipeIds().join(", ")}.`));
899
+ return 2;
900
+ }
901
+ const recipe = findRecipe(id);
902
+ if (recipe === undefined) {
903
+ process.stderr.write(ui.errorLine(`Unknown recipe \`${id}\`.`));
904
+ process.stderr.write(ui.hint("Run `mono-agent recipes list` to see available recipes."));
905
+ return 1;
906
+ }
907
+ process.stdout.write(renderRecipeShow(recipe));
908
+ return 0;
909
+ }
910
+ process.stderr.write(ui.errorLine(`Unknown recipes subcommand \`${sub}\`. Expected list or show.`));
911
+ return 2;
912
+ }
913
+ const SOURCE_TAG = {
914
+ env: ui.style.green("[env]"),
915
+ json: ui.style.cyan("[json]"),
916
+ default: ui.style.dim("[default]"),
917
+ };
918
+ /**
919
+ * Render the complete, source-annotated config view: every core section and
920
+ * field with its resolved value and whether it came from an env var, the JSON
921
+ * file, or the built-in default. This is the single discovery surface that
922
+ * replaced the old partial config panes.
923
+ */
924
+ export function renderConfigView(sections) {
925
+ let out = "";
926
+ for (const section of sections) {
927
+ const badgeStatus = section.status === "active" ? "ok" : "disabled";
928
+ out += `${ui.badge(badgeStatus)}${ui.style.bold(section.label)}\n`;
929
+ const width = section.fields.reduce((max, field) => Math.max(max, field.label.length), 0);
930
+ for (const field of section.fields) {
931
+ const tag = SOURCE_TAG[field.source];
932
+ const lock = field.redacted === true ? ` ${ui.style.dim("(secret)")}` : "";
933
+ const defaultRestatement = field.restatesDefault === true ? ` ${ui.style.dim("(same as default)")}` : "";
934
+ out += ` ${ui.style.gray(field.label.padEnd(width))} ${field.value}${lock}${defaultRestatement} ${tag}\n`;
935
+ }
936
+ }
937
+ return out;
938
+ }
939
+ /**
940
+ * `mono-agent config`: print the resolved configuration field-by-field with the
941
+ * source (env / json / default) of every value, then the channel summary. Read
942
+ * only — edits go in mono-agent.config.json and take effect on the next restart.
943
+ */
944
+ async function runConfig(args) {
945
+ const cwd = process.cwd();
946
+ const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
947
+ const env = process.env;
948
+ const jsonResult = await readMonoAgentConfigJson(configPath);
949
+ let config;
950
+ try {
951
+ config = await loadAppCoreConfig({ env, cwd, configPath });
952
+ }
953
+ catch (error) {
954
+ if (isAppCoreConfigError(error)) {
955
+ process.stderr.write(ui.errorLine(error.message));
956
+ if (jsonResult.missing) {
957
+ process.stderr.write(ui.hint(`No mono-agent config found at ${configPath}. Run \`mono-agent init\` to scaffold one.`));
958
+ }
959
+ else {
960
+ process.stderr.write(ui.hint("Fix the config above, then re-run `mono-agent config`."));
961
+ }
962
+ return 1;
963
+ }
964
+ throw error;
965
+ }
966
+ const sections = buildMonoAgentConfigView({
967
+ redacted: redactMonoAgentConfig(config),
968
+ json: jsonResult.json,
969
+ env,
970
+ });
971
+ const drivers = await resolveChannelDrivers({ env, cwd, configPath });
972
+ const channelViews = await collectChannelConfigViews(drivers, { env, cwd, configPath });
973
+ process.stdout.write(ui.banner("mono-agent", "resolved config") + "\n");
974
+ process.stdout.write(renderConfigView(sections));
975
+ if (channelViews.length > 0) {
976
+ process.stdout.write("\n" + ui.heading("Channels"));
977
+ process.stdout.write(renderConfigView(channelViews));
978
+ }
979
+ for (const warning of [
980
+ ...findUnknownAppConfigWarnings(jsonResult.json),
981
+ ...findJsonSecretConfigWarnings([...sections, ...channelViews]),
982
+ ...findRemovedConfigWarnings({ json: jsonResult.json, env }),
983
+ ]) {
984
+ process.stdout.write(`${ui.style.yellow(warning)}\n`);
985
+ }
986
+ const report = await validateMonoAgentFolder({ env, cwd, configPath, liveness: false, drivers });
987
+ const channels = report.sections.filter((section) => section.id.startsWith("channel:"));
988
+ if (channels.length > 0) {
989
+ process.stdout.write("\n" + ui.heading("Channel status"));
990
+ for (const section of channels) {
991
+ process.stdout.write(formatSection(section));
992
+ }
993
+ }
994
+ process.stdout.write("\n" + ui.style.dim("Source precedence: [env] > [json] > [default]. Edit mono-agent.config.json and run `mono-agent restart` to apply.") + "\n");
995
+ return 0;
996
+ }
349
997
  /** Render one validation section: a status badge, a bold label, and its details. */
350
998
  function formatSection(section) {
351
999
  let out = `${ui.badge(section.status)}${ui.style.bold(section.label)}\n`;
@@ -358,7 +1006,7 @@ function colorDetail(status, detail) {
358
1006
  if (status === "error") {
359
1007
  return ui.style.red(detail);
360
1008
  }
361
- if (detail.startsWith("[WARN]")) {
1009
+ if (detail.startsWith("[WARN]") || detail.startsWith("WARNING:")) {
362
1010
  return ui.style.yellow(detail);
363
1011
  }
364
1012
  return ui.style.dim(detail);
@@ -429,7 +1077,7 @@ async function runForeground(args) {
429
1077
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
430
1078
  logger: consoleLogger(),
431
1079
  });
432
- printAppStatus(app);
1080
+ await printAppStatus(app);
433
1081
  // Block until a shutdown signal. Returning here (the old behavior) let the
434
1082
  // process exit immediately whenever no channel owned a live handle — e.g. a
435
1083
  // traceability-only config, now that the operator console is retired and the
@@ -508,7 +1156,7 @@ function requireDarwin(command) {
508
1156
  process.stderr.write(ui.hint("Run `mono-agent start --foreground` to run in the foreground on this platform."));
509
1157
  return 1;
510
1158
  }
511
- export function printAppStatus(app) {
1159
+ export async function printAppStatus(app, options = {}) {
512
1160
  const trace = app.traceabilityStatus;
513
1161
  process.stdout.write(ui.rule("instance"));
514
1162
  process.stdout.write(ui.keyValue([
@@ -519,6 +1167,8 @@ export function printAppStatus(app) {
519
1167
  ],
520
1168
  ], 2));
521
1169
  const artifactDir = app.traceabilityStatus.kind === "running" ? app.traceabilityStatus.artifactDir : undefined;
1170
+ process.stdout.write(ui.rule("sandbox"));
1171
+ process.stdout.write(` ${describeSandboxStatus(app.sandboxStatus)}\n`);
522
1172
  process.stdout.write(ui.rule("observability"));
523
1173
  process.stdout.write(` ${describeExporter(app.exporterStatus, artifactDir)}\n`);
524
1174
  const channels = [...app.channelStatuses()];
@@ -528,6 +1178,43 @@ export function printAppStatus(app) {
528
1178
  process.stdout.write(` ${ui.channelBadge(status.kind)}${ui.style.bold(id.padEnd(11))} ${describeChannelStatus(status)}\n`);
529
1179
  }
530
1180
  }
1181
+ await writeAppRunsHealthDetail(app, options);
1182
+ }
1183
+ async function writeAppRunsHealthDetail(app, options) {
1184
+ const artifactDir = app.traceabilityStatus.kind === "running" ? app.traceabilityStatus.artifactDir : undefined;
1185
+ if (artifactDir === undefined || artifactDir.trim().length === 0) {
1186
+ return;
1187
+ }
1188
+ const reader = options.listRecordedRuns ?? listRecordedRuns;
1189
+ let result;
1190
+ try {
1191
+ result = await reader({ artifactDir, maxRuns: RUNS_HEALTH_MAX_RUNS, scope: "agent" });
1192
+ }
1193
+ catch (error) {
1194
+ result = {
1195
+ totalRuns: 0,
1196
+ runs: [],
1197
+ warnings: [`Unable to read run summaries: ${reasonOf(error)}`],
1198
+ };
1199
+ }
1200
+ const display = buildRunsHealthDisplay({
1201
+ artifactDir,
1202
+ totalRuns: result.totalRuns,
1203
+ runs: result.runs,
1204
+ warnings: result.warnings,
1205
+ includeSelectedSkills: true,
1206
+ runOwnerAlive: true,
1207
+ maxRuns: RUNS_HEALTH_MAX_RUNS,
1208
+ ...(app.selectedSkills === undefined ? {} : { selectedSkills: app.selectedSkills }),
1209
+ ...(options.nowMs === undefined ? {} : { nowMs: options.nowMs }),
1210
+ });
1211
+ process.stdout.write(ui.rule("runs health"));
1212
+ for (const detail of display.details) {
1213
+ process.stdout.write(` ${detail}\n`);
1214
+ }
1215
+ }
1216
+ function reasonOf(error) {
1217
+ return error instanceof Error ? error.message : String(error);
531
1218
  }
532
1219
  function describeExporter(status, artifactDir) {
533
1220
  if (status.kind !== "configured") {
@@ -539,7 +1226,7 @@ function describeExporter(status, artifactDir) {
539
1226
  parts.push(`app ${appUrl}`);
540
1227
  }
541
1228
  if (status.includeSensitiveData) {
542
- parts.push("includeSensitiveData=true");
1229
+ parts.push(ui.style.yellow(describeSensitiveDataExportWarning(status.endpoint)));
543
1230
  }
544
1231
  if (status.lastWarning !== undefined) {
545
1232
  parts.push(`last warning: ${status.lastWarning}`);
@@ -552,15 +1239,53 @@ function describeExporter(status, artifactDir) {
552
1239
  : `JSONL artifacts remain local at ${artifactDir}`);
553
1240
  return parts.join("; ");
554
1241
  }
555
- function describeChannelStatus(status) {
1242
+ function describeSandboxStatus(status) {
1243
+ const engineAvailability = status.engineAvailable === true
1244
+ ? "present"
1245
+ : status.engineAvailable === false
1246
+ ? "absent"
1247
+ : "not checked";
1248
+ const parts = [
1249
+ `effective: ${status.effective}`,
1250
+ `engine: ${status.engine ?? "none"} (${engineAvailability})`,
1251
+ ...(status.fallback === undefined ? [] : [`fallback: ${status.fallback}`]),
1252
+ `fallback active: ${status.fallbackActive ? "yes" : "no"}`,
1253
+ status.detail,
1254
+ ];
1255
+ const warning = status.warning ?? sandboxEffectiveStateWarning(status);
1256
+ if (warning !== undefined) {
1257
+ parts.push(ui.style.yellow(warning));
1258
+ }
1259
+ return parts.join("; ");
1260
+ }
1261
+ export function describeChannelStatus(status) {
556
1262
  if (status.kind === "running") {
557
1263
  const facts = Object.entries(status.summary)
558
- .map(([key, value]) => `${key}=${String(value)}`)
1264
+ .map(([key, value]) => `${key}=${formatChannelFactValue(value)}`)
559
1265
  .join(" ");
560
1266
  return facts.length === 0 ? "running" : `running (${facts})`;
561
1267
  }
562
1268
  return `${status.kind}: ${status.reason}`;
563
1269
  }
1270
+ /**
1271
+ * Render a channel-status summary value for the human-readable status line.
1272
+ * Objects and arrays are expanded to `key: value` pairs instead of the useless
1273
+ * `[object Object]` that a bare `String()` produced (E4: the webhook summary's
1274
+ * `invokeUrls` map printed `invokeUrls=[object Object]`).
1275
+ */
1276
+ function formatChannelFactValue(value) {
1277
+ if (value === null || typeof value !== "object") {
1278
+ return String(value);
1279
+ }
1280
+ if (Array.isArray(value)) {
1281
+ return `[${value.map(formatChannelFactValue).join(", ")}]`;
1282
+ }
1283
+ const entries = Object.entries(value);
1284
+ if (entries.length === 0) {
1285
+ return "{}";
1286
+ }
1287
+ return `{${entries.map(([key, inner]) => `${key}: ${formatChannelFactValue(inner)}`).join(", ")}}`;
1288
+ }
564
1289
  /**
565
1290
  * Block the foreground process until SIGINT/SIGTERM, then stop the app and
566
1291
  * resolve the exit code. A referenced no-op timer owns the event loop so the