@monster-orange/den 0.4.15 → 0.4.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,73 @@ 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.16] - 2026-11-08
8
+
9
+ ### Added
10
+
11
+ - **Change an agent's persona (AG-18).** **Change persona…** in the
12
+ agent pane's ⋯ menu (running or stopped), and `<cli> agents persona
13
+ <id> <persona> [--model <m>] [--now | --after-turn]`. Den stops the
14
+ session and resumes the same conversation with the new persona's role
15
+ prompt, permission mode and allowed commands; the history, id, name,
16
+ workspace, folder, tab and the model it runs stay. The dialog marks the
17
+ current persona and lists what changes. Mid-turn it never stops a turn
18
+ unasked: "It's working. Change after this turn, or stop it now?" (409
19
+ `busy`); After this turn waits for the turn to end, Stop it now stops
20
+ it. The wait lives in the server's memory: if Den restarts first, the
21
+ change is dropped, the record no longer marks it pending, and the audit
22
+ trail says "persona change cancelled: Den restarted". A person's only: an agent's token is refused (403), and there is no
23
+ MCP tool. Only a runtime that can resume offers it. The record keeps
24
+ `personaChanges`, and the audit trail records each change. Route `POST
25
+ /api/workspaces/<ws>/agents/<id>/persona`.
26
+
27
+ ### Changed
28
+
29
+ - **Bring in keeps the conversation's model, and says what the persona
30
+ changes (AG-17.1c).** It used to switch the conversation to the
31
+ persona's model, and drop MCP servers the conversation had outside Den,
32
+ without a word. Now the model stays the one its last answer came from
33
+ ("Keeps <model> from the conversation"; Advanced or `--model` picks
34
+ another), and a short list under the persona says "From now on it works
35
+ as <persona>: its role, its permissions (<mode>), Den's tools and
36
+ guards. Its history stays.", plus "It won't have: <servers>." for MCP
37
+ servers seen in its transcript that Den won't load. The persona of the
38
+ latest agent that worked in its folder is preselected (`suggest`), never
39
+ submitted. The list adds `model`, `lost` and `suggest`; the import takes
40
+ `model` and `version`.
41
+ - **The signed-out page shows Sign in with GitHub once the owner is
42
+ linked, with no reload.** With no owner yet it watches the session
43
+ probe (which now says `github.state` to a signed-out browser), and step 1
44
+ turns done and the button appears as soon as `web owner link` finishes.
45
+
46
+ - **The signed-out page explains Sign in with GitHub (SEC-2a-UX).** It
47
+ shows the same three steps whether or not an owner is linked: on the
48
+ machine Den runs on, run `<cli> web owner link` (or use Settings ›
49
+ Access there); enter the code it shows at github.com/login/device and
50
+ approve; come back and press Sign in with GitHub with the same account.
51
+ Once an owner is linked, step 1 shows as done ("Owner: @login") and the
52
+ button follows the steps. Below them: only the owner's GitHub account
53
+ can sign in this way, anyone else uses Approve this computer, and Learn
54
+ more opens docs/distributions.md, "Signing in with GitHub". English,
55
+ Spanish and Japanese.
56
+ - Another GitHub account is refused with "@login isn't the owner of this
57
+ Den. Only the owner's account can use Sign in with GitHub. Use Approve
58
+ this computer instead." It no longer says to ask the owner to add you:
59
+ a Den has one owner, and no one to add.
60
+
61
+ ### Fixed
62
+
63
+ - An open agent pane stayed on the old session after Change persona, Resume
64
+ or a rotation, showing "[the session exited (143)]" while the list showed
65
+ the new one running. A pane now follows its agent: it moves to the new
66
+ session in the same slot, keeps its scrollback, and says "Switched to
67
+ <persona>". After a session ends the page looks again every second for
68
+ 15 s, so the switch shows within about 2 s.
69
+
70
+ ### Migrations
71
+
72
+ - None.
73
+
7
74
  ## [0.4.15] - 2026-11-07
8
75
 
9
76
  ### Added
package/README.md CHANGED
@@ -10,8 +10,8 @@
10
10
  </p>
11
11
 
12
12
  <picture>
13
- <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.15/hero-dark.png">
14
- <img src="docs/images/0.4.15/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
13
+ <source media="(prefers-color-scheme: dark)" srcset="docs/images/0.4.16/hero-dark.png">
14
+ <img src="docs/images/0.4.16/hero-light.png" width="100%" alt="Den's Graph page: the demo workspace's services grouped by application, all healthy, with the status chip and the left rail">
15
15
  </picture>
16
16
 
17
17
  ## Set up by your agent
@@ -38,7 +38,7 @@ It doesn't replace your tools. Your repos, Docker, your test runners, your secre
38
38
  yours. Den connects them and turns on good practice from the start.
39
39
 
40
40
  <p align="center">
41
- <img src="docs/images/0.4.15/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
41
+ <img src="docs/images/0.4.16/up.gif" width="100%" alt="make up in the Graph page: every stack down, then demo-api, demo-web and demo-notes come up in dependency order until the status chip reads All 3 healthy">
42
42
  </p>
43
43
 
44
44
  ## How it helps
@@ -68,10 +68,10 @@ yours. Den connects them and turns on good practice from the start.
68
68
  lives in your own repo.
69
69
 
70
70
  <p align="center">
71
- <img src="docs/images/0.4.15/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
- <img src="docs/images/0.4.15/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
- <img src="docs/images/0.4.15/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
- <img src="docs/images/0.4.15/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
71
+ <img src="docs/images/0.4.16/agents.png" width="49%" alt="The Agents tab: the demo workspace's sessions on the left, and one open in a terminal pane on the right, running demo-api's test and then the service">
72
+ <img src="docs/images/0.4.16/workspaces.png" width="49%" alt="The Workspaces page: the demo workspace's repos with their health, mode and Start, Stop, Restart and Logs actions">
73
+ <img src="docs/images/0.4.16/status-drawer.png" width="49%" alt="The status drawer: nothing needs attention, and every service in the demo workspace, by application, with its latency">
74
+ <img src="docs/images/0.4.16/edge.png" width="49%" alt="The Edge page: the edge container up, its ports and TLS, and every route it serves, Den's own and each stack's, with its upstream">
75
75
  </p>
76
76
 
77
77
  ## Get started
@@ -1687,14 +1687,31 @@ the account's id and login, never a token, and no other account gets in.
1687
1687
  `web owner unlink`. Settings › Access does the same in the browser
1688
1688
  (Link your GitHub account, Unlink): shell-grade actions, for a signed-in
1689
1689
  browser and never an agent's token.
1690
- - **Sign in with GitHub** on the signed-out page shows a code and
1691
- `github.com/login/device`. The server runs the flow, and when the owner
1692
- approves it sets the same never-expiring session cookie a one-time link
1693
- sets, and the page reloads signed in. Another account sees "@<login>
1694
- isn't allowed on this Den". The token is thrown away either way. At most
1695
- three sign-ins wait for GitHub at once; a code expires as GitHub says
1696
- (15 minutes). With no owner linked the page says to run
1697
- `web owner link`.
1690
+ - **Sign in with GitHub** on the signed-out page shows the same three
1691
+ steps whether or not an owner is linked:
1692
+ 1. On the machine Den runs on, run `web owner link`, or use
1693
+ Settings › Access there.
1694
+ 2. It shows a code. Enter it at `github.com/login/device` and approve.
1695
+ 3. Come back to the signed-out page and press **Sign in with GitHub**,
1696
+ with the same GitHub account.
1697
+
1698
+ Once an owner is linked, step 1 shows as done ("Owner: @<login>") and
1699
+ the button follows the steps; before that there is no button. Below the
1700
+ steps the page says only the owner's GitHub account can sign in this
1701
+ way, and that anyone else uses **Approve this computer**; **Learn more**
1702
+ opens this section. With no owner yet, the page watches (the session
1703
+ probe, `GET /api/session`, says `github.state` to a signed-out
1704
+ browser): once the owner is linked on the machine, step 1 shows done
1705
+ and the button appears, with no reload.
1706
+
1707
+ - Pressing the button shows a code and `github.com/login/device`. The
1708
+ server runs the flow, and when the owner approves it sets the same
1709
+ never-expiring session cookie a one-time link sets, and the page reloads
1710
+ signed in. Another account sees "@<login> isn't the owner of this Den.
1711
+ Only the owner's account can use Sign in with GitHub. Use Approve this
1712
+ computer instead." Den has one owner, so there is no one to add. The
1713
+ token is thrown away either way. At most three sign-ins wait for GitHub
1714
+ at once; a code expires as GitHub says (15 minutes).
1698
1715
  - **Settings › Access** lists the linked devices (the browser, how it
1699
1716
  signed in, the client address when the edge vouched for one, created and
1700
1717
  last seen; "This device" on yours), each with **Revoke**, which ends its session at once (its
@@ -2180,7 +2197,8 @@ server scoped to its workspace.
2180
2197
  <cli> agents stop <id> [-w <ws>]
2181
2198
  <cli> agents resume <id> [-w <ws>]
2182
2199
  <cli> agents import -w <ws> [--runtime <r>] [--json] # conversations to bring in
2183
- <cli> agents import -w <ws> --session <id> --persona <p> # bring one in
2200
+ <cli> agents import -w <ws> --session <id> --persona <p> [--model <m>] # bring one in
2201
+ <cli> agents persona <id> <persona> [-w <ws>] [--model <m>] [--now | --after-turn] # change its persona
2184
2202
  <cli> agents personas [--json]
2185
2203
  <cli> agents doctor [-w <ws>] [--json] # can each runtime run here, and why not
2186
2204
  <cli> agents handback-check <file|-> # is a hand-back complete and plain (see Guardrails)
@@ -2251,6 +2269,24 @@ import -w <ws>` lists them too.
2251
2269
  Den's token, MCP config, hooks, guards and audit trail apply. It is an
2252
2270
  agent like any other: stop, resume, rename. The record notes
2253
2271
  `broughtInAt`.
2272
+ - **What the persona changes.** The persona is the role it takes on from
2273
+ now on: its role prompt, its permission mode and allowed commands, and
2274
+ Den's tools and guards. The history stays. The dialog says so under the
2275
+ persona list: "From now on it works as <persona>: its role, its
2276
+ permissions (<mode>), Den's tools and guards. Its history stays."
2277
+ - **The model stays the conversation's own.** Den reads the model of its
2278
+ last answer (`message.model` on the last assistant record, in the ends
2279
+ it reads) and runs it on that: "Keeps <model> from the conversation".
2280
+ Advanced (or `--model`) picks another. With no model in the
2281
+ transcript, the persona's applies.
2282
+ - **The MCP servers it won't have.** An agent runs with Den's MCP only
2283
+ (`--strict-mcp-config`). When the transcript shows tool calls from other
2284
+ servers (`mcp__<server>__…`), the dialog names them: "It won't have:
2285
+ <servers>." The list's `lost` field has them.
2286
+ - **A suggested persona.** The list's `suggest` is the persona of the
2287
+ latest agent of the workspace that worked in the conversation's folder
2288
+ (or a folder inside it, or one containing it), else the workspace's
2289
+ latest agent's. The dialog preselects it; you confirm with Bring in.
2254
2290
  - **One writer only.** A conversation written in the last minute is taken
2255
2291
  to be still open in another terminal: "Still open in another terminal.
2256
2292
  Close it there first" (HTTP 409 `open_elsewhere`). A conversation an
@@ -2260,8 +2296,43 @@ import -w <ws>` lists them too.
2260
2296
  - Only den-claude can bring one in for now. Other runtimes say "<runtime>
2261
2297
  can't bring in a conversation yet".
2262
2298
  - Routes: `GET /api/workspaces/<ws>/conversations[?runtime=<r>]` and `POST
2263
- /api/workspaces/<ws>/conversations/<id>/import` `{ persona, runtime?, id?
2264
- }`. A person's only, unless MCP `agent_import` is opted in.
2299
+ /api/workspaces/<ws>/conversations/<id>/import` `{ persona, runtime?, id?,
2300
+ model?, version? }`. A person's only, unless MCP `agent_import` is opted
2301
+ in.
2302
+
2303
+ #### Change an agent's persona
2304
+
2305
+ **Change persona…** (the agent pane's ⋯ menu, running or stopped) and
2306
+ `agents persona <id> <persona>` switch an agent to another persona on the
2307
+ same conversation. Den stops its session and resumes the conversation with
2308
+ the new persona's role prompt, permission mode and allowed commands (the
2309
+ old persona's mode and commands go). The history stays, and so do the
2310
+ agent's id, name, workspace, folder, grants, tab and slot, and the model it
2311
+ runs unless Advanced (`--model`) picks another. The dialog lists the
2312
+ personas with the current one marked, and the same "From now on it works
2313
+ as…" line as Bring in.
2314
+
2315
+ - **Never mid-turn without asking.** While it works (den-claude: its
2316
+ transcript's last record is a tool call, or a prompt or tool result with
2317
+ no answer yet), the change is refused: "It's working. Change after this
2318
+ turn, or stop it now?" (HTTP 409 `busy`). **After this turn**
2319
+ (`--after-turn`, `when: "after-turn"`) waits for the turn to end, up to
2320
+ 30 minutes, and then switches (the API answers 202 `pending` at once;
2321
+ the record marks it `pendingPersona` while it waits). The wait lives in
2322
+ the server's memory: if Den restarts first, the change is dropped, the
2323
+ mark is cleared at start, and the audit trail records "persona change
2324
+ cancelled: Den restarted";
2325
+ **Stop it now** (`--now`, `when: "now"`) stops it and switches.
2326
+ - **A person's only.** An agent could widen its own permissions with it,
2327
+ so an agent's token is refused (403), the CLI refuses inside an agent's
2328
+ session, and there is no MCP tool.
2329
+ - Only a runtime that can resume offers it (`personaChange: true` on the
2330
+ agent's row); others can't.
2331
+ - The record keeps each change in `personaChanges` (from, to, when, and
2332
+ who), and the audit trail records "persona <id> in <ws>: <from> → <to>,
2333
+ by <who>".
2334
+ - Route: `POST /api/workspaces/<ws>/agents/<id>/persona` `{ persona, model?,
2335
+ version?, when? }`.
2265
2336
 
2266
2337
  ### On the Workspaces page
2267
2338
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@monster-orange/den",
3
- "version": "0.4.15",
3
+ "version": "0.4.16",
4
4
  "description": "Den: a local multi-repo dev orchestrator, HTTPS edge and E2E harness",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,16 @@ export interface RuntimeConversation {
32
32
  lastAt: string;
33
33
  /** The folder it ran in. */
34
34
  cwd: string;
35
+ /**
36
+ * AG-17.1c: the model its last answer came from (the last assistant
37
+ * record's `message.model`), so Bring in keeps it. Absent when unknown.
38
+ */
39
+ model?: string;
40
+ /**
41
+ * AG-17.1c: the MCP servers its tools came from (`mcp__<server>__<tool>`
42
+ * tool calls in the ends read), sorted. Absent when none.
43
+ */
44
+ mcp?: string[];
35
45
  }
36
46
 
37
47
  /** Bytes read at each end of a transcript. */
@@ -114,22 +124,63 @@ function promptText(rec: Record<string, unknown>): string | null {
114
124
  return cleanName(text);
115
125
  }
116
126
 
117
- /** A transcript's cwd and title, from its two ends. */
127
+ /** A model name as a transcript records it: short, printable, not synthetic. */
128
+ const MODEL_RE = /^[A-Za-z0-9][A-Za-z0-9._:@/-]{0,127}$/;
129
+ /** An MCP tool's name: `mcp__<server>__<tool>`. */
130
+ const MCP_TOOL_RE = /^mcp__([A-Za-z0-9_.-]+?)__[A-Za-z0-9_.-]+$/;
131
+
132
+ /** An assistant record's tool calls' names. */
133
+ function toolNames(rec: Record<string, unknown>): string[] {
134
+ const content = (rec.message as { content?: unknown } | undefined)?.content;
135
+ if (!Array.isArray(content)) return [];
136
+ return content.flatMap((c) => {
137
+ const b = c as { type?: unknown; name?: unknown };
138
+ return b?.type === "tool_use" && typeof b.name === "string" ? [b.name] : [];
139
+ });
140
+ }
141
+
142
+ /** A transcript's summary from its two ends (AG-17.1, AG-17.1c). */
143
+ export interface TranscriptSummary {
144
+ cwd: string | null;
145
+ title: string | null;
146
+ /** The last assistant record's model, or null. */
147
+ model: string | null;
148
+ /** The MCP servers its tool calls name, sorted. */
149
+ mcp: string[];
150
+ }
151
+
152
+ /** A transcript's cwd, title, last model and MCP servers, from its two ends. */
118
153
  export function transcriptSummary(
119
154
  file: string,
120
155
  bytes = ENDS_BYTES,
121
- ): { cwd: string | null; title: string | null } {
156
+ ): TranscriptSummary {
122
157
  const { head, tail } = readEnds(file, bytes);
123
158
  let cwd: string | null = null;
124
159
  let prompt: string | null = null;
125
160
  let title: string | null = null;
161
+ let model: string | null = null;
162
+ const mcp = new Set<string>();
126
163
  const titleIn = (line: string) => {
127
164
  if (!line.includes("custom-title")) return;
128
165
  const r = parse(line);
129
166
  if (r?.type === "custom-title") title = cleanName(r.customTitle) ?? title;
130
167
  };
168
+ // AG-17.1c: the last answer's model, and the MCP servers its tools came from.
169
+ const assistantIn = (line: string) => {
170
+ if (!line.includes('"assistant"')) return;
171
+ const r = parse(line);
172
+ if (r?.type !== "assistant") return;
173
+ const m = (r.message as { model?: unknown } | undefined)?.model;
174
+ if (typeof m === "string" && MODEL_RE.test(m) && m !== "<synthetic>")
175
+ model = m;
176
+ for (const n of toolNames(r)) {
177
+ const hit = MCP_TOOL_RE.exec(n);
178
+ if (hit) mcp.add(hit[1]);
179
+ }
180
+ };
131
181
  for (const line of head) {
132
182
  titleIn(line);
183
+ assistantIn(line);
133
184
  if (cwd && prompt) continue;
134
185
  if (!line.includes('"cwd"') && !line.includes('"user"')) continue;
135
186
  const r = parse(line);
@@ -138,8 +189,73 @@ export function transcriptSummary(
138
189
  cwd = r.cwd;
139
190
  prompt ??= promptText(r);
140
191
  }
141
- for (const line of tail) titleIn(line);
142
- return { cwd, title: title ?? prompt };
192
+ for (const line of tail) {
193
+ titleIn(line);
194
+ assistantIn(line);
195
+ }
196
+ return { cwd, title: title ?? prompt, model, mcp: [...mcp].sort() };
197
+ }
198
+
199
+ /** Written this recently, a turn whose last record is an answer is still going. */
200
+ export const TURN_QUIET_MS = 5_000;
201
+
202
+ /**
203
+ * AG-18: whether a conversation's turn is still going, from its tail: the
204
+ * last user or assistant record. An answer that ended (`end_turn`, a stop
205
+ * sequence, the token limit) is idle; a tool call, or a prompt or tool
206
+ * result with no answer yet, is busy; an answer with no stop reason is busy
207
+ * until the file has been quiet TURN_QUIET_MS. An interrupted turn is idle.
208
+ * No transcript: idle (nothing was asked yet).
209
+ */
210
+ export function transcriptBusy(
211
+ file: string | null,
212
+ now = Date.now(),
213
+ bytes = ENDS_BYTES,
214
+ ): boolean {
215
+ if (!file) return false;
216
+ let mtime: number;
217
+ try {
218
+ mtime = fs.statSync(file).mtimeMs;
219
+ } catch {
220
+ return false;
221
+ }
222
+ const { head, tail } = readEnds(file, bytes);
223
+ const lines = tail.length ? [...head, ...tail] : head;
224
+ for (let i = lines.length - 1; i >= 0; i--) {
225
+ const line = lines[i];
226
+ if (!line.includes('"user"') && !line.includes('"assistant"')) continue;
227
+ const r = parse(line);
228
+ if (!r || r.isSidechain === true || r.isMeta === true) continue;
229
+ if (r.type === "assistant") {
230
+ const msg = r.message as { stop_reason?: unknown } | undefined;
231
+ const stop = msg?.stop_reason;
232
+ if (stop === "tool_use" || toolNames(r).length) return true;
233
+ if (
234
+ stop === "end_turn" ||
235
+ stop === "stop_sequence" ||
236
+ stop === "max_tokens"
237
+ )
238
+ return false;
239
+ return now - mtime < TURN_QUIET_MS;
240
+ }
241
+ if (r.type === "user") {
242
+ const content = (r.message as { content?: unknown } | undefined)?.content;
243
+ const text =
244
+ typeof content === "string"
245
+ ? content
246
+ : Array.isArray(content)
247
+ ? ((
248
+ content.find(
249
+ (c) => (c as { type?: unknown })?.type === "text",
250
+ ) as { text?: unknown } | undefined
251
+ )?.text ?? "")
252
+ : "";
253
+ if (typeof text === "string" && text.startsWith("[Request interrupted"))
254
+ return false;
255
+ return true;
256
+ }
257
+ }
258
+ return false;
143
259
  }
144
260
 
145
261
  /** A folder's absolute forms: as given and its real path. */
@@ -236,6 +352,8 @@ export function claudeConversations(
236
352
  title: s.title,
237
353
  lastAt: new Date(f.mtimeMs).toISOString(),
238
354
  cwd,
355
+ ...(s.model ? { model: s.model } : {}),
356
+ ...(s.mcp.length ? { mcp: s.mcp } : {}),
239
357
  });
240
358
  }
241
359
  return out;
@@ -70,7 +70,11 @@ import { catalogFind } from "./catalog-runtime.js";
70
70
  import { locateRuntime, type RuntimeFind } from "./runtime-search.js";
71
71
  import { claudeProjectsDir } from "../usage/paths.js";
72
72
  import { cleanName, transcriptTitle } from "./transcript-follow.js";
73
- import { claudeConversations, transcriptSummary } from "./conversations.js";
73
+ import {
74
+ claudeConversations,
75
+ transcriptBusy,
76
+ transcriptSummary,
77
+ } from "./conversations.js";
74
78
 
75
79
  /**
76
80
  * `/rename "x"` keeps the quotes in customTitle: one matching pair around
@@ -507,6 +511,14 @@ export const denClaude: Runtime = {
507
511
  return null;
508
512
  }
509
513
  },
514
+ // AG-18: mid-turn, from its transcript's tail (never the whole file).
515
+ turnBusy(token, env = process.env) {
516
+ try {
517
+ return transcriptBusy(transcriptFile(token, env));
518
+ } catch {
519
+ return false;
520
+ }
521
+ },
510
522
  // AG-4b, unofficial (catalog sessionName): `/rename` appends a
511
523
  // {"type":"custom-title","customTitle":…} record to the transcript; the
512
524
  // latest one, read on from the last ask (never the whole file), less one
@@ -103,6 +103,25 @@ export interface AgentRecord {
103
103
  * the list tags it. Start fresh and Relaunch keep the choices.
104
104
  */
105
105
  overrides?: Override[];
106
+ /**
107
+ * AG-18: each persona change, oldest first: from, to, when, and who (a
108
+ * person's sign-in, or "cli").
109
+ */
110
+ personaChanges?: PersonaChange[];
111
+ /**
112
+ * AG-18: a persona change waiting for this agent's turn to end. The wait
113
+ * lives in the server's memory, so a restart drops it; the upkeep at
114
+ * start clears this and audits the cancel (cancelStalePersonaChanges).
115
+ */
116
+ pendingPersona?: { to: string; since: string; by?: string };
117
+ }
118
+
119
+ /** One persona change of an agent (AG-18). */
120
+ export interface PersonaChange {
121
+ from: string;
122
+ to: string;
123
+ at: string;
124
+ by?: string;
106
125
  }
107
126
 
108
127
  /** A setting a session can override for one launch (AG-10.9). */
@@ -207,6 +207,11 @@ export interface Runtime {
207
207
  ): RuntimeConversation[];
208
208
  /** AG-17.1: the folder conversation `token` ran in, or null. Never throws. */
209
209
  conversationCwd?(token: string, env?: NodeJS.ProcessEnv): string | null;
210
+ /**
211
+ * AG-18: whether conversation `token` is mid-turn (den-claude: its
212
+ * transcript's tail). None: the runtime can't tell. Never throws.
213
+ */
214
+ turnBusy?(token: string, env?: NodeJS.ProcessEnv): boolean;
210
215
  /**
211
216
  * What the runtime prints when it exits at once (0.4.8): `noConversation`
212
217
  * when a resume finds nothing to continue, `idInUse` when a new
@@ -83,6 +83,13 @@ export function githubClientId(configured: string | undefined): string {
83
83
  return (configured ?? MO_DEN_GITHUB_CLIENT_ID).trim();
84
84
  }
85
85
 
86
+ /**
87
+ * The docs section the signed-out page's Learn more opens (SEC-2a-UX):
88
+ * docs/distributions.md, "Signing in with GitHub", in the public repo.
89
+ */
90
+ export const GITHUB_SIGNIN_DOCS =
91
+ "https://github.com/Monster-Orange/den/blob/main/docs/distributions.md#signing-in-with-github";
92
+
86
93
  /** What a GitHub button says while no client id is configured. */
87
94
  export function noClientIdMessage(cli: string): string {
88
95
  return (