@monster-orange/den 0.4.7 → 0.4.9

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 (44) hide show
  1. package/CHANGELOG.md +174 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +243 -1
  4. package/package.json +1 -1
  5. package/schemas/den.plugin.v1.json +3 -2
  6. package/src/shared/agents/den-claude.ts +57 -3
  7. package/src/shared/agents/guardrails/hooks.json +23 -0
  8. package/src/shared/agents/guardrails/instructions.md +23 -0
  9. package/src/shared/agents/guardrails/rotation-hook.ts +127 -0
  10. package/src/shared/agents/guardrails/stop-hook.ts +48 -2
  11. package/src/shared/agents/guardrails.ts +38 -7
  12. package/src/shared/agents/handoff.ts +118 -0
  13. package/src/shared/agents/personas.ts +11 -1
  14. package/src/shared/agents/records.ts +122 -1
  15. package/src/shared/agents/rotation.ts +362 -0
  16. package/src/shared/agents/runtime.ts +27 -0
  17. package/src/shared/den-config.ts +72 -1
  18. package/src/shared/git/gh-pr-state.ts +32 -0
  19. package/src/shared/plugins/manifest.ts +3 -0
  20. package/src/shared/usage/paths.ts +16 -0
  21. package/src/shared/usage/report.ts +377 -0
  22. package/src/shared/usage/sources.ts +127 -0
  23. package/src/shared/usage/transcript.ts +319 -0
  24. package/src/ui/cli/agent-ops.ts +678 -51
  25. package/src/ui/cli/agents-command.ts +192 -22
  26. package/src/ui/cli/index.ts +13 -1
  27. package/src/ui/cli/usage-command.ts +323 -0
  28. package/src/ui/mcp/api.ts +10 -1
  29. package/src/ui/mcp/server.ts +34 -0
  30. package/src/ui/mcp/tools.ts +50 -0
  31. package/src/ui/server/agent-resume-grant.ts +53 -0
  32. package/src/ui/server/agent-routes.ts +247 -9
  33. package/src/ui/server/audit.ts +68 -1
  34. package/src/ui/server/index.ts +16 -5
  35. package/src/ui/server/public/agents-page.js +400 -13
  36. package/src/ui/server/public/agents-section.js +17 -3
  37. package/src/ui/server/public/agents.html +44 -0
  38. package/src/ui/server/public/i18n/es.js +40 -0
  39. package/src/ui/server/public/i18n/ja.js +36 -0
  40. package/src/ui/server/public/shell.js +79 -0
  41. package/src/ui/server/request-guard.ts +21 -1
  42. package/src/ui/server/route-classes.ts +29 -0
  43. package/src/ui/server/session-routes.ts +60 -5
  44. package/src/ui/server/sudo-store.ts +36 -7
@@ -20,10 +20,17 @@
20
20
  * persona, runtime and model; a record whose session is gone is
21
21
  * listed as stopped or ended. A plain session is not an agent.
22
22
  * stop ends its session (TERM) and marks the record stopped.
23
- * resume only when its runtime declares resume and its session is not
24
- * running: a new session, in the same cwd, continuing the last run;
23
+ * resume only when its runtime declares resume, its record holds the
24
+ * runtime's resume token, and its session is not running: a new
25
+ * session, in the same cwd and workspace with the grants it had,
26
+ * continuing the last conversation (den-claude: --resume <token>);
25
27
  * when that session was never prompted, a fresh start under the
26
28
  * same conversation id (there is nothing to continue).
29
+ * --all-interrupted (AG-14.3): every agent interrupted when Den or
30
+ * the machine restarted that can resume.
31
+ * rm (AG-14.1b) forgets a stopped, interrupted or exited agent (never
32
+ * a running one); --stopped forgets every one of -w <ws> (or of
33
+ * every workspace). The runtime's own history stays.
27
34
  * doctor (AG-0.3, AG-0.5) each runtime of the catalog, installed
28
35
  * first: its status, the CLI it runs and where that came from
29
36
  * (workspace or user.yml setting, login shell, PATH, install
@@ -36,6 +43,14 @@
36
43
  * handback-check <file|-> (den#5) the hand-back check (handback.ts) on a
37
44
  * file or stdin: each problem on its own line, exit 1 when there
38
45
  * is one, 0 when the hand-back is complete and plain.
46
+ * relaunch (den#4, session rotation) once an agent's session rotated
47
+ * (after agents.rotate.after_compactions compactions, or its pull
48
+ * request merged) and its handoff.md passes the check: a fresh
49
+ * conversation, same persona, workspace, folder and grants, whose
50
+ * first prompt is the handoff. `resume` on a rotated agent
51
+ * refuses (exit 1) and names relaunch.
52
+ * handoff-check <id|file|-> (den#4) the handoff check (handoff.ts), like
53
+ * handback-check.
39
54
  *
40
55
  * Nothing here takes a grant: grants come from the org config only. The
41
56
  * host enforces agents.max_concurrent on every start, counting agents only.
@@ -45,14 +60,25 @@ import { DEFAULT_RUNTIME_ID } from "../../shared/agents/runtimes.js";
45
60
  import { initLoginPath } from "../../shared/agents/runtime-search.js";
46
61
  import { HostError } from "../../shared/sessions/client.js";
47
62
  import { checkHandback } from "../../shared/agents/handback.js";
63
+ import { checkHandoff } from "../../shared/agents/handoff.js";
64
+ import { handoffFile, reasonText } from "../../shared/agents/rotation.js";
65
+ import {
66
+ agentFilesDir,
67
+ listRecords,
68
+ readRecord,
69
+ } from "../../shared/agents/records.js";
48
70
  import {
49
71
  AgentError,
50
72
  listAgents,
51
73
  listPersonas as personaList,
52
74
  listRuntimes,
53
75
  loadAgentWorkspaces,
76
+ relaunchAgent,
77
+ removeAgent,
78
+ removeStopped,
54
79
  resolveAgentsDeps,
55
80
  resumeAgent,
81
+ resumeInterrupted,
56
82
  startAgent,
57
83
  stopAgent,
58
84
  type AgentsDeps,
@@ -80,19 +106,37 @@ export {
80
106
  } from "./agent-ops.js";
81
107
 
82
108
  export const AGENTS_HELP = {
83
- usage: "agents <list|start|stop|resume|personas|doctor|handback-check> ...",
109
+ usage:
110
+ "agents <list|start|stop|resume|relaunch|rm|personas|doctor|handback-check|handoff-check> ...",
84
111
  lines: [
85
112
  "Agents: a persona and a workspace, run by a runtime in a session that",
86
113
  "outlives the web server (see `{cli} sessions`).",
87
114
  "",
88
- "{cli} agents list -w <ws> [--json]",
115
+ "{cli} agents list -w <ws> [--stopped] [--json]",
116
+ " --stopped: only the stopped, interrupted and exited ones",
89
117
  "{cli} agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]",
90
118
  " runs in the workspace's dir as <dist>-<ws>:<id>; its MCP is pinned to <ws>",
91
119
  " (plus agents.<id>.grants); capped by agents.max_concurrent",
92
- "{cli} agents stop <id> [-w <ws>]",
120
+ "{cli} agents stop <id> [-w <ws>] [--no-wait]",
121
+ " waits up to 5 s for the session to end (else: still stopping, exit 1);",
122
+ " --no-wait: return once TERM is sent",
93
123
  "{cli} agents resume <id> [-w <ws>]",
94
- " only when its runtime declares resume and its session has ended;",
95
- " a session never prompted starts fresh under the same conversation id",
124
+ "{cli} agents resume --all-interrupted [-w <ws>]",
125
+ " the same conversation, persona, workspace and folder, when its runtime",
126
+ " can resume one and its session has ended; with nothing to resume yet,",
127
+ " it starts fresh under the same conversation id. --all-interrupted: every",
128
+ " agent interrupted when Den or the machine restarted. A rotated agent",
129
+ " is not resumed: relaunch it",
130
+ "{cli} agents relaunch <id> [-w <ws>]",
131
+ " session rotation: once a session ended after agents.rotate.after_compactions",
132
+ " compactions (default 3) or its pull request merged, and its handoff.md",
133
+ " passes the check, a fresh conversation (same persona, workspace, folder,",
134
+ " grants) whose first prompt is the handoff",
135
+ "{cli} agents rm <id> [-w <ws>]",
136
+ "{cli} agents rm --stopped [-w <ws>]",
137
+ " forget a stopped, interrupted or exited agent (never a running one);",
138
+ " the runtime keeps its own history. agents.remove_stopped_after in",
139
+ " user.yml (never, 1d, 7d) forgets stopped ones on its own",
96
140
  "{cli} agents personas [--json]",
97
141
  " Den's default pack, then agents.persona_packs (a later pack wins by id)",
98
142
  "{cli} agents doctor [-w <ws>] [--json]",
@@ -103,6 +147,10 @@ export const AGENTS_HELP = {
103
147
  " checks a hand-back to a person: URL, machine, branch, what changed, what",
104
148
  " to click and decisions are all there, every ID is explained, and each",
105
149
  " decision is a numbered yes/no question; exit 1 with what to fix",
150
+ "{cli} agents handoff-check <id|file|-> [--json]",
151
+ " checks an agent's handoff.md (or a file, or stdin): Objective, State, Open items, How to QA and Next step",
152
+ " are all there and filled in, and every ID is explained; exit 1 with what",
153
+ " to fix",
106
154
  "",
107
155
  "Runtimes: den-claude (default; Claude Code's claude), codex (OpenAI's Codex",
108
156
  "CLI), gemini (Google's Gemini CLI), grok (xAI's Grok Build), and generic (the",
@@ -120,17 +168,27 @@ export const AGENTS_HELP = {
120
168
 
121
169
  async function list(args: string[], d: AgentsResolved): Promise<number> {
122
170
  const where = "agents list";
123
- const p = parseArgs(where, args, { "--workspace": true, "--json": false });
171
+ const p = parseArgs(where, args, {
172
+ "--workspace": true,
173
+ "--json": false,
174
+ "--stopped": false,
175
+ });
124
176
  noExtra(where, p);
125
177
  const ws = workspaceFlag(where, p);
126
178
  if (!ws) throw new UsageError(`${where} needs -w <workspace>`);
127
- const rows = await listAgents(d, ws);
179
+ const rows = await listAgents(d, ws, {
180
+ stopped: p.flags["--stopped"] === true,
181
+ });
128
182
  if (p.flags["--json"]) {
129
183
  d.out(JSON.stringify(rows, null, 2));
130
184
  return 0;
131
185
  }
132
186
  if (!rows.length) {
133
- d.out(`no agents in ${ws}`);
187
+ d.out(
188
+ p.flags["--stopped"]
189
+ ? `no stopped agents in ${ws}`
190
+ : `no agents in ${ws}`,
191
+ );
134
192
  return 0;
135
193
  }
136
194
  for (const l of columns([
@@ -148,7 +206,9 @@ async function list(args: string[], d: AgentsResolved): Promise<number> {
148
206
  r.agent,
149
207
  r.persona ?? "-",
150
208
  r.runtime ?? "-",
151
- r.state,
209
+ r.rotation?.due
210
+ ? `${r.state}, ${r.rotation.ready ? "ready to relaunch" : "handoff due"} (${reasonText(r.rotation.due, r.rotation.compactions)})`
211
+ : r.state,
152
212
  r.model ?? "-",
153
213
  r.session && !r.session.exited ? String(r.session.pid) : "-",
154
214
  r.session?.startedAt ?? "-",
@@ -232,22 +292,40 @@ function idAndWorkspace(
232
292
  }
233
293
 
234
294
  async function stop(args: string[], d: AgentsResolved): Promise<number> {
235
- const { id, ws } = idAndWorkspace("agents stop", args);
236
- const { rec, session: s } = await stopAgent(d, id, ws);
295
+ // --no-wait: return once TERM is sent (before 0.4.8, always).
296
+ const wait = !args.includes("--no-wait");
297
+ const { id, ws } = idAndWorkspace(
298
+ "agents stop",
299
+ args.filter((a) => a !== "--no-wait"),
300
+ );
301
+ const {
302
+ rec,
303
+ session: s,
304
+ stillStopping,
305
+ } = await stopAgent(d, id, ws, {
306
+ wait,
307
+ });
308
+ if (stillStopping) {
309
+ d.err(
310
+ `${rec.id} is still stopping (sent TERM to ${s!.name}, pid ${s!.pid}; not ended after ${d.stopWaitMs / 1000} s)`,
311
+ );
312
+ return 1;
313
+ }
237
314
  d.out(
238
315
  s
239
- ? `stopped agent ${rec.id} (sent TERM to ${s.name}, pid ${s.pid})`
316
+ ? `stopped agent ${rec.id} (sent TERM to ${s.name}, pid ${s.pid}${wait && d.stopWaitMs > 0 ? "; it has ended" : ""})`
240
317
  : `agent ${rec.id} was not running; marked stopped`,
241
318
  );
242
319
  return 0;
243
320
  }
244
321
 
245
322
  async function resume(args: string[], d: AgentsResolved): Promise<number> {
323
+ if (args.includes("--all-interrupted")) return resumeAll(args, d);
246
324
  const { id, ws } = idAndWorkspace("agents resume", args);
247
325
  const s = await resumeAgent(d, id, ws);
248
326
  if (s.fresh)
249
327
  d.out(
250
- `agent ${s.rec.id}: starting fresh: it was never prompted, so there is no conversation to resume`,
328
+ `agent ${s.rec.id}: nothing to resume yet; started fresh (same conversation id, folder and scope)`,
251
329
  );
252
330
  return report(
253
331
  d,
@@ -256,6 +334,69 @@ async function resume(args: string[], d: AgentsResolved): Promise<number> {
256
334
  );
257
335
  }
258
336
 
337
+ /** `agents relaunch <id> [-w <ws>]` (den#4). */
338
+ async function relaunch(args: string[], d: AgentsResolved): Promise<number> {
339
+ const { id, ws } = idAndWorkspace("agents relaunch", args);
340
+ const s = await relaunchAgent(d, id, ws);
341
+ return report(
342
+ d,
343
+ s,
344
+ `relaunched agent ${s.rec.id} from its handoff (the last session ended after ${s.why}) as ${s.rec.sessionName} in ${s.rec.cwd}`,
345
+ );
346
+ }
347
+
348
+ /** `agents resume --all-interrupted [-w <ws>]` (AG-14.3). */
349
+ async function resumeAll(args: string[], d: AgentsResolved): Promise<number> {
350
+ const where = "agents resume";
351
+ const p = parseArgs(where, args, {
352
+ "--workspace": true,
353
+ "--all-interrupted": false,
354
+ });
355
+ noExtra(where, p);
356
+ const ws = workspaceFlag(where, p);
357
+ const r = await resumeInterrupted(d, ws ? [ws] : undefined);
358
+ if (!r.resumed.length && !r.failed.length) {
359
+ d.out(`no interrupted agents${ws ? ` in ${ws}` : ""}`);
360
+ return 0;
361
+ }
362
+ for (const x of r.resumed)
363
+ d.out(
364
+ `${x.fresh ? "started" : "resumed"} agent ${x.rec.id} as ${x.rec.sessionName} in ${x.rec.cwd}`,
365
+ );
366
+ for (const f of r.failed) d.err(`✗ agent ${f.agent}: ${f.why}`);
367
+ return r.failed.length ? 1 : 0;
368
+ }
369
+
370
+ /** `agents rm <id> [-w <ws>]`, `agents rm --stopped [-w <ws>]` (AG-14.1b). */
371
+ async function rm(args: string[], d: AgentsResolved): Promise<number> {
372
+ const where = "agents rm";
373
+ const p = parseArgs(where, args, { "--workspace": true, "--stopped": false });
374
+ noExtra(where, p, 1);
375
+ const ws = workspaceFlag(where, p);
376
+ const id = p.positional[0];
377
+ if (p.flags["--stopped"]) {
378
+ if (id) throw new UsageError(`${where}: give <id> or --stopped, not both`);
379
+ if (ws && !d.workspaceExists(ws))
380
+ throw new UsageError(`${where}: workspace "${ws}" does not exist`);
381
+ const wss = ws
382
+ ? [ws]
383
+ : [...new Set(listRecords(d.stateDir()).map((r) => r.workspace))];
384
+ const gone = await removeStopped(d, wss);
385
+ d.out(
386
+ gone.length
387
+ ? `removed ${gone.length} stopped ${gone.length === 1 ? "agent" : "agents"}: ${gone.map((g) => g.agent).join(", ")}`
388
+ : `no stopped agents${ws ? ` in ${ws}` : ""}`,
389
+ );
390
+ return 0;
391
+ }
392
+ if (!id) throw new UsageError(`${where} needs <id> or --stopped`);
393
+ const [gone] = await removeAgent(d, id, ws);
394
+ d.out(
395
+ `removed agent ${gone?.agent ?? id}; its conversation stays in the runtime's own history`,
396
+ );
397
+ return 0;
398
+ }
399
+
259
400
  async function listPersonas(
260
401
  args: string[],
261
402
  d: AgentsResolved,
@@ -296,17 +437,37 @@ async function readFileOrStdin(src: string): Promise<string> {
296
437
  return text;
297
438
  }
298
439
 
299
- /** `agents handback-check <file|-> [--json]`. */
440
+ /** `agents handback-check <file|-> [--json]`, or handoff-check (den#4). */
300
441
  async function handbackCheck(
301
442
  args: string[],
302
443
  d: AgentsResolved,
303
444
  deps: AgentsDoctorDeps,
445
+ kind: "handback" | "handoff" = "handback",
304
446
  ): Promise<number> {
305
- const where = "agents handback-check";
447
+ const where = `agents ${kind}-check`;
306
448
  const p = parseArgs(where, args, { "--json": false });
307
449
  if (p.positional.length !== 1 || p.rest)
308
- throw new UsageError(`${where} takes one file, or - for stdin`);
309
- const src = p.positional[0];
450
+ throw new UsageError(
451
+ kind === "handoff"
452
+ ? `${where} takes an agent id, a file, or - for stdin`
453
+ : `${where} takes one file, or - for stdin`,
454
+ );
455
+ let src = p.positional[0];
456
+ // handoff-check <id>: that agent's handoff.md, when no such file is here.
457
+ if (
458
+ kind === "handoff" &&
459
+ src !== "-" &&
460
+ !fs.existsSync(src) &&
461
+ /^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(src) &&
462
+ readRecord(d.stateDir(), src)
463
+ ) {
464
+ const file = handoffFile(agentFilesDir(d.stateDir(), src));
465
+ if (!fs.existsSync(file)) {
466
+ d.err(`✗ ${where}: no handoff.md yet for agent ${src} (${file})`);
467
+ return 1;
468
+ }
469
+ src = file;
470
+ }
310
471
  let text: string;
311
472
  try {
312
473
  text = await (deps.readHandback ?? readFileOrStdin)(src);
@@ -316,11 +477,12 @@ async function handbackCheck(
316
477
  );
317
478
  return 1;
318
479
  }
319
- const r = checkHandback(text);
480
+ const r = kind === "handoff" ? checkHandoff(text) : checkHandback(text);
481
+ const what = kind === "handoff" ? "handoff" : "hand-back";
320
482
  if (p.flags["--json"]) d.out(JSON.stringify(r, null, 2));
321
- else if (r.ok) d.out("✓ the hand-back is complete and plain");
483
+ else if (r.ok) d.out(`✓ the ${what} is complete and plain`);
322
484
  else {
323
- d.out("✗ the hand-back is not ready:");
485
+ d.out(`✗ the ${what} is not ready:`);
324
486
  for (const l of r.problems) d.out(` - ${l}`);
325
487
  }
326
488
  return r.ok ? 0 : 1;
@@ -441,6 +603,12 @@ export async function cmdAgents(
441
603
  case "resume":
442
604
  await loadAgentWorkspaces(deps);
443
605
  return await resume(rest, d);
606
+ case "relaunch":
607
+ await loadAgentWorkspaces(deps);
608
+ return await relaunch(rest, d);
609
+ case "rm":
610
+ await loadAgentWorkspaces(deps);
611
+ return await rm(rest, d);
444
612
  case "personas":
445
613
  return await listPersonas(rest, d);
446
614
  case "doctor":
@@ -449,6 +617,8 @@ export async function cmdAgents(
449
617
  return await doctor(rest, d, deps);
450
618
  case "handback-check":
451
619
  return await handbackCheck(rest, d, deps);
620
+ case "handoff-check":
621
+ return await handbackCheck(rest, d, deps, "handoff");
452
622
  default:
453
623
  throw new UsageError(`unknown agents command "${sub}"`);
454
624
  }
@@ -196,6 +196,7 @@ import { cmdEdge } from "./edge-command.js";
196
196
  import { cmdDoctor } from "./doctor-command.js";
197
197
  import { cmdSessions, SESSIONS_HELP } from "./sessions-command.js";
198
198
  import { AGENTS_HELP, cmdAgents } from "./agents-command.js";
199
+ import { USAGE_HELP, cmdUsage } from "./usage-command.js";
199
200
  import { cmdService } from "./service-command.js";
200
201
  import { cmdStack } from "./stack-command.js";
201
202
  import { cmdTheme } from "./theme-command.js";
@@ -536,6 +537,7 @@ const COMMAND_HELP: Record<string, { usage: string; lines: string[] }> = {
536
537
  },
537
538
  sessions: SESSIONS_HELP,
538
539
  agents: AGENTS_HELP,
540
+ usage: USAGE_HELP,
539
541
  };
540
542
 
541
543
  function resolveCliBaseApiUrl(explicitBaseApiUrl?: string): string | undefined {
@@ -588,6 +590,7 @@ const TELEMETRY_COMMANDS = new Set<string>([
588
590
  "mcp",
589
591
  "sessions",
590
592
  "agents",
593
+ "usage",
591
594
  "service",
592
595
  "theme",
593
596
  "plugin",
@@ -616,6 +619,8 @@ const CONFIG_FREE_COMMANDS = new Set<string>([
616
619
  // listing a session is never stranded by a config that fails (AG-3a).
617
620
  "sessions",
618
621
  "agents",
622
+ // den#6: reads local transcripts and agent records, no org data.
623
+ "usage",
619
624
  ]);
620
625
 
621
626
  function configCheckedCommand(command: string): boolean {
@@ -1082,6 +1087,10 @@ class DenCLI {
1082
1087
  process.exitCode = await cmdAgents(args.slice(1));
1083
1088
  break;
1084
1089
 
1090
+ case "usage":
1091
+ process.exitCode = await cmdUsage(args.slice(1));
1092
+ break;
1093
+
1085
1094
  case "mcp": {
1086
1095
  // Setup subcommands: register the server with agents.
1087
1096
  const { MCP_CONNECT_SUBCOMMANDS, runMcpConnect } =
@@ -5298,7 +5307,10 @@ class DenCLI {
5298
5307
  );
5299
5308
  console.log(" agents list -w <ws> The workspace's agents");
5300
5309
  console.log(
5301
- " agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, personas)",
5310
+ " agents start -w <ws> --persona <p> Run a persona in the workspace (stop, resume, rm, personas)",
5311
+ );
5312
+ console.log(
5313
+ " usage agents|report Agents' tokens and outcomes, from local transcripts (never sent)",
5302
5314
  );
5303
5315
  console.log();
5304
5316
  console.log(