@monster-orange/den 0.2.8 → 0.4.0

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 (158) hide show
  1. package/CHANGELOG.md +1731 -0
  2. package/README.md +4 -2
  3. package/bin/den +2 -1
  4. package/docs/distributions.md +730 -6
  5. package/make/den.mk +19 -4
  6. package/make/journeys.mk +2 -1
  7. package/make/local_dev.mk +6 -13
  8. package/make/secrets-1password.mk +18 -0
  9. package/make/test.mk +1 -1
  10. package/package.json +12 -2
  11. package/schemas/den.plugin.v1.json +156 -0
  12. package/schemas/den.theme.v1.json +269 -0
  13. package/scripts/auth-configure.ts +16 -8
  14. package/scripts/heal-config.ts +0 -5
  15. package/scripts/setup-1password.ts +41 -19
  16. package/scripts/setup-secrets.ts +67 -101
  17. package/src/pillars/api/global-setup.ts +1 -1
  18. package/src/pillars/journeys/sandbox.ts +23 -18
  19. package/src/pillars/journeys/tools/browser-session.ts +2 -2
  20. package/src/scripts/setup-default-workspace.ts +2 -2
  21. package/src/shared/agents/default-pack/be-dev.md +20 -0
  22. package/src/shared/agents/default-pack/devops.md +14 -0
  23. package/src/shared/agents/default-pack/fe-dev.md +20 -0
  24. package/src/shared/agents/default-pack/orchestrator.md +16 -0
  25. package/src/shared/agents/default-pack/planner.md +17 -0
  26. package/src/shared/agents/default-pack/reviewer.md +14 -0
  27. package/src/shared/agents/default-pack/ux-designer.md +15 -0
  28. package/src/shared/agents/den-claude.ts +173 -0
  29. package/src/shared/agents/generic.ts +52 -0
  30. package/src/shared/agents/personas.ts +218 -0
  31. package/src/shared/agents/records.ts +118 -0
  32. package/src/shared/agents/runtime.ts +192 -0
  33. package/src/shared/agents/runtimes.ts +47 -0
  34. package/src/shared/auth/headless-login.ts +1 -1
  35. package/src/shared/auth/sudo-vault.ts +93 -31
  36. package/src/shared/auth/token-providers/index.ts +7 -3
  37. package/src/shared/brand.ts +7 -0
  38. package/src/shared/config-error.ts +33 -3
  39. package/src/shared/constants.ts +3 -24
  40. package/src/shared/den-config.ts +257 -5
  41. package/src/shared/edge/config.ts +147 -21
  42. package/src/shared/edge/exec.ts +31 -0
  43. package/src/shared/edge/included.ts +169 -0
  44. package/src/shared/edge/render.ts +69 -14
  45. package/src/shared/edge/runtime.ts +128 -121
  46. package/src/shared/edge/status.ts +68 -8
  47. package/src/shared/edge/tls.ts +328 -0
  48. package/src/shared/edge/types.ts +25 -9
  49. package/src/shared/engine.ts +23 -0
  50. package/src/shared/environment-manager/default-workspace.ts +16 -1
  51. package/src/shared/environment-manager/health.ts +73 -38
  52. package/src/shared/environment-manager/scope.ts +168 -0
  53. package/src/shared/environment-manager/service-runner.ts +789 -43
  54. package/src/shared/environment-manager/up-targets.ts +132 -0
  55. package/src/shared/environment-manager/workspaces.ts +19 -0
  56. package/src/shared/health/rollup.ts +48 -0
  57. package/src/shared/open-url.ts +21 -2
  58. package/src/shared/org-sources.ts +78 -59
  59. package/src/shared/org.ts +53 -5
  60. package/src/shared/plugins/manifest.ts +406 -0
  61. package/src/shared/plugins/store.ts +358 -0
  62. package/src/shared/process/legacy-adopt.ts +285 -0
  63. package/src/shared/process/process-group.ts +202 -0
  64. package/src/shared/process/spawn-streaming.ts +7 -0
  65. package/src/shared/secret-env.ts +70 -0
  66. package/src/shared/secrets/doppler.ts +1 -1
  67. package/src/shared/secrets/index.ts +279 -50
  68. package/src/shared/secrets/none.ts +56 -0
  69. package/src/shared/secrets/strategies.ts +202 -0
  70. package/src/shared/secrets/types.ts +1 -1
  71. package/src/shared/sessions/client.ts +277 -0
  72. package/src/shared/sessions/den-pty.ts +97 -0
  73. package/src/shared/sessions/env.ts +79 -0
  74. package/src/shared/sessions/host.ts +667 -0
  75. package/src/shared/sessions/names.ts +85 -0
  76. package/src/shared/sessions/paths.ts +27 -0
  77. package/src/shared/sessions/protocol.ts +72 -0
  78. package/src/shared/sessions/pty.ts +141 -0
  79. package/src/shared/sessions/ring-buffer.ts +160 -0
  80. package/src/shared/sessions/types.ts +81 -0
  81. package/src/shared/smoke/git-remote.ts +40 -0
  82. package/src/shared/stacks/manifest-commands.ts +1 -1
  83. package/src/shared/stacks/namespace.ts +178 -0
  84. package/src/shared/stacks/owners.ts +303 -0
  85. package/src/shared/stacks/render.ts +8 -3
  86. package/src/shared/stacks/runtime.ts +100 -16
  87. package/src/shared/stacks/status.ts +63 -12
  88. package/src/shared/strategies/registry.ts +89 -0
  89. package/src/shared/theme/contract.ts +538 -0
  90. package/src/shared/types/index.ts +12 -3
  91. package/src/ui/cli/agents-command.ts +812 -0
  92. package/src/ui/cli/create.ts +24 -7
  93. package/src/ui/cli/doctor-command.ts +260 -0
  94. package/src/ui/cli/edge-command.ts +56 -10
  95. package/src/ui/cli/env-probe.ts +266 -0
  96. package/src/ui/cli/flag-parser.ts +18 -5
  97. package/src/ui/cli/health-display.ts +25 -0
  98. package/src/ui/cli/index.ts +580 -60
  99. package/src/ui/cli/notice.ts +20 -0
  100. package/src/ui/cli/plugin-command.ts +849 -0
  101. package/src/ui/cli/sessions-command.ts +718 -0
  102. package/src/ui/cli/stack-command.ts +63 -2
  103. package/src/ui/cli/test-runner.ts +11 -9
  104. package/src/ui/cli/theme-command.ts +136 -0
  105. package/src/ui/cli/web-command.ts +47 -23
  106. package/src/ui/mcp/api.ts +28 -2
  107. package/src/ui/mcp/scope.ts +145 -20
  108. package/src/ui/mcp/server.ts +27 -2
  109. package/src/ui/mcp/tools.ts +94 -12
  110. package/src/ui/server/appearance-prefs.ts +95 -0
  111. package/src/ui/server/audit.ts +35 -0
  112. package/src/ui/server/daemon-reaper.ts +29 -1
  113. package/src/ui/server/engine-stamp.ts +83 -0
  114. package/src/ui/server/github-service.ts +23 -44
  115. package/src/ui/server/graph-page.ts +20 -0
  116. package/src/ui/server/graph-prefs.ts +44 -1
  117. package/src/ui/server/http-errors.ts +293 -0
  118. package/src/ui/server/http-helpers.ts +22 -3
  119. package/src/ui/server/index.ts +539 -51
  120. package/src/ui/server/public/appearance.js +531 -0
  121. package/src/ui/server/public/edge-page.js +248 -0
  122. package/src/ui/server/public/edge.html +251 -0
  123. package/src/ui/server/public/environments.html +65 -83
  124. package/src/ui/server/public/error-page.js +247 -0
  125. package/src/ui/server/public/error.html +260 -0
  126. package/src/ui/server/public/graph-model.js +332 -14
  127. package/src/ui/server/public/graph.css +141 -47
  128. package/src/ui/server/public/graph.js +223 -44
  129. package/src/ui/server/public/header.js +396 -600
  130. package/src/ui/server/public/health-fit.js +248 -0
  131. package/src/ui/server/public/health-rollup.js +128 -0
  132. package/src/ui/server/public/i18n/es.js +287 -0
  133. package/src/ui/server/public/i18n/ja.js +276 -0
  134. package/src/ui/server/public/index.html +139 -438
  135. package/src/ui/server/public/local-dev.html +3468 -4044
  136. package/src/ui/server/public/logs.html +71 -208
  137. package/src/ui/server/public/panel.js +14 -2
  138. package/src/ui/server/public/release.html +175 -389
  139. package/src/ui/server/public/services.html +596 -770
  140. package/src/ui/server/public/settings-page.js +759 -0
  141. package/src/ui/server/public/settings-registry.js +521 -0
  142. package/src/ui/server/public/settings.html +590 -0
  143. package/src/ui/server/public/shell.js +3869 -0
  144. package/src/ui/server/public/topbar.js +1413 -0
  145. package/src/ui/server/public/version-label.js +12 -0
  146. package/src/ui/server/public/workspace-picker.js +74 -34
  147. package/src/ui/server/readonly-routes.ts +37 -3
  148. package/src/ui/server/services-list.ts +94 -17
  149. package/src/ui/server/settings-distribution.ts +74 -0
  150. package/src/ui/server/shell-prefs.ts +192 -0
  151. package/src/ui/server/theme-routes.ts +292 -0
  152. package/src/ui/server/theme-runtime.ts +407 -0
  153. package/src/ui/server/ui-assets.ts +16 -1
  154. package/src/ui/server/ui-language.ts +50 -0
  155. package/src/ui/server/workspace-routes.ts +48 -18
  156. package/themes/den/den-theme.json +72 -0
  157. package/themes/den/favicon.ico +0 -0
  158. package/themes/den/mark.svg +6 -0
package/CHANGELOG.md CHANGED
@@ -4,6 +4,1737 @@ 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.0] - 2026-11-06
8
+
9
+ 0.4.0-dev.2 and later are built on 0.3.0 and include all of it (below).
10
+
11
+ ### Added
12
+
13
+ - **`<cli> doctor --probe`** (0.4.0-dev.3, AG spec §5a-env; a consumer's
14
+ dogfood finding). Opt-in: it starts a short probe session through the
15
+ real session host for a plain session and for each available runtime's
16
+ agent env (built as `agents start` builds it, running a tiny node script
17
+ instead of the runtime's CLI). The probe prints key names and whether
18
+ each is credential-shaped, never values. It exits 1, one row per finding,
19
+ when a runtime's `scrubEnv` name or a credential-shaped variable reaches
20
+ either; without the PTY module it says unavailable, not failed. Plain
21
+ `doctor` still never spawns anything, and adds a note to run `<cli>
22
+ doctor --probe` when agents are configured (an `agents:` block or an
23
+ agent record).
24
+ - **`sessions start --keep-env NAME`** (0.4.0-dev.3): one exact name per
25
+ flag, repeatable, no globs. The session host's log names each kept
26
+ variable and who asked, never its value, and marks a credential-shaped one
27
+ (it is still passed). Refused for an agent (`DEN_AGENT` set); `agents
28
+ start` has no `--keep-env`.
29
+ - **Runtime contract: `envPassthrough` and `scrubEnv`** (0.4.0-dev.3). A
30
+ runtime names extra caller variables it needs (den-claude:
31
+ `CLAUDE_CONFIG_DIR`) and names it owns that must never reach a session
32
+ (exact, or a prefix ending in `*`; den-claude: `CLAUDECODE`,
33
+ `CLAUDE_CODE_*`). Every runtime's `scrubEnv` is removed from every
34
+ session's env last, whatever put it back. `LaunchOutput.warnings` are
35
+ printed at start.
36
+ - **den-claude passes `--permission-mode`** (0.4.0-dev.3; a consumer's
37
+ dogfood finding): `default` unless the persona's `permission_mode` sets
38
+ `default`, `acceptEdits`, `plan` or `bypassPermissions` (anything else is
39
+ an error naming the key), so `approvals: "terminal"` means prompts reach
40
+ the terminal. `bypassPermissions` prints a one-line warning at start.
41
+ - **`GET /api/activity?address=workspace:<ws>`** (0.4.0-dev.3; a consumer's
42
+ dogfood finding): an agent's cross-workspace call under a grant, which has
43
+ no repo or service, is listed under the workspace it reached and the
44
+ agent's own. Before, it showed only in the server log.
45
+ - **Agents: personas, runtimes and the agent record** (0.4.0-dev.1, AG-2a).
46
+ `<cli> agents start -w <ws> --persona <p> [--runtime <r>] [--model <m>]
47
+ [--id <id>]` resolves the persona and the runtime, asks the runtime for
48
+ argv, env and files, and starts the agent as session `<dist>-<ws>:<id>`
49
+ in the workspace's checkout dir (what `/api/workspaces` reports as its
50
+ `resolvedDirs`), capped by `agents.max_concurrent`. `agents stop <id>`
51
+ ends its session (TERM) and marks it stopped; `agents resume <id>`, only
52
+ when its runtime declares resume and its session is not running, starts a
53
+ new session in the same cwd continuing the last run. `agents personas
54
+ [--json]` lists the personas. `agents list -w <ws>` now shows each
55
+ agent's persona, runtime, model, state and cwd. Attach with `sessions
56
+ attach`.
57
+ - **The agent record**: `<stateDir>/agents/<id>.json` (0600, directory
58
+ 0700): id, persona, runtime, model, workspace, cwd, session, start and
59
+ stop times, and the runtime's resume handle. Never the env or the argv.
60
+ An id is unique in a distribution: the default is the persona's id, or
61
+ `<persona>-<ws>` when another workspace has that one.
62
+ - **Personas** are markdown files with YAML front matter; the body is the
63
+ role prompt. The format is a Claude Code agent file read as it is
64
+ (`name`, `description`, `model`, `tools`; `model: inherit` means none),
65
+ with Den's own keys beside it: `id`, `role`, `runtime`, `standing`,
66
+ `context` and `argv`. A pack is a directory of them; a pack with an
67
+ `agents/` directory (a Claude Code plugin) is read from there. Den ships
68
+ a default pack: planner and orchestrator (standing), fe-dev, be-dev,
69
+ devops, reviewer and ux-designer, with short runtime-neutral prompts.
70
+ - **Runtimes** are strategy kind `runtime` (`<cli> doctor` lists it).
71
+ `den-claude` (default) finds `claude` on PATH and reads its version from
72
+ the install without running it; it launches `claude --session-id <id>
73
+ --mcp-config <file> --strict-mcp-config [--model <m>]
74
+ --append-system-prompt <role prompt>` and resumes with `--resume <id>`.
75
+ `generic` runs the persona's `argv` template, substituting `{model}`
76
+ and `{mcp_config}`; it declares no resume.
77
+ - **An agent's MCP is pinned to its workspace**: its MCP config
78
+ (`<stateDir>/agent-files/<id>/mcp.json`, 0600) runs `<cli> mcp
79
+ --workspace <ws> --actor agent:<id>`, plus `--grant <ws>` for each
80
+ workspace in `agents.<id>.grants`.
81
+ - **`<cli> mcp --grant <ws>`** (0.4.0-dev.1, AG spec §5b; repeatable): a
82
+ scoped server also serves that workspace, only when the org config's
83
+ `agents.<id>.grants` lists it (a flag alone is refused, exit 2). A call
84
+ naming a granted workspace is reported before it runs, and the web
85
+ server's audit entry names both workspaces (`workspace` and
86
+ `targetWorkspace`). `workspaces` lists the scoped workspace and its
87
+ grants; tests run when the focused workspace is either. Logs and manifest
88
+ checks stay on the scoped workspace's own services.
89
+
90
+ - **Sessions: terminals that outlive the web server** (0.4.0-dev.0,
91
+ AG-3a). `<cli> sessions start --workspace <ws> --name <n> [--cwd <dir>]
92
+ -- <argv…>` runs argv in a PTY named `<dist>-<ws>:<n>`; `sessions list
93
+ [-w <ws>] [--json]` lists them (`-w` shows that workspace's only);
94
+ `sessions attach <name|id>` is a raw terminal on one (output fans out to
95
+ every attachment, scrollback replays first, SIGWINCH resizes, ctrl-]
96
+ detaches and leaves it running); `sessions kill <name|id> [--signal
97
+ INT|TERM|KILL]` signals its process group (TERM by default) or, once it
98
+ has exited, removes it and its scrollback. A name is the full name,
99
+ `<ws>:<n>`, a bare `<n>` only one session has, or the id. Usage errors
100
+ exit 2.
101
+ - A **session host** owns every PTY: `<cli> sessions host`, one detached
102
+ process per distribution (its own session and process group), started
103
+ on demand by the first client and serving `<stateDir>/sessions.sock`
104
+ (mode 0600). Restarting the web server, or the CLI that started a
105
+ session, leaves it running. A second host finds the first and stands
106
+ down; a socket nobody answers on is stale and is replaced. `sessions
107
+ host --stop` stops it (its running sessions end). The host logs to
108
+ `<stateDir>/logs/<cli>/sessions-host.log`. Its protocol is versioned
109
+ (newline JSON, v1).
110
+ - **Scrollback** is a 2 MB ring per session, flushed to
111
+ `<stateDir>/sessions/<id>.log` (0600), so after a host restart a session
112
+ is listed as exited and attaching replays its history. A record holds
113
+ the name, cwd, pid, times and exit code, never argv or env.
114
+ - **`agents.max_concurrent`** (default 4) caps the sessions running at
115
+ once; a start past it is refused, naming the key. A value that is not a
116
+ whole number of at least 1 is an error naming the key.
117
+ - **`<cli> agents list -w <ws> [--json]`** (0.4.0-dev.0): that workspace's
118
+ sessions as agents. Persona and runtime come from the agent record
119
+ (0.4.0-dev.1); a session started with `sessions start` has them blank.
120
+ - **`<cli> doctor` lists the session backend** (0.4.0-dev.0): `den-pty`,
121
+ available when the PTY module loads, else why not. It is never a doctor
122
+ failure.
123
+
124
+ ### Changed
125
+
126
+ - **A session's environment is an allowlist** (0.4.0-dev.3, AG spec
127
+ §5a-env; a consumer's dogfood finding). Every session started an agent
128
+ with the caller's whole environment, so an agent started from inside
129
+ Claude Code inherited `CLAUDECODE` and `CLAUDE_CODE_*` (Claude Code then
130
+ turned transcripts off, so resume could not work, and the agent could
131
+ message peers as its parent), and every secret in the caller's env reached
132
+ every agent. Now every session's env starts empty: `PATH`, `HOME`, `USER`,
133
+ `LOGNAME`, `SHELL`, `TERM` (default `xterm-256color`), `COLORTERM`,
134
+ `LANG`, `LC_*`, `TZ` and `TMPDIR`; then, for a plain session, the names
135
+ given with `--keep-env`; for an agent, its runtime's `envPassthrough`, its
136
+ launch env and `DEN_AGENT=agent:<id>`; then every runtime's `scrubEnv` is
137
+ removed. `sessions start` names the variables it left out, never values.
138
+ An agent's MCP server gets the distribution's location (`DEN_ROOT`,
139
+ `<PREFIX>_ROOT`, `<PREFIX>_HOME`, when set) in its own config. The
140
+ credential patterns test workers are scrubbed with now live in Den
141
+ (`src/shared/secret-env.ts`), and the tests import them.
142
+ - **`agents resume` of a session that was never prompted starts fresh**
143
+ (0.4.0-dev.3; a consumer's dogfood finding): there is no conversation to
144
+ continue, so it starts under the same conversation id (`--session-id`, not
145
+ `--resume`) and says "starting fresh: it was never prompted, so there is
146
+ no conversation to resume". The session host records whether a session
147
+ ever got input with a line end (`prompted`), never the input.
148
+ - **`agents start` and `resume` report an early exit** (0.4.0-dev.3; a
149
+ consumer's dogfood finding): Den watches the new session for up to 3 s;
150
+ a non-zero exit prints the code and the last lines it printed (at most
151
+ 10, escapes stripped, secret-shaped values redacted) and exits 1. Before,
152
+ it printed "resumed agent …" and exited 0 while the agent's CLI had
153
+ exited 1.
154
+ - **`agents list` shows agents only** (0.4.0-dev.3; a consumer's dogfood
155
+ finding): a session without an agent record (from `sessions start`) is
156
+ left out; `sessions list` shows it.
157
+ - **`sessions attach` prints its banner first** (0.4.0-dev.3; a consumer's
158
+ dogfood finding): `[attached to …]`, then the scrollback replay.
159
+ - **An MCP scope refusal names the agent's grants** (0.4.0-dev.3; a
160
+ consumer's dogfood finding): `(agent:<id>; grants: <a>, <b>)` or
161
+ `(agent:<id>; no grants)`.
162
+ - **A PTY module is an optional dependency** (0.4.0-dev.0, Q-AG-1):
163
+ `@lydell/node-pty` 1.1.0, exact. Its binaries are prebuilt per platform
164
+ (macOS and Linux, x64 and arm64; Windows) in packages npm installs with
165
+ it, with no install script and nothing downloaded. Installed with
166
+ `--omit=optional`, or on another platform, Den runs as before and
167
+ `sessions start` says den-pty is unavailable and why.
168
+ - **`agents list --json`** (0.4.0-dev.1): each row has `state` (running,
169
+ exited, stopped or ended) and, for an agent with a record, `persona`,
170
+ `runtime`, `model`, `workspace` and `cwd`; `session` is absent for a
171
+ record whose session is gone.
172
+
173
+ ### Migrations
174
+
175
+ - **Plugins with a `den` range that ends below 0.4.0** (for example
176
+ `>=0.3.0-0 <0.4.0`) are skipped on 0.4.0, and `validate` fails them:
177
+ widen the range (for example `<1.0.0`). A plugin with no `den` range,
178
+ as Den's own themes, needs nothing.
179
+ - `agents:` is a new, optional block of the org config. Any key other
180
+ than `max_concurrent` and `persona_packs` is an agent id; an invalid value
181
+ is an error naming the key:
182
+
183
+ ```yaml
184
+ agents:
185
+ max_concurrent: 4
186
+ persona_packs: [personas] # relative to the distribution root
187
+ planner:
188
+ grants: [other-workspace] # widens agent planner's MCP scope
189
+ ```
190
+
191
+ ## [0.3.0] - 2026-11-06
192
+
193
+ 0.3.0-dev.31 and later include all of 0.2.7 (below).
194
+
195
+ ### Added
196
+
197
+ - **R26a's 422 page: "Config is invalid"** (0.3.0-dev.40). A page
198
+ navigation with a config that fails validation gets the shell frame
199
+ with a table of each problem (file, key path, message), the command
200
+ that shows them in a terminal (`<cli> validate`), "Reload once fixed",
201
+ and links to Logs and Settings, in en, ja and es. The rail stays
202
+ usable: `/settings` and `/logs` load whatever the config holds, and so
203
+ do the routes the shell and those pages need (`/api/settings`,
204
+ `/api/themes`, `/api/scope`, `/api/workspaces/settings`,
205
+ `/api/sudo-password/status`, `/api/environments` (dev.41), besides `/api/server-info`,
206
+ `/api/server-log`, `/api/changelog` and `/api/web/restart`). Any other
207
+ page answers 422 with this page.
208
+ - Org config validation lists every problem, not the first: a
209
+ ConfigError carries `issues` (`{ file, key, message }`), its message is
210
+ one line per issue as each always read, and the 422 JSON body gains
211
+ `issues`.
212
+ - **`up --dry-run` says what up will do about a pre-dev.37 record or a
213
+ held port** (0.3.0-dev.40, a consumer's dev.39 nit; it said only "Would
214
+ up"): "would adopt pid N (port P), nothing started", "would drop the
215
+ record (nothing running), then start it", or "port P held outside the
216
+ repo: would not start" (exit 1, as the real up). It signals nothing and
217
+ writes no record.
218
+ - Everything in 0.2.8 (scoped MCP for agents, orgs that own their sources,
219
+ config validation that stops every command) is in 0.3.0.
220
+ - **The UI in Japanese and Spanish** (0.3.0-dev.39, R19; R10's and R21's
221
+ Language).
222
+ - Catalogs for en, ja and es. en is shell.js's `MESSAGES.en`, the
223
+ fallback; ja and es are `public/i18n/<lang>.js`, which the server
224
+ loads before shell.js for a page in that language (with
225
+ `<html lang>`), so the first paint is already in it. Every lookup is
226
+ `t(key, vars)`; a `key.one` (any CLDR category) is the plural form
227
+ `Intl.PluralRules` picks for `n`, and numbers go through
228
+ `Intl.NumberFormat`.
229
+ - The language is a user pref, `language:` in `config/local/user.yml`
230
+ (absent: English), read and written by `/api/settings` (`ui_languages`
231
+ lists the choices; anything else is a 400). The user menu has a
232
+ Language section, each language named in itself, Settings → Appearance
233
+ a Language row, and ⌘K a "Language: …" command; a pick reloads the
234
+ page in it.
235
+ - The whole shell is converted: rail, top bar and scope menus, status
236
+ chip and drawer, ⌘K palette (its Settings rows too), Runs chip and
237
+ dock, lock popover, user menu, What's new, the stale-server pill and
238
+ restart overlay, the engine and network banners, the error toasts and
239
+ the 404 and 500 pages. The pages' own content (Workspaces, Graph,
240
+ Services, Logs, Tests, Environments, Releases, Edge and the Settings
241
+ page's rows) is still English: they move to `t()` page by page.
242
+ - Never translated: names (services, workspaces, repos, themes,
243
+ languages), logs, command output, run labels, and "Copy details"
244
+ (it is for a report, so it stays English).
245
+ - Guards: `i18n.test.ts` fails when any catalog lacks a key en has, has
246
+ one en doesn't, drops a placeholder, or uses a plural form its
247
+ language doesn't; when the code looks up a key en lacks; and when a
248
+ node-side `EN_FALLBACK` drifts from en. `i18n-shell.browser.test.ts`
249
+ draws the shell in a pseudo-locale with every overlay open and fails
250
+ on any word that skipped `t()`.
251
+ - English shows exactly what it did, apart from the new Language
252
+ controls.
253
+
254
+ - **Errors people can see and act on** (0.3.0-dev.38, R26, Ward QA
255
+ 2026-11-05: "we don't handle 500 or 404s from the den server with
256
+ anything remotely obvious ... just text").
257
+ - A page request that fails gets the shell frame (rail, top bar, status
258
+ chip) around an error page (`error.html`, `error-page.js`). 404 "Page
259
+ not found" shows the path, "Did you mean …" from the command palette's
260
+ own matcher (`rankCommands`), links to Workspaces and Search (⌘K) and
261
+ the list of pages; a path under `/plugins/<id>/` says which plugin it
262
+ belongs to and that it isn't installed. 500 "Something went wrong"
263
+ shows the method and route template (never query values), the time,
264
+ a request id, Retry and Copy details, and no stack: the server log
265
+ has it under the same request id (`✗ GET /route failed (req
266
+ <id>): …`). Assets and non-browser requests keep a plain 404.
267
+ - Every `/api/*` error is JSON, one shape:
268
+ `{ ok: false, error, code, requestId }` (`code`: `not_found`,
269
+ `bad_request`, `internal`, `method_not_allowed`, `engine_stale`, …). It
270
+ holds at the response for every route, whatever the route sends, and a
271
+ route's own fields (`runCmd`, …) are kept. Every response carries
272
+ `X-Request-Id`; a failed request's log line carries the id.
273
+ - `denShell.api(method, url, body?)`: the one client path for a call. It
274
+ resolves with the data or throws a typed error (`DenApiError`: status,
275
+ code, requestId, network); one no page catches becomes a toast with
276
+ the message, the request id, Retry and Copy details
277
+ (`denShell.errorToast`). The shell's own calls, the scope and focus
278
+ menus, Services' workspace panel, the Workspaces page's add, create,
279
+ delete, rename, auto-start, mode and open-in-editor/terminal actions,
280
+ Releases' config save and deploy start, and the log search use it.
281
+ - When fetches fail at the network level (the server stopped or is
282
+ restarting), a banner says "Den's server isn't responding — retrying",
283
+ retries with backoff (1 s doubling to 15 s, or Retry now), and clears
284
+ itself when the server answers; meanwhile the status chip reads
285
+ "Status unknown", never a stale green.
286
+ - **A server older than the engine on disk says so** (0.3.0-dev.38, MD-2).
287
+ After a pin upgrades Den under a running web server, the server keeps the
288
+ old engine in memory while serving the new pages, so their new routes
289
+ 404. The server records the engine's version and build id (a hash of its
290
+ package.json) at startup and compares them with the engine on disk on
291
+ page requests (cached for 3 s). On a mismatch every page leads with a
292
+ banner, "Den was updated to <new>. This server is still running <old>.
293
+ Restart it: `<cli> web restart` (or your service restart).", an unknown
294
+ route answers that message (`/api`: code `engine_stale` with `loaded`
295
+ and `onDisk`) instead of a 404, and a failed MCP tool call names both
296
+ versions and the restart. `/api/server-info` gains `loaded` and `onDisk`
297
+ (`{ version, buildId }`) and `engineStale`; its `version` is now the
298
+ engine the server runs.
299
+ - **DOC-1: the README's screenshots, generated** (0.3.0-dev.40; `npm run docs:shots`,
300
+ `scripts/doc-shots.mjs`). Runs Den on a scratch copy of
301
+ `examples/distribution` with the `examples/demo` repos as its workspace (a
302
+ docker shim, a random localhost port, a throwaway HOME, and a server env
303
+ built from HOME, PATH and the DEN_* it needs, never the caller's), and
304
+ writes to `docs/images/<version>/` (package.json's version without the
305
+ prerelease suffix): `hero-dark.png` and `hero-light.png` (the Graph page,
306
+ 2x), `social-preview.png`, `workspaces.png`, `graph.png`,
307
+ `status-drawer.png`, `edge.png`, `runs-dock.png`, the `make up`
308
+ animation (`up.gif` with ffmpeg, else `up.webp` with img2webp, else
309
+ skipped with a message) and `alt.json`, the alt text. Size budgets: hero
310
+ 400 KB a theme, the rest 200 KB, the animation 5 MB; over budget, the run
311
+ fails. Nothing reaches the tree unless every shot passes.
312
+ - The capture guard (`scripts/doc-shots-guard.js`): before each shot, the
313
+ page's innerText, every title, alt, aria-label and placeholder, and SVG
314
+ text go through the org-name gate's own matcher, now shared
315
+ (`scripts/org-names.js`, used by `org-name-gate.mjs` too). A hit aborts
316
+ the run naming the shot and the term, never the text around it, and
317
+ writes no image; so does a path under the real HOME. Without
318
+ DEN_ORG_NAMES or DEN_ORG_NAMES_FILE the script refuses (exit 2);
319
+ `--unguarded` is for local layout work only.
320
+ - The images stay out of the npm package: `files` has `!docs/images/`,
321
+ and dist-smoke fails on any image in the tarball.
322
+ - **`npm run docs:links`** (`scripts/docs-links.js`) checks that every
323
+ relative link in README.md and docs/ reaches a file and, with a
324
+ `#fragment`, a heading.
325
+
326
+ - **`stack_namespace`: a workspace can choose its stacks' Compose
327
+ namespace** (0.3.0-dev.36, Ward 2026-11-05). By default nothing changes:
328
+ a stack runs as Compose project `<stack>-<workspace>` (`default`:
329
+ `<stack>-main`), and so do its containers, volumes and networks. A
330
+ workspace can opt out with `stack_namespace: <slug>`, which replaces that
331
+ suffix: several workspaces can then share one namespace, and a stack
332
+ keeps its data when its repo moves to another workspace.
333
+ - Where: `workspaces.<ws>.stack_namespace` in
334
+ `config/local/workspaces.yml`, or `stack_namespace` on a blueprint
335
+ workspace (`defaults.workspaces.<n>`, next to `org`). First match wins:
336
+ workspaces.yml, the blueprint, then the workspace's own name.
337
+ `workspace seed` carries the blueprint's value into the workspace it
338
+ seeds. workspaces.yml overriding a different blueprint value is a
339
+ warning naming both, as for `org`. The value is lowercase letters,
340
+ digits and dashes; anything else is a config error naming the key.
341
+ - One resolver: every stack path (`stack` commands, `/api/stacks`,
342
+ Den's in-process compose health checks, a stack service's container
343
+ lookup, `workspace deactivate`'s running-stacks note) turns a workspace
344
+ into its suffix through `resolveStackWorkspaces`.
345
+ - A shared namespace means a shared instance, keyed by checkout. Two
346
+ workspaces bringing up the same stack from the same checkout (repo
347
+ path and branch) share one project: `stack down` (and `down <repo>`)
348
+ from one while another active workspace in the namespace still holds
349
+ it detaches instead of stopping it; the last one stops it
350
+ (`stack down --force` stops it regardless). A workspace skipping the
351
+ stack as already healthy is recorded as an owner too. The owners are
352
+ kept in `owners.json` in the project's edge state directory, and
353
+ re-derived on every decision (active, same namespace, still holds the
354
+ stack), like Den's own service refcount. Bringing it up from a
355
+ different checkout or branch is refused (`ServiceConflictError`,
356
+ `identity_mismatch`), naming both checkouts and the namespace: Den
357
+ never swaps the containers under another workspace.
358
+ - Visible: `workspace list` gains a Stacks column when a workspace's
359
+ namespace is not its default; the Workspaces page header shows a
360
+ "stacks: <ns>" chip; `GET /api/workspaces` adds `stackNamespaces`, each
361
+ workspace's resolved `stack_namespace`, its `source` (`workspace`,
362
+ `blueprint` or `default`), its `default` and whether it `differs`.
363
+ - `DEN_STACK_WORKSPACE` still wins over all of it, for that one process:
364
+ a per-command override (it also bypasses the owner refcount), not a
365
+ migration tool. To move a workspace's stacks onto another namespace
366
+ for good, set `stack_namespace`.
367
+ - **The graph's edges flow when a card is selected, and stay on top**
368
+ (0.3.0-dev.35, R22b, Ward's QA of dev.34). Selecting a card only
369
+ thickened its edges, and the edge layer painted under the group frames,
370
+ repo frames and cards, so an opaque frame hid an edge or an arrowhead
371
+ that crossed it (dev.34's routed cross-group edges too). The edge layer
372
+ now paints above the frames and cards, still taking no pointer events
373
+ (cards under an edge stay clickable). A highlighted edge carries a
374
+ marching-dash overlay, in its kind's colour, moving from the dependent
375
+ to its dependency (data-f to data-t) at about one cycle a second: a
376
+ selected card's dependencies flow out of it and its dependents flow in.
377
+ Under reduced motion nothing moves: a highlighted edge is drawn thicker
378
+ in the accent colour.
379
+ - **Workspaces: Local Dev is an ops page** (0.3.0-dev.35, R24a–e, Ward
380
+ 2026-11-05: "the UX is a bit of a kitchen sink"). One job: run your
381
+ workspaces.
382
+ - R24a: the rail item, the page title and its header read
383
+ **Workspaces** (same icon and place). `/` and `/local-dev` still serve
384
+ it; the palette finds it as "Local Dev" too. The nav label key is
385
+ `n.workspaces`.
386
+ - R24b: the "⚙ Settings" footer under the workspace list is gone; "+ new
387
+ workspace" stays.
388
+ - R24c: the selected workspace's header adds **Open in Graph →**
389
+ (`/graph?ws=<name>`) and a ⋯ menu: Workspace settings
390
+ (`/settings?ws=<name>#workspace`, the old "Settings ↗" tab), Rename,
391
+ Delete.
392
+ - R24d: **one repo table** replaces the Graph, Applications and Config
393
+ tabs. Per repo: the health dot and rollup ("2/2 healthy", or the
394
+ failing check), the branch, the mode with "use local / use managed",
395
+ the last run's result and time (from the runs dock), Start, Stop,
396
+ Restart, Logs (in the dock) and ⋯ (terminal, editor, auto-start,
397
+ clone, validate, remove). A repo with several services expands to a
398
+ sub-row each (keyboard: the ▸ button, `aria-expanded`). Problems sort
399
+ first, then the startup order, with the fix inline: the status
400
+ drawer's text and Start/Logs buttons, from one source
401
+ (`topbar.js problemFix`). **Group by** None, Application (as `den
402
+ apps`, each application's Up/Restart/Down in its row) or Org, a user
403
+ pref (`graph.groupBy`). **Find** uses the Graph page's matcher. "+ Add
404
+ repo" sits under the table; "Validate a manifest" folds under it. The
405
+ empty state is unchanged. A real `<table>` with `th scope`; every
406
+ action button is named with its repo or service.
407
+ - R24e: on Workspaces the runs dock shows the selected workspace's runs
408
+ (a run belongs to every workspace that has its repo; one with no repo,
409
+ such as Sync, always shows), with an **All runs** toggle in the bar.
410
+ `denShell.runs.scope({ ws })`, and `runs.add(…, { ws })` tags a run.
411
+ - Not changed: the Graph page, Services, Settings and every API; no
412
+ consumer config needs a change.
413
+
414
+ - **The Graph page** (0.3.0-dev.32, R22, Ward 2026-11-05). Develop ›
415
+ Graph, after Local Dev, at `/graph`: the workspace graph alone, full
416
+ width under its page header, with the same toolbar, zoom, Maximize
417
+ (which now hides the page header too) and details panel as Local Dev's
418
+ Graph tab. It shows the workspace Local Dev would (`?ws=`, else the
419
+ view scope when it is one workspace, else the focus). Local Dev keeps
420
+ the workspace list, the workspace's actions and its tabs, Graph tab
421
+ included. Overview and Security stay out for now.
422
+ - **A page header on every page** (0.3.0-dev.32, R20). The shell draws the
423
+ prototype's `.ph` on each page that has no header of its own: the page's
424
+ title (`600 22px/1.2`, its one h1, which used to be visually hidden) and
425
+ a 13px muted line with the scope, `org / workspace / env`, kept in step
426
+ with the top bar (Local Dev and Graph name the workspace they show). Pages
427
+ keep their content below it. `denShell.pageHeader()` gives a page the
428
+ header's `act` (the primary action, at the end) and `tabs` (`.ptabs`,
429
+ below it), and `setScope` / `setSub` for the line. Local Dev's workspace
430
+ tabs are page tabs now: real tabs (`role="tab"`, `aria-selected`) at the
431
+ prototype's sizes (8px 12px, 14px, a 2px accent rule under the selected
432
+ one).
433
+ - **The stacks' routes on the Edge page and in `edge routes`**
434
+ (0.3.0-dev.32, from a consumer). The routers Den writes into
435
+ `edge/dynamic/stack-*.yml` when a stack comes up are neither Den's host
436
+ routes nor `edge include` files, so nothing listed them. They are listed
437
+ last, source "stack" with the file; `/api/edge` adds `stacks` and
438
+ `stackErrors` (a file that doesn't parse is named, never fatal).
439
+
440
+ - **The runs dock** (0.3.0-dev.30, R16). One output dock for every page, at
441
+ the foot of the content column, ported from the shell prototype: a bar
442
+ with "Runs", the run tabs (12px mono, a dot that pulses while running,
443
+ red when the run failed) and Show/Hide runs; open, a 170px output. The
444
+ Runs chip (R7) shows and hides it and counts the running tabs; an empty
445
+ dock links to the page that ran the last run. The bar carries Copy and
446
+ Clear for the selected run, a tab closes with its × (or Delete), ←/→ move
447
+ between tabs, and the dock's top edge drags to resize it. Pages and
448
+ plugins use one path, `denShell.runs` (`start`, `append`, `finish`, `set`,
449
+ `select`, `clear`, `remove`, `show`/`hide`/`toggle`, `list`, `onChange`);
450
+ a run started there gets "✓ done in {s}s" when it finishes. Since R16a
451
+ (below) the dock starts collapsed and open/closed is a user pref.
452
+ - **The Edge page** (0.3.0-dev.30, PL-SPEC §10). A built-in page in the
453
+ rail's Ship group, shown once `/api/edge` reports the edge configured
454
+ (`/api/edge?probe=1`, which asks no Docker). Read-only: the container and
455
+ its ports, the TLS strategy (`edge.tls`: none, mkcert or files) with
456
+ where it came from (set, mapped from the legacy keys, or implicit), CA
457
+ trust and the CA's directory, the cert and its names, the domains, and
458
+ every route: Den's own, then the routers in the included dynamic files
459
+ (`edge include`), each marked by its source file.
460
+ - **Room for Local Dev's graph** (0.3.0-dev.30, R23, Ward QA 2026-11-05).
461
+ The workspace list, the output and two toolbar rows left the graph at 40%
462
+ Fit. The workspace list now collapses to a 40px strip (« in its header;
463
+ » brings it back), remembered per user (`graph.workspaces`: `open` or
464
+ `collapsed`, on the Settings page under Graph). A Maximize button in the
465
+ graph's zoom group, or `F` with focus outside a field, hides the list,
466
+ the workspace header, the action row and the tabs; Esc restores. Fit
467
+ measures the whole area, so its zoom rises once maximized.
468
+ - **Included routers in `edge routes` and `/api/edge`** (0.3.0-dev.30,
469
+ PL-SPEC §10, from a consumer review). Routers defined in the distribution's
470
+ included dynamic files are served by Traefik but were never listed. Den
471
+ now reads their `http.routers` (read-only) and lists them after its own
472
+ routes with their source file; `/api/edge` adds `included`,
473
+ `includedErrors` (a file that doesn't parse is named, never fatal) and
474
+ `tls.from` / `tls.ca`. The rendered edge config is unchanged.
475
+
476
+ - **`<cli> relocate <repo> <path>`** (0.3.0-dev.26, from a consumer). A
477
+ checkout moved on disk: every workspace's dev-managed entries for the repo
478
+ (bare and `<repo>/<svc>`) now point at the new path, keeping each entry's
479
+ other settings. Managed entries (Den's own clones) are left alone, and focus
480
+ and the active set never change. It refuses a missing path, one whose
481
+ manifest name or GitHub remote names a different repo, and a repo no
482
+ workspace has. Since 0.3.0-dev.27 (from a consumer's dev.26 run) it also
483
+ refuses a folder where nothing names the repo (an empty or unrelated
484
+ folder): the manifest name, or the GitHub remote against the catalog's
485
+ `gh` (or the repo name when the catalog has none), must match.
486
+ `--force` accepts such a folder. The refusal's `--force` hint keeps
487
+ `--dry-run` and `-w` when given (0.3.0-dev.28). `--dry-run` shows the changes without writing; `-w` limits it
488
+ to one workspace.
489
+ - **`<cli> register <path> -w <workspace>`** (0.3.0-dev.26). `-w` writes that
490
+ workspace, active or not, and leaves focus alone. It used to fail with
491
+ "-w/--workspace is not valid with register". The no-TTY usage line names `-w` too
492
+ (0.3.0-dev.27).
493
+
494
+ - **The "What's new" pop-up shows the newest release only** (0.3.0-dev.16).
495
+ A "Show all versions (N earlier)" button in its footer lists every
496
+ earlier version in the same pop-up, and "Show latest only" collapses it
497
+ again. It always opens collapsed, at the top.
498
+ - **Themes (`den.theme/v1`).** The contract is `schemas/den.theme.v1.json`:
499
+ color tokens for dark and light, type, shape, a mark and self-hosted fonts.
500
+ `<cli> theme add <repo>[/<subpath>]@<tag|commit>` validates a theme and
501
+ vendors it into the distribution (`plugins/<id>/`, `plugins.lock.json`, shared with future plugin kinds). `theme add` never replaces a plugin dir it did not vendor (a local, unlocked plugin) or another kind's lock entry; `theme list` fails on a leftover 0.3.0-dev.0 `themes.lock.json`, naming it; `theme add` refuses a `file://` source (0.3.0-dev.7, below). A theme's and a font's `license` follow one rule: an SPDX expression, never UNLICENSED/NONE. `theme list` reports a theme that fails this Den's rules as "no longer valid" with the reasons, apart from "changed since it was added"; validate explains the license and CSS-value rules instead of printing "must NOT be valid". Local themes (the distribution's own, in `plugins/<id>/` with no lock entry) are listed as `local` and validated; `theme validate` with no argument checks every installed theme; `theme new <id>` starts a local theme from the built-in one;
502
+ `theme list` checks themes against the lock; `theme validate` and `theme
503
+ remove`. Contrast is checked per pair in both modes. A branch ref needs
504
+ `--track`. Den bundles its own theme, `den` 2.0.0. The public set and the
505
+ authoring guide: https://github.com/Monster-Orange/den-themes. See "Themes"
506
+ in `docs/distributions.md`.
507
+ - **The web UI is themed (0.3.0-dev.5).** Every page draws its colors from
508
+ the active theme, in dark or light: the engineer's choice (user.yml
509
+ `appearance: { theme, mode }`, from the header's new Mode and Theme menus),
510
+ else the org config's `brand.theme`, else `den`; an unknown or invalid id falls
511
+ back with one server-log warning. The server serves `/theme.css`,
512
+ `/theme/<id>/<file>` (only the files the manifest names) and `/api/themes`.
513
+ Switching writes user.yml only (no edge, services or restart). No page,
514
+ stylesheet or UI script hard-codes a color any more (a test holds that).
515
+ The header's version pill shows the running build, prerelease suffix
516
+ included (`v0.3.0-dev.5`, not the changelog's `v0.3.0`).
517
+ - **Plugins (`den.plugin/v1`, PL-1a and PL-2a, 0.3.0-dev.7).** The contract
518
+ is `schemas/den.plugin.v1.json`: `id`, `name`, `version`, `license` (the
519
+ theme schema's SPDX rule, by `$ref`), `den` (a version range, required),
520
+ `maintainer`, `readme`, `provides` (one entry per extension point: `theme`
521
+ with its `manifest`, `page` with its `entry` and `nav`, and the other kinds
522
+ by name) and `capabilities` (a closed vocabulary: `ui.nav`, `api.read`,
523
+ `api.actions:`, `secrets:`, `net:`, `exec:`, `send:`). A directory holding
524
+ only a `den-theme.json` is an implicit theme plugin, so every theme keeps
525
+ working. Every `plugins/<id>/` is a plugin: **local** when it has no lock
526
+ entry (the distribution's own, any kind; being present is its approval),
527
+ **added** when `plugin add` vendored it. A `plugins/<id>/` with neither
528
+ manifest is an error in `plugin list` and `plugin validate`, never
529
+ skipped, as are the id `den` and a directory name that is not an id.
530
+ - `<cli> plugin list`: every plugin with its version, kinds and `local`
531
+ or its source and ref; added ones are checked against the lock's hash
532
+ (as `theme list` does).
533
+ - `<cli> plugin validate [dir]`: every local and added plugin, or one dir
534
+ (a plugin, a distribution, or a repo of `themes/<id>/` and
535
+ `plugins/<id>/`). Admission rules: a `maintainer` (1); a browser- or
536
+ server-tier plugin's README names its tier and says what it must never
537
+ do (2; themes are exempt); an SPDX license and licensed fonts (3). A
538
+ local plugin that declares `api.actions`, `secrets`, `exec`, `net` or
539
+ `send` and has files git has not committed gets a warning (never an
540
+ error): committing it is its review.
541
+ - `<cli> plugin add <repo>[/<subpath>]@<tag|commit> [--track] [--yes]`,
542
+ `plugin update <id> @<ref>` and `plugin remove <id>`. The lock entry's
543
+ `kinds` and `approved_capabilities` come from the manifest; a plugin that
544
+ asks for capabilities needs `--yes`, and `update` asks again only when the
545
+ set grows. `remove` and `update` are for added plugins: a local one is
546
+ edited or deleted in git. A den-plugin.json plugin is vendored whole
547
+ (every file but dot-files); a theme still vendors only its named files,
548
+ so existing lock hashes still match.
549
+ - `add` and `update` refuse a `file://` source: "for your own plugin, put
550
+ it in plugins/<id>/ as a local plugin; to try a published one before
551
+ tagging, push a branch and use --track". This replaces 0.3.0-dev.2's
552
+ warning.
553
+ - `plugins.allow` in `den.yml` lists the `<host>/<owner>` pairs (or bare
554
+ hosts) `add` may fetch from. Absent: any host. A malformed value is an
555
+ error.
556
+ - `theme add | list | validate | remove` are `plugin` filtered to kind
557
+ theme, with the same output and exit codes; `theme new` is unchanged.
558
+ The web UI also finds a theme a `den-plugin.json` provides.
559
+ See "Plugins" in `docs/distributions.md`.
560
+ - **PL-SPEC draft-4 rulings (0.3.0-dev.8).**
561
+ - Rule 1 covers themes: a bare `den-theme.json`'s `author` is its
562
+ maintainer, and a theme plugin without one fails `plugin validate`.
563
+ `theme new` sets the author to the brand's name.
564
+ - The `den` range is checked (semver, prereleases included).
565
+ `plugin validate` and `theme validate` fail a plugin whose range
566
+ excludes the running Den, naming the range and the version. At load,
567
+ the web UI skips such a theme, whether the range is in its
568
+ `den-theme.json` or in the `den-plugin.json` that provides it, with one
569
+ server-log notice, and draws with the next choice (yours, then
570
+ `brand.theme`, then `den`). It never crashes. Den now depends on
571
+ `semver` (pinned).
572
+ - `plugin list` and `theme list` warn, never fail, about a lock entry
573
+ with a `file://` source (from 0.3.0-dev.2), and say how to fix it.
574
+ - **`plugin validate <id>` and `theme validate <id>` mean `plugins/<id>/`**
575
+ (0.3.0-dev.8). An id that names a dir in `plugins/` or a lock entry is
576
+ never read as a cwd-relative path. A bare name that isn't an id (such as
577
+ `x`) means `plugins/x/` when no such path exists. Anything else is a path,
578
+ as before. `theme validate den` checks the built-in theme. `theme
579
+ validate <id>` of a plugin that isn't a theme gives one ✗.
580
+ - **The theme reaches the type, the mark and links.** Every page draws text
581
+ in `var(--sans)` / `var(--mono)` / `var(--display)` (no page names a font
582
+ any more; a test holds that, like the color rule), so a theme's fonts
583
+ apply; `/theme.css` ends a theme's stack with Den's when it has no generic
584
+ family. The header shows the active theme's mark (SVG, `svg_dark` in dark
585
+ mode, or text in its `shape`) and redraws it on a live switch; favicon.ico
586
+ stays the favicon. Links in body content are underlined (subtly, in a tint
587
+ of their own color), so a red accent never makes a link read as an error.
588
+ - **Back to the org default.** The Theme menu starts with "Org default
589
+ (<name>)", which stores `appearance.theme: null`, so you keep following
590
+ `brand.theme`. Picking a theme by name stores its id, even the theme that
591
+ is the org default today (0.3.0-dev.11; dev.8 stored null for it): that
592
+ is a pin, selected as itself and labelled "<name> (pinned)".
593
+ `/api/themes` gives `org_default`. The popover's controls are named
594
+ (Mode, Theme; the button is "Appearance" with `aria-expanded` and
595
+ `aria-controls`); Escape closes it and returns focus to the button.
596
+
597
+ - **What fails `validate` never loads (0.3.0-dev.9).** The theme runtime's
598
+ verdict is now `plugin validate`'s for the whole plugin. That covers a bare
599
+ `den-theme.json`'s `author`, a `den-plugin.json`'s `maintainer`, README,
600
+ capabilities and `den` range, and the theme contract. A theme that fails
601
+ is left out of the Theme menu (even when it is your stored choice: you then
602
+ follow the org default) and is skipped at load with one server-log notice.
603
+ The fallback is the same as for the range: yours, then `brand.theme`, then
604
+ `den`. `/api/themes` still lists it, with `ok: false` and the problem.
605
+ - **A prerelease Den explains a range without `-0` (0.3.0-dev.9).** When
606
+ the running Den is a prerelease and only that keeps the range from
607
+ matching, the refusal adds "prerelease builds need a -0 range, e.g.
608
+ >=0.3.0-0", built from the range's own lower bound.
609
+ - **The left rail: grouped, pinnable, collapsible** (0.3.0-dev.17, R11,
610
+ R11a, R12, R13, R14). Pages sit under Develop (Local Dev, Services, Logs),
611
+ Verify (Tests) and Ship (Environments, Releases), with Settings at the
612
+ foot. Groups and items come from one registry in `shell.js`;
613
+ a distribution adds its own with `denShell.registerGroup({id, label,
614
+ order, tag})` and `denShell.registerNav({group, id, label, href, icon,
615
+ order, badge})`, and its labels with `denShell.addMessages(lang, {...})`.
616
+ - **Pinned**: ☆ on a Services card or in the graph's details pins a repo
617
+ or service to the top of the rail with its health dot ("Pinned shop");
618
+ × or Delete on the pinned row unpins it.
619
+ - **Badges**: Services shows how many services are down, live from the
620
+ health row; Logs how many log streams are open.
621
+ - **Collapse** (« at the foot) narrows the rail to its icons, 216 to 56px;
622
+ the headings and labels stay for screen readers. Under 720px the rail is
623
+ a drawer behind ☰.
624
+ - Pins and collapse are stored in user.yml `shell` (`GET`/`POST
625
+ /api/settings` `shell: {pins, collapsed}`); setting them changes nothing
626
+ else (the theme-switch isolation test covers it).
627
+ - **The shell matches the prototype's sizes** (0.3.0-dev.17, R1, R2, R3,
628
+ R9, R10): one 48px top bar with the brand, the workspace pill and Env as
629
+ key + value buttons, the health row as a chip, "What's new" and the ◐ ▾
630
+ menu (now with "All settings"); the rail's type, spacing and current-page
631
+ bar. `scripts/shell-fidelity.mjs` measures the real pages against the
632
+ prototype at 1440 and 1024 (1px, 0.5px for type) and lists any allowed
633
+ difference with its reason.
634
+
635
+ - **The scope breadcrumb is three menus** (0.3.0-dev.18, R4): `/ ORG
636
+ <org> ▾ / WORKSPACE <ws> ▾ / ENV <env> ▾`, each the prototype's `.ib`
637
+ button with a `.pop` menu (✓ on the current entry). The menus are ARIA
638
+ menus: ↓ or Enter opens one, ↑/↓, Home and End move, Enter picks,
639
+ Escape (or Tab) closes it and gives focus back to its button, and a
640
+ click outside closes it. They replace the workspace pill and its menu,
641
+ the env select, the interim View select and Services' own workspace
642
+ select. Below 860px the keys, separators and the org hide; below 720px
643
+ the scope does (the narrow layout, R11b, comes later).
644
+ - **Org:** one org is a static label with no ▾, as before (ORG-SPEC
645
+ C4). `denShell.setOrgs([{ id, title, workspaces? }], current,
646
+ onSelect?)` turns two or more into an Org ▾ menu; a pick calls
647
+ `onSelect(id)` and fires `den:org` on window. Nothing feeds it a list
648
+ yet: ORG-1b plugs `/api/orgs` in.
649
+ - **Workspace:** the view scope. "All active (N)" (with two or more
650
+ active) and each active workspace, the focused one tagged `focus`.
651
+ Picking one stores the view (user.yml `view`, `POST /api/scope`) and
652
+ reloads the page; it never moves the focus. A separate "Set <ws> as
653
+ focus" item (when the workspace in view isn't the focus) does Local
654
+ Dev's focus action, and "Manage workspaces…" opens Local Dev.
655
+ - **Env:** the same envs, value and browser storage as the select it
656
+ replaces; `#env-sel` stays (hidden) as the value pages read and the
657
+ `change` event they listen for.
658
+ - **A view scope: all active workspaces, or one** (0.3.0-dev.18). The
659
+ health row covered every active workspace while Services, Tests, Logs
660
+ and `status` showed only the focused one, so the row above a page listed
661
+ repos the page didn't. One source now says which repos are in view
662
+ (`src/shared/environment-manager/scope.ts`), and the health row,
663
+ `/api/services` (every page's list) and `<cli> status` all read it.
664
+ - "All active" is the default: it is what the health row already showed,
665
+ and with one active workspace it is the same view. A scope that is no
666
+ longer active falls back to all, never to an empty page.
667
+ - **The view is not the focus.** The focus stays the default target for
668
+ commands; the view only filters what pages show. Switching it writes
669
+ user.yml `view` and nothing else: no edge route, container, daemon or
670
+ workspaces.yml change, and the server doesn't restart (the
671
+ theme-switch isolation test covers it). `GET`/`POST /api/scope`.
672
+ - **A shared service is listed once, with its owners**: a repo that two
673
+ workspaces in view define shows one card (`shop · ws-a, ws-b`) and one
674
+ health dot, its deps and auto-start from the first (focus first).
675
+ `/api/services` rows carry `inWorkspaces`, `/api/health` rows carry
676
+ `workspaces`.
677
+ - **`<cli> status`** shows the same view and says so ("Scope: all active
678
+ (a, b, c) · focus: a"). Use `--scope <workspace>` for one (`--all`
679
+ already means `--verbose` there); `-w <workspace>` still checks that
680
+ one workspace.
681
+ - Local Dev edits one workspace at a time: it opens on the view's
682
+ workspace when that is one, else the focus.
683
+ - **A ready path for distribution scripts** (0.3.0-dev.18). A script that
684
+ loads before shell.js pushes functions onto `window.denShellQueue` (an
685
+ array); each runs with `denShell` once the shell exists, and a later push
686
+ runs at once. `den-shell:ready` fires on window when the shell is up.
687
+ - **`den:pins`** (0.3.0-dev.18): any pin change fires it on window
688
+ (`detail.pins`), and every pin toggle on the page repaints from it.
689
+ - **`org.title_source`** in `/api/<cli>/config` (0.3.0-dev.18; added beside
690
+ the existing fields, ORG-SPEC C3): `org.title`, `orgs`, or `id`.
691
+ - **A Settings page** (0.3.0-dev.19, R21): `/settings`, in the shell,
692
+ with the prototype's page header, sections, rows, segmented buttons,
693
+ swatches and navigation list, applied to every setting Den has.
694
+ - **Categories** on the left (a nav; the current one `aria-current`),
695
+ their rows on the right: Appearance, Editor & terminal, Navigation,
696
+ Graph, Workspace, Command line, Distribution, then any a distribution
697
+ or plugin registers (tagged with its `from`).
698
+ - **Search** (`/` focuses it) filters rows by label, description and
699
+ keywords; a match stays under its category's heading, empty
700
+ categories hide, "No settings match" shows when nothing does, and the
701
+ count is announced. The URL keeps `#<category>` and `?q=`, so a
702
+ setting can be linked to; reload and back/forward work.
703
+ - **Every row shows its scope**: User (config/local/user.yml),
704
+ Workspace (that workspace in config/local/workspaces.yml) or
705
+ Distribution (read-only). The Workspace rows follow the Workspace you
706
+ view; with All active, the section asks which workspace.
707
+ - **Writes go only to per-user, gitignored state**, as before:
708
+ `POST /api/settings` (user.yml) and `POST /api/workspaces/settings`
709
+ (workspaces.yml), with the same keys. Nothing writes except an
710
+ explicit change: a control writes on its change (the per-workspace
711
+ editor on Save, as before) and says "✓ Saved". Loading, searching,
712
+ moving between categories and changing the hash or query write
713
+ nothing. The page never writes the distribution's config.
714
+ - **Distribution rows are read-only**: the product name, GitHub org, org
715
+ labels, web port, environments, edge domain and the org default theme,
716
+ each with "Set by the distribution in config/<file>.yml". Your own
717
+ override of the same thing (your theme) is a separate User row.
718
+ `GET /api/settings` gains `distribution` (read from the org config
719
+ only), `user_web_port`, `local_dev` and `cli_tabs` (read-only).
720
+ - **Today's settings moved in, unchanged in storage**: Default IDE,
721
+ Per-language IDE and Default terminal (Local Dev's ⚙ panel); mode and
722
+ theme (◐ ▾ stays as a shortcut, and both drive the same prefs and
723
+ repaint each other); the graph prefs; the per-workspace editor (Local
724
+ Dev's workspace Settings tab). `log_window_lines`, `local_dev`,
725
+ `cli_tabs` and your `web.port` show read-only with the command that
726
+ sets them.
727
+ - **Navigation** (new): hide or show each rail page and move it within
728
+ its group; reorder and unpin pins; collapse the rail. Stored in
729
+ user.yml `shell` as `hidden` and `order` (page ids), beside `pins` and
730
+ `collapsed`. A view preference only: no edge route, container, daemon,
731
+ pid or restart changes (the theme-switch isolation test covers it).
732
+ - **The old entry points land on the matching category**: Local Dev's
733
+ ⚙ (`#editor`), its workspace Settings tab (`#workspace`, that
734
+ workspace), `/local-dev?settings=1` (redirected to `#editor`), the
735
+ rail's Settings item and "All settings" in ◐ ▾ (`#appearance`). The
736
+ old panel is gone. Arriving at a category (a link, the hash,
737
+ back/forward) marks it current and puts focus on its heading.
738
+ - **The SETTINGS registry** (0.3.0-dev.19, R21), beside NAV and GROUPS on
739
+ `window.denShell` (and through `window.denShellQueue`):
740
+ `registerSettingsCategory({ id, label, order, description?, from? })`
741
+ and `registerSetting({ id, category, label, description, keywords,
742
+ scope: user|workspace|distribution, control, read(ctx), write?(value,
743
+ ctx), keys? })`; `settings()`, `settingsCategories()`; `den:settings`
744
+ fires on any change. A distribution row with a `write` is refused.
745
+ Controls: `seg`, `radio`, `select`, `number`, `toggle`, `text`,
746
+ `readonly` and `custom` (`render(el, ctx, { value, save })`). Den's own
747
+ rows are in `public/settings-registry.js`; a test fails if a key
748
+ `POST /api/settings` accepts has no row.
749
+ - **`denShell.setNavPrefs({ hidden?, order? })`, `setPins(ids)`,
750
+ `setCollapsed(bool)` and `orderedNav()`** (0.3.0-dev.19): the rail's
751
+ Navigation prefs, saved to `/api/settings` `shell`.
752
+ - **The top bar's search, status, runs and lock** (0.3.0-dev.20, R5, R6,
753
+ R7, R8, R10, R15, R17, R18). It ships together with the Settings page
754
+ (dev.19, never released on its own): the palette finds every setting,
755
+ and the user menu's "All settings" opens it. The bar now reads ☰, brand, scope, search,
756
+ then the Status, Runs and lock chips, "What's new" and the ◐ ▾ menu, at
757
+ the prototype's sizes (`scripts/shell-fidelity.mjs` measures it). The
758
+ search field gives way first, so the scope's parts never run into each
759
+ other; below 960px the scope's keys hide (860px in the prototype, whose
760
+ chips are narrower), below 860px the chips keep only their dots and
761
+ icons, and below 720px the scope hides and the search field is an icon.
762
+ - **Search and the ⌘K palette.** "Search pages, services, actions…"
763
+ (⌘K or Ctrl+K anywhere, or a click) opens a palette: pages, apps,
764
+ services with their health dot, Restart and Logs for each service,
765
+ Appearance (System, Dark, Light) and every installed theme. It ranks a
766
+ label's prefix, then a word in it, then anywhere, then keywords and
767
+ the kind, then the letters in order; every word must match. ↑/↓ move,
768
+ Enter runs, Escape closes and gives focus back. It is a modal dialog
769
+ with a combobox and a listbox (`aria-activedescendant`).
770
+ - **`denShell.registerCommand({id, label, keywords, kind, run})`**
771
+ (also from `window.denShellQueue`) adds a palette row; `kind` is
772
+ `page`, `app`, `service`, `action` or `setting`. Every row of the
773
+ SETTINGS registry (Den's, a distribution's or a plugin's) is a
774
+ `setting` row on every page: "terminal" finds Default terminal, and
775
+ Enter opens `/settings?q=<label>#<category>`.
776
+ `unregisterCommand(id)` and `commands()` too. `denShell.openMenu(btn,
777
+ items)` opens a `.pop` menu any script can use.
778
+ - **The status chip** replaces the health dot row and its "+N" chip:
779
+ "All N healthy", or "ok/N healthy · k down" with a bad border, from the
780
+ same rollup as the rail's Services badge. It shows "Checking…" until
781
+ the first cycle is in and paints once: no partial red on a cold load.
782
+ A pending check is pending, never down.
783
+ - **The status drawer** (the chip opens it): a right-hand modal panel,
784
+ focus on ✕, Escape back to the chip. "Needs attention" has one card
785
+ per down service, with what was checked, the error and when, and
786
+ Start, Logs and Pin; "Everything" lists each application ok/N, each
787
+ service with its time in ms. What the row's tooltips said is in each
788
+ row's title.
789
+ - **The Runs chip** counts the runs this page started (service and
790
+ workspace commands, syncs, test runs, deploys; not log tails), with a
791
+ pulse while one runs. Until the output dock (dev.21) it shows the
792
+ page's own output pane (marked `data-den-output`), else links to the
793
+ page of the last run.
794
+ - **The lock chip** (🔒 Locked / 🔓 Unlocked) holds the admin-password
795
+ form that was Local Dev's sudo banner, with Save to 1Password, in a
796
+ popover: "Den asks once per session. Agents can't unlock." It is the
797
+ one unlock on every page; the banner is gone. Local Dev still asks
798
+ 1Password for a stored password on load; other pages ask only when
799
+ the popover opens. Start and Restart from the drawer or the palette
800
+ follow the same lock rule, then run on Local Dev (which runs them on
801
+ arrival when you are on another page).
802
+ - **The user menu** (◐ ▾) is the prototype's `.pop`: Appearance, Theme
803
+ and "All settings" (`/settings#appearance`), and a hidden slot for
804
+ Language. Its ◐ ▾ glyphs are hidden from screen readers; its name is
805
+ "Appearance". Opening any of these overlays writes
806
+ nothing; switching appearance or theme from the menu or the palette
807
+ writes user.yml `appearance` only (the theme-switch isolation test
808
+ covers it).
809
+ - `header.js` exports `requireUnlock`, `isUnlocked`, `onLockChange`,
810
+ `setServiceRunner` and `takePendingAction` for pages.
811
+ - **One h1 per page**: the brand title in the bar is a span now, not an
812
+ h1, so a page's own heading is its one. A page with none (Services,
813
+ Logs, …) gets a visually hidden h1 named from its title.
814
+ - **Raster theme marks** (0.3.0-dev.22): a mark can be a PNG or a WebP
815
+ (`png` / `png_dark`, `webp` / `webp_dark`), for a mark that exists only
816
+ as a raster. One image kind per mark (svg, png or webp): two fail
817
+ validate, naming both. `theme validate` and `theme add` check each file's
818
+ first bytes against its extension, a 64 KB cap and at least 44×44 px
819
+ (twice the bar's 22px mark), reading the PNG or WebP header itself.
820
+ `/theme/<id>/<file>` serves them as `image/png` and `image/webp`, still
821
+ only the files the manifest names. The top bar draws one as an image at
822
+ 22px, clipped to the mark's `shape`, the dark file in dark mode. SVG and
823
+ text marks are unchanged.
824
+ - **The browser tab shows the theme's icon** (0.3.0-dev.22): the theme's
825
+ new optional `favicon` (`.png`, `.svg` or `.ico`, magic-byte checked, at
826
+ most 64 KB), else its mark's file (the dark variant in dark mode), else
827
+ Den's own icon. The shell sets it on every page (no page links
828
+ `/favicon.ico` any more) and changes it with the theme and the mode,
829
+ System following the OS included. Its URL,
830
+ `/theme-favicon/<id>?mode=<mode>&v=<version>-<hash>`, changes with each,
831
+ so a switch is never answered from cache. The built-in `den` theme names
832
+ Den's icon as its `favicon`; `theme new` leaves it out, so a new theme's
833
+ tab shows its own mark.
834
+ - **Secrets backends are registered strategies, and `none` is one**
835
+ (0.3.0-dev.23). Each built-in (`none`, `env`, `doppler`, `1password`)
836
+ registers through one registry (`src/shared/strategies/registry.ts`, kind
837
+ `secrets`) as `{ id, kind, detect(), capabilities }`: a factory, a label,
838
+ its CLI, which ref parts it needs, and optional `setup` and env
839
+ injection. `detect()` is a PATH check that never prompts or prints; a
840
+ backend whose CLI is missing reports "not available: The Doppler CLI
841
+ (doppler) is not on PATH" instead of failing a spawn. `secrets.provider:
842
+ none` resolves nothing and never runs a CLI. `den create` now scaffolds
843
+ `secrets: { provider: none }`, with the choices in a comment, so a new
844
+ distribution depends on no store. See "Secrets" in
845
+ `docs/distributions.md`.
846
+ - **The edge's TLS is a registered strategy: `edge.tls`** (0.3.0-dev.25,
847
+ TLS-1a). `none`, `mkcert` and `files` register through the strategy
848
+ registry as kind `tls`, as the secrets backends do. `tls: none` serves
849
+ http only (no https entrypoint, no cert work, mkcert never run);
850
+ `{ strategy: mkcert }` issues the edge's wildcard cert from a local CA,
851
+ its own or `ca:` (an existing CA, never minted or uninstalled);
852
+ `{ strategy: files, include, mounts }` serves the certs your included
853
+ config holds (`ca:` optional, for health checks). An unknown strategy,
854
+ or a key a strategy does not take, is a validation error naming the
855
+ built-ins. mkcert's `detect()` is a PATH check: with it missing,
856
+ `validate`, `doctor`, `edge up` and `edge trust` say "mkcert is not
857
+ installed" instead of a spawn error. `edge trust` / `untrust` exist under
858
+ mkcert, and `edge trust` also under `files` with a `ca` (it installs that
859
+ existing CA, as `caroot` did); elsewhere they exit 1 ("edge trust needs
860
+ edge.tls.strategy: mkcert (or files with a ca)").
861
+ `den create` scaffolds `edge.tls: none`, with the mkcert and files forms
862
+ in a comment. `/api/edge` adds `tls: { strategy, implicit }`. See "The
863
+ edge and TLS" in `docs/distributions.md`.
864
+ - **`<cli> doctor`** (0.3.0-dev.25): each strategy kind (secrets, tls),
865
+ the strategy selected, whether it was implicit, and its `detect()` with
866
+ the reason. Read-only: a PATH check, never a backend CLI, a prompt or a
867
+ secret value. Exit 1 when a strategy that will be used cannot work.
868
+
869
+ ### Changed
870
+
871
+ - **`down` works under an invalid config** (0.3.0-dev.40): it is
872
+ config-free, so stopping is never stranded. With a config that fails
873
+ validation it says so ("Stopping anyway, without the config's org
874
+ data") and stops from the daemon records and compose projects; `down -a`
875
+ too. Every other command still stops first.
876
+ - **Only up and restart detach by default** (0.3.0-dev.40, DOC-1). Every
877
+ string command but down and setup ran detached, so `preflight` run from
878
+ the Services page never finished. A command other than up and restart
879
+ now runs to completion unless it says `detachable: true`.
880
+ - **English plurals** (0.3.0-dev.40): en has the `.one` forms (`org.n.one`
881
+ "{n} workspace", and the status, badge, drawer and changelog counts), so
882
+ the org menu no longer says "1 workspaces". The catalog test fails when a
883
+ count another catalog makes singular has no en `.one`.
884
+ - **The graph's top-level groups wrap to the stage** (0.3.0-dev.34, a
885
+ consumer's dogfood finding: 31 services in 10 org groups). Top-down,
886
+ every org (or application) group sat in one row, so the whole graph
887
+ fitted only at 15–24%, where cards are hard to read. Each top-level
888
+ group is now laid out on its own by the same layered layout (a group's
889
+ inside is unchanged), and the groups wrap into rows (top-down) or
890
+ columns (left-right), choosing the wrap that fits the stage at the
891
+ highest zoom; the edges between groups are routed through the gaps
892
+ between rows. A 31-card, 10-org graph fits at 45% at 1440×900 (was
893
+ 22%) and 33% at 1024×768 (was 14%).
894
+ - **The automatic fit shows the whole graph** (0.3.0-dev.34). A graph
895
+ that needed less than 25% used to open on its top at 60%; the first
896
+ paint now fits it as Fit does (down to 10%).
897
+
898
+ - **The Graph page follows the top bar's workspace scope** (0.3.0-dev.33,
899
+ a consumer's dogfood finding). With the top bar's Workspace on "All
900
+ active", `/graph` used to show (and name) the focused workspace only. It
901
+ now shows every active workspace in one graph, as the Services page
902
+ lists them: one card per service, a service several workspaces define
903
+ listed once with its owners (`demo-api · main, notes`). One workspace
904
+ picked shows that one, and `?ws=` still wins. The page header's line
905
+ names what is shown (`acme / All active / local`). A card's actions run
906
+ in the workspace that owns its repo (the first in view, focus first).
907
+ Local Dev keeps one workspace, the list's selection: its actions, its
908
+ Applications and Config tabs and its Graph tab are per workspace.
909
+ - **Stack routes on the Edge page and in `edge routes`: scheme and
910
+ upstream** (0.3.0-dev.33, a consumer's dogfood finding). A stack
911
+ router's URL is shown with its scheme like Den's own rows: `https://`
912
+ when the router has TLS or is on the `websecure` entrypoint, else
913
+ `http://`. UPSTREAM shows the service's `loadBalancer` server URL when
914
+ the router's file defines it, else the Traefik service's name. The
915
+ routers in `/api/edge` (`included`, `stacks`) carry `secure` and
916
+ `upstream`; included routers get the same.
917
+ - **Page tabs are WAI-ARIA tabs** (0.3.0-dev.33, a consumer's dogfood
918
+ finding: ArrowRight, Home and End left focus on Graph). Local Dev's
919
+ workspace tabs and any tabs `denShell.pageHeader().tabs(spec)` makes
920
+ share one helper, `denShell.wireTabs(tablist, opts)`: one tab stop (the
921
+ selected tab is `tabindex=0`, the rest `-1`), ←/→ move and wrap, Home and
922
+ End go to the ends, and a tab is selected as focus moves to it
923
+ (automatic activation). Each tab's `aria-controls` names its
924
+ `role="tabpanel"`, whose `aria-labelledby` names the tab, and the other
925
+ panels are hidden. Local Dev's "Settings ↗" link now sits beside the
926
+ tablist rather than in it. `pageHeader().tabs` is a function now: the
927
+ shell no longer adds a hidden, unlabelled, empty `.ptabs` tablist to
928
+ every page, only the one a page asks for.
929
+
930
+ - **POST /api/settings: null clears a pref** (0.3.0-dev.32, from a
931
+ consumer). A key set to `null` (`{"graph":{"workspaces":null}}`, or in
932
+ `shell` or `appearance`) is removed from config/local/user.yml instead of
933
+ failing with 400, and a block left empty goes too; a whole block set to
934
+ `null` (`{"graph":null}`) removes it. `graph:` now keeps only the keys
935
+ someone set (it used to write every default), so setting a pref and
936
+ clearing it again leaves user.yml byte-identical.
937
+ - **Edge page: the cert-names card under tls `files`** (0.3.0-dev.32, from a
938
+ consumer) reads "Names the included certs should cover": Den issues
939
+ nothing there. mkcert keeps "Certificate names (planned)".
940
+
941
+ - **The pages' output panes are gone; their runs are tabs in the runs
942
+ dock** (0.3.0-dev.30, R16). Local Dev's output panel (one tab per
943
+ system, log tails, the dismissed tabs per workspace), Services' output
944
+ pane, Tests' output pane and its service command strip (Stop / Detach is
945
+ now a button in the dock's bar while the command runs), and Releases'
946
+ deploy log (each deploy session is a tab: selecting one attaches its
947
+ log, its × closes the session; the session chips are gone) all write to
948
+ the dock. The output lines are the same text with the same line classes;
949
+ k6's progress lines still update in place, Local Dev still keeps its
950
+ last 1000 lines per tab. Auto-scroll now pauses on every page when you
951
+ scroll up (Local Dev's rule).
952
+ - **The runs dock starts collapsed, and stays as you leave it** (0.3.0-dev.30,
953
+ R16a, Ward QA 2026-11-05). A run starting never opens it: its tab
954
+ appears and the Runs chip pulses, and you open it (Show runs, the chip or
955
+ a tab). Only an action whose whole point is output opens it: Logs on a
956
+ service (Services) and a card's output in Local Dev's graph. Open or
957
+ closed and its height are user prefs (`shell.dock`, `shell.dockHeight`
958
+ in `/api/settings`, the Settings page's Navigation row "Open the runs
959
+ dock"), so they hold across pages, reloads and browsers; nothing is kept
960
+ per browser tab any more. Dragging its top edge stops at half the window.
961
+ - **Settings: the search and the category list stay put** (0.3.0-dev.30,
962
+ R21a, Ward QA 2026-11-05). The page header with its search box and the
963
+ category list on the left are sticky while the rows scroll; a category
964
+ click scrolls its section to just below the header.
965
+
966
+ - **Existing edge configs render exactly as before** (0.3.0-dev.25). The
967
+ legacy keys map to `edge.tls` (EXT-1 rule 6): `issue_cert: false` is
968
+ `files`, `caroot` is `mkcert` with `ca`, and an `edge:` block with
969
+ neither is `mkcert` with the edge's own CA. A test renders each shape
970
+ (and its explicit `edge.tls` form) and compares the Traefik dynamic and
971
+ compose files, cert names and paths, route URLs and the mkcert call
972
+ byte for byte with 0.3.0-dev.24's own output. Under `files` with no `ca`,
973
+ `edge trust` now refuses (it minted a CA that signs nothing the edge
974
+ serves); with a `ca` it installs that CA, as before.
975
+ - **No built-in secret locations** (0.3.0-dev.23). Den no longer assumes a
976
+ vault, item, field, key or account of its own: the default 1Password
977
+ vault, developer-keys item and sudo field, the default account domain, and
978
+ the flat `<PREFIX>_LOCAL_ADMIN_PWD` key that doppler and env used for the
979
+ saved sudo password are gone. The saved sudo password and the journeys
980
+ agent key come only from `secrets.refs.sudo` and
981
+ `secrets.refs.journeys_agent_key`; without them each says "not configured:
982
+ set secrets.provider and secrets.refs.<name> in config/<cli>.yml" and
983
+ runs no CLI (the interactive unlock still works). A `1password`
984
+ distribution with no refs gets one warning in the web server's startup
985
+ log and in `validate`. The 1Password developer-keys setup writes to
986
+ `secrets.developer_keys: { vault, item }` (else the vault and item of
987
+ `refs.sudo`). See Migrations.
988
+ - **`make up` / `make web` check secrets only when `secrets.required` is
989
+ set** (0.3.0-dev.23). Otherwise the gate passes silently, whatever the
990
+ provider, and never asks the backend. `make <cli>-setup-secrets` runs the
991
+ selected strategy's setup; with `none`, or no provider set, it says there
992
+ is nothing to set up and exits 0.
993
+ - **`/api/sudo-password/vault-status` is backend-neutral** (0.3.0-dev.23):
994
+ `{ backend, label, configured, backendAvailable, reason?, hasStored }`,
995
+ and the unlock form says "Save to <backend>". `load-from-vault` answers
996
+ `{ ok: false, reason: "not-configured", message }` with no `refs.sudo`.
997
+ - **A local-jwt signing key that is not found names its provider**
998
+ (0.3.0-dev.23): "... not found in env or the doppler secrets backend
999
+ (secrets.provider)". Its resolution is unchanged.
1000
+
1001
+ ### Deprecated
1002
+
1003
+ - **The implicit mkcert default (set `edge.tls`)** (0.3.0-dev.25). An
1004
+ `edge:` block with no `tls`, `caroot` or `issue_cert` still means mkcert
1005
+ with a local CA, and warns once per run (`validate`, `doctor`, `edge up`,
1006
+ `stack up`): "edge.tls is not set; defaulting to mkcert with a local CA.
1007
+ Set edge.tls explicitly." `edge.caroot` and `edge.issue_cert` still work,
1008
+ and `validate` and `doctor` name the `edge.tls` line that says the same.
1009
+ - **The implicit doppler default (set `secrets.provider`)** (0.3.0-dev.23).
1010
+ An unset `secrets.provider` still means `doppler`, but only when something
1011
+ actually resolves a secret; the first time it does, Den warns once:
1012
+ "secrets.provider is not set; the implicit doppler default is deprecated
1013
+ and will be removed in a later minor. Set it explicitly: doppler,
1014
+ 1password, env or none." A distribution that resolves no secret never
1015
+ runs `doppler` and never sees the warning.
1016
+ - **`opAvailable` in `/api/sudo-password/vault-status`** (0.3.0-dev.23):
1017
+ an alias of `backendAvailable`, removed in a later minor. Read
1018
+ `backendAvailable` (and `backend`, `reason`).
1019
+
1020
+ ### Fixed
1021
+
1022
+ - **`web up` under an invalid config reports the server ready**
1023
+ (0.3.0-dev.42, a consumer's dev.41 finding). It waited for
1024
+ `/api/health`, which answers 422 by design then, and exited 1 with
1025
+ "web did not become ready" while the server ran. Readiness now asks
1026
+ `/api/server-info` (answered under any config; `/api/health` for a
1027
+ server too old for it), and the 422 "Config is invalid" page counts as
1028
+ the shell serving.
1029
+ - **No page throws under an invalid config** (0.3.0-dev.41, a consumer's
1030
+ dev.40 finding: "envs.map is not a function" on every page).
1031
+ `/api/environments` is config-free (it already fell back to `local`),
1032
+ the env picker and the Logs toolbar treat an error answer as an empty
1033
+ list, and a test loads every page with an invalid config and expects
1034
+ no uncaught error.
1035
+ - **A refused `edge down` says "not stopping it"**, not "not replacing
1036
+ it"; `edge` rejects an unknown flag (a mistyped `--take-over` is never
1037
+ ignored) (0.3.0-dev.41).
1038
+ - **A pre-dev.37 record whose port is held by another process** no
1039
+ longer warns "nothing running is it" before saying what holds the port
1040
+ (0.3.0-dev.41).
1041
+ - **Safety: an edge another state dir owns is never replaced**
1042
+ (0.3.0-dev.40, P1). The edge's container and Compose project are named
1043
+ by `edge.org`, so a scratch root with its own `DEN_EDGE_DIR` but the real
1044
+ `edge.org` recreated the real edge with its scratch certs and routes
1045
+ (`stack up` brings the edge up first). The edge container now carries
1046
+ `den.edge.root=<realpath of its state dir>` and `den.edge.org=<org>`, and
1047
+ `edge up`, `stack up`/`restart` (their implicit edge up) and `edge down`
1048
+ touch an existing `<org>-edge` only when its `den.edge.root` is their own
1049
+ state dir. Otherwise they refuse before writing or starting anything,
1050
+ exit 1: "edge <org>-edge belongs to <its dir>; not replacing it". An
1051
+ unlabelled (pre-dev.40) edge is adopted only from the default state dir,
1052
+ never under a `DEN_EDGE_DIR` that overrides it (the refusal names its
1053
+ mounts' dir when known). Under such an override a refusal also warns:
1054
+ "Give this root a distinct edge.org." `edge up --take-over` is the only
1055
+ override, and nothing implies it. The routes (the dynamic files) are
1056
+ byte-identical. See "The edge and TLS" in docs/distributions.md.
1057
+ - **A recreated edge keeps running stacks routed** (0.3.0-dev.40, P1). A
1058
+ new or restarted edge container starts on its own network only, so every
1059
+ running stack's routes answered 502 until that stack's next `stack up`.
1060
+ After `edge up` (and `stack up`/`restart`'s implicit edge up) the edge
1061
+ now rejoins the routed networks of each stack that has its routes file
1062
+ (`dynamic/stack-<project>.yml`) in this edge's state dir and whose
1063
+ Compose project is running, with one line per network ("rejoined
1064
+ <network> (<project>)"). Stacks that are down are skipped; a network the
1065
+ edge is on already is left alone; a project with no routes file in this
1066
+ state dir is never touched. `stack up` connects through the same helper.
1067
+ - **`validate` reports an org config error** (0.3.0-dev.40, a consumer's
1068
+ 0.2.8 nit): each problem with its file and key path, exit 1; the
1069
+ manifests are still checked.
1070
+ - **The scoped `manifest_validate` refusal names the whole path**
1071
+ (0.3.0-dev.40, a consumer's 0.2.8 nit): it was cut at 80 characters,
1072
+ mid-filename. Up to 160 it is whole; a longer one keeps its start and its
1073
+ filename and elides the middle with "…".
1074
+ - **A named `up <repo>/<svc>` whose port is held refuses before spawning**
1075
+ (0.3.0-dev.40, a consumer's dev.39 finding): it started the server once,
1076
+ which died on EADDRINUSE, before refusing. It now refuses as the
1077
+ repo-level up does ("<repo>/<svc> not started"), with or without a
1078
+ pre-dev.37 record. And a listener in the repo but in Den's own process
1079
+ group is said as that ("in Den's own process group; not adopted"), not
1080
+ as "outside <repoPath>".
1081
+ - **Graph cards** (0.3.0-dev.40, DOC-1): a service card's red "down"
1082
+ border goes when it recovers (only repo cards toggled it); before the
1083
+ first health round cards say "checking…", as the Workspaces table does,
1084
+ not "no health check"; the details panel's Opens link follows the edge's
1085
+ TLS (`http://` and the http port for `tls: none`; it always said
1086
+ `https://`).
1087
+ - **Records from before 0.3.0-dev.37 reach adoption from the CLI**
1088
+ (0.3.0-dev.39, a consumer's dev.38 finding). dev.38 decided right when
1089
+ asked, but the CLI's paths never asked. Every up, down and healthy-skip
1090
+ now reads a service's record through one accessor
1091
+ (`settledDaemonRecord`) that settles a pre-dev.37 record first: adopted
1092
+ (its group recorded) or dropped (said, nothing signalled).
1093
+ - `up <repo>` on a healthy service attached to the old record and left
1094
+ it without a group, so a later `down` could not stop it. It now adopts
1095
+ the running process ("adopted pre-dev.37 process (pid …, port …)"),
1096
+ starts nothing, and `down <repo>` stops it.
1097
+ - `down <repo>/<svc>` of a service with an up of its own pointed at the
1098
+ repo's down ("it is stopped by repox's down") and stopped nothing. Its
1099
+ down is its own: the group its up recorded.
1100
+ - A healthy service with no record whose port is held by a process in
1101
+ its repo is adopted the same way; one held from outside the repo is
1102
+ said ("port N is in use by pid … (cwd …), outside …: left alone"),
1103
+ never claimed or signalled, and nothing is started on it (a named
1104
+ target fails; a dependency warns).
1105
+ - An up whose process exits non-zero during startup (a second server
1106
+ that died on EADDRINUSE) keeps no record and no process group, so a
1107
+ later `down` never signals a dead group while the real listener runs.
1108
+ - The CLI prints what was adopted, dropped or found busy; before, those
1109
+ lines only flashed in the spinner.
1110
+ - `down -a` warns about records no active workspace reaches ("… is in no
1111
+ active workspace: down -a left it alone") and leaves them alone, where
1112
+ it skipped them silently.
1113
+ - **`restart` of a service with only an `up` works** (0.3.0-dev.39, a
1114
+ consumer's dev.38 finding). `restart repox/web` failed with `command
1115
+ "restart" not found` and left the old group running. With no restart or
1116
+ down command, a restart stops the recorded group, then runs the up
1117
+ detached, as `down` then `up` would.
1118
+ - **`down <repo>` and `down -a` stop a repo's per-service daemons**
1119
+ (0.3.0-dev.38, a consumer's dev.37 finding). A repo with services of
1120
+ their own (`up repox` started `repox/web` and `repox/api`, each with
1121
+ its own process group) and no down of its own printed "down repox
1122
+ (nothing to stop)" and left both running. A repo-level down with no
1123
+ down command now stops every service record of the repo through the
1124
+ per-record path (its refcount, its down command or its recorded group);
1125
+ `down -a` does it for each repo of the workspace. The mirror of
1126
+ dev.35's `up` multi-service fix.
1127
+ - **Records from before 0.3.0-dev.37 are adopted or dropped, never
1128
+ trusted blindly** (0.3.0-dev.38, a consumer's dev.37 finding). A record
1129
+ with no process group made `up` print "✔ up repox/api" while nothing ran,
1130
+ and `down` give up on it. One rule now serves `up` and `down`: the
1131
+ listener on the service's known port (its `port`, the port in its
1132
+ health URL, or `--port`/`-p` in its up command) whose cwd is inside the
1133
+ repo is adopted (its process group is recorded: "adopted pre-dev.37
1134
+ process (pid N, port P)"); else a recorded pid whose group shows every
1135
+ token of the up command (leading `VAR=value` and an `npx`/`npm exec`
1136
+ prefix stripped) is adopted; else the record is stale: warned, dropped,
1137
+ nothing signalled. `up` then reports an adopted service already running
1138
+ and starts a stale one; `down` stops an adopted one through the
1139
+ TERM→KILL group path. A port held by a process outside the repo is
1140
+ named and left alone.
1141
+ - **A restart records only the up** (0.3.0-dev.38). A service with a down
1142
+ and an up but no restart ran one detached `sh -c "<down> && <up>"`, so
1143
+ its recorded process group held the down too. It is now the down step,
1144
+ then a detached up step, and the group is the up's.
1145
+
1146
+ - **Graph: a selected card's flowing edges are visible** (0.3.0-dev.37,
1147
+ Ward QA of dev.35). The marching dashes were drawn in the same colour on
1148
+ a full-strength line, so they barely showed. A lit edge's line is now a
1149
+ faint track in its kind's colour and the dashes run wider on top of it;
1150
+ under reduced motion the lit edge stays solid and thicker, as before.
1151
+ - **Test workers inherit no credentials** (0.3.0-dev.37). `tests/setup.ts`
1152
+ strips credential-shaped variables (by name, family or token shape) from
1153
+ the environment before any suite runs, so a failing assertion on a
1154
+ spawn env can no longer print a developer's tokens.
1155
+ - **`down` stops what a detached `up` started** (0.3.0-dev.37, a
1156
+ consumer's dogfood finding). `<cli> down <repo>` for a service with no
1157
+ down command and no container printed ✔ and stopped nothing: daemon
1158
+ records kept no process. A detached `up` now records its process group
1159
+ (`pgid`, and the leader's start time as `pgidStartedAt`) in the daemon
1160
+ record, and `down` sends the group SIGTERM, waits up to 3s
1161
+ (`DEN_DOWN_GRACE_MS`), then SIGKILL. Before each signal it checks the
1162
+ pid still belongs to the recorded process (same start time), so a
1163
+ reused pid is never signalled: that service had already exited, and
1164
+ `down` says so and leaves the process alone. After a declared down
1165
+ succeeds, a recorded group still running is stopped too. When `down`
1166
+ cannot stop what it knows is running (a record written before dev.37,
1167
+ with no group; a group that survives SIGKILL), it warns, and an explicit
1168
+ `down <repo>` exits non-zero (a workspace-wide `down` warns and goes
1169
+ on). With nothing recorded, the CLI says "nothing to stop" instead of ✔.
1170
+ `down --dry-run` lists a service with a recorded group as one it stops.
1171
+ - **A compose-stack repo's Start and Restart work on the Workspaces page**
1172
+ (0.3.0-dev.37, a consumer's dev.35 finding). The repo row's buttons
1173
+ were disabled, "has no up command", for a repo whose up is its compose
1174
+ stack's. `/api/services` now gives each local service its effective
1175
+ commands (a stack's synthesized up/down/restart included) and
1176
+ `upTargets`: what `<cli> up <repo>` starts (`resolveUpTargets`), as the
1177
+ table's addresses. The repo row's Start and Restart are enabled when it
1178
+ is non-empty and run exactly those addresses.
1179
+ - **The Workspaces table says "checking…" until the first health round
1180
+ lands** (0.3.0-dev.37). Before it, repo and service health cells read
1181
+ "unknown (not checked)" or "no health check".
1182
+ - **Find shows only the sub-rows it matches** (0.3.0-dev.37). A Find that
1183
+ matched one service of a repo opened every one of the repo's sub-rows;
1184
+ now only the matching ones show under their repo (a match on the repo
1185
+ itself still shows them all).
1186
+
1187
+ - **`<cli> up <repo>` no longer fails fast on a multi-service dependency**
1188
+ (0.3.0-dev.35; backported to 0.2.8). A dependency with no `up` of its
1189
+ own but with per-service ups (two services, each with its `up`) was run
1190
+ as the repo, `command "up" not found for service "<repo>"`, and the run
1191
+ aborted before the repo you asked for. `up <repo>` and `up -a` now make
1192
+ one decision (`resolveUpTargets`): a repo with its own `up` starts as
1193
+ before; one with none but per-service ups starts its services (all of
1194
+ them, or the ones its dependents name in `depends_on`); only one with
1195
+ no `up` at all is skipped, said as `-a` says it ("Skipping (no local up
1196
+ command): …"), and nothing waits on it. A dependency whose `up` fails
1197
+ still fails fast.
1198
+
1199
+ - **The graph fits on first paint** (0.3.0-dev.34, a consumer's dogfood
1200
+ finding). With "All active", `/graph` opened at 60% with 17 of 31 cards
1201
+ outside the stage at 1440×900 (23 at 1024×768) until Fit was clicked:
1202
+ the org frames land after the first draw, the graph was drawn again,
1203
+ and only the first draw was fitted (a render a newer one overtook could
1204
+ also fit the stale layout). The view now fits once the final layout is
1205
+ drawn, and again whenever the set of cards changes, unless you zoomed
1206
+ or panned since. Local Dev's per-workspace graph likewise.
1207
+ - **The Workspace picker on `/graph` replaces `?ws=`** (0.3.0-dev.34, a
1208
+ consumer's dogfood finding). On `/graph?ws=w2`, "All active" changed the
1209
+ pill but the URL kept `?ws=w2`, which wins, so the header and the graph
1210
+ stayed on w2, after a reload too. A choice there now drops `?ws=` (All
1211
+ active) or sets it (a workspace), replacing the history entry, and the
1212
+ pill shows the workspace a fresh `?ws=` names: the pill, URL, the
1213
+ header's line and the graph agree.
1214
+ - **The runs dock is a tablist only while it holds tabs** (0.3.0-dev.34).
1215
+ With no runs, `.den-runs-l` was a `role=tablist` holding a "No runs"
1216
+ span and no tab; the role and its label come back with the first run.
1217
+ - **`theme-switch-isolation` is deterministic under load** (0.3.0-dev.34;
1218
+ it failed 5/6 then 1/6 on a busy machine). Its file snapshot covered the
1219
+ throwaway HOME, where a container runner's CLI that the health check
1220
+ calls writes its own update hint on its own schedule; it now covers
1221
+ Den's files (the distribution and `DEN_HOME`). The server gets a port
1222
+ from the OS on localhost (not a random pick that could land on another
1223
+ session's server), and readiness waits for the server whose
1224
+ `/api/server-info` names this distribution's root.
1225
+
1226
+ - **Stale daemon records are dropped** (0.3.0-dev.33, a consumer's
1227
+ dogfood finding). A record whose repo had been relocated (no pid,
1228
+ `containers: []`) was kept forever because its address still resolved,
1229
+ and the Runs dock showed a permanent tab. The startup sweep and the
1230
+ health reaper now drop a record of this root's, or an unstamped one,
1231
+ whose `repoPath` is not on disk, or whose `repoPath` is no longer where
1232
+ the service resolves and none of its candidate containers (captured,
1233
+ or the dev-op's at its old path) is running; the relocated checkout's
1234
+ own container, which carries the repo's name, does not count.
1235
+ Another root's records are never touched, and a record that resolves
1236
+ to its own checkout with nothing to verify is still kept (it may be a
1237
+ live non-opctl process). Removals are logged with the sweep's and the
1238
+ reaper's lines.
1239
+ - **Graph group boxes fit their content** (0.3.0-dev.33, a consumer's
1240
+ dogfood finding). On `/graph` a repo box with one row of cards drew
1241
+ about twice the height it needs: top-down, ELK reads a compound node's
1242
+ minimum size in its own layout axes, so the repo box's minimum width
1243
+ (386px) became its height. The minimum is passed in that order now; the
1244
+ boxes are their padding plus their cards, and keep the header's width.
1245
+ - **Fit keeps the graph above the legend and the zoom group**
1246
+ (0.3.0-dev.33, a consumer's dogfood finding). After Fit, a legend on
1247
+ two lines covered the last group's bottom ~10px. Fit's height now leaves
1248
+ out the overlays' measured height (and a gap), so no card or group
1249
+ border sits under them.
1250
+
1251
+ - **CA trust on a fronted edge reads "not needed: fronted"** (0.3.0-dev.32,
1252
+ from a consumer). With `edge.publish: false` a proxy in front owns
1253
+ browser trust (BOOT-1), so the Edge page's CA trust tile is neutral
1254
+ instead of a red "not trusted", and `edge status` says the same.
1255
+ - **Fit follows the graph's size on maximize and restore** (0.3.0-dev.32,
1256
+ R23, from a consumer). Fit stopped at 35%, so a big graph at 1440×900 read
1257
+ 35% maximized or not, and didn't fit either way. Fit now fits the whole
1258
+ graph, by width and height, down to 10% (zooming out goes as far as
1259
+ that), and a fitted view refits when the graph is maximized or restored.
1260
+
1261
+ - **A seed without a terminal no longer resets a workspace that has
1262
+ entries** (SEED-0, 0.3.0-dev.29; also 0.2.7). `<cli> init` and
1263
+ `<cli> workspace seed` from a script or `make` replaced every entry of the
1264
+ default (or blueprint) workspaces with managed clones, dev-managed
1265
+ checkouts included, without asking. Without a TTY they now refuse (exit 1,
1266
+ nothing written) and name `--reset`; on a TTY they still ask, default No.
1267
+ `init --reset` and `workspace seed --reset` reset as before. A script that
1268
+ relied on the silent reset must pass `--reset`.
1269
+ - **`<cli> init` in the distribution root seeds through a symlinked path**
1270
+ (0.3.0-dev.29): it compared paths without resolving links and ran the
1271
+ manifest scaffolder instead.
1272
+
1273
+ - **`create`'s "Next:" hint names an absolute path** (0.3.0-dev.26) when the
1274
+ new directory is not under the cwd, instead of `cd ../../../..`.
1275
+
1276
+ - **A fresh distribution runs no gh for a placeholder org** (0.3.0-dev.25,
1277
+ from a consumer's dev.24 run): `den create` wrote `github_org:
1278
+ your-github-org`, and Local Dev then ran `gh repo list your-github-org`
1279
+ on every load. The scaffold now leaves `github_org` commented out, with
1280
+ an example. With `github_org` unset or still a placeholder
1281
+ (`your-github-org`, `your-org`, `<your-org>`, `example-org`) GitHub is
1282
+ "not configured": `/api/github/repos` is empty and runs no gh (no
1283
+ fallback to catalog owners or a git remote), and `register`'s GitHub
1284
+ list ignores a placeholder.
1285
+ - **The lock's not-configured note names your config file** (0.3.0-dev.25):
1286
+ with no `secrets.refs.sudo`, the admin-password popover says "Saving the
1287
+ password is off: set secrets.provider and secrets.refs.sudo in
1288
+ config/acme.yml.", the distribution's real file, from the new
1289
+ `configFile` field of `/api/sudo-password/vault-status`.
1290
+ - **`register` with no terminal exits cleanly** (0.3.0-dev.25): with stdin
1291
+ closed or piped, the interactive `register` crashed with
1292
+ ERR_USE_AFTER_CLOSE. It now exits 2 (a usage error, since 0.2.8's
1293
+ check; it exited 1 before): "register needs a terminal (stdin is not a
1294
+ TTY)", naming the non-interactive form, `register <path> [--name
1295
+ <repo>] [--gh <owner/repo>]`.
1296
+ - **`bin/den` sets no backend account** (0.3.0-dev.24, from a consumer's
1297
+ dev.23 run): the launcher exported `OP_ACCOUNT` with a placeholder
1298
+ domain for every command, whatever the secrets backend, so it reached
1299
+ the web server even under doppler. It now adds nothing; the 1password
1300
+ strategy sets it from `secrets.onepassword.account`, or the caller's own
1301
+ is kept.
1302
+ - **`backendAvailable` is true for a usable backend even with no sudo ref**
1303
+ (0.3.0-dev.24): dev.23 reported false whenever `secrets.refs.sudo` was
1304
+ unset, so the deprecated `opAvailable` alias changed meaning. With no
1305
+ ref it now reflects detect() (a PATH check, nothing spawned) for an
1306
+ explicitly chosen backend; `configured: false` says the ref is missing.
1307
+ An unset provider and `none` still report false and are never probed.
1308
+ - **Raster marks are scaled to fit, never cropped** (0.3.0-dev.23).
1309
+ - **Den adds no 1Password account to anything it starts** (0.3.0-dev.23).
1310
+ The web server daemon (`web up`, `web restart`) was always given
1311
+ `OP_ACCOUNT`, with a built-in domain when unset, and `den.mk` exported
1312
+ one to every recipe. Now only the `1password` strategy gives one, from
1313
+ `secrets.onepassword.account` or your own `OP_ACCOUNT`; no other backend
1314
+ gives any, and `den.mk` sets none.
1315
+ - **1Password make targets exist only when 1Password is selected**
1316
+ (0.3.0-dev.23). `make <cli>-setup-1password` and `make auth-configure`
1317
+ moved to `make/secrets-1password.mk`, which `den.mk` includes only when
1318
+ `secrets.provider` (or `<PREFIX>_SECRETS_PROVIDER`) is `1password`; every
1319
+ other distribution no longer has them.
1320
+ - **"Register a managed repo" never lists the engine's owner's repos**
1321
+ (0.3.0-dev.23). With no `github_org`, the org fell back to the git remote
1322
+ of the engine's own checkout, so a linked or git-installed engine listed
1323
+ its owner's repos for an unrelated project. The fallback is now the
1324
+ distribution's own checkout (only when it is the top of its own repo),
1325
+ else the "set github_org" error. The 1Password setup scripts likewise
1326
+ read `developer_keys.env_file` and write `admin-auth-<env>.json` under the
1327
+ distribution root, not the engine's.
1328
+ - **Settings keeps your pick marked while it scrolls** (0.3.0-dev.22, SA's
1329
+ dev.21 run): a click or Enter on a category no longer walks
1330
+ `aria-current` through every category the smooth scroll passes. Your
1331
+ pick holds until the scroll ends (`scrollend`, else once it is still,
1332
+ within about a second); then the scroll-spy takes over again.
1333
+ - **Below 480px the top bar shows only the mark** (0.3.0-dev.22, SA's
1334
+ dev.21 run): the product name is visually hidden there and stays the
1335
+ brand link's accessible name; from 480px it truncates as in dev.21.
1336
+ - **0.2.6 is in the 0.3 line** (0.3.0-dev.21): `type: container` health
1337
+ checks, the server identity check behind `web restart` and `web down`,
1338
+ the MCP server's identity, and `service install --plan`. See 0.2.6 below.
1339
+ - **Settings marks the category you picked** (0.3.0-dev.21, SA's dev.20
1340
+ run): `/settings#distribution`, or a click on the last short section,
1341
+ marked the one before it as current, because the page could not scroll
1342
+ far enough for the section to reach the top. At the end of the page,
1343
+ your pick wins if it is in view.
1344
+ - **"IDE for this workspace" saves on change** (0.3.0-dev.21), like every
1345
+ other row; its Save button is gone.
1346
+ - **The top bar fits at 400px** (0.3.0-dev.21): a long product name
1347
+ truncates and the stale-server pill shows only its ⚠ (named "Stale
1348
+ server: restart"), so the page no longer scrolls sideways.
1349
+ - **Status drawer rows are named by service, state and latency**
1350
+ (0.3.0-dev.21), e.g. "shop/db, healthy, 9 ms". What the check ran is
1351
+ the row's description and tooltip, not its name.
1352
+ - **The rail's Settings item opens Settings** (0.3.0-dev.19): in dev.18 it
1353
+ went to `/local-dev?settings=1`, which nothing read, so it landed on
1354
+ Local Dev. It now goes to `/settings`, and the old URL redirects there.
1355
+ Plain `/settings` opens at the top, on the title and the search box; a
1356
+ link that names a category (`/settings#editor`) scrolls to it and puts
1357
+ focus on its heading.
1358
+ - **The rail's × keeps focus in the rail** (0.3.0-dev.19, SA's dev.18
1359
+ run), as Delete does: the next pinned row, else the previous one, else
1360
+ the first item (Enter on "Unpin <id>" dropped focus to the top of the
1361
+ document). One function for both.
1362
+ - **The narrow rail is a modal drawer** (0.3.0-dev.19, SA's dev.18 run).
1363
+ Below 720px, ☰ moves focus into the rail (the current page), makes the
1364
+ top bar and the page `inert` behind a scrim, and keeps Tab inside;
1365
+ Escape or a click on the scrim closes it, sets `aria-expanded` back and
1366
+ returns focus to ☰ (Escape did nothing before).
1367
+ - **Names screen readers read** (0.3.0-dev.19, SA's dev.18 run): a rail
1368
+ group is named by its label alone (`aria-labelledby` points at the label,
1369
+ not the heading with its tag; Chrome read "XGlobex"); a tagged menu
1370
+ row reads "default, focus", not "defaultfocus"; the ✓ on a checked menu
1371
+ row is drawn with no alt text (`aria-checked` says it); ◐ ▾'s glyphs are
1372
+ hidden from readers. A scope button's `aria-controls` names its menu
1373
+ only while the menu is open.
1374
+ - **A page registered before its group no longer warns** (0.3.0-dev.19,
1375
+ a consumer's dev.18 run). Queuing a page before its group is a legitimate
1376
+ order; the warning now waits until the queue has drained and a tick has
1377
+ passed after `den-shell:ready`, and fires only for a group that is still
1378
+ missing (the same for a setting and its category).
1379
+ - **Opening a run's output no longer hides the lowest graph cards**
1380
+ (0.3.0-dev.19, a consumer's dev.18 run). Automatic fits (first draw, layout
1381
+ changes, and the re-fit as the output panel opens) now show the whole
1382
+ graph whenever it fits at 35% zoom, the Fit button's limit; only a graph
1383
+ too big for that starts at its top at 60%. The panel already had its
1384
+ own room below the graph: stopping the fit at 60% cut the lowest cards
1385
+ off at the graph's bottom edge, where the panel begins, so a click
1386
+ landed on the panel.
1387
+ - **The ORG label is the org's name, not its id** (0.3.0-dev.18). With no
1388
+ `org.title` in the org config, the label was the id (`edge.org`). It is
1389
+ now `org.title`, then the `orgs:` label of `github_org` (the name the
1390
+ Environments page shows), then the id.
1391
+ - **A pin toggle follows the rail** (0.3.0-dev.18). Unpinning from the rail
1392
+ (× or Delete) left the Services card's ☆ and the graph panel's reading
1393
+ "Unpin X" (`aria-pressed=true`) until a reload.
1394
+ - **Delete on a pinned row keeps focus in the rail** (0.3.0-dev.18): on the
1395
+ next pinned row, else the previous one, else the first item (it fell to
1396
+ the top of the document).
1397
+ - **A pinned row is named by its id** (0.3.0-dev.18), not "shop-web×":
1398
+ the × is its own control, out of the tab order, named "Unpin
1399
+ shop-web", its glyph hidden from screen readers; the row has
1400
+ `aria-keyshortcuts="Delete"`.
1401
+ - **One main landmark per page** (0.3.0-dev.18). Every page's content is in
1402
+ `<main id="den-content">` (the skip link's target); the four pages that
1403
+ had a `<main>` of their own around part of it, and the two that had
1404
+ none, now have exactly one.
1405
+ - **A distribution group is named once** (0.3.0-dev.18): its tag is hidden
1406
+ from screen readers and read as the group's description ("from globex"),
1407
+ not "Globex globex".
1408
+ - **Registration mistakes warn** (0.3.0-dev.18): a non-number `order`, or
1409
+ a `registerNav` into a group not registered yet, logs a `console.warn`
1410
+ naming the field. The page waits for its group instead of being lost.
1411
+ - **Local Dev's output panel no longer covers graph cards** (0.3.0-dev.18).
1412
+ When the panel opened (a run's tab), the graph's viewport shrank but the
1413
+ graph kept its fit, so the lower cards sat under the panel and couldn't
1414
+ be clicked. A viewport that changes size re-fits the graph unless you
1415
+ have panned or zoomed it yourself.
1416
+
1417
+ - **An app is never painted down before its checks have answered**
1418
+ (0.3.0-dev.17). One cold load in twenty painted an app red for ~100ms:
1419
+ only its one down service had answered, and as the only row it read as
1420
+ the entry service. A poll that overlapped the first one started from
1421
+ nothing and drew what had landed. Until every check of a repo has
1422
+ answered (a pending check has), the repo is unknown (neutral, "still
1423
+ checking"); a repo the last complete cycle described keeps its earlier
1424
+ answers meanwhile. The rail's Services badge and pinned dots read the same
1425
+ rollup, so they count only completed downs.
1426
+ - **Shell accessibility** (0.3.0-dev.17): a "Skip to content" link comes
1427
+ first on every page; the rail is one tab stop (the current page) with
1428
+ ↑/↓, Home and End moving within it; the Logs link's name is "Logs" or
1429
+ "Logs, 2 open", not "Logs0"; the Tests page's title is "<title> · Tests".
1430
+ - **"What's new" is a modal dialog, and reopens at the top** (0.3.0-dev.17).
1431
+ A reopen after scrolling kept the old position: the scroll was reset
1432
+ while the overlay was still hidden, where it doesn't stick; it is reset
1433
+ once the dialog shows. The box is `role="dialog"` with `aria-modal` and
1434
+ its heading as the label; it takes focus (the versions toggle, else Got
1435
+ it), keeps Tab inside, and gives focus back on close (to the version pill
1436
+ after a first-visit open). A browser test covers Escape, × and Got it,
1437
+ collapsed and expanded; CI installs Playwright's chromium for it.
1438
+ - **One bar on Logs** (0.3.0-dev.17). The cols/rows toggle moved into the
1439
+ Logs toolbar from a band of its own.
1440
+ - **One workspace control on Services** (0.3.0-dev.18). Services' own
1441
+ workspace select (beside the top bar's) is gone: the top bar's Workspace
1442
+ ▾ says what the page lists; its status moved to the Output bar.
1443
+ - The appearance control (◐) sits at the header's far right on every page
1444
+ and never leaves the screen: the nav shrinks and scrolls and the health
1445
+ dots give way first (the Tests page's top bar overflowed at 1440 and 1024).
1446
+ - Org colors from the config are no longer text: the graph's org frames and
1447
+ the panel's breadcrumb label in `--text` with the color as a dot (they read
1448
+ at 2.5:1 on a light ground), and the frame's ground is neutral. A test
1449
+ bans a config color as `color:`.
1450
+ - The active workspace row in the Local Dev sidebar is a calm neutral tint
1451
+ in both modes (it was a loud accent tint in light mode).
1452
+ - `plugin list` with broken dirs in `plugins/` (0.3.0-dev.8, reported by
1453
+ consumers on dev.7) prints exactly one row for every directory there, every
1454
+ time. Before, it crashed with no rows when `plugins.lock.json` couldn't be
1455
+ parsed, skipped a symlink, and hid a dir whose lock entry named another
1456
+ id. `plugin validate` reports each of these too. Nothing in a plugin dir
1457
+ makes either command throw.
1458
+ - A dir name that isn't an id (such as `plugins/x/`) now gives the real
1459
+ rule: "2–40 characters: lowercase letters, digits or dashes, starting with
1460
+ a letter". A missing manifest is reported as well (0.3.0-dev.8).
1461
+ - A dir with no manifest reads "local (plugins/scratch): no den-plugin.json
1462
+ or den-theme.json, so it isn't a plugin" in `plugin list`. "Not valid
1463
+ under this <cli>: …" is now kept for a recognized plugin that fails
1464
+ validation (0.3.0-dev.8).
1465
+ - `theme list` still shows themes only, and still exits 0 when other plugin
1466
+ dirs are broken. It now adds one line, "N plugin dir(s) have errors: run
1467
+ `<cli> plugin list`" (0.3.0-dev.8).
1468
+
1469
+ - `theme list`'s footer counts every broken plugin dir, whether or not it
1470
+ is a row above, and gets the plural right: "1 plugin dir has errors" /
1471
+ "4 plugin dirs have errors: run `<cli> plugin list`" (0.3.0-dev.9; it
1472
+ counted only the hidden ones, as "N plugin dir(s)").
1473
+ - `plugin validate` says the same as `plugin list` for a dir with no
1474
+ manifest: "no den-plugin.json or den-theme.json, so it isn't a plugin",
1475
+ from one constant. `plugin list`'s and `theme list`'s id column fits the
1476
+ longest id, so rows stay aligned with ids up to 40 characters
1477
+ (0.3.0-dev.9).
1478
+ - **The header's health row no longer cuts repos off** (0.3.0-dev.9, a
1479
+ regression from the one-line header earlier in 0.3.0). The row was clipped
1480
+ silently: whole repos were hidden, a name could be cut mid-word, and a
1481
+ hidden repo that was down didn't show. It now:
1482
+ - orders problems first: down, then degraded, then healthy, then unknown
1483
+ (0.3.0-dev.11; dev.9 put unknown before healthy), within and across
1484
+ application boxes;
1485
+ - shows only items that fit whole;
1486
+ - ends with a "+N" chip for the rest, named "N more repos" and saying
1487
+ "+N · 1 down" when a hidden repo has a problem. The chip opens the full
1488
+ list; Escape closes it and returns focus to the chip.
1489
+ - fits again on resize, when fonts load, and when the rest of the header
1490
+ squeezes it.
1491
+
1492
+ An interim fix until the slice-3 status chip. The new module is
1493
+ `public/health-fit.js`.
1494
+
1495
+ - **One repo health rule for the header, the chip, the graph and
1496
+ `<cli> status`** (0.3.0-dev.10). The header row and `status` read only
1497
+ the entry service, so a repo whose web answered but whose mailpit
1498
+ container was gone read healthy. It sorted with the healthy repos and
1499
+ never reached the chip's count, while the graph showed the down service.
1500
+ All four now use one function, `public/health-rollup.js`, typed for
1501
+ TypeScript in `src/shared/health/rollup.ts`. Only checked signals count:
1502
+ - **down**: the entry service (the manifest `entrypoint`, else the
1503
+ service named like the repo, else the only one) is down;
1504
+ - **degraded**: another checked service is down;
1505
+ - **ok**: at least one signal is checked and none fails;
1506
+ - **unknown**: nothing is checked. It is neutral, never green, and sorts
1507
+ last, after healthy (0.3.0-dev.11). A service not yet probed is
1508
+ unknown. `status` keeps the graph's order.
1509
+
1510
+ A repo of 26 services with one probe is judged by that probe.
1511
+ - `status` prints one row per repo with its rolled-up state and names
1512
+ what is down, e.g. "degraded" with "mailpit down". It says "N not
1513
+ checked" when services aren't probed. An entrypoint-less repo is
1514
+ rolled up too, and `--verbose` still lists every service.
1515
+ - The graph's repo card shows degraded in `--warn`.
1516
+ - In the row, same-state repos sort the focused workspace's first, then
1517
+ by name. `/api/health` rows carry `focused`.
1518
+ - **The health row no longer hides everything behind one big application
1519
+ box** (0.3.0-dev.10).
1520
+ - When the boxes don't all fit, the row shows repos one by one. It places
1521
+ every down or degraded repo that fits first, and a problem that is too
1522
+ wide never blocks a later one. Then it adds the rest, in order.
1523
+ - The chip shows the richest text that fits ("+9 · 1 down, 1 degraded",
1524
+ then "+9 · 1 down"). Its name and tooltip always carry the full count.
1525
+ - The row now takes the header's free space. Before, it shrank to its
1526
+ content, and the page's own spacer took half of what was left.
1527
+ - **The health row is never an empty box on a cold load** (0.3.0-dev.11).
1528
+ Before the first health result (the application and editor lookups and
1529
+ the stream come first) the row painted nothing, and since dev.10 it takes
1530
+ the header's free space, so it read as a wide empty box with no refresh
1531
+ button. It now shows a neutral "…" and the refresh button at once. A fit
1532
+ that runs before the row or its items have a width keeps everything
1533
+ boxed and runs again next frame (`fitPlan` in `public/health-fit.js`).
1534
+ It fits again on the first data and when fonts load. The brand, version
1535
+ badge and pills beside the row never shrink for it.
1536
+ - **The health row's fit, checked against a real 6+3 workspace**
1537
+ (0.3.0-dev.11). Measured names of 71 to 182px: all nine flat need about
1538
+ 1070px, so a 1002px row shows 7 and "+2", and a 1282px row shows both
1539
+ boxes whole. The fit was right; these numbers are now a test, with an
1540
+ 11-repo 7+3+1 case for the flat fallback.
1541
+ - **The "+N" chip names a hidden problem in its text** (0.3.0-dev.12).
1542
+ With only room for the chip, a hidden degraded repo read "+11" with an
1543
+ amber ring, and "1 degraded" was only in its accessible name. The ladder
1544
+ is now "+11 · 1 down, 1 degraded", then the worst one alone ("+11 · 1
1545
+ down"), then compact ("+11 · ⚠2", 0.3.0-dev.13), down and degraded named
1546
+ the same way. Bare "+N" only when no
1547
+ hidden repo has a problem. The fit keeps room for that text
1548
+ (`fitPlan`'s `chipWidthOf`).
1549
+ - **Escape from the chip's list returns focus to the chip, even after a
1550
+ re-fit** (0.3.0-dev.12). A health tick or a font load while the list was
1551
+ open laid the row out again with a new chip node, so focus fell to the
1552
+ page (the breadcrumb). The chip is now one node across re-fits, keeps its
1553
+ focus, and stays the list's anchor; when a re-fit drops the chip, focus
1554
+ moves to the row's refresh button.
1555
+ - **The health row settles once on a fresh load** (0.3.0-dev.13). It
1556
+ reshuffled three or four times between 0.6s and 3.6s as streamed results
1557
+ arrived. Now a first cycle (a page load, an environment switch) keeps the
1558
+ "…" placeholder until the whole cycle is in (or 8s pass with a slow probe
1559
+ still out), then fits once. Later data repaints are debounced by 300ms;
1560
+ resize and font re-fits stay immediate (`paintGate` in
1561
+ `public/health-fit.js`).
1562
+ - **The refresh button is never clipped** (0.3.0-dev.13). At 124px,
1563
+ "+11 · 1 degraded" pushed ⟳ out of the row. The chip's ladder ends in a
1564
+ compact form, "+11 · ⚠1" (every hidden problem counted), used before the
1565
+ problem would be dropped; its name and tooltip keep the full text. If
1566
+ even that doesn't fit, the chip's text gives way, not ⟳. The chip is
1567
+ measured at its natural width.
1568
+ - **One shell for every page: a top bar and a left rail** (0.3.0-dev.14,
1569
+ shell slice 2). Every page had its own copy of the header, nav and header
1570
+ CSS (six copies; Environments loaded none of the header's behaviour).
1571
+ `public/shell.js` now draws one frame on every page, and the pages keep
1572
+ only their content:
1573
+ - **Top bar** (48px): the theme's mark and the title in the theme's
1574
+ display face (`var(--display)`; the pages had used the mono face), the
1575
+ version pill, the health row and chip (unchanged), the workspace focus
1576
+ pill, one env picker and ◐.
1577
+ - **Rail**: Local Dev, Services, Environments, Releases, Logs, Tests, the
1578
+ current page marked `aria-current="page"`. ↑/↓, Home and End move
1579
+ between pages; it collapses to icons below 1100px (labels stay for
1580
+ assistive tech). No horizontal page scroll at 1024.
1581
+ - **One env picker for the whole shell.** Tests, Services and Logs each
1582
+ had their own; the choice is now one control, remembered across pages.
1583
+ Local Dev and Releases keep checking local health whatever it says
1584
+ (`data-health-env`), and Releases' own selector is now labelled as the
1585
+ deploy target it is.
1586
+ - A page joins the shell with `<div id="den-shell" data-page="…">` and
1587
+ `<script src="/shell.js">`; `header.js` drives it (`envReady()`,
1588
+ `healthEnv()`).
1589
+ - **Header fixes** (0.3.0-dev.14): a long workspace name ends in an
1590
+ ellipsis with the full name in its tooltip, and names in the bar never
1591
+ wrap mid-word; the Tests page's "ENV" label no longer collides with its
1592
+ select (the picker is the shell's).
1593
+ - **Environments groups services under their org again** (0.3.0-dev.14).
1594
+ Every service showed under "no org": the page read each row's `org` from
1595
+ `/api/health`, which never sends one. It now takes each repo's org from
1596
+ `/api/orgs?repos=1`, like the graph.
1597
+ - **The first health paint waits for the whole cycle** (0.3.0-dev.14). A
1598
+ stream that broke after its first results counted as a finished cycle,
1599
+ so a cold load could paint a partial row. Only the server's `done` ends
1600
+ a cycle now; a broken stream is completed from `/api/health` (or the 8s
1601
+ cap paints what landed, and a check with no result is never shown as
1602
+ down). `healthCycle` in `public/health-fit.js`.
1603
+ - **The Theme menu says when your theme isn't in use** (0.3.0-dev.10). A
1604
+ stored theme that fails validation selects nothing, instead of looking
1605
+ like "Org default". One line explains: "Your theme <id> isn't valid
1606
+ (<first problem>), so <name> is in use", or "isn't installed". Picking
1607
+ any option replaces the stored choice.
1608
+
1609
+ ### Migrations
1610
+
1611
+ - 0.3.0-dev.40, the edge's owner labels: plan one `edge up`. The edge's
1612
+ compose.yml gains `den.edge.root` and `den.edge.org` labels, so the first
1613
+ `edge up` (or `stack up`) after upgrading recreates the edge once, from
1614
+ its own state dir. Routes drop only while the container restarts, then
1615
+ stay up: the edge rejoins running stacks' networks. Run it once, from
1616
+ the edge's own dir, when a brief blip is acceptable. A scratch or test root that sets `DEN_EDGE_DIR` needs an
1617
+ `edge.org` of its own; with the real one it is refused.
1618
+ - 0.3.0-dev.40, commands that detach: a command other than up and restart
1619
+ that should keep running detached says `detachable: true`.
1620
+ - 0.3.0-dev.39, the UI language: nothing required. With no `language:`
1621
+ in `user.yml` the UI is English, as before. A distribution or plugin
1622
+ that adds its own words with `denShell.addMessages(lang, table)` can
1623
+ add `ja` and `es` tables the same way.
1624
+ - 0.3.0-dev.39, pre-dev.37 records: nothing required. Their next `up`,
1625
+ `down` or `restart` from the CLI or the web adopts or drops them (said).
1626
+ - 0.3.0-dev.38, the restart banner: **restart the web server once after
1627
+ installing.** The first upgrade from a server older than dev.38 can't
1628
+ show the "Den was updated" banner: the old server has no MD-2 code, so
1629
+ it keeps serving the new pages from the old engine until it restarts
1630
+ (`<cli> web restart`, or your service restart). From dev.38 on, the
1631
+ banner says when a restart is needed.
1632
+ - 0.3.0-dev.38, `/api` error bodies: **they changed from text to JSON.**
1633
+ Every `/api/*` error now answers `Content-Type: application/json` with
1634
+ `{ ok: false, error, code, requestId }`, where some routes sent
1635
+ `text/plain` (a message, or an empty body). A script that read the text
1636
+ should read `.error`; a status check needs nothing. Den's own pages and
1637
+ the MCP server read the new shape.
1638
+ - 0.3.0-dev.38, records from before dev.37: nothing required. A service
1639
+ still running from such a record is adopted on its next `up` or `down`
1640
+ (by its port, else its pid); one that can't be matched is dropped with a
1641
+ warning and never signalled.
1642
+ - 0.3.0-dev.37, `down` and process groups: nothing required. A service
1643
+ started by an older Den has no recorded group, so its first `down
1644
+ <repo>` after the upgrade (for a service with no down command and no
1645
+ container) warns, drops that record and exits non-zero: stop it
1646
+ yourself once; its next `up` records the group.
1647
+ - 0.3.0-dev.36, `stack_namespace`: optional, no change required. Without it
1648
+ every Compose project, container, volume and network keeps its name.
1649
+ `stack_namespace` in a blueprint needs Den 0.3.0-dev.36 or later: an
1650
+ older Den rejects the unknown key.
1651
+ - 0.3.0-dev.25, edge TLS. Nothing is required: every existing edge config
1652
+ renders as before (only `edge trust` under `issue_cert: false` with no
1653
+ `caroot` now refuses). To silence the warnings, say what your config already means,
1654
+ in the org config (`config/<cli>.yml`):
1655
+ - **No `tls`, `caroot` or `issue_cert`** (the implicit default):
1656
+
1657
+ ```yaml
1658
+ edge:
1659
+ tls: { strategy: mkcert }
1660
+ ```
1661
+
1662
+ Or, if the edge needs no https, `tls: none`.
1663
+ - **`caroot: <ca>`**: replace it with
1664
+
1665
+ ```yaml
1666
+ edge:
1667
+ tls: { strategy: mkcert, ca: <ca> }
1668
+ ```
1669
+
1670
+ - **`issue_cert: false`** (with `include` and `mounts`): replace it with
1671
+
1672
+ ```yaml
1673
+ edge:
1674
+ tls:
1675
+ strategy: files
1676
+ include: edge/dynamic # was edge.include
1677
+ mounts: { <host path>: <container path> } # was edge.mounts
1678
+ ```
1679
+
1680
+ `include` and `mounts` may also stay where they were. With `caroot`
1681
+ too, move it to `ca: <ca>` under `tls`.
1682
+ - `edge.tls` beside `caroot` or `issue_cert` is an error: move them.
1683
+ - `github_org: your-github-org` from an earlier `den create`: set your
1684
+ real org, or delete the line.
1685
+ - 0.3.0-dev.23, secrets. Nothing is required of a distribution that uses
1686
+ no secrets; the YAML below goes in the org config (`config/<cli>.yml`).
1687
+ - **Relied on the old 1Password defaults** (the saved sudo password or
1688
+ the journeys key in the developer-keys item): add the old layout as
1689
+ your own refs. These are the exact former defaults; `<PREFIX>` is your
1690
+ env prefix (`ACME` for `acme`, `DEN` for Den's own checkout).
1691
+
1692
+ ```yaml
1693
+ secrets:
1694
+ provider: 1password
1695
+ onepassword:
1696
+ account: example.1password.com # your account; Den injects none now
1697
+ refs:
1698
+ sudo:
1699
+ { vault: Employee, item: Developer API Keys, field: <PREFIX>_LOCAL_ADMIN_PWD }
1700
+ journeys_agent_key:
1701
+ { vault: Employee, item: Developer API Keys, field: CURSOR_API_KEY }
1702
+ developer_keys: { vault: Employee, item: Developer API Keys }
1703
+ ```
1704
+ - **Relied on doppler or env for the saved sudo password** (the flat
1705
+ `<PREFIX>_LOCAL_ADMIN_PWD` key):
1706
+
1707
+ ```yaml
1708
+ secrets:
1709
+ provider: doppler # or env
1710
+ refs:
1711
+ sudo: { key: <PREFIX>_LOCAL_ADMIN_PWD }
1712
+ ```
1713
+
1714
+ - **Relied on the implicit doppler default**: it still works. To silence
1715
+ the deprecation warning, say so: `secrets: { provider: doppler }`. A
1716
+ distribution that uses no secrets store can set `secrets: { provider:
1717
+ none }`.
1718
+ - A page or script that read `opAvailable` from
1719
+ `/api/sudo-password/vault-status` reads `backendAvailable`.
1720
+ - A `make auth-configure` or `make <cli>-setup-1password` in your own
1721
+ scripts needs `secrets.provider: 1password`.
1722
+ - None. Optional: set `brand.theme: <id>` in the org config to give the
1723
+ distribution's web UI its own default theme.
1724
+ - A `plugins/<id>/` with no `den-plugin.json` or `den-theme.json` now fails
1725
+ `plugin list` and `plugin validate`: give it a manifest or move it out of
1726
+ `plugins/`. A theme added from a `file://` path (0.3.0-dev.2 warned) stays
1727
+ listed, but re-add it from its published URL before committing the lock.
1728
+ - 0.3.0-dev.20: the header's health row (`#health`) and Local Dev's
1729
+ `#sudo-banner` are gone, and so are `planRow`, `fitPlan`, `moreChip`,
1730
+ `mountMoreChip`, `openMoreList` and `closeMoreList` in
1731
+ `public/health-fit.js`. A distribution page that read them reads the
1732
+ `<cli>:health-update` event (unchanged) or the status chip (`#den-status`)
1733
+ instead, and unlocks with `requireUnlock()` from `/header.js`.
1734
+ - 0.3.0-dev.8: a `den-theme.json` used as a plugin needs an `author`
1735
+ (`theme new` sets one). A plugin's `den` range is now enforced: write
1736
+ `>=0.3.0-0 <0.4.0` to admit Den's 0.3.0 prereleases, because `>=0.3.0`
1737
+ excludes them.
7
1738
  ## [0.2.8] - 2026-11-05
8
1739
 
9
1740
  ### Added