dsh-morning-paper 0.1.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.
package/CHANGELOG.md ADDED
@@ -0,0 +1,57 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0
4
+
5
+ First release.
6
+
7
+ ### The briefing
8
+
9
+ - Computed from the durable session log, **never generated by a model**: zero
10
+ tokens, no hallucination, byte-identical for the same log and clock.
11
+ - Reports what a transcript buries: a per-turn timeline (duration, steps, tools,
12
+ tokens, outcome), file churn counted per file, where the wall clock went
13
+ (per-tool time, longest call, tool-vs-model split), a per-turn spend ranking,
14
+ shell-command counts, failures nobody retried with the exact failing command,
15
+ and your own asks in the window.
16
+ - Never quotes the agent's reply. The lead is one line of *outcome*.
17
+ - Pending approvals come from the durable `approval/asked` / `approval/decided`
18
+ pair, so they survive a host restart. Turn outcomes read the full reason set,
19
+ including `blocked` (waiting on you) and `interrupted` (a crash left it open).
20
+ - Cost from the four disjoint `assistant/message.usage` buckets, with the
21
+ cache-hit share derived from billed input only.
22
+ - Fork-inherited events are excluded from the counts and disclosed in a note, so
23
+ a forked session cannot inflate your agent's work with its parent's.
24
+ - A context **gauge** on the front page: a filled bar, the percentage, the raw
25
+ token figures and the route it measured, against the routed model's own
26
+ declared capacity — read with `ctx.llm.resolveModelInfo()`, the same call
27
+ automatic compaction uses, on the route taken from the newest `request/header`
28
+ event. It turns hot once the window is three-quarters full. A plugin-config
29
+ ceiling is a fallback for adapters that declare nothing, and is labelled as an
30
+ assumption. With neither, it says "unknown" rather than inventing a percentage.
31
+ The `/briefing` text rendering draws the same gauge in ASCII.
32
+
33
+ ### The page
34
+
35
+ - A Conversation View tab labelled **Morning Paper**, next to Chat and
36
+ Trajectory. No DOM decoration, no `MutationObserver`.
37
+ - Paged like a paper: **Front**, **The Log**, **The Ledger** — a page is offered
38
+ only when it has content, so a quiet session is a single sheet. Inner pages
39
+ carry a running head and a folio; one footer row holds navigation and controls.
40
+ - Newspaper typography: masthead, dateline, boxed bulletin, drop-cap lede,
41
+ columns, small print. One injected stylesheet, a fixed cream-and-ink palette,
42
+ and every text tier contrast-checked in the selftest.
43
+ - Refreshes itself: on turn settle, on returning to the tab, and on a two-speed
44
+ poll backed by a metadata-only tail probe, so the full log read happens only
45
+ when the log actually grew.
46
+ - `Mark all read` advances the reading marker **without blanking the page**; a
47
+ first visit shows the whole session so the tab is never empty on arrival.
48
+ - It never acts for you. The page reports a pending approval, it never answers it.
49
+
50
+ ### Also
51
+
52
+ - `/briefing [--since <seq>]` prints the same document as plain text.
53
+ - Typed refusals: `BAD_REQUEST`, `SESSION_NOT_FOUND`, `MARKER_AHEAD`,
54
+ `LOG_TOO_LARGE`, `SUBAGENT_OWNED`, `BRIEFING_FAILED`.
55
+ - Context hygiene: answering a briefing appends nothing and reaches no model.
56
+ - 131 assertions in `selftest.mjs`, plus `verify-real-log.mjs` against the
57
+ session logs on the machine.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 damlys99
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,370 @@
1
+ # dsh-morning-paper
2
+
3
+ [![license](https://img.shields.io/npm/l/dsh-morning-paper)](LICENSE)
4
+ [![dsh-plugin](https://img.shields.io/badge/dsh--plugin-ready-478CBF)](https://github.com/topics/dsh-plugin)
5
+
6
+ The front page for a session you left running: **what happened, what broke, and
7
+ what is waiting on you** — computed from the durable event log, never written by
8
+ a model.
9
+
10
+ It is a real Conversation View tab labelled **Morning Paper**, sitting next to
11
+ **Chat** and **Trajectory**, and it is set like a newspaper: a masthead, a
12
+ dateline, a boxed bulletin for anything waiting on you, a lede with a drop cap,
13
+ columns, and small print.
14
+
15
+ ![The front page: an action bulletin for a pending approval, a one-line lead, the activity summary, what you asked, and context weather](assets/1-front.png)
16
+
17
+ *Screenshots on this page are generated by `node tools/screenshot.mjs` from the
18
+ shipped stylesheet and render tree, so they cannot drift from the code.*
19
+
20
+ ```sh
21
+ $ dsh --profile web # …you leave the tab open and come back 42 minutes later
22
+ ```
23
+
24
+ ```
25
+ ============================================================================
26
+ THE MORNING PAPER - refactor auth
27
+ away 42m | 751 event(s) | 28m of wall clock | turn 18 | 0 action(s) required
28
+ ============================================================================
29
+
30
+ LEAD: Turn 17 completed.
31
+
32
+ WHAT IT DID
33
+ 9 turn(s): 8 finished, 0 blocked, 0 failed, 0 cancelled, 0 interrupted
34
+ 140 tool call(s), 12 file(s) touched, 79 command(s) run (0 failed)
35
+
36
+ TURNS
37
+ turn took steps tools tokens outcome
38
+ 10 57s 8 8 3,079,044 completed
39
+ 11 4m 35 42 13,573,537 completed
40
+ 13 9m 56 58 25,512,399 completed
41
+ 18 24s 2 1 1,131,559 open
42
+
43
+ FILES (12 touched)
44
+ 12x src/auth/session.ts
45
+ 9x src/auth/token.ts
46
+ 4x src/auth/__tests__/session.test.ts
47
+ ... and 9 more file(s)
48
+
49
+ WHERE THE TIME WENT
50
+ ##########...... 60.6% bash 1m (79 calls)
51
+ ######.......... 37.8% ask_user_question 42s (1 calls)
52
+ #............... 0.6% edit 1s (25 calls)
53
+ longest single call: ask_user_question 42s
54
+ tool execution was 6.5% of the window; the rest was model time
55
+
56
+ WHERE THE MONEY WENT
57
+ turn 13 25,512,399 tokens 41% of the window 58 tools completed
58
+ turn 11 13,573,537 tokens 22% of the window 42 tools completed
59
+ turn 14 7,787,236 tokens 13% of the window 14 tools completed
60
+
61
+ BUSINESS
62
+ 61,938,602 tokens | cache hit 99.9% | 11 message(s) from you
63
+ delivered:
64
+ - docs/refactor-plan.md
65
+
66
+ CORRECTIONS
67
+ 3 tool failure(s): edit/FS_STALE_VERSION x2, read/FS_NOT_OBSERVED x1
68
+ 1 model retry(ies)
69
+
70
+ UNRESOLVED (1)
71
+ 18:24 edit failed: FS_STALE_VERSION (never retried)
72
+
73
+ YOU ASKED
74
+ - split the session token into its own module, keep it backward compatible
75
+
76
+ WEATHER
77
+ ########................ 34% 340,115 of 1,000,000 tokens
78
+ route: deepseek-official/deepseek-flash
79
+ ```
80
+
81
+ *(Illustrative output — the shape is exactly what `/briefing` prints.)*
82
+
83
+ The tab renders that same document as a three-page newspaper; `/briefing` prints
84
+ it as plain text in the transcript. One computed briefing, two renderings.
85
+
86
+ ## What it says that the chat does not
87
+
88
+ A chat is a linear list of messages. It cannot tell you:
89
+
90
+ - **which files took the churn** — `22x selftest.mjs` is a fact no single
91
+ message contains;
92
+ - **where the wall clock went** — per-tool execution time, the longest single
93
+ call, and the split between tool execution and model time (6.5% of the window
94
+ above — the rest was the model thinking, which no transcript shows);
95
+ - **which single turn ate the spend** — turn 13 was 41% of a 62M-token window;
96
+ - **what failed and was never retried** — a failure followed by a later success
97
+ of the same tool is treated as handled; one with no later success is listed
98
+ with the exact failing command;
99
+ - **what you asked for** an hour ago, filtered to your own messages.
100
+
101
+ **It deliberately does not quote the agent's reply.** That is readable in the
102
+ chat, and restating it is what makes a briefing worthless. The lead is one line
103
+ of *outcome*; everything else is aggregation.
104
+
105
+ ## Three pages
106
+
107
+ The tab is paginated like a paper, and a page only exists when it has something
108
+ to say — a quiet session is a single sheet, not three pages of nothing.
109
+
110
+ | Page | Carries |
111
+ |---|---|
112
+ | **1 · Front** | the action bulletin, the lead outcome, the what-it-did summary, what you asked, and context weather |
113
+ | **2 · The Log** | the per-turn timeline, unresolved failures with their commands, and the corrections line |
114
+ | **3 · The Ledger** | file churn, delivered files, where the time went, where the money went |
115
+
116
+ Page one gets the full nameplate; inner pages get a running head and a folio
117
+ (`Page 2 of 3`), with section links at the top and one footer row holding
118
+ previous/next beside the read controls. Switching sessions returns you to page one.
119
+
120
+ ![The Log: a per-turn timeline with duration, steps, tools, tokens and outcome; unresolved failures with their commands; corrections](assets/2-log.png)
121
+
122
+ *Page 2 · The Log — the per-turn timeline, the failures nobody retried, and the
123
+ corrections line. This is the page that shows a single turn taking 16 minutes and
124
+ 3.1M tokens.*
125
+
126
+ ![The Ledger: file churn per file, delivered files, per-tool time with bars, and a per-turn spend ranking](assets/3-ledger.png)
127
+
128
+ *Page 3 · The Ledger — file churn counted per file, what was delivered, where the
129
+ wall clock went, and which turns ate the spend.*
130
+
131
+ `/briefing` prints the same document as **one continuous column**, because a
132
+ transcript cannot be flipped. Pagination is presentation, not data, so the text
133
+ rendering and the tab share one computed briefing.
134
+
135
+ ## Why it is trustworthy
136
+
137
+ **The briefing is computed, never generated.** Zero tokens, no hallucination,
138
+ byte-identical for the same log and clock. If you want prose about a session,
139
+ `/briefing` plus the agent can produce it — this plugin deliberately does not.
140
+
141
+ **It never enters your context.** The page is derived state. Nothing here calls
142
+ `inject()`, appends a session event, or sends anything to a model. The one path
143
+ that touches the transcript is the `/briefing` command, which you invoke. The
144
+ selftest asserts this.
145
+
146
+ ## Two scope rules
147
+
148
+ A front page answers two different questions, so two different reads:
149
+
150
+ | Question | Source |
151
+ |---|---|
152
+ | *What happened while I was away?* | only events after the seq your browser last showed you |
153
+ | *What needs me right now?* | the **whole** log — an approval asked before you left is still waiting, and a turn that ended `blocked` an hour ago is still blocked |
154
+
155
+ **Fork-inherited events are excluded from the "what happened" counts and named in
156
+ a notes line.** If you forked a session, its inherited prefix did not happen
157
+ here; `readSession` reports the exact prefix length, so the page cannot inflate
158
+ your agent's work with someone else's.
159
+
160
+ ## Install
161
+
162
+ From a checkout:
163
+
164
+ ```sh
165
+ git clone https://github.com/damlys99/dsh-morning-paper
166
+ dsh plugin --profile web add -w link:/absolute/path/to/dsh-morning-paper
167
+ # restart dsh web, then reload the page
168
+ ```
169
+
170
+ From npm (once the package is published):
171
+
172
+ ```sh
173
+ dsh plugin --profile web add -w dsh-morning-paper
174
+ ```
175
+
176
+ Requires DSH `>=0.1.2-alpha.1 <0.2.0-0`. The row is added to the profile
177
+ automatically; a restart is required because both the host module and the client
178
+ bundle are composed at boot, and `patchReload: live` only watches patch files.
179
+
180
+ Uninstall:
181
+
182
+ ```sh
183
+ dsh plugin --profile web remove dsh-morning-paper
184
+ ```
185
+
186
+ ## The context ceiling
187
+
188
+ WEATHER is a gauge: a filled bar, the percentage beside it, the raw figures, and
189
+ the route it measured — because a percentage buried in a sentence is the one
190
+ thing you cannot read at a glance. It compares the session's current request
191
+ pressure against the routed model's own declared capacity. DeepSeek's adapter declares one — every model in
192
+ its catalog carries a `contextWindow`, 1,000,000 tokens on the current routes —
193
+ and the plugin reads it through `ctx.llm.resolveModelInfo()`, the same call
194
+ automatic compaction uses to decide when to compact. So the percentage is the
195
+ number DSH itself is working from, not an estimate.
196
+
197
+ The route is read from the newest `request/header` event in the log rather than
198
+ from the agent's options, because a session on the default model never sets a
199
+ provider explicitly, and the ceiling belongs to the route that actually ran.
200
+
201
+ A plugin-config ceiling is a **fallback for adapters that declare nothing**:
202
+
203
+ ```yaml
204
+ # ~/.dsh/profiles/web/cordis.patch.yml
205
+ - id: morning-paper
206
+ config:
207
+ contextWindow: 128000
208
+ ```
209
+
210
+ A model-declared capacity always wins over it, and a configured one is labelled
211
+ wherever it is shown because it is an assumption rather than a fact. An invalid
212
+ value is ignored. With neither, the page reports the token count and says the
213
+ ceiling is unknown rather than inventing a percentage.
214
+
215
+ ## The command
216
+
217
+ ```sh
218
+ /briefing # the whole session, as text in the transcript
219
+ /briefing --since 812 # only what happened after seq 812
220
+ ```
221
+
222
+ ## What reads what
223
+
224
+ | Need | Seam |
225
+ |---|---|
226
+ | The log | `ctx.sessionQuery.readSession()` — concrete, so no search backend is required |
227
+ | The log tail | `ctx.sessionQuery.listEvents()` — metadata only; the cheap half of the auto-refresh poll |
228
+ | The title | `ctx.sessionQuery.readTitle()` |
229
+ | Pending approvals | durable `approval/asked` minus `approval/decided` — survives a host restart |
230
+ | Turn outcome | `turn/end` reasons: `completed`, `blocked`, `error`, `aborted`, `max-tokens`, `interrupted` |
231
+ | Failures, retries, compactions | `tool/result.error`, `llm/retry*`, `compaction/*` |
232
+ | Cost | `assistant/message.usage` — disjoint input / cache-read / cache-write / output buckets |
233
+ | Context pressure | `ctx.tokenMeter.measure()` + `ctx.llm.resolveModel()` for the model's declared window |
234
+ | Pending questions | the live `user-questions/request` waterfall (in memory only — see limits) |
235
+ | The route | `ctx.webServer.register({ kind: 'exact', path: '/morning-paper' })` |
236
+ | The page | `conversation.view`, one tab labelled **Morning Paper**, next to Chat and Trajectory. No DOM decoration, no `MutationObserver` |
237
+
238
+ Optional services (`agents`, `tokenMeter`, `llm`, `commands`) are resolved with
239
+ `ctx.get` at use time. A composition without them still mounts and still serves
240
+ the durable half of the page; a headless profile loses only the weather line.
241
+
242
+ ## Refusals
243
+
244
+ Every guard runs before any work, so a refused request changes nothing.
245
+
246
+ | Code | Status | When |
247
+ | --- | --- | --- |
248
+ | `BAD_REQUEST` | 400 | Missing or malformed `sessionId` / `sinceSeq` / `format` |
249
+ | `SESSION_NOT_FOUND` | 404 | No live or persisted session with that id |
250
+ | `MARKER_AHEAD` | 409 | Your reading marker is past the log tail. The browser forgets the marker and shows the whole session rather than a stale page |
251
+ | `LOG_TOO_LARGE` | 413 | Above 20,000 events, which is more than this plugin will read in one request |
252
+ | `SUBAGENT_OWNED` | 409 | A subagent session's briefing belongs to its parent |
253
+ | `BRIEFING_FAILED` | 502 | The log could not be read |
254
+
255
+ ## Deliberate limits
256
+
257
+ - **Pending *questions* are live-only.** `dsh-user-questions` declares no durable
258
+ session event, so a pending question cannot be found in a cold log after a host
259
+ restart. The watermark is set when the waterfall opens and cleared when it
260
+ settles, so it can never nag about a question you already answered — but it is
261
+ also absent after a restart. Approvals have no such limitation.
262
+ - **No full-text search.** `dsh-session-query-sqlite` ships mounted with
263
+ `openAt: never`, so this plugin uses only the concrete reads and depends on no
264
+ search backend.
265
+ - **Windows are truncated, not refused.** Above 5,000 new events the page
266
+ summarises the newest 5,000 and says so in a note.
267
+ - **`readSession` clones the whole log.** That is the ceiling that produces
268
+ `LOG_TOO_LARGE`; if it bites on real sessions, the fallback is a tail read of
269
+ the JSONL, which would couple this plugin to the on-disk format.
270
+
271
+ ## Page behaviour
272
+
273
+ - **A first visit shows the whole session.** A page that greets you with silence
274
+ reads as broken, so the first render is the full history and the small print
275
+ says so. `Mark all read` starts the tracking window from that point.
276
+ - **After that, the window is yours.** The dateline reads
277
+ `since 18:12 · away 3h 12m` and covers only what happened since you last read
278
+ it; a whole-session read says `whole session · 45m of work` and never claims an
279
+ away duration it did not measure.
280
+ - **It refreshes itself.** A turn that just ended is read a beat after the run
281
+ state flips; while the agent is working the tab polls every 5s, every 20s when
282
+ idle, and immediately when you come back to it. The recurring poll asks the
283
+ host for the **log tail only** — the expensive log read happens only when the
284
+ tail actually moved — and nothing is polled while the tab is in the
285
+ background.
286
+ - **`Mark all read` never blanks the page.** It advances the reading marker and
287
+ leaves what you just read on screen, with a line saying when you marked it; the
288
+ next new events appear on the following refresh. The previous behaviour —
289
+ clearing the page on the click — read as "my paper just vanished".
290
+ `Show whole session` forgets the marker to re-read everything.
291
+ - **The footer is one row**, like a paper's folio line: page navigation on the
292
+ left, the read/refresh controls on the right, with the small print beneath it.
293
+ A single-sheet paper shows no navigation at all.
294
+ - **A quiet session still renders a page** saying there is no news, rather than an
295
+ empty tab that looks broken.
296
+ - **It never acts for you.** An approval item tells you what is waiting and where
297
+ to answer it; the page does not re-implement the approval surface.
298
+ - **The palette is fixed, not themed.** A newspaper is cream paper with dark ink
299
+ in every theme, so the sheet and the ink are literal values rather than theme
300
+ tokens. Every text tier is contrast-checked in the selftest: body ink **15.7:1**,
301
+ secondary **10.4:1**, the faint tier **7.2:1** on the sheet, and the accent
302
+ heading **5.6:1** on the bulletin tint.
303
+ This is a deliberate correction: the first version mixed the sheet from
304
+ `--dsw-alias-*` variables whose names I had guessed, and `--dsw-alias-bg-elevated`
305
+ / `--dsw-alias-border-secondary` do not exist, so the sheet fell back to cream
306
+ while the label token resolved light — unreadable. The selftest now fails if the
307
+ palette ever depends on a theme token again.
308
+
309
+ ## Tests
310
+
311
+ ```sh
312
+ node selftest.mjs # 131 assertions, no network, no browser, no build
313
+ node verify-real-log.mjs # build briefings from the session logs on this machine
314
+ node tools/screenshot.mjs # regenerate the images on this page (repo tool)
315
+ ```
316
+
317
+ `tools/screenshot.mjs` renders the three pages with the shipped `PAPER_CSS` and
318
+ `paperTree`, photographs them with headless chromium, and rewrites
319
+ `screenshots.json`. It needs a chromium binary and ImageMagick, so it is a repo
320
+ tool rather than runtime code, and it is excluded from the published package.
321
+
322
+ `selftest.mjs` covers the pure core, every refusal, the request decoder, the HTTP
323
+ surface, the live watermark, the command, the text renderer, and the **real
324
+ client bundle** evaluated in a `node:vm` sandbox — including its render tree,
325
+ built with a test element factory, and the stylesheet, so page content and
326
+ styling are covered without React.
327
+
328
+ `verify-real-log.mjs` reads the logs DSH actually wrote under
329
+ `$DSH_HOME/sessions/` and builds a briefing from each, checking determinism,
330
+ non-mutation and renderability on real event payloads. It is a dev tool: session
331
+ logs are concatenated zstd frames, so it shells out to the `zstd` CLI, which the
332
+ plugin itself never does — the plugin reads through `ctx.sessionQuery`.
333
+
334
+ Real-log verification has already earned its keep twice:
335
+
336
+ - it caught the page emitting a non-ASCII truncation marker and unsanitized model
337
+ text, so quoted content is now stripped of control characters and clipped with
338
+ an ASCII marker;
339
+ - it caught the tool-call id being read from the wrong place. The id lives at
340
+ `tool/result.message.source.callId`; without it there was no pairing, no
341
+ per-tool timing, no command counting, and failure groups had no tool name. The
342
+ entire *Where the time went* section exists because real data found that.
343
+
344
+ ## Caveats
345
+
346
+ - **The route discloses session content to any caller on the loopback
347
+ interface.** `GET /morning-paper?sessionId=…` returns a session title, file
348
+ paths, failing commands, failure codes and token counts without the
349
+ GUI's session cookie — the same posture as the rest of the plugin HTTP surface,
350
+ but the payload here is conversation content, so it is worth stating plainly.
351
+ Session ids are unguessable UUIDs, the response is derived and capped, and a
352
+ cross-site browser request is blocked because the response carries no CORS
353
+ headers. If you bind this port beyond loopback, treat the whole plugin HTTP
354
+ surface as trusted-network-only, not just this plugin.
355
+ - **The page is a `conversation.view` entry.** That slot id and its `label`
356
+ option are published contracts in `docs/subsystems/slots.md`, not private
357
+ markup — unlike a DOM-decorating plugin, a restructured composer or dock cannot
358
+ break this one.
359
+ - **The stylesheet is injected, not bundled.** One `<style>` element with a
360
+ `dmp-` class prefix is appended to `document.head` on first render. There is no
361
+ CSS file and no build step, so there is no CSS-module contract to depend on.
362
+ - **Verified against DSH `0.1.5-rc.1` contracts**, in a browser-less harness and
363
+ against real session logs, and then used in a live browser — that pass is what
364
+ produced the footer, read-state and dateline corrections above.
365
+ - **Not published to npm yet**, so the checkout install above is the working path
366
+ today.
367
+
368
+ ## License
369
+
370
+ MIT
Binary file
Binary file
Binary file
@@ -0,0 +1,11 @@
1
+ # dsh-morning-paper bundle patch: add the row to whichever profile installs it.
2
+ #
3
+ # The plugin needs `sessionQuery` and `webServer`, both of which are mounted by
4
+ # `@deepseek-ai/dsh-base`, so no extra service rows are required. The optional
5
+ # services it reads when present (`agents`, `tokenMeter`, `llm`, `commands`) are
6
+ # resolved with `ctx.get` at use time rather than declared as hard dependencies,
7
+ # so a headless composition without them still mounts and still serves the
8
+ # durable half of the briefing.
9
+ - insert:
10
+ - id: morning-paper
11
+ name: dsh-morning-paper