@mono-agent/agent-app 0.3.0 → 0.4.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 (68) hide show
  1. package/README.md +9 -5
  2. package/dist/adapter-send-tools-main.d.ts +3 -0
  3. package/dist/adapter-send-tools-main.d.ts.map +1 -0
  4. package/dist/adapter-send-tools-main.js +20 -0
  5. package/dist/adapter-send-tools-main.js.map +1 -0
  6. package/dist/adapter-send-tools.d.ts +59 -0
  7. package/dist/adapter-send-tools.d.ts.map +1 -0
  8. package/dist/adapter-send-tools.js +254 -0
  9. package/dist/adapter-send-tools.js.map +1 -0
  10. package/dist/app-config.d.ts +40 -17
  11. package/dist/app-config.d.ts.map +1 -1
  12. package/dist/app-config.js +188 -68
  13. package/dist/app-config.js.map +1 -1
  14. package/dist/app.d.ts +41 -20
  15. package/dist/app.d.ts.map +1 -1
  16. package/dist/app.js +250 -91
  17. package/dist/app.js.map +1 -1
  18. package/dist/backfill.d.ts +79 -0
  19. package/dist/backfill.d.ts.map +1 -0
  20. package/dist/backfill.js +246 -0
  21. package/dist/backfill.js.map +1 -0
  22. package/dist/background.d.ts +0 -4
  23. package/dist/background.d.ts.map +1 -1
  24. package/dist/background.js +78 -50
  25. package/dist/background.js.map +1 -1
  26. package/dist/channels.d.ts +7 -0
  27. package/dist/channels.d.ts.map +1 -1
  28. package/dist/channels.js +26 -3
  29. package/dist/channels.js.map +1 -1
  30. package/dist/cli.d.ts +52 -3
  31. package/dist/cli.d.ts.map +1 -1
  32. package/dist/cli.js +315 -111
  33. package/dist/cli.js.map +1 -1
  34. package/dist/doctor.d.ts +8 -0
  35. package/dist/doctor.d.ts.map +1 -1
  36. package/dist/doctor.js +148 -48
  37. package/dist/doctor.js.map +1 -1
  38. package/dist/index.d.ts +6 -4
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +3 -2
  41. package/dist/index.js.map +1 -1
  42. package/dist/launchd.d.ts +0 -2
  43. package/dist/launchd.d.ts.map +1 -1
  44. package/dist/launchd.js +0 -2
  45. package/dist/launchd.js.map +1 -1
  46. package/dist/memory-recall-main.d.ts +3 -0
  47. package/dist/memory-recall-main.d.ts.map +1 -0
  48. package/dist/memory-recall-main.js +30 -0
  49. package/dist/memory-recall-main.js.map +1 -0
  50. package/dist/memory-recall.d.ts +111 -0
  51. package/dist/memory-recall.d.ts.map +1 -0
  52. package/dist/memory-recall.js +275 -0
  53. package/dist/memory-recall.js.map +1 -0
  54. package/dist/sessions.d.ts +21 -0
  55. package/dist/sessions.d.ts.map +1 -0
  56. package/dist/sessions.js +45 -0
  57. package/dist/sessions.js.map +1 -0
  58. package/dist/ui.d.ts +54 -0
  59. package/dist/ui.d.ts.map +1 -0
  60. package/dist/ui.js +153 -0
  61. package/dist/ui.js.map +1 -0
  62. package/package.json +22 -17
  63. package/skills/mono-agent-composer/SKILL.md +6 -5
  64. package/skills/mono-agent-composer/references/config-blueprint.md +37 -18
  65. package/skills/mono-agent-composer/references/discovery-questions.md +38 -23
  66. package/skills/mono-agent-composer/references/feature-coverage.md +12 -9
  67. package/skills/mono-agent-composer/references/package-map.md +5 -5
  68. package/skills/mono-agent-composer/references/validation.md +5 -4
package/dist/cli.js CHANGED
@@ -1,19 +1,28 @@
1
1
  #!/usr/bin/env node
2
+ import { stat } from "node:fs/promises";
2
3
  import { basename, resolve } from "node:path";
3
4
  import process from "node:process";
4
5
  import { fileURLToPath } from "node:url";
5
6
  import { startMonoAgentApp } from "./app.js";
7
+ import { phoenixAppBaseUrl } from "./app-config.js";
8
+ import { runBackfill } from "./backfill.js";
6
9
  import { defaultBackgroundDeps, resolveInstanceTarget, restartBackground, startBackground, statusBackground, stopBackground, tailLogs, } from "./background.js";
7
10
  import { validateMonoAgentFolder } from "./doctor.js";
8
11
  import { initMonoAgentFolder } from "./init.js";
9
12
  import { installComposerSkill } from "./install-skill.js";
13
+ import { purgeSessions } from "./sessions.js";
14
+ import * as ui from "./ui.js";
10
15
  const DEFAULT_LOG_LINES = 200;
16
+ // Node's maximum setInterval/setTimeout delay (2^31 - 1 ms, ~24.8 days). A
17
+ // referenced timer at this delay keeps the foreground event loop alive without
18
+ // busy-waiting; larger values silently overflow to a 1ms delay.
19
+ const KEEP_ALIVE_INTERVAL_MS = 2_147_483_647;
11
20
  const BACKGROUND_COMMANDS = ["start", "restart", "stop", "status", "logs"];
12
- const KNOWN_COMMANDS = ["init", "validate", "start", "restart", "stop", "status", "logs", "install-skill"];
21
+ const KNOWN_COMMANDS = ["init", "validate", "start", "restart", "stop", "status", "logs", "install-skill", "backfill"];
13
22
  export function parseCliArgs(argv) {
14
23
  const [command, ...rest] = argv;
15
24
  if (command === undefined || command === "help" || command === "--help" || command === "-h") {
16
- return { command: "help", noConsole: false, force: false, foreground: false, follow: false };
25
+ return { command: "help", force: false, foreground: false, follow: false, all: false, dryRun: false };
17
26
  }
18
27
  if (!KNOWN_COMMANDS.includes(command)) {
19
28
  throw new Error(`Unknown command \`${command}\`. Expected ${KNOWN_COMMANDS.join(", ")}.`);
@@ -21,32 +30,41 @@ export function parseCliArgs(argv) {
21
30
  const cmd = command;
22
31
  const isLogs = cmd === "logs";
23
32
  let configPath;
24
- let port;
25
33
  let model;
26
34
  let fallbackModels;
27
35
  let memory;
28
36
  let envFile;
29
37
  let target;
30
- let noConsole = false;
31
38
  let force = false;
32
39
  let foreground = false;
33
40
  let follow = false;
34
41
  let lines;
42
+ let run;
43
+ let all = false;
44
+ let since;
45
+ let until;
46
+ let dryRun = false;
35
47
  for (let i = 0; i < rest.length; i += 1) {
36
48
  const flag = rest[i];
37
49
  switch (flag) {
38
50
  case "--config":
39
51
  configPath = requireValue(rest, ++i, flag);
40
52
  break;
41
- case "--port": {
42
- const raw = requireValue(rest, ++i, flag);
43
- const parsed = Number(raw);
44
- if (!Number.isInteger(parsed) || parsed < 0 || parsed > 65_535) {
45
- throw new Error("--port must be an integer between 0 and 65535.");
46
- }
47
- port = parsed;
53
+ case "--run":
54
+ run = requireValue(rest, ++i, flag);
55
+ break;
56
+ case "--all":
57
+ all = true;
58
+ break;
59
+ case "--since":
60
+ since = requireValue(rest, ++i, flag);
61
+ break;
62
+ case "--until":
63
+ until = requireValue(rest, ++i, flag);
64
+ break;
65
+ case "--dry-run":
66
+ dryRun = true;
48
67
  break;
49
- }
50
68
  case "--model":
51
69
  model = requireValue(rest, ++i, flag);
52
70
  break;
@@ -78,9 +96,6 @@ export function parseCliArgs(argv) {
78
96
  case "--force":
79
97
  force = true;
80
98
  break;
81
- case "--no-console":
82
- noConsole = true;
83
- break;
84
99
  case "--foreground":
85
100
  foreground = true;
86
101
  break;
@@ -112,17 +127,20 @@ export function parseCliArgs(argv) {
112
127
  return {
113
128
  command: cmd,
114
129
  ...(configPath === undefined ? {} : { configPath }),
115
- ...(port === undefined ? {} : { port }),
116
130
  ...(model === undefined ? {} : { model }),
117
131
  ...(fallbackModels === undefined ? {} : { fallbackModels }),
118
132
  ...(memory === undefined ? {} : { memory }),
119
133
  ...(envFile === undefined ? {} : { envFile }),
120
134
  ...(target === undefined ? {} : { target }),
121
- noConsole,
122
135
  force,
123
136
  foreground,
124
137
  follow,
125
138
  ...(lines === undefined ? {} : { lines }),
139
+ ...(run === undefined ? {} : { run }),
140
+ all,
141
+ ...(since === undefined ? {} : { since }),
142
+ ...(until === undefined ? {} : { until }),
143
+ dryRun,
126
144
  };
127
145
  }
128
146
  /**
@@ -146,38 +164,66 @@ function requireValue(args, index, flag) {
146
164
  }
147
165
  return value;
148
166
  }
149
- const HELP_TEXT = `mono-agent — config-first agent host
150
-
151
- Usage:
152
- mono-agent init [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]
153
- Scaffold mono-agent.config.json, IDENTITY.md, and .mono-agent/ in the
154
- current folder. Existing files are never overwritten.
155
-
156
- mono-agent validate [--config <path>] [--env-file <path>]
157
- Load every config section and report what would run, wait, or fail.
158
-
159
- mono-agent start [--config <path>] [--port <n>] [--no-console] [--env-file <path>] [--foreground|-f]
160
- Start the agent as a background macOS service (launchd), print its
161
- instance info, and return. Re-running restarts the running instance.
162
- Use --foreground (-f) to run in the blocking foreground instead.
163
-
164
- mono-agent restart [--config <path>] [--port <n>] [--no-console]
165
- Restart the background instance for this config (starts it if stopped).
166
-
167
- mono-agent stop [--config <path>]
168
- Stop the background instance and remove its LaunchAgent.
169
-
170
- mono-agent status [--config <path>]
171
- Show this config's instance plus any other running instances.
172
-
173
- mono-agent logs [--config <path>] [--follow|-f] [--lines <n>]
174
- Print (and optionally follow) the background instance's log files.
175
-
176
- mono-agent install-skill [--target claude|codex|both] [--force]
177
- Copy the bundled mono-agent-composer skill into ~/.claude/skills and
178
- ~/.codex/skills (default: both). Refuses to overwrite without --force.
179
-
180
- Background mode runs the agent under launchd, keeping it alive across logins
167
+ const HELP_COMMANDS = [
168
+ {
169
+ signature: "mono-agent init [--model <ref>] [--fallback-models <csv>] [--memory lite|journal|bujo]",
170
+ lines: [
171
+ "Scaffold mono-agent.config.json, IDENTITY.md, and .mono-agent/ in the",
172
+ "current folder. Existing files are never overwritten.",
173
+ ],
174
+ },
175
+ {
176
+ signature: "mono-agent validate [--config <path>] [--env-file <path>]",
177
+ lines: ["Load every config section and report what would run, wait, or fail."],
178
+ },
179
+ {
180
+ signature: "mono-agent start [--config <path>] [--env-file <path>] [--foreground|-f]",
181
+ lines: [
182
+ "Start the agent as a background macOS service (launchd), print its",
183
+ "instance info, and return. Re-running restarts the running instance.",
184
+ "Refuses to start without a valid mono-agent.config.json in the folder.",
185
+ "Use --foreground (-f) to run in the blocking foreground instead.",
186
+ ],
187
+ },
188
+ {
189
+ signature: "mono-agent restart [--config <path>] [--force]",
190
+ lines: [
191
+ "Restart the background instance for this config (starts it if stopped).",
192
+ "--force also clears the persisted pi sessions so it starts with fresh",
193
+ "conversations instead of resuming saved ones (durable memory is untouched).",
194
+ ],
195
+ },
196
+ {
197
+ signature: "mono-agent stop [--config <path>]",
198
+ lines: ["Stop the background instance and remove its LaunchAgent."],
199
+ },
200
+ {
201
+ signature: "mono-agent status [--config <path>]",
202
+ lines: ["Show this config's instance plus any other running instances."],
203
+ },
204
+ {
205
+ signature: "mono-agent logs [--config <path>] [--follow|-f] [--lines <n>]",
206
+ lines: ["Print (and optionally follow) the background instance's log files."],
207
+ },
208
+ {
209
+ signature: "mono-agent install-skill [--target claude|codex|both] [--force]",
210
+ lines: [
211
+ "Copy the bundled mono-agent-composer skill into ~/.claude/skills and",
212
+ "~/.codex/skills (default: both). Refuses to overwrite without --force.",
213
+ ],
214
+ },
215
+ {
216
+ signature: "mono-agent backfill (--run <id> | --all) [--since <iso>] [--until <iso>]\n" +
217
+ " [--dry-run] [--config <path>] [--env-file <path>]",
218
+ lines: [
219
+ "Export already-recorded run artifacts to the configured Phoenix exporter",
220
+ "with their historical timestamps. Trace ids are deterministic per run, so",
221
+ "re-running overwrites rather than duplicating. --dry-run maps and",
222
+ "serializes without sending.",
223
+ ],
224
+ },
225
+ ];
226
+ const HELP_NOTES = `Background mode runs the agent under launchd, keeping it alive across logins
181
227
  (auto-restarting only on crash) until you run stop. Secrets are read from the
182
228
  .env file in the working directory, the same as foreground mode. The background
183
229
  commands require macOS; elsewhere use start --foreground.
@@ -188,19 +234,38 @@ pi:<provider>:<model> (e.g. pi:ollama:gemma4:31b).
188
234
  A .env file in the current folder is loaded automatically when present;
189
235
  already-exported shell variables take precedence.
190
236
  `;
237
+ /** Build the colorized help screen (plain text when color is disabled). */
238
+ export function renderHelp() {
239
+ let out = ui.banner("mono-agent", "config-first agent host") + "\n";
240
+ out += ui.heading("Usage");
241
+ for (const entry of HELP_COMMANDS) {
242
+ const [first, ...rest] = entry.signature.split("\n");
243
+ out += ` ${ui.style.bold(ui.style.cyan(first ?? ""))}\n`;
244
+ for (const cont of rest) {
245
+ out += ` ${ui.style.cyan(cont)}\n`;
246
+ }
247
+ for (const line of entry.lines) {
248
+ out += ` ${ui.style.dim(line)}\n`;
249
+ }
250
+ out += "\n";
251
+ }
252
+ out += ui.style.dim(HELP_NOTES);
253
+ return out;
254
+ }
191
255
  export async function runCli(argv) {
192
256
  let args;
193
257
  try {
194
258
  args = parseCliArgs(argv);
195
259
  }
196
260
  catch (error) {
197
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP_TEXT}`);
261
+ process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
262
+ process.stdout.write(`\n${renderHelp()}`);
198
263
  return 2;
199
264
  }
200
265
  loadCliEnvFile(resolve(process.cwd(), args.envFile ?? ".env"));
201
266
  switch (args.command) {
202
267
  case "help":
203
- process.stdout.write(HELP_TEXT);
268
+ process.stdout.write(renderHelp());
204
269
  return 0;
205
270
  case "init":
206
271
  return await runInit(args);
@@ -215,6 +280,15 @@ export async function runCli(argv) {
215
280
  return await runBackgroundCommand(args, args.command);
216
281
  case "install-skill":
217
282
  return await runInstallSkill(args);
283
+ case "backfill":
284
+ return await runBackfill({
285
+ ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
286
+ ...(args.run === undefined ? {} : { run: args.run }),
287
+ all: args.all,
288
+ ...(args.since === undefined ? {} : { since: args.since }),
289
+ ...(args.until === undefined ? {} : { until: args.until }),
290
+ dryRun: args.dryRun,
291
+ });
218
292
  }
219
293
  }
220
294
  async function runInit(args) {
@@ -225,18 +299,19 @@ async function runInit(args) {
225
299
  ...(args.memory === undefined ? {} : { memory: args.memory }),
226
300
  });
227
301
  for (const path of result.created) {
228
- process.stdout.write(`created ${path}\n`);
302
+ process.stdout.write(`${ui.badge("ok")}${ui.style.green("created")} ${path}\n`);
229
303
  }
230
304
  for (const path of result.skipped) {
231
- process.stdout.write(`kept ${path}\n`);
305
+ process.stdout.write(ui.style.dim(` kept ${path}`) + "\n");
232
306
  }
233
307
  if (result.knowledgeFiles.length > 0) {
234
- process.stdout.write(`\nIdentity references existing knowledge: ${result.knowledgeFiles.join(", ")}\n`);
308
+ process.stdout.write(`\nIdentity references existing knowledge: ${ui.style.cyan(result.knowledgeFiles.join(", "))}\n`);
235
309
  }
236
- process.stdout.write("\nNext steps:\n" +
237
- ` 1. Edit ${result.configPath} (model, channels, skills, memory, sandbox).\n` +
238
- " 2. mono-agent validate\n" +
239
- " 3. mono-agent start\n");
310
+ process.stdout.write("\n" +
311
+ 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` +
314
+ ` ${ui.style.bold("3.")} mono-agent start\n`);
240
315
  return 0;
241
316
  }
242
317
  async function runInstallSkill(args) {
@@ -248,11 +323,11 @@ async function runInstallSkill(args) {
248
323
  });
249
324
  }
250
325
  catch (error) {
251
- process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
326
+ process.stderr.write(ui.errorLine(error instanceof Error ? error.message : String(error)));
252
327
  return 1;
253
328
  }
254
329
  for (const path of result.installed) {
255
- process.stdout.write(`installed ${path}\n`);
330
+ process.stdout.write(`${ui.badge("ok")}${ui.style.green("installed")} ${path}\n`);
256
331
  }
257
332
  return 0;
258
333
  }
@@ -264,24 +339,72 @@ async function runValidate(args) {
264
339
  configPath: resolve(cwd, args.configPath ?? "mono-agent.config.json"),
265
340
  });
266
341
  for (const section of report.sections) {
267
- process.stdout.write(`${statusIcon(section.status)} ${section.label}\n`);
268
- for (const detail of section.details) {
269
- process.stdout.write(` ${detail}\n`);
270
- }
342
+ process.stdout.write(formatSection(section));
271
343
  }
272
- process.stdout.write(report.ok ? "\nConfig is ready to start.\n" : "\nFix the errors above, then re-run mono-agent validate.\n");
344
+ process.stdout.write(report.ok
345
+ ? `\n${ui.style.green("✓ Config is ready to start.")}\n`
346
+ : `\n${ui.hint("Fix the errors above, then re-run mono-agent validate.")}`);
273
347
  return report.ok ? 0 : 1;
274
348
  }
275
- function statusIcon(status) {
276
- switch (status) {
277
- case "ok":
278
- return "[ok] ";
279
- case "waiting":
280
- return "[waiting] ";
281
- case "disabled":
282
- return "[off] ";
283
- case "error":
284
- return "[error] ";
349
+ /** Render one validation section: a status badge, a bold label, and its details. */
350
+ function formatSection(section) {
351
+ let out = `${ui.badge(section.status)}${ui.style.bold(section.label)}\n`;
352
+ for (const detail of section.details) {
353
+ out += ` ${colorDetail(section.status, detail)}\n`;
354
+ }
355
+ return out;
356
+ }
357
+ function colorDetail(status, detail) {
358
+ if (status === "error") {
359
+ return ui.style.red(detail);
360
+ }
361
+ if (detail.startsWith("[WARN]")) {
362
+ return ui.style.yellow(detail);
363
+ }
364
+ return ui.style.dim(detail);
365
+ }
366
+ /**
367
+ * Gate for `start`/`restart`: refuse unless the directory has a present, valid
368
+ * config. First the config FILE must exist (env vars alone are not enough — a
369
+ * folder without a config is not a configured agent). Then run the structural
370
+ * validation with `liveness:false` (network probes only yield `waiting`, never
371
+ * `error`, so skipping them keeps the verdict but avoids ~6s of timeouts) and
372
+ * refuse on any `error` section. `waiting` (e.g. Ollama/Phoenix not up yet) is
373
+ * runtime-soft and never blocks.
374
+ */
375
+ export async function ensureStartable(args) {
376
+ const cwd = process.cwd();
377
+ const configPath = resolve(cwd, args.configPath ?? "mono-agent.config.json");
378
+ if (!(await pathExists(configPath))) {
379
+ return { ok: false, code: 2, kind: "missing-config", configPath };
380
+ }
381
+ const report = await validateMonoAgentFolder({ env: process.env, cwd, configPath, liveness: false });
382
+ if (!report.ok) {
383
+ return { ok: false, code: 1, kind: "validation", report };
384
+ }
385
+ return { ok: true };
386
+ }
387
+ function printPreflightFailure(result) {
388
+ if (result.kind === "missing-config") {
389
+ process.stderr.write(ui.errorLine(`No mono-agent config found at ${result.configPath}.`));
390
+ process.stderr.write(ui.hint("Run `mono-agent init` to scaffold one, or pass --config <path>."));
391
+ return;
392
+ }
393
+ process.stderr.write(ui.heading("Cannot start: config has errors"));
394
+ for (const section of result.report.sections) {
395
+ if (section.status === "error") {
396
+ process.stderr.write(formatSection(section));
397
+ }
398
+ }
399
+ process.stderr.write(ui.hint("Run `mono-agent validate` for the full report, fix the errors, then retry."));
400
+ }
401
+ async function pathExists(path) {
402
+ try {
403
+ await stat(path);
404
+ return true;
405
+ }
406
+ catch {
407
+ return false;
285
408
  }
286
409
  }
287
410
  async function runStart(args) {
@@ -292,33 +415,47 @@ async function runStart(args) {
292
415
  }
293
416
  /**
294
417
  * The blocking worker: builds the responder, starts every configured channel
295
- * plus the operator console and traceability, and stays alive until a signal.
296
- * This is what launchd invokes (via `start --foreground`) and what users get
297
- * with `--foreground`/`-f`.
418
+ * plus traceability, and stays alive until a signal. This is what launchd
419
+ * invokes (via `start --foreground`) and what users get with `--foreground`/`-f`.
298
420
  */
299
421
  async function runForeground(args) {
422
+ const pre = await ensureStartable(args);
423
+ if (!pre.ok) {
424
+ printPreflightFailure(pre);
425
+ return pre.code;
426
+ }
300
427
  const app = await startMonoAgentApp({
301
428
  cwd: process.cwd(),
302
429
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
303
- ...(args.port === undefined ? {} : { operatorConsolePort: args.port }),
304
- ...(args.noConsole ? { operatorConsole: false } : {}),
305
430
  logger: consoleLogger(),
306
431
  });
307
432
  printAppStatus(app);
308
- installSignalHandlers(app);
309
- return 0;
433
+ // Block until a shutdown signal. Returning here (the old behavior) let the
434
+ // process exit immediately whenever no channel owned a live handle — e.g. a
435
+ // traceability-only config, now that the operator console is retired and the
436
+ // trace heartbeat timer is unref'd.
437
+ return await waitForShutdownSignal(app);
310
438
  }
311
439
  async function runBackgroundCommand(args, command) {
312
440
  const guard = requireDarwin(command);
313
441
  if (guard !== undefined) {
314
442
  return guard;
315
443
  }
444
+ // Refuse to launch (or relaunch) an unconfigured/broken folder BEFORE writing
445
+ // the plist and bootstrapping launchctl — otherwise the worker would crash and
446
+ // launchd's KeepAlive would retry it forever. stop/status/logs stay ungated so
447
+ // a broken instance can still be inspected and torn down.
448
+ if (command === "start" || command === "restart") {
449
+ const pre = await ensureStartable(args);
450
+ if (!pre.ok) {
451
+ printPreflightFailure(pre);
452
+ return pre.code;
453
+ }
454
+ }
316
455
  const target = await resolveInstanceTarget({
317
456
  args: {
318
457
  ...(args.configPath === undefined ? {} : { configPath: args.configPath }),
319
458
  ...(args.envFile === undefined ? {} : { envFile: args.envFile }),
320
- ...(args.port === undefined ? {} : { port: args.port }),
321
- noConsole: args.noConsole,
322
459
  },
323
460
  env: process.env,
324
461
  cwd: process.cwd(),
@@ -329,7 +466,7 @@ async function runBackgroundCommand(args, command) {
329
466
  case "start":
330
467
  return await startBackground(target, deps);
331
468
  case "restart":
332
- return await restartBackground(target, deps);
469
+ return args.force ? await runForceRestart(target, deps) : await restartBackground(target, deps);
333
470
  case "stop":
334
471
  return await stopBackground(target, deps);
335
472
  case "status":
@@ -338,6 +475,27 @@ async function runBackgroundCommand(args, command) {
338
475
  return await tailLogs(target, deps, { follow: args.follow, lines: args.lines ?? DEFAULT_LOG_LINES });
339
476
  }
340
477
  }
478
+ /**
479
+ * `restart --force`: stop the worker, purge its persisted pi-session store, then
480
+ * start fresh. Stopping first guarantees the worker is not writing sessions while
481
+ * they are deleted; the runtime recreates the store on the next session, and the
482
+ * agent's durable memory lives elsewhere, so only resumable transcripts are dropped.
483
+ */
484
+ async function runForceRestart(target, deps) {
485
+ const stopCode = await stopBackground(target, deps);
486
+ if (stopCode !== 0) {
487
+ return stopCode;
488
+ }
489
+ const result = await purgeSessions({ env: process.env, cwd: target.cwd, configPath: target.configPath });
490
+ if (result.removed) {
491
+ const count = result.files === 0 ? "" : ` (${result.files} session file${result.files === 1 ? "" : "s"})`;
492
+ process.stdout.write(`${ui.badge("ok")}${ui.style.bold("Cleared persisted sessions")}${count}.\n`);
493
+ }
494
+ else {
495
+ process.stdout.write(ui.style.dim("No persisted sessions to clear (in-memory or none on disk).") + "\n");
496
+ }
497
+ return await startBackground(target, deps);
498
+ }
341
499
  /**
342
500
  * Background service mode is launchd-specific. On other platforms point the
343
501
  * user at the still-supported blocking foreground path.
@@ -346,22 +504,53 @@ function requireDarwin(command) {
346
504
  if (process.platform === "darwin") {
347
505
  return undefined;
348
506
  }
349
- process.stderr.write(`Background service mode (mono-agent ${command}) requires macOS (launchd).\n` +
350
- "Run `mono-agent start --foreground` to run in the foreground on this platform.\n");
507
+ process.stderr.write(ui.errorLine(`Background service mode (mono-agent ${command}) requires macOS (launchd).`));
508
+ process.stderr.write(ui.hint("Run `mono-agent start --foreground` to run in the foreground on this platform."));
351
509
  return 1;
352
510
  }
353
- function printAppStatus(app) {
354
- if (app.operatorConsole !== undefined) {
355
- process.stdout.write(`operator console ${app.operatorConsole.appUrl}\n`);
356
- }
357
- process.stdout.write(`config ${app.configPath}\n`);
511
+ export function printAppStatus(app) {
358
512
  const trace = app.traceabilityStatus;
359
- process.stdout.write(trace.kind === "running"
360
- ? `traceability running (source ${trace.sourceId})\n`
361
- : `traceability ${trace.kind}: ${trace.reason}\n`);
362
- for (const [id, status] of app.channelStatuses()) {
363
- process.stdout.write(`${id.padEnd(17)} ${describeChannelStatus(status)}\n`);
513
+ process.stdout.write(ui.rule("instance"));
514
+ process.stdout.write(ui.keyValue([
515
+ ["config", app.configPath],
516
+ [
517
+ "traceability",
518
+ trace.kind === "running" ? `running (source ${trace.sourceId})` : `${trace.kind}: ${trace.reason}`,
519
+ ],
520
+ ], 2));
521
+ const artifactDir = app.traceabilityStatus.kind === "running" ? app.traceabilityStatus.artifactDir : undefined;
522
+ process.stdout.write(ui.rule("observability"));
523
+ process.stdout.write(` ${describeExporter(app.exporterStatus, artifactDir)}\n`);
524
+ const channels = [...app.channelStatuses()];
525
+ if (channels.length > 0) {
526
+ process.stdout.write(ui.rule("channels"));
527
+ for (const [id, status] of channels) {
528
+ process.stdout.write(` ${ui.channelBadge(status.kind)}${ui.style.bold(id.padEnd(11))} ${describeChannelStatus(status)}\n`);
529
+ }
530
+ }
531
+ }
532
+ function describeExporter(status, artifactDir) {
533
+ if (status.kind !== "configured") {
534
+ return `${status.kind}: ${status.reason}`;
535
+ }
536
+ const parts = [`phoenix ${status.endpoint}`];
537
+ const appUrl = phoenixAppBaseUrl(status.endpoint);
538
+ if (appUrl !== undefined) {
539
+ parts.push(`app ${appUrl}`);
540
+ }
541
+ if (status.includeSensitiveData) {
542
+ parts.push("includeSensitiveData=true");
364
543
  }
544
+ if (status.lastWarning !== undefined) {
545
+ parts.push(`last warning: ${status.lastWarning}`);
546
+ }
547
+ if (status.lastError !== undefined) {
548
+ parts.push(`last error: ${status.lastError}`);
549
+ }
550
+ parts.push(artifactDir === undefined
551
+ ? "JSONL artifacts remain local"
552
+ : `JSONL artifacts remain local at ${artifactDir}`);
553
+ return parts.join("; ");
365
554
  }
366
555
  function describeChannelStatus(status) {
367
556
  if (status.kind === "running") {
@@ -372,19 +561,34 @@ function describeChannelStatus(status) {
372
561
  }
373
562
  return `${status.kind}: ${status.reason}`;
374
563
  }
375
- function installSignalHandlers(app) {
376
- let stopping = false;
377
- const stop = async (signal) => {
378
- if (stopping) {
379
- return;
380
- }
381
- stopping = true;
382
- process.stdout.write(`\nReceived ${signal}; stopping mono agent app...\n`);
383
- await app.stop();
384
- process.exit(0);
385
- };
386
- process.on("SIGINT", () => void stop("SIGINT"));
387
- process.on("SIGTERM", () => void stop("SIGTERM"));
564
+ /**
565
+ * Block the foreground process until SIGINT/SIGTERM, then stop the app and
566
+ * resolve the exit code. A referenced no-op timer owns the event loop so the
567
+ * process stays alive even with no channel handle (signal listeners alone do
568
+ * NOT keep Node running, and the trace heartbeat is unref'd). Cleared on stop so
569
+ * the loop drains cleanly without a forceful `process.exit`. Exported for tests.
570
+ */
571
+ export function waitForShutdownSignal(app) {
572
+ return new Promise((resolve) => {
573
+ const keepAlive = setInterval(() => { }, KEEP_ALIVE_INTERVAL_MS);
574
+ let stopping = false;
575
+ const onSignal = (signal) => {
576
+ if (stopping) {
577
+ return;
578
+ }
579
+ stopping = true;
580
+ process.off("SIGINT", onSignal);
581
+ process.off("SIGTERM", onSignal);
582
+ clearInterval(keepAlive);
583
+ void (async () => {
584
+ process.stdout.write("\n" + ui.hint(`Received ${signal}; stopping mono agent app…`));
585
+ await app.stop();
586
+ resolve(0);
587
+ })();
588
+ };
589
+ process.on("SIGINT", onSignal);
590
+ process.on("SIGTERM", onSignal);
591
+ });
388
592
  }
389
593
  function consoleLogger() {
390
594
  return {
@@ -409,7 +613,7 @@ if (isDirectCliInvocation) {
409
613
  }
410
614
  })
411
615
  .catch((error) => {
412
- process.stderr.write(`${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
616
+ process.stderr.write(`${ui.style.red("✗")} ${error instanceof Error ? error.stack ?? error.message : String(error)}\n`);
413
617
  process.exitCode = 1;
414
618
  });
415
619
  }