@monster-orange/den 0.4.14 → 0.4.15

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 (54) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +7 -7
  3. package/docs/distributions.md +144 -8
  4. package/package.json +1 -1
  5. package/src/shared/actor-token.ts +12 -0
  6. package/src/shared/agents/conversations.ts +33 -4
  7. package/src/shared/agents/den-claude.ts +13 -3
  8. package/src/shared/agents/records.ts +18 -0
  9. package/src/shared/agents/runtime.ts +6 -1
  10. package/src/shared/claim-code.ts +163 -0
  11. package/src/shared/cli-command.ts +31 -0
  12. package/src/shared/den-config.ts +12 -0
  13. package/src/shared/environment-manager/workspaces.ts +20 -0
  14. package/src/shared/web-session.ts +64 -5
  15. package/src/ui/cli/agent-ops.ts +75 -22
  16. package/src/ui/cli/agents-command.ts +4 -1
  17. package/src/ui/cli/doctor-command.ts +29 -3
  18. package/src/ui/cli/doctor-onboard.ts +26 -0
  19. package/src/ui/cli/flag-parser.ts +9 -1
  20. package/src/ui/cli/index.ts +29 -3
  21. package/src/ui/cli/onboard-command.ts +180 -0
  22. package/src/ui/cli/onboard-reads.ts +613 -0
  23. package/src/ui/cli/repo-command.ts +156 -10
  24. package/src/ui/cli/web-command.ts +92 -1
  25. package/src/ui/mcp/api.ts +14 -2
  26. package/src/ui/mcp/server.ts +12 -2
  27. package/src/ui/mcp/tools.ts +368 -1
  28. package/src/ui/server/agent-routes.ts +2 -5
  29. package/src/ui/server/approval-routes.ts +390 -0
  30. package/src/ui/server/approvals.ts +416 -0
  31. package/src/ui/server/github-routes.ts +12 -4
  32. package/src/ui/server/held-actions.ts +203 -0
  33. package/src/ui/server/http-helpers.ts +35 -0
  34. package/src/ui/server/index.ts +286 -16
  35. package/src/ui/server/lifecycle-exec.ts +70 -0
  36. package/src/ui/server/onboard-routes.ts +132 -0
  37. package/src/ui/server/pair-routes.ts +7 -4
  38. package/src/ui/server/public/agents-page.js +91 -4
  39. package/src/ui/server/public/error-page.js +27 -3
  40. package/src/ui/server/public/header.js +1 -1
  41. package/src/ui/server/public/i18n/es.js +29 -0
  42. package/src/ui/server/public/i18n/ja.js +31 -0
  43. package/src/ui/server/public/index.html +1 -1
  44. package/src/ui/server/public/local-dev.html +5 -4
  45. package/src/ui/server/public/persona-ui.css +21 -0
  46. package/src/ui/server/public/release.html +0 -1
  47. package/src/ui/server/public/settings-registry.js +3 -1
  48. package/src/ui/server/public/settings.html +9 -1
  49. package/src/ui/server/public/shell.js +313 -0
  50. package/src/ui/server/request-guard.ts +2 -0
  51. package/src/ui/server/route-classes.ts +28 -1
  52. package/src/ui/server/session-routes.ts +11 -3
  53. package/src/ui/server/workspace-routes.ts +21 -10
  54. package/themes/den/den-theme.json +6 -6
package/CHANGELOG.md CHANGED
@@ -4,6 +4,133 @@ 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.15] - 2026-11-07
8
+
9
+ ### Added
10
+
11
+ - **Onboarding reads for agents, the CLI and the browser (ON-3).** Three new
12
+ MCP read tools, always registered, each with a CLI twin and a web API
13
+ route (GET, a read in the route table) that a page can show as it is:
14
+ - `onboard_status`, `<cli> onboard status [--json]`,
15
+ `/api/onboard/status`: what's set up (a distribution, Node, git, Docker,
16
+ the web server, workspaces, repos, a running service, Den's MCP), each
17
+ `ok`, `warn` or `todo`, and the one next step in plain words with its
18
+ command. It never runs an agent's CLI to check Den's MCP.
19
+ - `repos_detect`, `<cli> repo detect <folder> [--json]`,
20
+ `/api/onboard/repos`: the git repos in a folder, one or two levels down,
21
+ how each one runs (the same detection as `repo add`), and suggested
22
+ workspaces (the repos in one parent folder go in one workspace named
23
+ after it). It never writes, runs a repo's tools or follows a symlink,
24
+ skips dot-folders, `node_modules` and `vendor`, and stops at 50 repos.
25
+ - `manifest_draft`, `<cli> repo add <path> --dry-run --json`,
26
+ `/api/onboard/manifest`: the side-car manifest `repo add` would write,
27
+ checked by the same validator, with nothing written. Several ways to
28
+ start the repo is a question with its choices, as `repo add` asks.
29
+ - A path for `repos_detect` or `manifest_draft` must be a folder inside the
30
+ person's home folder and outside Den's state folder; anything else is
31
+ refused with the reason. A workspace-scoped MCP server can't scan folders,
32
+ and drafts only inside its workspace's repos.
33
+ - **Onboarding writes, held for the person (ON-3).** Three new MCP write
34
+ tools, `workspace_create`, `repo_add` and `service_try`, on by default
35
+ and gated by the person's Allow (not by `mcp.actions`). They change
36
+ nothing by themselves: each asks Den to hold the change, and every Den page opens a
37
+ dialog in the person's browser, "The `<agent>` agent wants to: … ·
38
+ Review · Later · Deny · Allow". Den writes the line and the Review from
39
+ the action and its arguments (`repo_add`'s Review is its dry run); the
40
+ agent's note is only quoted. Allow runs it once, from the stored
41
+ arguments checked again, through the code paths the pages and the CLI
42
+ use (the Workspaces page's create, `repo add --json`, `up` as the
43
+ Services page starts it). Deny, 10 minutes with no answer, or the agent
44
+ being stopped run nothing. The agent waits up to 2 minutes, then
45
+ `approval_check` reads where its request stands; meanwhile it is
46
+ "waiting" on the Agents page. Routes: `POST /api/approvals`,
47
+ `GET /api/approvals`, `GET /api/approvals/<id>`,
48
+ `POST /api/approvals/<id>/decide`. See [Onboarding writes, held for the
49
+ person](docs/distributions.md#onboarding-writes-held-for-the-person).
50
+ - **`mcp.approvals: false`** in the distribution config turns the three
51
+ held writes off: they aren't registered, and `POST /api/approvals`
52
+ answers 403 `approvals_off`. `doctor` (its `mcp:approvals` check in
53
+ `--json`), the MCP server's instructions and `mcp --list` (on stderr) say
54
+ which is in effect. A value that isn't `true` or `false` is an error.
55
+ - **Who asked.** An agent's token shows its name ("The `<agent>` agent
56
+ wants to:"). A request with no agent identity reads "Something on this
57
+ machine (no agent identity) wants to:", with "If you didn't ask an agent
58
+ to do this, Deny." under it. In English, Spanish and Japanese.
59
+ - **Only the person approves.** Allow and Deny need a browser signed in on
60
+ this Den and holding its approval key, even on localhost. Every sign-in
61
+ mints the key, handed over only in the sign-in's own answer (a 303's
62
+ `#ak=` fragment, or the sign-in poll); the session keeps its hash and
63
+ `GET /api/session` never returns it. A cookie alone (any localhost port
64
+ receives it), the CLI's bearer, an agent's token or an anonymous request
65
+ from this machine is refused with `signin_to_approve`.
66
+ - **Only the person sees what waits.** `GET /api/approvals` needs what
67
+ Allow needs: the cookie, that session's CSRF token and the approval key.
68
+ Anything else gets 403 `signin_to_approve` with a count (`waiting`), and
69
+ a signed-out page shows a banner to sign in. An agent's token lists only
70
+ its own requests and their outcomes. An ask with no agent token gets a
71
+ ticket that reads that one request (`X-Den-Approval-Ticket`); Den's MCP
72
+ server keeps it and never shows it to the agent.
73
+ - **The setup link.** `<cli> web claim-link [--json]` prints a one-time
74
+ link (`/auth?claim=<code>`, 30 minutes) that signs this browser in, for
75
+ an install that hands the person over to the browser. Opened twice, it
76
+ says it was used and that someone else may have opened it.
77
+
78
+ ### Changed
79
+
80
+ - Pairing a device, approving one, and linking or unlinking the GitHub
81
+ owner (`POST /api/web/pair`, `/api/web/approve`, `/api/web/owner/link`,
82
+ `/api/web/owner/unlink`) need the person: the CLI's bearer, or a
83
+ signed-in browser holding its approval key. On plain localhost,
84
+ Settings › Access asks you to sign in before pairing.
85
+ - Inside an agent's session (`DEN_AGENT` or `DEN_ACTOR_TOKEN` set), the CLI
86
+ refuses every command that lets a browser in: `web login`, `web approve`,
87
+ `web owner link|unlink`, `web claim-link`; `web open` opens the plain
88
+ page.
89
+ - What a test run writes and Den serves (`/report/*`, the Playwright
90
+ report, and `/reports/security/*`) is served sandboxed (an opaque
91
+ origin, no scripts reaching Den's), with nosniff, no-store and
92
+ no-referrer.
93
+ - A workspace name is at most 64 characters.
94
+ - **A Name is the agent's id (AG-10.10b).** A Name given at start (the
95
+ dialog's Name, the API's `name`, the CLI's new `--name`) makes the id its
96
+ slug: "API spike" starts `api-spike`, titled "API spike". The dialog says
97
+ so as you type. A Name that matches a running agent in the workspace, by
98
+ id or by title, is refused: the dialog says "api-spike is already
99
+ running · Open it" and Start is off. A stopped one offers Resume it, or
100
+ Start makes `api-spike-2`. No two live agents in a workspace share a
101
+ title. With no Name the persona's id stays the default; `--id` is exact.
102
+ - The Den theme's light mode (2.0.1): `--ok`, `--warn`, `--bad`,
103
+ `--remote` and `--accent-text` are darker, so a status pill (the token
104
+ on a tint of itself) meets WCAG AA (4.5:1) on every surface, `--sunken`
105
+ included. They measured 3.7 to 4.4:1 there. Dark mode is unchanged.
106
+
107
+ ### Fixed
108
+
109
+ - The signed-out page's Open Terminal link was `ssh://<host>` with no
110
+ account, so an SSH app filled in its own default (an iPad's asked for
111
+ `user@…`'s password). The link and the ssh one-liner are now
112
+ `<user>@<host>`, the account Den runs as, and the note says "Log in with
113
+ your <machine> account (<user>) and its password, or an SSH key."
114
+ `web.ssh_user` names another account; `web.ssh_user: ""` leaves it off.
115
+ - Text dimmed with opacity fell under WCAG AA on the light theme. Not
116
+ installed editors and terminals in Settings (3.1:1, their "not found"
117
+ 2.2:1), a repo already in the workspace in Add service (1.9:1), the
118
+ Release table's loading sub-line (3.7:1), and a primary button on hover
119
+ (4.4:1). They are full-strength now, in `--muted`; hover darkens.
120
+ - `/reports/security/*` was always 404: the `/report` prefix shadowed it.
121
+ - Bring in on a shared repos root (the usual layout) listed every
122
+ conversation under the root, so workspaces showed the same 50 and their
123
+ own were crowded out. A conversation is a workspace's when it ran in one
124
+ of its repos' folders or inside one, in its own folder, or exactly at a
125
+ shared root; the 50 cap applies after that. Another workspace's
126
+ conversation is refused by id: "This conversation belongs to <folder>,
127
+ not to workspace <ws>" (CLI, API and MCP).
128
+
129
+ ### Migrations
130
+
131
+ None. Sessions signed in before 0.4.15 hold no approval key: sign in again
132
+ to approve or to pair a device.
133
+
7
134
  ## [0.4.14] - 2026-11-07
8
135
 
9
136
  ### Added
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/hero-dark.png">
14
- <img src="docs/images/0.4.14/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.15/hero-dark.png">
14
+ <img src="docs/images/0.4.15/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.14/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.15/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.14/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.14/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.14/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.14/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.15/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.15/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.15/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.15/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
@@ -83,6 +83,9 @@ question for the person (`ask`, sometimes with `choices`).
83
83
  | ---------------------------------------------------------- | ---------------------------------------------------------------------------- |
84
84
  | `npx @monster-orange/den create <name> [dir] --yes --json` | `{ ok, dir, cli, existed, files, port, portPicked?, next }` |
85
85
  | `<cli> repo add <path\|git-url> --json` | `{ ok, existed, repo, workspace, mode, path, manifest, services, next }` |
86
+ | `<cli> repo detect <folder> --json` | `{ ok, path, repos, workspaces, truncated, skipped, rule, next }` |
87
+ | `<cli> repo add <path> --dry-run --json` | `{ ok, dryRun, repo, workspace, path, manifest, draft, validation, next }` |
88
+ | `<cli> onboard status --json` | `{ ok, checks: [{ id, status, detail, fix?, ask? }], next }` |
86
89
  | `<cli> up <repo> --json` | `{ ok, workspace, services: [{ address, result, health, detail? }], next }` |
87
90
  | `<cli> web up --no-open --json` | `{ ok, url, port, version? }` |
88
91
  | `<cli> doctor --json` | `{ ok, checks: [{ id, status, detail, url?, port?, fix?, ask? }] }` |
@@ -121,6 +124,115 @@ question for the person (`ask`, sometimes with `choices`).
121
124
  else the focused one, else a new `default` workspace.
122
125
  - The same repo again answers `ok` with `existed: true`. A different repo
123
126
  under the same name is an error that offers `relocate` or `--name`.
127
+ - `--dry-run` (a local path only) runs the same detection and validator and
128
+ writes nothing: no side-car, no workspace entry, no catalog entry. The
129
+ answer carries the side-car as YAML (`draft`), the validator's answer
130
+ (`validation`), where it would go (`manifest.file`, `written: false`) and,
131
+ in `next`, the same command without `--dry-run`. A git URL is refused: it
132
+ would have to be cloned first.
133
+
134
+ ### Onboarding reads
135
+
136
+ Three reads help a person, or an agent, finish setting Den up. Each one is
137
+ a CLI command, an MCP read tool and a web API route, with the same answer
138
+ (the browser can show the route's JSON as it is):
139
+
140
+ | CLI | MCP tool | Web API (GET) |
141
+ | ---------------------------------------- | ---------------- | ----------------------- |
142
+ | `<cli> onboard status [--json]` | `onboard_status` | `/api/onboard/status` |
143
+ | `<cli> repo detect <folder> [--json]` | `repos_detect` | `/api/onboard/repos` |
144
+ | `<cli> repo add <path> --dry-run --json` | `manifest_draft` | `/api/onboard/manifest` |
145
+
146
+ - **`onboard status`** says what's set up and the one next step. `checks`
147
+ are, in order: `distribution` (this folder has its own `bin/<cli>`),
148
+ `node`, `git`, `docker` and `web` (the same probes as `doctor --json`),
149
+ `workspaces` (how many, and the active ones), `repos` (how many are
150
+ added), `service` (how many services Den started are running) and `mcp`.
151
+ Each is `ok`, `warn` or `todo`, with a `fix` command or an `ask` for the
152
+ person when there is one. `next` is `{ id, step, command }`: the step in
153
+ plain words and the command for it, or `id: "done"` when everything is
154
+ set up. `mcp` never runs an agent's CLI: it is `ok` when the question
155
+ came through Den's MCP, or when a client's config file names this
156
+ distribution's server; with only Claude Code or Codex installed (they
157
+ keep their list behind their own CLI) it is a `warn` pointing at
158
+ `<cli> mcp clients`. It exits 0 whenever it answers.
159
+ - **`repo detect <folder>`** finds the git repos in one folder: the folder
160
+ itself when it is a repo, else its subfolders one and two levels down (a
161
+ repo's own subfolders are not scanned). For each repo: `manifest` (`repo`,
162
+ `sidecar` or `none`), `runs` (what [`repo add`](#repo-add) would detect)
163
+ or `ask` and `choices`, the suggested `workspace`, and `registered` when
164
+ it is already in one. `workspaces` groups them by one simple rule: the
165
+ repos in one parent folder go in one workspace named after that folder,
166
+ and a repo directly in the scanned folder gets the scanned folder's name.
167
+ It skips dot-folders, `node_modules` and `vendor`, never follows a
168
+ symlink, stops at 50 repos or 5,000 folder entries (`truncated: true`),
169
+ and never writes or runs a repo's tools.
170
+ - **`repo add --dry-run`** is the draft: see [`repo add`](#repo-add).
171
+ - **The path rule** (`repos_detect`, `manifest_draft` and both routes): a
172
+ path is required, absolute or starting with `~/`. It is resolved with its
173
+ symlinks, and must be a folder inside the person's home folder and outside
174
+ Den's own state folder; anything else is refused with the reason (a 400
175
+ from the route, `Refused:` from the tool, exit 1 from the CLI).
176
+
177
+ ### Onboarding writes, held for the person
178
+
179
+ An agent finishes setup with three MCP write tools. None of them changes
180
+ anything by itself: each asks Den to hold the change, and the person's
181
+ browser opens a dialog on whatever Den page is open: "The `<agent>` agent
182
+ wants to: … · Review · Later · Deny · Allow". A request with no agent
183
+ token (an MCP server outside an agent's session, or anything else on this
184
+ machine) reads "Something on this machine (no agent identity) wants to:",
185
+ with "If you didn't ask an agent to do this, Deny." under it.
186
+
187
+ | MCP tool | What Allow runs |
188
+ | ------------------ | ------------------------------------------------------- |
189
+ | `workspace_create` | The Workspaces page's create: an empty workspace |
190
+ | `repo_add` | `repo add <path> --json`, a local folder inside home |
191
+ | `service_try` | `up` for one service, as the Services page's Start does |
192
+
193
+ - **Den writes the dialog.** Its line and its Review come from the action
194
+ and its arguments, never from the agent. `repo_add`'s Review is its dry
195
+ run: the folder, the workspace (new or not) and the side-car Den would
196
+ write. The agent's `note` is shown only quoted, as "Agent says".
197
+ - **Bad arguments are refused before anything waits**: the same checks as
198
+ the page, `repo add` and the path rule of
199
+ [Onboarding reads](#onboarding-reads).
200
+ - **Only the person approves.** Allow and Deny need a browser signed in on
201
+ this Den holding its approval key: a session cookie alone, the CLI's
202
+ bearer, an agent's token, or an anonymous request from this machine (an
203
+ agent with a shell) is refused with `signin_to_approve`. On a fresh Den,
204
+ `<cli> web claim-link` prints a one-time setup link that signs the
205
+ browser in.
206
+ - **Allow runs it once**, from the arguments stored when it was asked,
207
+ checked again first. Deny, 10 minutes with no answer, or the agent being
208
+ stopped run nothing. An agent may have 5 requests waiting; a restart drops
209
+ what waits (the agent asks again).
210
+ - **The agent waits** up to 2 minutes for the answer, then gets the request
211
+ id: `approval_check` says `pending`, `done` (with what the run said),
212
+ `denied` or `expired`. While a request waits, its agent is "waiting" on
213
+ the Agents page.
214
+ - **Only the person sees what waits.** `GET /api/approvals` needs what
215
+ Allow needs: the session cookie, that session's CSRF token
216
+ (`X-Den-CSRF`) and the approval key. Anything else gets 403
217
+ `signin_to_approve` with only `waiting`, a count, so a signed-out page
218
+ can show "Something is waiting for your approval. Sign in to see it."
219
+ An agent's token lists its own requests and their outcomes (`mine`),
220
+ never another agent's. An ask with no agent token gets a `ticket` in the
221
+ reply; `X-Den-Approval-Ticket: <ticket>` reads that one request and no
222
+ other. Den's MCP server keeps it and never shows it to the agent.
223
+ - **Routes:** `POST /api/approvals` (`{ action, args, note? }`, 202 with the
224
+ request), `GET /api/approvals` (the person's: what waits; an agent's
225
+ token: its own), `GET /api/approvals/<id>` (the asking agent's own, the
226
+ ticket holder's, or the person's) and
227
+ `POST /api/approvals/<id>/decide` (`{ decision: "allow" | "deny" }`).
228
+ Every ask and decision is in the activity trail.
229
+ - **On by default; `mcp.approvals: false` turns them off.** The person's
230
+ Allow is the gate, not `mcp.actions`. Off, the three tools aren't
231
+ registered and `POST /api/approvals` answers 403 `approvals_off`.
232
+ `doctor` (and its `mcp:approvals` check in `--json`), the MCP server's
233
+ instructions and `mcp --list` (on stderr) say which is in effect. A
234
+ workspace-scoped MCP server refuses `workspace_create` and `repo_add`:
235
+ they change Den beyond its workspace.
124
236
 
125
237
  ### up, doctor and mcp connect
126
238
 
@@ -1434,6 +1546,15 @@ user, and the CLI does that for you:
1434
1546
  host name (up to the first dot). It is never taken from the browser's
1435
1547
  Host. The ssh one-liner's target is the Host the browser used, without
1436
1548
  the port (the machine's host name if it had none).
1549
+ - **The ssh account.** The ssh one-liner and the Open Terminal link log in
1550
+ as `<user>@<host>`, where user is the account the Den server runs as, so
1551
+ an SSH app doesn't fill in its own default. The note under the button
1552
+ says "Log in with your <machine> account (<user>) and its password, or
1553
+ an SSH key." `web.ssh_user` names another account (`config/local/user.yml`,
1554
+ else the distribution's `config/den.yml`; read at once). `web.ssh_user:
1555
+ ""` leaves the account off, for a distribution that doesn't want it on a
1556
+ page anyone on the network can see. It takes letters, digits, `.`, `_`
1557
+ and `-`; anything else is refused at start, naming the key.
1437
1558
  - **Codes.** About 30 bits (Crockford base32: no I, L, O or U; typed in any
1438
1559
  case, the dash optional); the page polls with a separate 128-bit id. A
1439
1560
  code expires after 10 minutes and works once; at most five wait at once
@@ -1785,13 +1906,17 @@ to one workspace for one agent:
1785
1906
  (`map: "org-wide"`: every active workspace's graph merged, `workspaces`
1786
1907
  naming them); with one, that workspace's (`map: "workspace"`).
1787
1908
  - **Filtered.** `workspaces` lists `<ws>` only. `logs_search` and `logs_tail`
1788
- take only `<ws>`'s services, `manifest_validate` takes only an absolute path
1789
- inside one of its repos (or the content), and `workspace_sync` syncs `<ws>`.
1909
+ take only `<ws>`'s services, `manifest_validate` and `manifest_draft` take
1910
+ only an absolute path inside one of its repos (or, for `manifest_validate`,
1911
+ the content), and `workspace_sync` syncs `<ws>`. `repos_detect` is refused:
1912
+ scanning folders reaches outside any workspace, so onboarding is for the
1913
+ unscoped server.
1790
1914
  Tests run against the focused workspace, so `tests_available` and
1791
1915
  `run_tests` are refused unless `<ws>` is the focused one.
1792
1916
  - **Not workspace data**, served as unscoped: `environments`,
1793
- `security_latest`, `release_status`, `edge_status` and `org_summary` (the
1794
- org's and the distribution's, not a workspace's).
1917
+ `security_latest`, `release_status`, `edge_status`, `onboard_status` (counts
1918
+ and checks only) and `org_summary` (the org's and the distribution's, not
1919
+ a workspace's).
1795
1920
  - **The org at a glance.** `org_summary` gives every agent every workspace
1796
1921
  and repo with status counts, the graph, the edge and the environments
1797
1922
  (see [Tools](#tools)). Detail and actions stay scoped: for workspaces
@@ -1857,10 +1982,12 @@ mcp:
1857
1982
  envs: [local] # the default; production is refused even if listed
1858
1983
  logs:
1859
1984
  remote: false # the default; true lets the log tools read non-local sources
1985
+ approvals: true # the default: the held writes, each run on the person's Allow
1860
1986
  ```
1861
1987
 
1862
1988
  An unknown action or environment name is an error, so a typo never turns a
1863
- guard off.
1989
+ guard off; so is an `approvals` that isn't `true` or `false`. See
1990
+ [Onboarding writes, held for the person](#onboarding-writes-held-for-the-person).
1864
1991
 
1865
1992
  ### Tools
1866
1993
 
@@ -1883,6 +2010,9 @@ Read tools (always registered):
1883
2010
  | `stacks` | `/api/stacks` | What `stack ls/ps/health` show: project, routes, health; an empty list says `why` |
1884
2011
  | `edge_status` | `/api/edge` | The edge: up or down, its routes and cert names; `https` (TLS strategy, who supplies the cert, whether HTTPS is serving) |
1885
2012
  | `org_summary` | several (read only) | Every workspace and repo with status counts, the graph, the edge and the environments; scoped, `yours` adds your workspace's services |
2013
+ | `onboard_status` | `/api/onboard/status` | What's set up and the one next step: `checks` (`ok`, `warn`, `todo`) and `next` (`step`, `command`); see [Onboarding reads](#onboarding-reads) |
2014
+ | `repos_detect` | `/api/onboard/repos` | The git repos in `path` (one or two levels down), how each runs, and suggested `workspaces`; never writes or follows a symlink; refused on a scoped server |
2015
+ | `manifest_draft` | `/api/onboard/manifest` | The side-car `repo add` would write for `path`, validated (`draft`, `validation`), nothing written; `ask` and `choices` when it can start in several ways (call again with `run`) |
1886
2016
 
1887
2017
  **Health states.** A service with no verdict says why in `why`: no health
1888
2018
  check in the manifest for the env, remote with no check or `target.url`
@@ -2045,7 +2175,7 @@ An agent is a **persona** (who) in a **workspace** (where), run by a
2045
2175
  server scoped to its workspace.
2046
2176
 
2047
2177
  ```
2048
- <cli> agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--id <id>]
2178
+ <cli> agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>] [--name <name>] [--id <id>]
2049
2179
  <cli> agents list -w <ws> [--json]
2050
2180
  <cli> agents stop <id> [-w <ws>]
2051
2181
  <cli> agents resume <id> [-w <ws>]
@@ -2065,6 +2195,9 @@ server scoped to its workspace.
2065
2195
  agent there, running or stopped, starts another: `planner-2`,
2066
2196
  `planner-3`. A stopped agent is continued by Resume or Start fresh,
2067
2197
  never by a new start. `--id` is exact: a running id is refused.
2198
+ `--name "API spike"` makes the id its slug, `api-spike`, titled as
2199
+ typed; a running agent there with that id or title is refused, and a
2200
+ stopped one makes it `api-spike-2`.
2068
2201
  - The **agent record**, `<stateDir>/agents/<id>.json` (0600), holds its id,
2069
2202
  persona, runtime, model, workspace, cwd, session and times, and what it was
2070
2203
  started with from the Start dialog: `version` (the model's, "latest" or
@@ -2100,8 +2233,11 @@ server scoped to its workspace.
2100
2233
  #### Bring in a conversation
2101
2234
 
2102
2235
  A conversation you started in a terminal outside Den (Claude Code, in the
2103
- workspace's folder, a folder inside it, or a folder of one of its repos,
2104
- a repo added by path outside it too) can become a Den agent. Agents ›
2236
+ workspace's own folder or one inside it, or a folder of one of its repos,
2237
+ a repo added by path outside it too) can become a Den agent. When
2238
+ workspaces share one repos root, only each one's repos' folders count,
2239
+ and the root itself only exactly; another workspace's conversation is
2240
+ refused by id. Agents ›
2105
2241
  **Bring in** lists them, newest first, with the persona list; `agents
2106
2242
  import -w <ws>` lists them too.
2107
2243
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.14",
3
+ "version": "0.4.15",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -199,6 +199,18 @@ export function revokedAgentIds(stateDir: string): Set<string> {
199
199
  return out;
200
200
  }
201
201
 
202
+ /**
203
+ * Whether agent `agentId` holds a token that isn't revoked: stopping it
204
+ * deletes its tokens, `web logout --all` marks them revoked (ON-3: a held
205
+ * write of an agent that is gone never runs).
206
+ */
207
+ export function agentTokenLive(stateDir: string, agentId: string): boolean {
208
+ const actor = `agent:${agentId}`;
209
+ return Object.values(load(stateDir).tokens).some(
210
+ (r) => r.actor === actor && !r.revokedAt,
211
+ );
212
+ }
213
+
202
214
  /** What a presented token was minted as, live or not; null when unknown. */
203
215
  export function lookupActorToken(
204
216
  stateDir: string,
@@ -159,13 +159,25 @@ function forms(dir: string): string[] {
159
159
  * newest first, at most MAX_CONVERSATIONS. Never throws: a history it
160
160
  * can't read is no conversations.
161
161
  */
162
+ /**
163
+ * Where a workspace's conversations ran (AG-17.1): in or under one of
164
+ * `under`, or exactly in one of `exact` (a shared repos root: its other
165
+ * subfolders are other workspaces' or no repo's).
166
+ */
167
+ export interface ConversationScope {
168
+ under: string[];
169
+ exact?: string[];
170
+ }
171
+
162
172
  export function claudeConversations(
163
- folder: string,
173
+ scope: string | ConversationScope,
164
174
  env: NodeJS.ProcessEnv = process.env,
165
175
  ): RuntimeConversation[] {
176
+ const sc = typeof scope === "string" ? { under: [scope] } : scope;
166
177
  const root = claudeProjectsDir(env);
167
- const dirs = forms(folder);
168
- const slugs = dirs.map(claudeSlug);
178
+ const dirs = sc.under.flatMap(forms);
179
+ const exact = (sc.exact ?? []).flatMap(forms);
180
+ const slugs = [...dirs, ...exact].map(claudeSlug);
169
181
  let projects: string[];
170
182
  try {
171
183
  projects = fs.readdirSync(root);
@@ -208,7 +220,16 @@ export function claudeConversations(
208
220
  continue;
209
221
  }
210
222
  const cwd = s.cwd;
211
- if (!cwd || !dirs.some((d) => within(cwd, d))) continue;
223
+ // Filtered before the cap, so others' conversations never crowd out
224
+ // this workspace's.
225
+ if (
226
+ !cwd ||
227
+ !(
228
+ dirs.some((d) => within(cwd, d)) ||
229
+ exact.some((d) => path.resolve(cwd) === d)
230
+ )
231
+ )
232
+ continue;
212
233
  seen.add(f.id);
213
234
  out.push({
214
235
  id: f.id,
@@ -220,6 +241,14 @@ export function claudeConversations(
220
241
  return out;
221
242
  }
222
243
 
244
+ /** Whether a conversation that ran in `cwd` is in `scope` (AG-17.1). */
245
+ export function inScope(cwd: string, scope: ConversationScope): boolean {
246
+ return (
247
+ scope.under.flatMap(forms).some((d) => within(cwd, d)) ||
248
+ (scope.exact ?? []).flatMap(forms).some((d) => path.resolve(cwd) === d)
249
+ );
250
+ }
251
+
223
252
  /** Whether a conversation last written at `lastAt` is still open elsewhere. */
224
253
  export function openElsewhere(lastAt: string, now = Date.now()): boolean {
225
254
  const t = Date.parse(lastAt);
@@ -70,7 +70,7 @@ import { catalogFind } from "./catalog-runtime.js";
70
70
  import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
71
71
  import { claudeProjectsDir } from "../usage/paths.js";
72
72
  import { cleanName, transcriptTitle } from "./transcript-follow.js";
73
- import { claudeConversations } from "./conversations.js";
73
+ import { claudeConversations, transcriptSummary } from "./conversations.js";
74
74
 
75
75
  /**
76
76
  * `/rename "x"` keeps the quotes in customTitle: one matching pair around
@@ -490,13 +490,23 @@ export const denClaude: Runtime = {
490
490
  return transcriptFile(token, env) !== null;
491
491
  },
492
492
  // AG-17.1: its transcripts for the folder (conversations.ts).
493
- listSessions(cwd, env = process.env) {
493
+ listSessions(scope, env = process.env) {
494
494
  try {
495
- return claudeConversations(cwd, env);
495
+ return claudeConversations(scope, env);
496
496
  } catch {
497
497
  return [];
498
498
  }
499
499
  },
500
+ // AG-17.1: the folder a conversation ran in (only the ends of its file),
501
+ // so a foreign one is refused by name.
502
+ conversationCwd(token, env = process.env) {
503
+ try {
504
+ const file = transcriptFile(token, env);
505
+ return file ? transcriptSummary(file).cwd : null;
506
+ } catch {
507
+ return null;
508
+ }
509
+ },
500
510
  // AG-4b, unofficial (catalog sessionName): `/rename` appends a
501
511
  // {"type":"custom-title","customTitle":…} record to the transcript; the
502
512
  // latest one, read on from the last ask (never the whole file), less one
@@ -166,6 +166,24 @@ export interface AgentUsage {
166
166
  /** An agent id: the same rules as a workspace or session name. */
167
167
  export const AGENT_ID_RE = WORKSPACE_NAME_RE;
168
168
 
169
+ /**
170
+ * AG-10.10b: the agent id a Name gives, so the name works in the CLI too:
171
+ * lowercase, spaces to "-", only id characters, at most 64. Null when
172
+ * nothing of it is left ("!!!"). The page's nameSlug is the same rule.
173
+ */
174
+ export function nameSlug(name: string): string | null {
175
+ const s = name
176
+ .trim()
177
+ .toLowerCase()
178
+ .replace(/\s+/g, "-")
179
+ .replace(/[^a-z0-9._-]/g, "")
180
+ .replace(/-{2,}/g, "-")
181
+ .replace(/^[^a-z0-9]+/, "")
182
+ .slice(0, 64)
183
+ .replace(/[-._]+$/, "");
184
+ return s && AGENT_ID_RE.test(s) ? s : null;
185
+ }
186
+
169
187
  export function agentsDir(stateDir: string): string {
170
188
  return path.join(stateDir, "agents");
171
189
  }
@@ -201,7 +201,12 @@ export interface Runtime {
201
201
  * transcript). Each id is one `resume` continues. None: this runtime
202
202
  * can't bring one in yet. Must never throw.
203
203
  */
204
- listSessions?(cwd: string, env?: NodeJS.ProcessEnv): RuntimeConversation[];
204
+ listSessions?(
205
+ scope: string | { under: string[]; exact?: string[] },
206
+ env?: NodeJS.ProcessEnv,
207
+ ): RuntimeConversation[];
208
+ /** AG-17.1: the folder conversation `token` ran in, or null. Never throws. */
209
+ conversationCwd?(token: string, env?: NodeJS.ProcessEnv): string | null;
205
210
  /**
206
211
  * What the runtime prints when it exits at once (0.4.8): `noConversation`
207
212
  * when a resume finds nothing to continue, `idInUse` when a new