@monster-orange/den 0.4.11 → 0.4.13

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 (50) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +212 -12
  4. package/package.json +2 -1
  5. package/src/shared/agents/catalog.ts +77 -0
  6. package/src/shared/agents/default-pack/be-dev.md +1 -0
  7. package/src/shared/agents/default-pack/devops.md +1 -0
  8. package/src/shared/agents/default-pack/fe-dev.md +1 -0
  9. package/src/shared/agents/default-pack/orchestrator.md +3 -1
  10. package/src/shared/agents/default-pack/planner.md +1 -0
  11. package/src/shared/agents/default-pack/reviewer.md +1 -0
  12. package/src/shared/agents/default-pack/tester.md +16 -0
  13. package/src/shared/agents/default-pack/ux-designer.md +1 -0
  14. package/src/shared/agents/den-claude.ts +151 -17
  15. package/src/shared/agents/permissions.ts +108 -0
  16. package/src/shared/agents/persona-store.ts +286 -0
  17. package/src/shared/agents/personas.ts +116 -12
  18. package/src/shared/agents/records.ts +57 -0
  19. package/src/shared/agents/roles.ts +288 -0
  20. package/src/shared/agents/runtime.ts +49 -0
  21. package/src/shared/agents/session-name.ts +110 -0
  22. package/src/shared/agents/transcript-follow.ts +180 -0
  23. package/src/shared/den-config.ts +27 -1
  24. package/src/ui/cli/agent-ops.ts +578 -11
  25. package/src/ui/cli/agents-command.ts +12 -2
  26. package/src/ui/cli/env-probe.ts +4 -1
  27. package/src/ui/mcp/tools.ts +1 -1
  28. package/src/ui/server/agent-paths.ts +52 -0
  29. package/src/ui/server/agent-routes.ts +329 -19
  30. package/src/ui/server/agents-layout-pref.ts +43 -0
  31. package/src/ui/server/editor-open-pref.ts +41 -0
  32. package/src/ui/server/index.ts +22 -0
  33. package/src/ui/server/public/agent-start.js +3 -2
  34. package/src/ui/server/public/agents-page.js +1037 -67
  35. package/src/ui/server/public/agents-section.js +1 -1
  36. package/src/ui/server/public/agents.html +236 -20
  37. package/src/ui/server/public/header.js +2 -0
  38. package/src/ui/server/public/i18n/es.js +139 -0
  39. package/src/ui/server/public/i18n/ja.js +139 -0
  40. package/src/ui/server/public/persona-ui.css +539 -0
  41. package/src/ui/server/public/persona-ui.js +796 -0
  42. package/src/ui/server/public/settings-page.js +4 -9
  43. package/src/ui/server/public/settings-registry.js +66 -0
  44. package/src/ui/server/public/settings.html +1 -0
  45. package/src/ui/server/public/shell.js +137 -0
  46. package/src/ui/server/public/term-input.js +129 -0
  47. package/src/ui/server/public/topbar.js +22 -4
  48. package/src/ui/server/route-classes.ts +29 -0
  49. package/src/ui/server/session-routes.ts +34 -1
  50. package/src/ui/server/ui-assets.ts +8 -0
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: be-dev
3
+ role: sde
3
4
  description: Implements back-end stories, test first, keeping contracts stable.
4
5
  context:
5
6
  [
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: devops
3
+ role: sde
3
4
  description: Builds and maintains pipelines, environments and service configuration.
4
5
  context: [the story, the workspace's service manifests, CI configuration]
5
6
  ---
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: fe-dev
3
+ role: sde
3
4
  description: Implements front-end stories, test first, to the design system.
4
5
  context:
5
6
  [
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: orchestrator
3
+ role: orchestrator
3
4
  description: Takes the next ready story, picks the persona for it and starts that agent.
4
5
  standing: true
5
6
  context: [ready work items, running agents in the workspace]
@@ -9,7 +10,8 @@ You are the orchestrator. Keep work flowing without overloading the machine
9
10
  or the people reviewing it.
10
11
 
11
12
  - Take the next ready story, in priority order. Pick the persona that fits it:
12
- fe-dev, be-dev or devops for implementation, ux-designer for design work.
13
+ fe-dev, be-dev or devops for implementation, ux-designer for design work,
14
+ tester to run the suites.
13
15
  - Start one agent per story, in its own worktree, and link it to the story.
14
16
  - Respect the workspace's concurrency cap; wait rather than queueing more.
15
17
  - When a pull request is opened, ask the reviewer for a review. Never approve
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: planner
3
+ role: planner
3
4
  description: Writes stories and their acceptance criteria for the workspace.
4
5
  standing: true
5
6
  context: [the workspace's specs and docs, open work items, the design system]
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: reviewer
3
+ role: reviewer
3
4
  description: Reviews a pull request adversarially. Never approves or merges.
4
5
  context: [the pull request and its story, the repo's conventions and tests]
5
6
  ---
@@ -0,0 +1,16 @@
1
+ ---
2
+ name: tester
3
+ role: tester
4
+ description: Runs the test suites and reports failures with repro steps.
5
+ context: [the story and its acceptance criteria, the repo's test commands]
6
+ ---
7
+
8
+ You are the tester. Find out what is broken before anyone ships it.
9
+
10
+ - Run the workspace's unit, integration and end-to-end suites, as the repo
11
+ documents them.
12
+ - For each failure, write the exact repro: the smallest failing command, what
13
+ it printed, and what you expected.
14
+ - Separate real failures from flaky ones; run a suspected flake again before
15
+ reporting it.
16
+ - Don't fix code. Report what you found to the person or agent who asked.
@@ -1,5 +1,6 @@
1
1
  ---
2
2
  name: ux-designer
3
+ role: designer
3
4
  description: Designs flows and screens with the design system, on request.
4
5
  context: [the story, the design system, existing screens of the product]
5
6
  ---
@@ -61,11 +61,26 @@ import {
61
61
  type LaunchOutput,
62
62
  type Runtime,
63
63
  type RuntimeDetect,
64
+ type RuntimeModel,
65
+ type RuntimePermissions,
64
66
  } from "./runtime.js";
67
+ import { PERMISSION_INTENTS, type PermissionIntent } from "./permissions.js";
65
68
  import { builtinEntry } from "./catalog.js";
66
69
  import { catalogFind } from "./catalog-runtime.js";
67
70
  import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
68
71
  import { claudeProjectsDir } from "../usage/paths.js";
72
+ import { cleanName, transcriptTitle } from "./transcript-follow.js";
73
+
74
+ /**
75
+ * `/rename "x"` keeps the quotes in customTitle: one matching pair around
76
+ * the whole name goes, for display only (0.4.13). Nothing inside: null.
77
+ */
78
+ export function unquoteTitle(name: string | null): string | null {
79
+ if (!name || name.length < 2) return name;
80
+ const q = name[0];
81
+ if ((q !== '"' && q !== "'") || name[name.length - 1] !== q) return name;
82
+ return cleanName(name.slice(1, -1));
83
+ }
69
84
 
70
85
  export const DEN_CLAUDE_ID = "den-claude";
71
86
  const BIN = "claude";
@@ -89,12 +104,91 @@ export const PERMISSION_MODES = [
89
104
  ] as const;
90
105
  export const DEFAULT_PERMISSION_MODE = "default";
91
106
 
92
- /** The persona's permission mode, checked; a bad one names the key. */
107
+ /**
108
+ * Den's permission intents (AG-13) as Claude Code's modes:
109
+ * https://code.claude.com/docs/en/iam (permission modes): `default` asks
110
+ * on first use of each tool, `acceptEdits` accepts file edits for the
111
+ * session, `bypassPermissions` skips every prompt.
112
+ */
113
+ export const INTENT_MODE: Record<PermissionIntent, string> = {
114
+ ask: "default",
115
+ edits: "acceptEdits",
116
+ all: "bypassPermissions",
117
+ };
118
+
119
+ /**
120
+ * One plain command prefix as Claude Code allow rules (the same docs,
121
+ * Bash rules): the command alone, and the command with arguments (`*`
122
+ * matches any run of characters). Deny rules (Den's state dir) still win:
123
+ * Claude Code checks deny before allow.
124
+ */
125
+ export const COMMAND_RULES = ["Bash({c})", "Bash({c} *)"];
126
+
127
+ /** How den-claude expresses permission intents (AG-13.2). */
128
+ export const DEN_CLAUDE_PERMISSIONS: RuntimePermissions = {
129
+ modes: {
130
+ ask: "--permission-mode default: asks before edits and commands",
131
+ edits:
132
+ "--permission-mode acceptEdits: file edits run without asking; commands ask",
133
+ all: "--permission-mode bypassPermissions: nothing asks",
134
+ },
135
+ commandRules: COMMAND_RULES,
136
+ };
137
+
138
+ /** A command prefix's allow rules (permissions.ts checked it). */
139
+ export function allowRules(commands: readonly string[]): string[] {
140
+ return commands.flatMap((c) =>
141
+ COMMAND_RULES.map((r) => r.split("{c}").join(c)),
142
+ );
143
+ }
144
+
145
+ /**
146
+ * Claude Code's models (AG-12.3): the aliases `claude --model` takes,
147
+ * each "latest" (the alias tracks its newest model) or pinned to one full
148
+ * model id. Claude Code has no command that lists models, so the list
149
+ * ships with each Den release (https://code.claude.com/docs/en/model-config:
150
+ * aliases opus, sonnet, haiku, or a full model name). Newest first.
151
+ */
152
+ export const DEN_CLAUDE_MODELS: readonly RuntimeModel[] = [
153
+ {
154
+ id: "opus",
155
+ label: "Opus",
156
+ versions: [
157
+ "claude-opus-5-5",
158
+ "claude-opus-5",
159
+ "claude-opus-4-8",
160
+ "claude-opus-4-7",
161
+ "claude-opus-4-6",
162
+ ],
163
+ latest: "claude-opus-5-5",
164
+ },
165
+ {
166
+ id: "sonnet",
167
+ label: "Sonnet",
168
+ versions: ["claude-sonnet-5-5", "claude-sonnet-5", "claude-sonnet-4-6"],
169
+ latest: "claude-sonnet-5-5",
170
+ },
171
+ {
172
+ id: "haiku",
173
+ label: "Haiku",
174
+ versions: ["claude-haiku-5-5", "claude-haiku-4-5"],
175
+ latest: "claude-haiku-5-5",
176
+ },
177
+ ];
178
+
179
+ /**
180
+ * The mode to launch with: the agent's intent (Start › Advanced), else the
181
+ * persona's `permission_mode`: one of Den's intents, or one of Claude
182
+ * Code's own modes (as before AG-13). A bad one names the key.
183
+ */
93
184
  function permissionMode(input: LaunchInput): string {
185
+ if (input.permission) return INTENT_MODE[input.permission];
94
186
  const m = input.persona.permissionMode ?? DEFAULT_PERMISSION_MODE;
187
+ if ((PERMISSION_INTENTS as readonly string[]).includes(m))
188
+ return INTENT_MODE[m as PermissionIntent];
95
189
  if (!(PERMISSION_MODES as readonly string[]).includes(m))
96
190
  throw new LaunchError(
97
- `${input.persona.source}: permission_mode "${m.replace(/[^\x20-\x7e]/g, "?").slice(0, 40)}" must be one of ${PERMISSION_MODES.join(", ")}`,
191
+ `${input.persona.source}: permission_mode "${m.replace(/[^\x20-\x7e]/g, "?").slice(0, 40)}" must be one of ${[...PERMISSION_INTENTS, ...PERMISSION_MODES].join(", ")}`,
98
192
  );
99
193
  return m;
100
194
  }
@@ -243,7 +337,9 @@ function common(input: LaunchInput): {
243
337
  }),
244
338
  )
245
339
  : null;
246
- const settings = input.stateDir || hooks ? settingsPath(input) : null;
340
+ const allow = allowRules(input.allow ?? []);
341
+ const settings =
342
+ input.stateDir || hooks || allow.length ? settingsPath(input) : null;
247
343
  return {
248
344
  argv: [
249
345
  "--mcp-config",
@@ -267,13 +363,18 @@ function common(input: LaunchInput): {
267
363
  ? {
268
364
  [settings]: `${JSON.stringify(
269
365
  {
270
- ...(input.stateDir
366
+ ...(input.stateDir || allow.length
271
367
  ? {
272
368
  permissions: {
273
- deny: stateDirDenyRules(
274
- input.stateDir,
275
- input.env?.HOME,
276
- ),
369
+ ...(allow.length ? { allow } : {}),
370
+ ...(input.stateDir
371
+ ? {
372
+ deny: stateDirDenyRules(
373
+ input.stateDir,
374
+ input.env?.HOME,
375
+ ),
376
+ }
377
+ : {}),
277
378
  },
278
379
  }
279
380
  : {}),
@@ -288,7 +389,9 @@ function common(input: LaunchInput): {
288
389
  warnings:
289
390
  mode === "bypassPermissions"
290
391
  ? [
291
- `⚠ persona ${input.persona.id} sets permission_mode: bypassPermissions: Claude Code will not ask before running any tool`,
392
+ input.permission === "all"
393
+ ? `⚠ agent ${input.agentId} starts with auto-approve all (bypassPermissions): Claude Code will not ask before running any tool`
394
+ : `⚠ persona ${input.persona.id} sets permission_mode: bypassPermissions: Claude Code will not ask before running any tool`,
292
395
  ]
293
396
  : [],
294
397
  };
@@ -313,6 +416,31 @@ export function firstPromptArg(text: string | undefined): string[] {
313
416
  return [t.startsWith("-") ? `Prompt: ${t}` : t];
314
417
  }
315
418
 
419
+ const TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
420
+
421
+ /**
422
+ * Claude Code's transcript for conversation `token`: <token>.jsonl in any
423
+ * project under its projects dir, or null (a bad token, none written yet).
424
+ */
425
+ export function transcriptFile(
426
+ token: string | undefined,
427
+ env: NodeJS.ProcessEnv = process.env,
428
+ ): string | null {
429
+ if (!token || !TOKEN_RE.test(token)) return null;
430
+ const root = claudeProjectsDir(env);
431
+ let projects: string[] = [];
432
+ try {
433
+ projects = fs.readdirSync(root);
434
+ } catch {
435
+ return null;
436
+ }
437
+ for (const p of projects) {
438
+ const file = path.join(root, p, `${token}.jsonl`);
439
+ if (fs.existsSync(file)) return file;
440
+ }
441
+ return null;
442
+ }
443
+
316
444
  export const denClaude: Runtime = {
317
445
  id: DEN_CLAUDE_ID,
318
446
  label: DEN_CLAUDE_CATALOG.name,
@@ -321,6 +449,9 @@ export const denClaude: Runtime = {
321
449
  denTools: true,
322
450
  firstPrompt: true,
323
451
  find: DEN_CLAUDE_FIND,
452
+ models: () =>
453
+ DEN_CLAUDE_MODELS.map((m) => ({ ...m, versions: [...m.versions] })),
454
+ permissions: DEN_CLAUDE_PERMISSIONS,
324
455
  detect,
325
456
  launch(input): LaunchOutput {
326
457
  const bin = binFor(input);
@@ -355,17 +486,20 @@ export const denClaude: Runtime = {
355
486
  },
356
487
  // Its transcript, <token>.jsonl in any project under its projects dir.
357
488
  conversationExists(token, env = process.env) {
358
- if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(token)) return false;
359
- const root = claudeProjectsDir(env);
360
- let projects: string[] = [];
489
+ return transcriptFile(token, env) !== null;
490
+ },
491
+ // AG-4b, unofficial (catalog sessionName): `/rename` appends a
492
+ // {"type":"custom-title","customTitle":…} record to the transcript; the
493
+ // latest one, read on from the last ask (never the whole file), less one
494
+ // pair of surrounding quotes. Null on any failure: it must never break a
495
+ // pane.
496
+ sessionName(rec, env = process.env) {
361
497
  try {
362
- projects = fs.readdirSync(root);
498
+ const file = transcriptFile(rec.resumeToken, env);
499
+ return file ? unquoteTitle(transcriptTitle(file)) : null;
363
500
  } catch {
364
- return false;
501
+ return null;
365
502
  }
366
- return projects.some((p) =>
367
- fs.existsSync(path.join(root, p, `${token}.jsonl`)),
368
- );
369
503
  },
370
504
  launchFailures: {
371
505
  noConversation: /no conversation found/i,
@@ -0,0 +1,108 @@
1
+ /**
2
+ * Permissions as intent (AG-13): what a person chooses, before any
3
+ * runtime's syntax.
4
+ *
5
+ * ask Ask for everything (the default)
6
+ * edits Auto-approve edits in the worktree
7
+ * all Auto-approve all (a session started so is always tagged)
8
+ *
9
+ * And extra allowed commands: plain command prefixes ("npm test"), one per
10
+ * entry, no patterns. Each runtime translates both into its own launch
11
+ * config (den-claude.ts); a runtime that can't express one says so, and it
12
+ * is never silently dropped. What a persona pack shares is this intent
13
+ * only, never vendor syntax.
14
+ */
15
+
16
+ export const PERMISSION_INTENTS = ["ask", "edits", "all"] as const;
17
+ export type PermissionIntent = (typeof PERMISSION_INTENTS)[number];
18
+ export const DEFAULT_INTENT: PermissionIntent = "ask";
19
+
20
+ /**
21
+ * A persona's `permission_mode` as an intent. Den's three intents are
22
+ * themselves; Claude Code's own modes from before AG-13 read as the intent
23
+ * they meant (default, acceptEdits, bypassPermissions). Anything else
24
+ * (Claude Code's plan, a typo) is not an intent: undefined, and the
25
+ * runtime decides (den-claude refuses a mode it doesn't know).
26
+ */
27
+ export function intentOf(
28
+ mode: string | undefined,
29
+ ): PermissionIntent | undefined {
30
+ if (mode === undefined) return DEFAULT_INTENT;
31
+ if ((PERMISSION_INTENTS as readonly string[]).includes(mode))
32
+ return mode as PermissionIntent;
33
+ if (mode === "default") return "ask";
34
+ if (mode === "acceptEdits") return "edits";
35
+ if (mode === "bypassPermissions") return "all";
36
+ return undefined;
37
+ }
38
+
39
+ /** The most extra commands one persona or launch takes. */
40
+ export const MAX_COMMANDS = 32;
41
+ /** The longest command prefix. */
42
+ export const MAX_COMMAND_LEN = 120;
43
+ /**
44
+ * What a plain prefix is made of: letters, digits, spaces and . _ / : = @
45
+ * % + , -. No shell syntax (quotes, ;, &, |, $, backticks, redirects) and
46
+ * no pattern characters (* ? [ ] ^ ( ) { }): Den translates a prefix, it
47
+ * never passes one through a shell.
48
+ */
49
+ const PREFIX_RE = /^[A-Za-z0-9][A-Za-z0-9 ._/:=@%+,-]*$/;
50
+ const PATTERN_RE = /[*?[\]^$(){}|]/;
51
+
52
+ export type CommandProblem = "pattern" | "invalid" | "long";
53
+
54
+ /** Why `c` isn't a plain prefix, or null when it is. */
55
+ export function commandProblem(c: string): CommandProblem | null {
56
+ if ([...c].length > MAX_COMMAND_LEN) return "long";
57
+ if (PATTERN_RE.test(c)) return "pattern";
58
+ if (!PREFIX_RE.test(c) || /\s{2,}/.test(c) || c !== c.trim())
59
+ return "invalid";
60
+ return null;
61
+ }
62
+
63
+ const shown = (v: unknown) =>
64
+ (JSON.stringify(v) ?? "undefined").replace(/[^\x20-\x7e]/g, "?").slice(0, 60);
65
+
66
+ /**
67
+ * A list of extra allowed commands, checked: an array of plain prefixes
68
+ * (each trimmed, duplicates dropped), at most MAX_COMMANDS. undefined or
69
+ * null is none. Anything else throws, naming the entry and why.
70
+ */
71
+ export function checkCommands(v: unknown, where: string): string[] {
72
+ if (v === undefined || v === null) return [];
73
+ if (!Array.isArray(v))
74
+ throw new Error(`${where}: must be a list of command prefixes`);
75
+ if (v.length > MAX_COMMANDS)
76
+ throw new Error(`${where}: at most ${MAX_COMMANDS} commands`);
77
+ const out: string[] = [];
78
+ v.forEach((x, i) => {
79
+ if (typeof x !== "string")
80
+ throw new Error(`${where}[${i}]: must be a string`);
81
+ const c = x.trim();
82
+ const why = c ? commandProblem(c) : "invalid";
83
+ if (why === "pattern")
84
+ throw new Error(
85
+ `${where}[${i}]: ${shown(c)} is a pattern: use a plain prefix ("npm test")`,
86
+ );
87
+ if (why === "long")
88
+ throw new Error(`${where}[${i}]: at most ${MAX_COMMAND_LEN} characters`);
89
+ if (why)
90
+ throw new Error(
91
+ `${where}[${i}]: ${shown(c)} must be a plain prefix: letters, digits, single spaces and . _ / : = @ % + , -`,
92
+ );
93
+ if (!out.includes(c)) out.push(c);
94
+ });
95
+ return out;
96
+ }
97
+
98
+ /** An intent, checked; a usage message naming `where` otherwise. */
99
+ export function checkIntent(v: unknown, where: string): PermissionIntent {
100
+ if (
101
+ typeof v === "string" &&
102
+ (PERMISSION_INTENTS as readonly string[]).includes(v)
103
+ )
104
+ return v as PermissionIntent;
105
+ throw new Error(
106
+ `${where}: ${shown(v)} must be one of ${PERMISSION_INTENTS.join(", ")}`,
107
+ );
108
+ }