@monster-orange/den 0.4.6 → 0.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,72 @@ All notable changes to Den are documented here. Each release lists the
4
4
  config and manifest changes a distribution must make to move to it under
5
5
  **Migrations**.
6
6
 
7
+ ## [0.4.7] - 2026-11-07
8
+
9
+ Agents read slices, wait in the background and hand back plainly (den#5,
10
+ the default-pack guardrails issue). Every persona in Den's default pack
11
+ gets Den's shared instructions and, on Claude Code, Den's hooks, with no
12
+ setup. `doctor` warns about polling loops it sees in the local dev metrics.
13
+
14
+ ### Added
15
+
16
+ - **The Stop hook reads every last message.** With a `## Hand-back`
17
+ heading it checks the whole hand-back, as before. Any other last message
18
+ gets the plain-writing check: every ID explained. It still never blocks
19
+ twice in a row.
20
+ - **`URL: none` in a hand-back.** Work with no page (a CLI change, a
21
+ refused request) says `URL: none`, with a reason if it likes. An empty
22
+ or missing URL still fails.
23
+
24
+ - **`down --json` and `web down --json`.** The clean-up steps answer in
25
+ JSON like every other onboarding step: `{ ok, workspace, services }` and
26
+ `{ ok, url, port, running, stopped }`. Both are safe to re-run.
27
+
28
+ - **Hooks for default-pack agents.** A Claude Code agent from Den's default
29
+ pack runs with Den's own hooks, in its own `--settings` file (never your
30
+ `~/.claude`). A `PreToolUse` hook on Bash and Read blocks reading all of
31
+ a log or a large file into the conversation (`cat`, `less`, `tail` from
32
+ a line such as `tail +1`, a Read with no limit; a plain `tail` passes) and says to read a slice or use a
33
+ subagent. It blocks a foreground `sleep`, alone or in an `until`/`while`/
34
+ `for` loop, and says to run in the background or use a wait primitive.
35
+ It blocks nothing else.
36
+ - **Shared persona instructions.** Every default-pack persona's role prompt
37
+ ends with Den's `guardrails/instructions.md`: write plainly, in short
38
+ sentences; say what every issue or ID is; ask each decision as a numbered
39
+ yes/no question; no caveat sections; and the hand-back template (URL,
40
+ machine, branch, what changed, what to click, decisions).
41
+ - **The hand-back check.** `<cli> agents handback-check <file|->` rejects a
42
+ hand-back with a missing part, an unexplained ID, or a decision that is
43
+ not a numbered yes/no question, and says what to fix. A `Stop` hook runs
44
+ the same check on a turn that ends with a `Hand-back` heading, and sends
45
+ Claude back to fix it once.
46
+ - **doctor warns about polling loops.** It reads the end of the local
47
+ `.dev-metrics/events.jsonl` (at most 8 MB) and warns when one command,
48
+ run from one place, ran more than 600 times in an hour:
49
+ `looks like a polling loop: status in services/api, 1200 calls in an hour (…)`.
50
+ It is local and sends nothing.
51
+ - **Each dev-metrics line records `cwd`**, relative to the distribution root
52
+ (`.` at the root, `outside` elsewhere), never an absolute or home path.
53
+ Like every field, it is sent only when you opt in.
54
+
55
+ ### Metrics (before → after)
56
+
57
+ - Polling loops a week: 18,600 → under 100 expected, since the hook blocks
58
+ them. The "after" is counted on the candidate's first week.
59
+ - Incomplete hand-backs: not measured → 0 for default-pack agents (the Stop
60
+ hook sends them back). The candidate's first run sets the baseline.
61
+ - Hand-backs with an unexplained ID: no baseline → 0 for default-pack
62
+ agents. The candidate's first run sets the baseline.
63
+ - doctor on a replay of a three-day polling burst: no warning → it warns.
64
+
65
+ ### Migrations
66
+
67
+ - None needed. To turn the guardrails off for Den's pack, set
68
+ `agents.default_pack: { hooks: false, instructions: false }`. To turn
69
+ them on for your own pack, write its `persona_packs` entry as
70
+ `{ path: <dir>, hooks: true, instructions: true }`. A persona's
71
+ `den_hooks` / `den_instructions` front matter wins over its pack's.
72
+
7
73
  ## [0.4.6] - 2026-11-07
8
74
 
9
75
  Den in five minutes, set up by your own agent (ON-2, with ON-4's dry run).
package/README.md CHANGED
@@ -10,8 +10,8 @@
10
10
  </p>
11
11
 
12
12
  <picture>
13
- <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.6/hero-dark.png">
14
- <img src="docs/images/0.4.6/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.7/hero-dark.png">
14
+ <img src="docs/images/0.4.7/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
15
15
  </picture>
16
16
 
17
17
  ## Set up by your agent
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
38
38
  yours. Den connects them and turns on good practice from the start.
39
39
 
40
40
  <p align="center">
41
- <img src="docs/images/0.4.6/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
41
+ <img src="docs/images/0.4.7/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
42
42
  </p>
43
43
 
44
44
  ## How it helps
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
68
68
  lives in your own repo.
69
69
 
70
70
  <p align="center">
71
- <img src="docs/images/0.4.6/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
- <img src="docs/images/0.4.6/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
- <img src="docs/images/0.4.6/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
- <img src="docs/images/0.4.6/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
71
+ <img src="docs/images/0.4.7/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
+ <img src="docs/images/0.4.7/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
+ <img src="docs/images/0.4.7/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
+ <img src="docs/images/0.4.7/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
75
75
  </p>
76
76
 
77
77
  ## Get started
@@ -87,6 +87,8 @@ question for the person (`ask`, sometimes with `choices`).
87
87
  | `<cli> web up --no-open --json` | `{ ok, url, port, version? }` |
88
88
  | `<cli> doctor --json` | `{ ok, checks: [{ id, status, detail, url?, port?, fix?, ask? }] }` |
89
89
  | `<cli> mcp connect --runtime <runtime> --json` | `{ ok, server, clients: [{ id, ok, said, file?, changed? }], restart, web }` |
90
+ | `<cli> down <repo> --json` | `{ ok, workspace, services, all? }` |
91
+ | `<cli> web down --json` | `{ ok, url, port, running, stopped }` |
90
92
 
91
93
  ### repo add
92
94
 
@@ -122,6 +124,10 @@ question for the person (`ask`, sometimes with `choices`).
122
124
 
123
125
  ### up, doctor and mcp connect
124
126
 
127
+ - `down --json` and `web down --json` are the clean-up steps. Each is safe
128
+ to re-run: `web down` answers `stopped: false` when the server wasn't
129
+ running, and `ok` is true once nothing answers on its port.
130
+
125
131
  - `up --json` runs `up` as usual, then reports each service it touched:
126
132
  `result` is `started`, `already-running`, `failed` or `skipped`, and
127
133
  `health` is the service's health after it started (a service with a
@@ -684,6 +690,18 @@ one is started (see Sessions).
684
690
  server, Den's MCP) and answers with one JSON object: see
685
691
  [Set up by your agent](#up-doctor-and-mcp-connect).
686
692
 
693
+ doctor also reads the end of the local `.dev-metrics/events.jsonl` (at
694
+ most 8 MB of it, however big the file) and warns when one command, run
695
+ from one place, ran more than 600 times in an hour. That is almost always
696
+ an agent polling. It is a warning, never a failure, and nothing is sent
697
+ (see [Dev metrics](#dev-metrics-local-unless-you-opt-in)):
698
+
699
+ ```
700
+ Acme doctor: dev-metrics (local, never sent)
701
+
702
+ ⚠ looks like a polling loop: status in services/api, 1200 calls in an hour (2026-11-04T09:00:00Z–2026-11-04T09:59:57Z)
703
+ ```
704
+
687
705
  ## den.yml: the product profile
688
706
 
689
707
  Every runtime name derives from `product.name`. Set only what you want to
@@ -2012,6 +2030,7 @@ server scoped to its workspace.
2012
2030
  <cli> agents resume <id> [-w <ws>]
2013
2031
  <cli> agents personas [--json]
2014
2032
  <cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
2033
+ <cli> agents handback-check <file|-> # is a hand-back complete and plain (see Guardrails)
2015
2034
  <cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
2016
2035
  ```
2017
2036
 
@@ -2215,6 +2234,8 @@ standing: true # starts with its workspace (later slices)
2215
2234
  context: [docs/specs, the design system] # what to read first
2216
2235
  runtime: generic # default den-claude
2217
2236
  permission_mode: plan # den-claude only; default `default`
2237
+ den_hooks: false # Den's guardrail hooks for this persona (see Guardrails)
2238
+ den_instructions: false # Den's shared instructions for this persona
2218
2239
  argv: ["some-cli", "--model", "{model}", "--mcp-config", "{mcp_config}"]
2219
2240
  ---
2220
2241
 
@@ -2236,6 +2257,104 @@ used without converting them. Files without front matter (a README) are
2236
2257
  skipped; two files with one id in a pack, or a pack that isn't a directory,
2237
2258
  are errors naming the file or the key.
2238
2259
 
2260
+ ### Guardrails: hooks, shared instructions, hand-backs
2261
+
2262
+ Every persona in Den's default pack gets two things with no setup: Den's
2263
+ **shared persona instructions** and, on den-claude, Den's **hooks**. Both
2264
+ are Den's own files, in `src/shared/agents/guardrails/` of the package.
2265
+
2266
+ **Shared instructions** (`instructions.md`) are appended to the role prompt
2267
+ (den-claude's `--append-system-prompt`; any runtime that takes the role
2268
+ prompt gets them). They say:
2269
+
2270
+ - write to people plainly, in short sentences;
2271
+ - never name an issue or ID without saying what it is;
2272
+ - ask each decision as a numbered yes/no question;
2273
+ - no caveat sections, and nothing the person doesn't need in order to act;
2274
+ - read a slice of a log, or hand it to a subagent; don't poll with `sleep`;
2275
+ - end a hand-back with the template below.
2276
+
2277
+ **Hooks** (`hooks.json`, a Claude Code settings fragment) go in the agent's
2278
+ own settings file, `--settings <stateDir>/agent-files/<id>/settings.json`.
2279
+ Den never writes your `~/.claude`; your own hooks still run.
2280
+
2281
+ - `PreToolUse` on `Bash|Read` (`pretooluse.mjs`, plain Node, no imports)
2282
+ blocks, with a reason Claude sees (exit 2):
2283
+ - reading all of a log or a large file into the conversation: a `Read`
2284
+ with no `limit`, or `cat`, `less`, or `tail` from a line (`tail +1`,
2285
+ `tail -n +1`), whose output reaches the conversation. A log is `*.log` or `*.log.<N>`, a `.jsonl` under
2286
+ `.dev-metrics/`, or a `.jsonl` under `.claude/projects/` (a transcript).
2287
+ Large is over 256 KB. `cat x.log | grep …`, `cat x.log > y`, a plain
2288
+ `tail x.log` (10 lines) and `tail -n 100 x.log` pass. The reason says to read a slice or hand the
2289
+ file to a subagent.
2290
+ - `sleep` in the foreground, alone or in an `until …; do sleep …; done`,
2291
+ `while` or `for` loop. A Bash call with `run_in_background`, and
2292
+ `sleep … &`, pass. The reason says to run in the background or use a
2293
+ wait primitive. Recent Claude Code versions have their own sleep guard,
2294
+ which answers first (and also stops `sleep … &`). The sleep is blocked
2295
+ either way; Den's hook covers runtimes and versions without that guard.
2296
+ - Nothing else. Quoted text, heredoc bodies and other tools always pass,
2297
+ and a hook that can't read its input lets the call through.
2298
+ - `Stop` (`stop-hook.ts`) checks a turn's last message, which is what the
2299
+ person reads. With a `Hand-back` heading it gets the whole hand-back
2300
+ check; any other message gets the plain-writing part only (every ID
2301
+ explained). If the check fails, Claude is told what to fix and keeps
2302
+ going (Claude Code's `decision: "block"`). It never blocks twice in a
2303
+ row (`stop_hook_active`), so it can't loop. Den adds no `SubagentStop` hook: a subagent reports to its parent
2304
+ agent, not to a person.
2305
+
2306
+ **The hand-back template**, which the check enforces:
2307
+
2308
+ ```markdown
2309
+ ## Hand-back
2310
+
2311
+ - URL: <the full link the person opens, with https://>
2312
+ - Machine: <the machine it runs on>
2313
+ - Branch: <the branch>
2314
+ - What changed: <what is different now>
2315
+ - What to click: <the steps to see it working>
2316
+ - Decisions:
2317
+ 1. <a question the person answers yes or no>?
2318
+ ```
2319
+
2320
+ `<cli> agents handback-check <file|-> [--json]` runs the same check on a
2321
+ file or stdin: exit 0 when the hand-back passes, 1 with each problem. It
2322
+ rejects a hand-back when:
2323
+
2324
+ - a part is missing or empty. A part is a line starting with its label and
2325
+ a colon (bold is fine), or a heading that is its label. URL must hold a
2326
+ link (`://`), or say `none` for work with no page (`URL: none — a CLI
2327
+ change` is fine). `Decisions: none` is allowed.
2328
+ - an ID is bare. An ID is `#<digits>`, or 1 to 4 capitals, an optional `-`,
2329
+ digits and an optional `.<digits>` (`R12`, `AG-14`, `KB-4.1`). Links,
2330
+ `inline code`, code blocks, and the URL and Branch parts are not checked.
2331
+ An ID is explained (glossed) when
2332
+ 1. a parenthetical of two or more words follows it: `#5 (the hooks issue)`;
2333
+ 2. a colon or a spaced dash and two or more words follow it:
2334
+ `R12: plain writing`;
2335
+ 3. it sits in a parenthetical of IDs only, right after a word:
2336
+ `plain writing (R12)`, `the issues (#5, #6)`;
2337
+ 4. or the same ID was explained earlier in the text.
2338
+ - a decision isn't a numbered yes/no question: each item must be numbered
2339
+ (`1.` or `1)`), end in `?`, not start with what, which, how, why, when,
2340
+ where or who, and not offer a choice (`or`).
2341
+
2342
+ **Turning them off or on.** They are on for Den's default pack and off for
2343
+ your own packs. A pack's setting applies to each of its personas; a
2344
+ persona's front matter (`den_hooks`, `den_instructions`) wins over it:
2345
+
2346
+ ```yaml
2347
+ agents:
2348
+ default_pack: { hooks: false } # Den's pack: no hooks, instructions kept
2349
+ persona_packs:
2350
+ - personas # your pack: neither
2351
+ - { path: more-personas, hooks: true, instructions: true } # both
2352
+ ```
2353
+
2354
+ A persona of yours that replaces a default one (same id) takes your
2355
+ pack's setting, not Den's pack's. Anything but `true` or `false`, or an
2356
+ unknown key, is an error naming the key.
2357
+
2239
2358
  ### Runtimes
2240
2359
 
2241
2360
  `<cli> doctor` lists the runtime kind. Den knows the runtimes in its
@@ -2418,6 +2537,11 @@ metrics:
2418
2537
  share: true
2419
2538
  ```
2420
2539
 
2540
+ Each line also records `cwd`: where the command ran, relative to the
2541
+ distribution root (`.` for the root), or `outside` for anywhere else. It is
2542
+ never an absolute or home path. [`<cli> doctor`](#doctor) uses it to warn
2543
+ about polling loops.
2544
+
2421
2545
  Only the boolean `true` counts. With it, and a Sentry DSN in
2422
2546
  `DEV_METRICS_SENTRY_DSN` (or a synced `dev-metrics.config`), each line is
2423
2547
  also sent, best effort. A DSN on its own never sends. `CI=true` and
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.6",
3
+ "version": "0.4.7",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -65,8 +65,8 @@ person (`ask`, sometimes with `choices`).
65
65
  in `package.json`. `--sidecar` asks for this explicitly, and fails if the repo has its own manifest.
66
66
  - If you can see which port the app listens on, add `--health http://localhost:<port>/`. Den then checks that it
67
67
  answers.
68
- 2. If the answer has `ask` and `choices`, Den couldn't tell how the repo starts, and it wrote nothing. Ask the person
69
- **one** question: the `ask` text, with the choices numbered. Then re-run with their answer:
68
+ 2. If the answer has `ask`, Den couldn't tell how the repo starts, and it wrote nothing. Ask the person **one**
69
+ question: the `ask` text, with any `choices` numbered (there may be none). Then re-run with their answer:
70
70
  `./bin/<name> repo add <repo> --run "<their command>" --json`.
71
71
  3. The answer names the repo (`repo`), the workspace it joined (`workspace`) and where its manifest is (`manifest`).
72
72
  A workspace is Den's name for a set of repos that run together. The first one is called `default`.
@@ -15,6 +15,10 @@
15
15
  * `<state>/sessions/**`. It binds a
16
16
  * cooperating agent only: the agent runs as the same OS user, so nothing
17
17
  * stops one that ignores its permission config from reading them.
18
+ * - Den's guardrail hooks (den#5, guardrails.ts), when the persona's
19
+ * guardrails include them: the same settings file carries
20
+ * guardrails/hooks.json's `hooks` (PreToolUse on Bash and Read, and Stop),
21
+ * alongside any hooks the user has; Den never writes ~/.claude.
18
22
  * - Each launch picks a fresh conversation id (--session-id); resume
19
23
  * continues it with --resume <id>, in the same cwd, with the same MCP.
20
24
  * - Permission prompts stay in the terminal (approvals: "terminal"):
@@ -41,6 +45,7 @@ import * as fs from "fs";
41
45
  import * as path from "path";
42
46
  import { configuredRuntimeBin } from "../den-config.js";
43
47
  import { rolePrompt } from "./personas.js";
48
+ import { guardrailHooks } from "./guardrails.js";
44
49
  import {
45
50
  LaunchError,
46
51
  mcpConfigJson,
@@ -221,7 +226,8 @@ function common(input: LaunchInput): {
221
226
  } {
222
227
  const file = mcpConfigPath(input);
223
228
  const mode = permissionMode(input);
224
- const settings = input.stateDir ? settingsPath(input) : null;
229
+ const hooks = input.persona.guardrails?.hooks ? guardrailHooks() : null;
230
+ const settings = input.stateDir || hooks ? settingsPath(input) : null;
225
231
  return {
226
232
  argv: [
227
233
  "--mcp-config",
@@ -241,13 +247,21 @@ function common(input: LaunchInput): {
241
247
  ],
242
248
  files: {
243
249
  [file]: mcpConfigJson(input.mcp),
244
- ...(settings && input.stateDir
250
+ ...(settings
245
251
  ? {
246
252
  [settings]: `${JSON.stringify(
247
253
  {
248
- permissions: {
249
- deny: stateDirDenyRules(input.stateDir, input.env?.HOME),
250
- },
254
+ ...(input.stateDir
255
+ ? {
256
+ permissions: {
257
+ deny: stateDirDenyRules(
258
+ input.stateDir,
259
+ input.env?.HOME,
260
+ ),
261
+ },
262
+ }
263
+ : {}),
264
+ ...(hooks ? { hooks } : {}),
251
265
  },
252
266
  null,
253
267
  2,
@@ -0,0 +1,27 @@
1
+ {
2
+ "hooks": {
3
+ "PreToolUse": [
4
+ {
5
+ "matcher": "Bash|Read",
6
+ "hooks": [
7
+ {
8
+ "type": "command",
9
+ "command": "{pretooluse}",
10
+ "timeout": 10
11
+ }
12
+ ]
13
+ }
14
+ ],
15
+ "Stop": [
16
+ {
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "{stop}",
21
+ "timeout": 30
22
+ }
23
+ ]
24
+ }
25
+ ]
26
+ }
27
+ }
@@ -0,0 +1,39 @@
1
+ ## How you work and write (every Den persona)
2
+
3
+ ### Writing to people
4
+
5
+ - Write plainly. Use short sentences.
6
+ - Never name an issue or an ID without saying what it is. Write "#5 (the
7
+ hooks issue)" or "the plain-writing rule (R12)", never a bare "#5" or "R12".
8
+ - Ask each decision as a numbered yes/no question. One question per number.
9
+ - No caveat sections. Leave out anything the person doesn't need in order to
10
+ act.
11
+
12
+ ### Reading and waiting
13
+
14
+ - Don't read a whole log or a large file into the conversation. Read a slice
15
+ (`tail -n 100`, `grep -n`), or hand the file to a subagent and ask for the
16
+ conclusion.
17
+ - Don't poll with `sleep` or `until …; do sleep …; done`. Run long work in the
18
+ background and wait to be told it ended, or use a wait primitive.
19
+
20
+ ### Handing work back
21
+
22
+ When you hand work back to a person, end with this, filled in. Den checks it:
23
+ a hand-back with a missing part, an unexplained ID, or a decision that is not
24
+ a numbered yes/no question is sent back to you to fix.
25
+
26
+ ```
27
+ ## Hand-back
28
+ - URL: <the full link the person opens, with https://>
29
+ - Machine: <the machine it runs on>
30
+ - Branch: <the branch>
31
+ - What changed: <what is different now, in a sentence or two>
32
+ - What to click: <the steps to see it working>
33
+ - Decisions:
34
+ 1. <a question the person answers yes or no>?
35
+ ```
36
+
37
+ Write "Decisions: none" when there are none, and "URL: none" (with a short
38
+ reason) when the work has no page to open. Every message you end a turn
39
+ with is read by a person, hand-back or not: explain each ID in it.