@monster-orange/den 0.4.6 → 0.4.8
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 +160 -0
- package/README.md +7 -7
- package/docs/distributions.md +244 -1
- package/package.json +1 -1
- package/schemas/den.plugin.v1.json +3 -2
- package/skills/install-den/SKILL.md +2 -2
- package/src/shared/agents/den-claude.ts +38 -5
- package/src/shared/agents/guardrails/hooks.json +27 -0
- package/src/shared/agents/guardrails/instructions.md +39 -0
- package/src/shared/agents/guardrails/pretooluse.mjs +430 -0
- package/src/shared/agents/guardrails/stop-hook.ts +66 -0
- package/src/shared/agents/guardrails.ts +114 -0
- package/src/shared/agents/handback.ts +241 -0
- package/src/shared/agents/personas.ts +66 -7
- package/src/shared/agents/records.ts +114 -0
- package/src/shared/agents/runtime.ts +12 -0
- package/src/shared/den-config.ts +97 -6
- package/src/shared/dev-metrics/emit.ts +31 -4
- package/src/shared/dev-metrics/polling.ts +135 -0
- package/src/shared/engine.ts +14 -2
- package/src/shared/plugins/manifest.ts +3 -0
- package/src/shared/usage/paths.ts +16 -0
- package/src/shared/usage/report.ts +283 -0
- package/src/shared/usage/sources.ts +127 -0
- package/src/shared/usage/transcript.ts +262 -0
- package/src/ui/cli/agent-ops.ts +495 -53
- package/src/ui/cli/agents-command.ts +170 -14
- package/src/ui/cli/doctor-command.ts +20 -0
- package/src/ui/cli/flag-parser.ts +1 -0
- package/src/ui/cli/index.ts +119 -2
- package/src/ui/cli/usage-command.ts +256 -0
- package/src/ui/cli/web-command.ts +3 -1
- package/src/ui/mcp/api.ts +10 -1
- package/src/ui/mcp/server.ts +34 -0
- package/src/ui/mcp/tools.ts +50 -0
- package/src/ui/server/agent-resume-grant.ts +53 -0
- package/src/ui/server/agent-routes.ts +230 -9
- package/src/ui/server/audit.ts +68 -1
- package/src/ui/server/index.ts +16 -5
- package/src/ui/server/public/agents-page.js +333 -11
- package/src/ui/server/public/agents-section.js +17 -3
- package/src/ui/server/public/agents.html +44 -0
- package/src/ui/server/public/i18n/es.js +28 -0
- package/src/ui/server/public/i18n/ja.js +25 -0
- package/src/ui/server/public/shell.js +66 -0
- package/src/ui/server/request-guard.ts +21 -1
- package/src/ui/server/route-classes.ts +23 -0
- package/src/ui/server/session-routes.ts +52 -4
- package/src/ui/server/sudo-store.ts +36 -7
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,166 @@ 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.8] - 2026-11-07
|
|
8
|
+
|
|
9
|
+
Stopped agents stay in the list, and come back where they were (AG-14, the
|
|
10
|
+
stopped-agents slice). Den also counts what each agent used and what it
|
|
11
|
+
got done, read on this machine only (den#6, the agent-usage issue).
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **Stopped agents stay listed.** They sit below the running ones, each
|
|
16
|
+
with why it stopped: stopped by you, exited (with its code), or
|
|
17
|
+
interrupted by a restart. Each row has Resume, Start fresh and Remove.
|
|
18
|
+
- **Resume is the same conversation.** Den runs the runtime's own resume
|
|
19
|
+
with the conversation id it saved (Claude Code: `--resume <id>`). The
|
|
20
|
+
agent comes back in its own folder, with its workspace and grants. A
|
|
21
|
+
runtime that can't resume (codex, gemini, grok), or an agent with no
|
|
22
|
+
saved id, offers Start fresh only and says why.
|
|
23
|
+
- **Remove in one click, with Undo.** The toast offers Undo for 10 seconds.
|
|
24
|
+
**Clear stopped** removes every stopped agent in view. A running agent
|
|
25
|
+
is never removed.
|
|
26
|
+
- **After a restart, one line.** "2 agents were interrupted when Acme
|
|
27
|
+
restarted · Resume all · Review". Standing personas, and personas with
|
|
28
|
+
`resume_on_restart: true`, resume on their own.
|
|
29
|
+
- **The CLI:** `agents list --stopped`, `agents resume --all-interrupted`,
|
|
30
|
+
`agents rm <id>` and `agents rm --stopped`.
|
|
31
|
+
- **`agent_resume`, an MCP action for orchestrators.** Off unless
|
|
32
|
+
`mcp.actions` names it. It resumes agents of its own workspace or a
|
|
33
|
+
granted one, never itself. The web server checks the same rule.
|
|
34
|
+
- **`usage agents` and `usage report`.** Each agent's tokens and outcome
|
|
35
|
+
(pull request opened or merged, tests green, wall-clock time) are read
|
|
36
|
+
from its transcripts and saved in its record, matched by the
|
|
37
|
+
conversation id. `usage report` writes a weekly summary, compared with
|
|
38
|
+
the last. Transcripts stay on the machine, and only counts are kept.
|
|
39
|
+
- **The `usage` plugin kind** (server tier) adds a transcript reader for
|
|
40
|
+
another runtime. Den ships one for Claude Code.
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- **The audit trail survives a restart.** The last 200 actions are kept in
|
|
45
|
+
`<state dir>/audit.jsonl` (0600) and read back at start.
|
|
46
|
+
- **An agent after a restart says Interrupted**, not Exited. An agent that
|
|
47
|
+
exited says Exited (code n), even after the session host forgets it.
|
|
48
|
+
- **An agent record notes its grants**, its end and exit code, and its
|
|
49
|
+
usage and outcome.
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **A resume never loses the conversation.** Stopping an agent without
|
|
54
|
+
typing, then resuming it, could fail for good: Den started a new
|
|
55
|
+
conversation under the same id, and Claude Code refused it as "already
|
|
56
|
+
in use". Now Den decides before it launches. For Claude Code it looks
|
|
57
|
+
for the conversation's transcript: if it is there, or someone typed in
|
|
58
|
+
the last session, it resumes with `--resume <id>`; if not, it starts
|
|
59
|
+
fresh under the same id, in the same folder and scope, and says "nothing
|
|
60
|
+
to resume yet; started fresh". Every launch also marks the record
|
|
61
|
+
`launched: true`, for runtimes Den can't check this way. If Claude Code
|
|
62
|
+
still says there is no conversation (Den now reads past its trailing
|
|
63
|
+
terminal codes), Den starts fresh once. If a
|
|
64
|
+
fresh start says the id is in use, Den resumes once instead. If both
|
|
65
|
+
fail, the error names the id and Start fresh (the route answers 502
|
|
66
|
+
`resume_failed`).
|
|
67
|
+
- **No "+ Start agent" for a workspace that doesn't exist** (`?ws=` with
|
|
68
|
+
an unknown name).
|
|
69
|
+
- **`agents stop` waits for the agent to end.** It returns once the
|
|
70
|
+
agent's session has ended, so `agents stop X && agents resume X` works.
|
|
71
|
+
It waits up to 5 seconds. Still running then, it prints
|
|
72
|
+
`X is still stopping` and exits 1. `--no-wait` returns at once, as
|
|
73
|
+
before. The stop route answers 200 with `stopped: true` once the session
|
|
74
|
+
has ended, or 202 with `stillStopping: true`.
|
|
75
|
+
- **The Agents page never shows All in place of its `?ws=`.** A workspace
|
|
76
|
+
that isn't active gets one line, "Workspace west isn't active · Switch
|
|
77
|
+
to west", and the list shows that workspace alone. Clear stopped acts on
|
|
78
|
+
exactly the workspaces listed. A name no workspace has gets the same
|
|
79
|
+
line and an empty list.
|
|
80
|
+
- **Den's sudo is `/usr/bin/sudo`.** A `sudo` placed earlier on `PATH` no
|
|
81
|
+
longer gets the admin password. Den looks on `PATH` only when there is no
|
|
82
|
+
`/usr/bin/sudo`. `DEN_SYSTEM_SUDO` names another absolute path.
|
|
83
|
+
- **`mcp` refuses an argument it doesn't take.** A typo such as
|
|
84
|
+
`--worksapce` exits 2 with the usage, instead of starting an unscoped
|
|
85
|
+
server.
|
|
86
|
+
|
|
87
|
+
### Metrics (before → after)
|
|
88
|
+
|
|
89
|
+
- Share of agent sessions with attributed tokens and outcome: ~0 → ≥90%.
|
|
90
|
+
On the test fixture (11 agents): 0% → 91%. The one left out runs on
|
|
91
|
+
codex, which no usage reader covers yet.
|
|
92
|
+
|
|
93
|
+
### Migrations
|
|
94
|
+
|
|
95
|
+
- None needed. To remove stopped agents on their own, set
|
|
96
|
+
`agents.remove_stopped_after` in `config/local/user.yml` to `1d` or `7d`
|
|
97
|
+
(the default is `never`). Interrupted agents are never removed this way.
|
|
98
|
+
To let an orchestrator resume agents, add `agent_resume` to
|
|
99
|
+
`mcp.actions`.
|
|
100
|
+
|
|
101
|
+
## [0.4.7] - 2026-11-07
|
|
102
|
+
|
|
103
|
+
Agents read slices, wait in the background and hand back plainly (den#5,
|
|
104
|
+
the default-pack guardrails issue). Every persona in Den's default pack
|
|
105
|
+
gets Den's shared instructions and, on Claude Code, Den's hooks, with no
|
|
106
|
+
setup. `doctor` warns about polling loops it sees in the local dev metrics.
|
|
107
|
+
|
|
108
|
+
### Added
|
|
109
|
+
|
|
110
|
+
- **The Stop hook reads every last message.** With a `## Hand-back`
|
|
111
|
+
heading it checks the whole hand-back, as before. Any other last message
|
|
112
|
+
gets the plain-writing check: every ID explained. It still never blocks
|
|
113
|
+
twice in a row.
|
|
114
|
+
- **`URL: none` in a hand-back.** Work with no page (a CLI change, a
|
|
115
|
+
refused request) says `URL: none`, with a reason if it likes. An empty
|
|
116
|
+
or missing URL still fails.
|
|
117
|
+
|
|
118
|
+
- **`down --json` and `web down --json`.** The clean-up steps answer in
|
|
119
|
+
JSON like every other onboarding step: `{ ok, workspace, services }` and
|
|
120
|
+
`{ ok, url, port, running, stopped }`. Both are safe to re-run.
|
|
121
|
+
|
|
122
|
+
- **Hooks for default-pack agents.** A Claude Code agent from Den's default
|
|
123
|
+
pack runs with Den's own hooks, in its own `--settings` file (never your
|
|
124
|
+
`~/.claude`). A `PreToolUse` hook on Bash and Read blocks reading all of
|
|
125
|
+
a log or a large file into the conversation (`cat`, `less`, `tail` from
|
|
126
|
+
a line such as `tail +1`, a Read with no limit; a plain `tail` passes) and says to read a slice or use a
|
|
127
|
+
subagent. It blocks a foreground `sleep`, alone or in an `until`/`while`/
|
|
128
|
+
`for` loop, and says to run in the background or use a wait primitive.
|
|
129
|
+
It blocks nothing else.
|
|
130
|
+
- **Shared persona instructions.** Every default-pack persona's role prompt
|
|
131
|
+
ends with Den's `guardrails/instructions.md`: write plainly, in short
|
|
132
|
+
sentences; say what every issue or ID is; ask each decision as a numbered
|
|
133
|
+
yes/no question; no caveat sections; and the hand-back template (URL,
|
|
134
|
+
machine, branch, what changed, what to click, decisions).
|
|
135
|
+
- **The hand-back check.** `<cli> agents handback-check <file|->` rejects a
|
|
136
|
+
hand-back with a missing part, an unexplained ID, or a decision that is
|
|
137
|
+
not a numbered yes/no question, and says what to fix. A `Stop` hook runs
|
|
138
|
+
the same check on a turn that ends with a `Hand-back` heading, and sends
|
|
139
|
+
Claude back to fix it once.
|
|
140
|
+
- **doctor warns about polling loops.** It reads the end of the local
|
|
141
|
+
`.dev-metrics/events.jsonl` (at most 8 MB) and warns when one command,
|
|
142
|
+
run from one place, ran more than 600 times in an hour:
|
|
143
|
+
`looks like a polling loop: status in services/api, 1200 calls in an hour (…)`.
|
|
144
|
+
It is local and sends nothing.
|
|
145
|
+
- **Each dev-metrics line records `cwd`**, relative to the distribution root
|
|
146
|
+
(`.` at the root, `outside` elsewhere), never an absolute or home path.
|
|
147
|
+
Like every field, it is sent only when you opt in.
|
|
148
|
+
|
|
149
|
+
### Metrics (before → after)
|
|
150
|
+
|
|
151
|
+
- Polling loops a week: 18,600 → under 100 expected, since the hook blocks
|
|
152
|
+
them. The "after" is counted on the candidate's first week.
|
|
153
|
+
- Incomplete hand-backs: not measured → 0 for default-pack agents (the Stop
|
|
154
|
+
hook sends them back). The candidate's first run sets the baseline.
|
|
155
|
+
- Hand-backs with an unexplained ID: no baseline → 0 for default-pack
|
|
156
|
+
agents. The candidate's first run sets the baseline.
|
|
157
|
+
- doctor on a replay of a three-day polling burst: no warning → it warns.
|
|
158
|
+
|
|
159
|
+
### Migrations
|
|
160
|
+
|
|
161
|
+
- None needed. To turn the guardrails off for Den's pack, set
|
|
162
|
+
`agents.default_pack: { hooks: false, instructions: false }`. To turn
|
|
163
|
+
them on for your own pack, write its `persona_packs` entry as
|
|
164
|
+
`{ path: <dir>, hooks: true, instructions: true }`. A persona's
|
|
165
|
+
`den_hooks` / `den_instructions` front matter wins over its pack's.
|
|
166
|
+
|
|
7
167
|
## [0.4.6] - 2026-11-07
|
|
8
168
|
|
|
9
169
|
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.
|
|
14
|
-
<img src="docs/images/0.4.
|
|
13
|
+
<source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.8/hero-dark.png">
|
|
14
|
+
<img src="docs/images/0.4.8/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.
|
|
41
|
+
<img src="docs/images/0.4.8/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.
|
|
72
|
-
<img src="docs/images/0.4.
|
|
73
|
-
<img src="docs/images/0.4.
|
|
74
|
-
<img src="docs/images/0.4.
|
|
71
|
+
<img src="docs/images/0.4.8/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.8/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.8/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.8/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
|
package/docs/distributions.md
CHANGED
|
@@ -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
|
|
@@ -491,6 +497,11 @@ in the distribution's config always wins over the one in user.yml.
|
|
|
491
497
|
`backendAvailable`, `reason` and `hasStored` (`opAvailable` is a
|
|
492
498
|
deprecated alias of `backendAvailable`, kept for one minor).
|
|
493
499
|
|
|
500
|
+
**Den's sudo is `/usr/bin/sudo`.** Den checks and caches the admin
|
|
501
|
+
password with `/usr/bin/sudo` only, never a `sudo` found earlier on `PATH`.
|
|
502
|
+
Only on a machine with no `/usr/bin/sudo` does it look on `PATH`.
|
|
503
|
+
`DEN_SYSTEM_SUDO=<absolute path>` names another system sudo.
|
|
504
|
+
|
|
494
505
|
The admin password is for sudo, and nothing else (LK-0). It is no sign-in
|
|
495
506
|
and no access check: starting, stopping and resuming agents, sessions and
|
|
496
507
|
terminals, and a service whose commands run no sudo, never ask for it.
|
|
@@ -684,6 +695,18 @@ one is started (see Sessions).
|
|
|
684
695
|
server, Den's MCP) and answers with one JSON object: see
|
|
685
696
|
[Set up by your agent](#up-doctor-and-mcp-connect).
|
|
686
697
|
|
|
698
|
+
doctor also reads the end of the local `.dev-metrics/events.jsonl` (at
|
|
699
|
+
most 8 MB of it, however big the file) and warns when one command, run
|
|
700
|
+
from one place, ran more than 600 times in an hour. That is almost always
|
|
701
|
+
an agent polling. It is a warning, never a failure, and nothing is sent
|
|
702
|
+
(see [Dev metrics](#dev-metrics-local-unless-you-opt-in)):
|
|
703
|
+
|
|
704
|
+
```
|
|
705
|
+
Acme doctor: dev-metrics (local, never sent)
|
|
706
|
+
|
|
707
|
+
⚠ looks like a polling loop: status in services/api, 1200 calls in an hour (2026-11-04T09:00:00Z–2026-11-04T09:59:57Z)
|
|
708
|
+
```
|
|
709
|
+
|
|
687
710
|
## den.yml: the product profile
|
|
688
711
|
|
|
689
712
|
Every runtime name derives from `product.name`. Set only what you want to
|
|
@@ -1818,6 +1841,9 @@ the server: only registered tools (so actions only as named in
|
|
|
1818
1841
|
the tool's text to stdout and exits 1 when the tool reports an error (a
|
|
1819
1842
|
refusal, a failed action, the web server down).
|
|
1820
1843
|
|
|
1844
|
+
`mcp` takes only the arguments its usage lists. Any other (a typo such as
|
|
1845
|
+
`--worksapce`) exits 2 with the usage, before anything starts.
|
|
1846
|
+
|
|
1821
1847
|
### Config
|
|
1822
1848
|
|
|
1823
1849
|
Without an `mcp:` block in the org config (`config/<name>.yml`) only the read
|
|
@@ -1900,6 +1926,12 @@ Action tools (only those named in `mcp.actions`): `service_up`,
|
|
|
1900
1926
|
`workspace_sync` (`/api/workspace/sync`) and `run_tests` (`/api/run`). There
|
|
1901
1927
|
is no deploy tool.
|
|
1902
1928
|
|
|
1929
|
+
`agent_resume` (AG-14.4) resumes a stopped or interrupted agent, for an
|
|
1930
|
+
orchestrator. It is off unless `mcp.actions` names it. A scoped server
|
|
1931
|
+
(`--workspace`) resumes agents of its own workspace or a granted one, never
|
|
1932
|
+
itself. The web server checks the same rule, and the resume route is the
|
|
1933
|
+
only agent route an agent's token reaches.
|
|
1934
|
+
|
|
1903
1935
|
### Guards
|
|
1904
1936
|
|
|
1905
1937
|
- **Lifecycle only, local only.** The service tools run a manifest's `up`,
|
|
@@ -2012,6 +2044,7 @@ server scoped to its workspace.
|
|
|
2012
2044
|
<cli> agents resume <id> [-w <ws>]
|
|
2013
2045
|
<cli> agents personas [--json]
|
|
2014
2046
|
<cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
|
|
2047
|
+
<cli> agents handback-check <file|-> # is a hand-back complete and plain (see Guardrails)
|
|
2015
2048
|
<cli> sessions attach <ws>:<id> # its terminal; ctrl-] detaches
|
|
2016
2049
|
```
|
|
2017
2050
|
|
|
@@ -2084,7 +2117,14 @@ The routes: `GET /api/workspaces/<ws>/agents`, `POST
|
|
|
2084
2117
|
agent of the workspaces in view, and their terminals in the browser.
|
|
2085
2118
|
|
|
2086
2119
|
- **The list** is grouped by workspace and follows the scope bar (or the
|
|
2087
|
-
page's `?ws=<ws>`, which "Open in Agents →" sets).
|
|
2120
|
+
page's `?ws=<ws>`, which "Open in Agents →" sets). A `?ws=` is never
|
|
2121
|
+
replaced by All. A workspace that isn't active gets one line, "Workspace
|
|
2122
|
+
west isn't active · Switch to west", and the list shows it alone.
|
|
2123
|
+
Switch makes it active and views it, as the top bar's Workspace menu
|
|
2124
|
+
does. A name no workspace has gets the same line, an empty list, and no
|
|
2125
|
+
"+ Start agent".
|
|
2126
|
+
`GET /api/sessions?ws=<ws>` lists that workspace even when it isn't
|
|
2127
|
+
active, and says `requested: { workspace, active, known }`. Each row has its
|
|
2088
2128
|
state, persona (a `sessions start` session is labelled "session"),
|
|
2089
2129
|
runtime and directory, and "waiting" when it waits on you (once the
|
|
2090
2130
|
session host can tell, AG-6a; not yet). An agent whose
|
|
@@ -2120,6 +2160,84 @@ text frame `{"type":"resize","cols":c,"rows":r}` resizes, and the server
|
|
|
2120
2160
|
sends `{"type":"session",…}` first and `{"type":"exit","exitCode":n}` when
|
|
2121
2161
|
the session ends.
|
|
2122
2162
|
|
|
2163
|
+
### Stopped agents: Resume, Start fresh, Remove
|
|
2164
|
+
|
|
2165
|
+
An agent that stops stays in the list, below the running ones (AG-14, the
|
|
2166
|
+
stopped-agents slice). Its row says why it stopped: "Stopped by you",
|
|
2167
|
+
"Exited (code n)", or "Interrupted" when Den or the machine restarted under
|
|
2168
|
+
it. Each row has three buttons:
|
|
2169
|
+
|
|
2170
|
+
- **Resume** continues the same conversation. Den runs the runtime's own
|
|
2171
|
+
resume with the conversation id it saved (Claude Code: `--resume <id>`).
|
|
2172
|
+
The agent comes back in the folder it had, with its workspace and its
|
|
2173
|
+
grants. A grant you took out of the config since is gone; one you added
|
|
2174
|
+
is not taken. Resume is never a new conversation fed the old prompt.
|
|
2175
|
+
Den decides before it launches. For Claude Code it looks for the
|
|
2176
|
+
conversation's transcript (`<id>.jsonl` under
|
|
2177
|
+
`$CLAUDE_CONFIG_DIR/projects`, else `~/.claude/projects`): if it is
|
|
2178
|
+
there, or someone typed in the last session, the resume is
|
|
2179
|
+
`--resume <id>`. If not, nobody ever typed, so Den starts fresh under the
|
|
2180
|
+
same id, folder and scope, and says "nothing to resume yet; started
|
|
2181
|
+
fresh" (the route adds `fresh: true`). For a runtime Den can't check,
|
|
2182
|
+
the record decides: every launch marks it `launched: true`, and a
|
|
2183
|
+
launched record resumes. As a backstop, if the runtime still says there
|
|
2184
|
+
is no conversation, Den starts fresh once and says so.
|
|
2185
|
+
If a fresh start says the id is already in use, Den resumes once
|
|
2186
|
+
instead. If both fail, the CLI exits 1 and the route answers 502 with
|
|
2187
|
+
code `resume_failed`, naming the id and Start fresh.
|
|
2188
|
+
- **Start fresh** starts a new conversation: same persona, workspace and id.
|
|
2189
|
+
- **Remove** takes the row away in one click. The toast offers **Undo** for
|
|
2190
|
+
10 seconds. Den keeps the removed record for an hour, then forgets it. It
|
|
2191
|
+
never touches the runtime's own history.
|
|
2192
|
+
|
|
2193
|
+
**Stop waits for the agent to end.** `agents stop <id>` returns once the
|
|
2194
|
+
session has ended, so a resume right after it works. It waits up to 5
|
|
2195
|
+
seconds. If the session is still running then, it prints `<id> is still
|
|
2196
|
+
stopping` and exits 1. `--no-wait` returns once TERM is sent. The route
|
|
2197
|
+
`POST /api/workspaces/<ws>/agents/<id>/stop` answers the same way: 200
|
|
2198
|
+
with `{ ok, agent, wasRunning, stopped: true }` once the session has
|
|
2199
|
+
ended, or 202 with `stopped: false, stillStopping: true`.
|
|
2200
|
+
|
|
2201
|
+
A runtime that can't resume (codex, gemini, grok), or an agent with no
|
|
2202
|
+
saved conversation id, shows only Start fresh, and says why. **Clear
|
|
2203
|
+
stopped**, beside + Start agent, removes every stopped agent in view. A
|
|
2204
|
+
running agent is never removed: stop it first. None of this asks for the
|
|
2205
|
+
admin password.
|
|
2206
|
+
|
|
2207
|
+
**After a restart**, the agents it cut off show Interrupted, and one line
|
|
2208
|
+
sits above the list: "2 agents were interrupted when Acme restarted ·
|
|
2209
|
+
Resume all · Review". Resume all resumes each one that can. Review marks
|
|
2210
|
+
them and moves to the first. Agents whose persona is `standing: true`, or
|
|
2211
|
+
has `resume_on_restart: true`, resume on their own when the web server
|
|
2212
|
+
starts.
|
|
2213
|
+
|
|
2214
|
+
**Removed on their own.** Stopped agents can go after a while:
|
|
2215
|
+
|
|
2216
|
+
```yaml
|
|
2217
|
+
# config/local/user.yml
|
|
2218
|
+
agents:
|
|
2219
|
+
remove_stopped_after: 7d # never (the default), 1d or 7d
|
|
2220
|
+
```
|
|
2221
|
+
|
|
2222
|
+
An interrupted agent is never removed this way. Any other value fails,
|
|
2223
|
+
naming the key.
|
|
2224
|
+
|
|
2225
|
+
The CLI does the same:
|
|
2226
|
+
|
|
2227
|
+
```sh
|
|
2228
|
+
./bin/acme agents list --stopped # only the stopped ones
|
|
2229
|
+
./bin/acme agents resume planner # the same conversation
|
|
2230
|
+
./bin/acme agents resume --all-interrupted # every interrupted one that can
|
|
2231
|
+
./bin/acme agents rm planner # one stopped agent
|
|
2232
|
+
./bin/acme agents rm --stopped [-w north] # every stopped one
|
|
2233
|
+
```
|
|
2234
|
+
|
|
2235
|
+
The routes (a person's only, audited): `POST
|
|
2236
|
+
/api/workspaces/<ws>/agents/<id>/resume`, `…/fresh` and `…/remove`, and
|
|
2237
|
+
`POST /api/agents/clear-stopped` (`{ workspaces }`), `/api/agents/restore`
|
|
2238
|
+
(`{ agents }`, the Undo) and `/api/agents/resume-interrupted` (`{
|
|
2239
|
+
workspaces }`).
|
|
2240
|
+
|
|
2123
2241
|
### An agent's environment
|
|
2124
2242
|
|
|
2125
2243
|
An agent's environment starts empty and gets, in order:
|
|
@@ -2215,6 +2333,8 @@ standing: true # starts with its workspace (later slices)
|
|
|
2215
2333
|
context: [docs/specs, the design system] # what to read first
|
|
2216
2334
|
runtime: generic # default den-claude
|
|
2217
2335
|
permission_mode: plan # den-claude only; default `default`
|
|
2336
|
+
den_hooks: false # Den's guardrail hooks for this persona (see Guardrails)
|
|
2337
|
+
den_instructions: false # Den's shared instructions for this persona
|
|
2218
2338
|
argv: ["some-cli", "--model", "{model}", "--mcp-config", "{mcp_config}"]
|
|
2219
2339
|
---
|
|
2220
2340
|
|
|
@@ -2236,6 +2356,104 @@ used without converting them. Files without front matter (a README) are
|
|
|
2236
2356
|
skipped; two files with one id in a pack, or a pack that isn't a directory,
|
|
2237
2357
|
are errors naming the file or the key.
|
|
2238
2358
|
|
|
2359
|
+
### Guardrails: hooks, shared instructions, hand-backs
|
|
2360
|
+
|
|
2361
|
+
Every persona in Den's default pack gets two things with no setup: Den's
|
|
2362
|
+
**shared persona instructions** and, on den-claude, Den's **hooks**. Both
|
|
2363
|
+
are Den's own files, in `src/shared/agents/guardrails/` of the package.
|
|
2364
|
+
|
|
2365
|
+
**Shared instructions** (`instructions.md`) are appended to the role prompt
|
|
2366
|
+
(den-claude's `--append-system-prompt`; any runtime that takes the role
|
|
2367
|
+
prompt gets them). They say:
|
|
2368
|
+
|
|
2369
|
+
- write to people plainly, in short sentences;
|
|
2370
|
+
- never name an issue or ID without saying what it is;
|
|
2371
|
+
- ask each decision as a numbered yes/no question;
|
|
2372
|
+
- no caveat sections, and nothing the person doesn't need in order to act;
|
|
2373
|
+
- read a slice of a log, or hand it to a subagent; don't poll with `sleep`;
|
|
2374
|
+
- end a hand-back with the template below.
|
|
2375
|
+
|
|
2376
|
+
**Hooks** (`hooks.json`, a Claude Code settings fragment) go in the agent's
|
|
2377
|
+
own settings file, `--settings <stateDir>/agent-files/<id>/settings.json`.
|
|
2378
|
+
Den never writes your `~/.claude`; your own hooks still run.
|
|
2379
|
+
|
|
2380
|
+
- `PreToolUse` on `Bash|Read` (`pretooluse.mjs`, plain Node, no imports)
|
|
2381
|
+
blocks, with a reason Claude sees (exit 2):
|
|
2382
|
+
- reading all of a log or a large file into the conversation: a `Read`
|
|
2383
|
+
with no `limit`, or `cat`, `less`, or `tail` from a line (`tail +1`,
|
|
2384
|
+
`tail -n +1`), whose output reaches the conversation. A log is `*.log` or `*.log.<N>`, a `.jsonl` under
|
|
2385
|
+
`.dev-metrics/`, or a `.jsonl` under `.claude/projects/` (a transcript).
|
|
2386
|
+
Large is over 256 KB. `cat x.log | grep …`, `cat x.log > y`, a plain
|
|
2387
|
+
`tail x.log` (10 lines) and `tail -n 100 x.log` pass. The reason says to read a slice or hand the
|
|
2388
|
+
file to a subagent.
|
|
2389
|
+
- `sleep` in the foreground, alone or in an `until …; do sleep …; done`,
|
|
2390
|
+
`while` or `for` loop. A Bash call with `run_in_background`, and
|
|
2391
|
+
`sleep … &`, pass. The reason says to run in the background or use a
|
|
2392
|
+
wait primitive. Recent Claude Code versions have their own sleep guard,
|
|
2393
|
+
which answers first (and also stops `sleep … &`). The sleep is blocked
|
|
2394
|
+
either way; Den's hook covers runtimes and versions without that guard.
|
|
2395
|
+
- Nothing else. Quoted text, heredoc bodies and other tools always pass,
|
|
2396
|
+
and a hook that can't read its input lets the call through.
|
|
2397
|
+
- `Stop` (`stop-hook.ts`) checks a turn's last message, which is what the
|
|
2398
|
+
person reads. With a `Hand-back` heading it gets the whole hand-back
|
|
2399
|
+
check; any other message gets the plain-writing part only (every ID
|
|
2400
|
+
explained). If the check fails, Claude is told what to fix and keeps
|
|
2401
|
+
going (Claude Code's `decision: "block"`). It never blocks twice in a
|
|
2402
|
+
row (`stop_hook_active`), so it can't loop. Den adds no `SubagentStop` hook: a subagent reports to its parent
|
|
2403
|
+
agent, not to a person.
|
|
2404
|
+
|
|
2405
|
+
**The hand-back template**, which the check enforces:
|
|
2406
|
+
|
|
2407
|
+
```markdown
|
|
2408
|
+
## Hand-back
|
|
2409
|
+
|
|
2410
|
+
- URL: <the full link the person opens, with https://>
|
|
2411
|
+
- Machine: <the machine it runs on>
|
|
2412
|
+
- Branch: <the branch>
|
|
2413
|
+
- What changed: <what is different now>
|
|
2414
|
+
- What to click: <the steps to see it working>
|
|
2415
|
+
- Decisions:
|
|
2416
|
+
1. <a question the person answers yes or no>?
|
|
2417
|
+
```
|
|
2418
|
+
|
|
2419
|
+
`<cli> agents handback-check <file|-> [--json]` runs the same check on a
|
|
2420
|
+
file or stdin: exit 0 when the hand-back passes, 1 with each problem. It
|
|
2421
|
+
rejects a hand-back when:
|
|
2422
|
+
|
|
2423
|
+
- a part is missing or empty. A part is a line starting with its label and
|
|
2424
|
+
a colon (bold is fine), or a heading that is its label. URL must hold a
|
|
2425
|
+
link (`://`), or say `none` for work with no page (`URL: none — a CLI
|
|
2426
|
+
change` is fine). `Decisions: none` is allowed.
|
|
2427
|
+
- an ID is bare. An ID is `#<digits>`, or 1 to 4 capitals, an optional `-`,
|
|
2428
|
+
digits and an optional `.<digits>` (`R12`, `AG-14`, `KB-4.1`). Links,
|
|
2429
|
+
`inline code`, code blocks, and the URL and Branch parts are not checked.
|
|
2430
|
+
An ID is explained (glossed) when
|
|
2431
|
+
1. a parenthetical of two or more words follows it: `#5 (the hooks issue)`;
|
|
2432
|
+
2. a colon or a spaced dash and two or more words follow it:
|
|
2433
|
+
`R12: plain writing`;
|
|
2434
|
+
3. it sits in a parenthetical of IDs only, right after a word:
|
|
2435
|
+
`plain writing (R12)`, `the issues (#5, #6)`;
|
|
2436
|
+
4. or the same ID was explained earlier in the text.
|
|
2437
|
+
- a decision isn't a numbered yes/no question: each item must be numbered
|
|
2438
|
+
(`1.` or `1)`), end in `?`, not start with what, which, how, why, when,
|
|
2439
|
+
where or who, and not offer a choice (`or`).
|
|
2440
|
+
|
|
2441
|
+
**Turning them off or on.** They are on for Den's default pack and off for
|
|
2442
|
+
your own packs. A pack's setting applies to each of its personas; a
|
|
2443
|
+
persona's front matter (`den_hooks`, `den_instructions`) wins over it:
|
|
2444
|
+
|
|
2445
|
+
```yaml
|
|
2446
|
+
agents:
|
|
2447
|
+
default_pack: { hooks: false } # Den's pack: no hooks, instructions kept
|
|
2448
|
+
persona_packs:
|
|
2449
|
+
- personas # your pack: neither
|
|
2450
|
+
- { path: more-personas, hooks: true, instructions: true } # both
|
|
2451
|
+
```
|
|
2452
|
+
|
|
2453
|
+
A persona of yours that replaces a default one (same id) takes your
|
|
2454
|
+
pack's setting, not Den's pack's. Anything but `true` or `false`, or an
|
|
2455
|
+
unknown key, is an error naming the key.
|
|
2456
|
+
|
|
2239
2457
|
### Runtimes
|
|
2240
2458
|
|
|
2241
2459
|
`<cli> doctor` lists the runtime kind. Den knows the runtimes in its
|
|
@@ -2418,11 +2636,36 @@ metrics:
|
|
|
2418
2636
|
share: true
|
|
2419
2637
|
```
|
|
2420
2638
|
|
|
2639
|
+
Each line also records `cwd`: where the command ran, relative to the
|
|
2640
|
+
distribution root (`.` for the root), or `outside` for anywhere else. It is
|
|
2641
|
+
never an absolute or home path. [`<cli> doctor`](#doctor) uses it to warn
|
|
2642
|
+
about polling loops.
|
|
2643
|
+
|
|
2421
2644
|
Only the boolean `true` counts. With it, and a Sentry DSN in
|
|
2422
2645
|
`DEV_METRICS_SENTRY_DSN` (or a synced `dev-metrics.config`), each line is
|
|
2423
2646
|
also sent, best effort. A DSN on its own never sends. `CI=true` and
|
|
2424
2647
|
`DEV_METRICS_DISABLED=1` stop sending even when opted in.
|
|
2425
2648
|
|
|
2649
|
+
## Agent usage: tokens and outcomes, read locally
|
|
2650
|
+
|
|
2651
|
+
`<cli> usage agents [-w <ws>] [--json]` reads each agent's transcripts on
|
|
2652
|
+
this machine and saves its tokens and outcome in its record: the pull
|
|
2653
|
+
request it opened, whether it merged, whether its last test run passed,
|
|
2654
|
+
and its wall-clock time. It matches transcripts to agents by the
|
|
2655
|
+
conversation id Den saved at start. It prints the share of agents with
|
|
2656
|
+
both. Transcripts never leave the machine, and their text is never kept:
|
|
2657
|
+
only counts.
|
|
2658
|
+
|
|
2659
|
+
`<cli> usage report [--days 7] [--json]` writes a weekly summary to
|
|
2660
|
+
`<state dir>/usage/<date>.json` and `.md`, compared with the last one:
|
|
2661
|
+
calls, cache re-reads, polls, sleeps, compactions, corrections and the
|
|
2662
|
+
attributed share.
|
|
2663
|
+
|
|
2664
|
+
Den reads Claude Code's transcripts (`$CLAUDE_CONFIG_DIR/projects`, else
|
|
2665
|
+
`~/.claude/projects`). A plugin can add a reader for another runtime with
|
|
2666
|
+
the `usage` kind (server tier). A runtime with no reader shows "no usage
|
|
2667
|
+
source reads <runtime>".
|
|
2668
|
+
|
|
2426
2669
|
## Pinning and upgrading
|
|
2427
2670
|
|
|
2428
2671
|
`package.json` pins an exact Den version (`create` writes the version that
|
package/package.json
CHANGED
|
@@ -70,7 +70,7 @@
|
|
|
70
70
|
"pattern": "^(?!/)(?!.*\\.\\.)[A-Za-z0-9._/-]+$"
|
|
71
71
|
},
|
|
72
72
|
"kind": {
|
|
73
|
-
"description": "files tier: theme, catalog, persona-pack. browser tier: page. server tier: mcp-tools, work-items, runtime, session-backend, notifier, secrets, identity, health-source.",
|
|
73
|
+
"description": "files tier: theme, catalog, persona-pack. browser tier: page. server tier: mcp-tools, work-items, runtime, session-backend, notifier, secrets, identity, health-source, usage.",
|
|
74
74
|
"enum": [
|
|
75
75
|
"theme",
|
|
76
76
|
"catalog",
|
|
@@ -83,7 +83,8 @@
|
|
|
83
83
|
"notifier",
|
|
84
84
|
"secrets",
|
|
85
85
|
"identity",
|
|
86
|
-
"health-source"
|
|
86
|
+
"health-source",
|
|
87
|
+
"usage"
|
|
87
88
|
]
|
|
88
89
|
},
|
|
89
90
|
"provide": {
|
|
@@ -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
|
|
69
|
-
|
|
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,
|
|
@@ -54,6 +59,7 @@ import {
|
|
|
54
59
|
import { builtinEntry } from "./catalog.js";
|
|
55
60
|
import { catalogFind } from "./catalog-runtime.js";
|
|
56
61
|
import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
|
|
62
|
+
import { claudeProjectsDir } from "../usage/paths.js";
|
|
57
63
|
|
|
58
64
|
export const DEN_CLAUDE_ID = "den-claude";
|
|
59
65
|
const BIN = "claude";
|
|
@@ -221,7 +227,8 @@ function common(input: LaunchInput): {
|
|
|
221
227
|
} {
|
|
222
228
|
const file = mcpConfigPath(input);
|
|
223
229
|
const mode = permissionMode(input);
|
|
224
|
-
const
|
|
230
|
+
const hooks = input.persona.guardrails?.hooks ? guardrailHooks() : null;
|
|
231
|
+
const settings = input.stateDir || hooks ? settingsPath(input) : null;
|
|
225
232
|
return {
|
|
226
233
|
argv: [
|
|
227
234
|
"--mcp-config",
|
|
@@ -241,13 +248,21 @@ function common(input: LaunchInput): {
|
|
|
241
248
|
],
|
|
242
249
|
files: {
|
|
243
250
|
[file]: mcpConfigJson(input.mcp),
|
|
244
|
-
...(settings
|
|
251
|
+
...(settings
|
|
245
252
|
? {
|
|
246
253
|
[settings]: `${JSON.stringify(
|
|
247
254
|
{
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
...(input.stateDir
|
|
256
|
+
? {
|
|
257
|
+
permissions: {
|
|
258
|
+
deny: stateDirDenyRules(
|
|
259
|
+
input.stateDir,
|
|
260
|
+
input.env?.HOME,
|
|
261
|
+
),
|
|
262
|
+
},
|
|
263
|
+
}
|
|
264
|
+
: {}),
|
|
265
|
+
...(hooks ? { hooks } : {}),
|
|
251
266
|
},
|
|
252
267
|
null,
|
|
253
268
|
2,
|
|
@@ -303,6 +318,24 @@ export const denClaude: Runtime = {
|
|
|
303
318
|
warnings: c.warnings,
|
|
304
319
|
};
|
|
305
320
|
},
|
|
321
|
+
// Its transcript, <token>.jsonl in any project under its projects dir.
|
|
322
|
+
conversationExists(token, env = process.env) {
|
|
323
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/.test(token)) return false;
|
|
324
|
+
const root = claudeProjectsDir(env);
|
|
325
|
+
let projects: string[] = [];
|
|
326
|
+
try {
|
|
327
|
+
projects = fs.readdirSync(root);
|
|
328
|
+
} catch {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
return projects.some((p) =>
|
|
332
|
+
fs.existsSync(path.join(root, p, `${token}.jsonl`)),
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
launchFailures: {
|
|
336
|
+
noConversation: /no conversation found/i,
|
|
337
|
+
idInUse: /already in use/i,
|
|
338
|
+
},
|
|
306
339
|
approvals: "terminal",
|
|
307
340
|
envPassthrough: ["CLAUDE_CONFIG_DIR"],
|
|
308
341
|
// Claude Code's session plumbing: the session id, its messaging socket and
|