diff --git a/docs/conventions/topic-docs/CHANGELOG.md b/docs/conventions/topic-docs/CHANGELOG.md index 84b7c2544a..2d1d156feb 100644 --- a/docs/conventions/topic-docs/CHANGELOG.md +++ b/docs/conventions/topic-docs/CHANGELOG.md @@ -1,5 +1,94 @@ # Changelog — topic-docs convention +## 2.4.0 — 2026-07-27 + +- **An Ephemeral row joins the tier table** (additive). The table sorts + documents by one question — does anything downstream enforce against + this? — which cannot express lifetime, so its finest-grained cell, the + memory tier, conflated state a later reader must find with files + nothing downstream ever reads again. With no row naming the second + kind, two plugins answered the same unasked question differently: + `adhd:clarify` reached for the session scratchpad — an undocumented + harness path (zero occurrences in the full docs corpus, keyed by + working directory, and declined three times upstream as a supported + surface) — while `architecture:improve` had independently settled on a + `mktemp` temp file. That divergence, not a shared mistake, is the + trigger: the convention registry calls for an owner doc before a + second plugin adopts, and two incompatible answers were already in + the tree. This row names the tier and moves `adhd:clarify` off the + scratchpad; `architecture:improve` keeps its existing temp behavior + and merely gains the contract's name for it. The row states five rules (one deterministic path; the lifetime + outlives the call, so a producer that returns a path never deletes the + file before returning; never the scratchpad; nothing durable; the FORM + of any temp-root override is a manifest `userConfig` rather than a + tracked key), carries a re-derivation trigger, and records why + git-visibility, promotion-stage, and write-contention each needed no + change. + + A sweep of every markdown producer in `plugins/**` for throwaway-file + behavior found seven more surfaces the new row governs, and all seven + are corrected here rather than deferred, because a row that leaves + known contradictions standing in the tree is not yet a contract. + `/planning:interview`'s dense-round decision table moves out of the + memory slice into this tier: the skill's own text names the ledger and + terminal as the record, a resumed session picks up from the first open + ledger checkbox, and the plugin's binding never listed the file — so + it was memory-tier state nothing read. `/education:teach` moves the + other way and is the reason the row is a **classification**, not a + destination: its concept HTML *is* that concept's lesson artifact in a + workspace `resume` reopens, so it is machine state and stays there — + the defect was a bullet titled "Ephemeral placement" offering the + workspace **or** OS temp for one artifact. Its `primer` action, which + creates no workspace at all, is the genuinely ephemeral half and had + no resolvable path before. One further producer is recorded but not + changed here: `/prototype:explore-directions` writes its standalone + HTML mockup to "an OS temp **or** gitignored scratch location", the + same non-deterministic branch rule 1 forbids — but choosing which of + the two wins has to be reconciled against that plugin's own throwaway + discipline, which deliberately locates prototypes next to the + production code they mimic. That is a design question, not a typo, and + it gets its own change. Every other sweep hit is producer-consumed + plumbing (a `mktemp` file the producer itself reads and hands to no + one) or deliberate machine state — `/education:quiz-me`'s report + library is the clearest of the latter, since its `recall` action reads + those reports back weeks later. + + Rule 2 is stated because both existing adopters hand their file back + as a path for the user to open — a `finally` cleanup would race the + reader and return a dead path. The row deliberately does **not** + promise the file dies with the session: no documented Claude Code + mechanism prunes that temp tree (`cleanupPeriodDays` is scoped to + `~/.claude/`, a different tree), so the honest guarantee is that the + file OUTLIVES the invocation and is reclaimed only when the platform + reclaims its temp tree. That is why one run writes one file and never + a tree. + + Rule 1 also constrains the template's **shape**, not just its root. + The `XXXXXX` placeholders must be trailing, because BSD `mktemp` on + macOS substitutes only trailing Xs — so a template appending an + extension after them (`-XXXXXX.html`) cannot create the file + on macOS. Two adopters had independently written exactly that form, + which is the evidence that naming the temp root was not a sufficient + rule; a producer wanting a meaningful filename takes the `-d` form and + writes a fixed name inside the run directory, which is why the row + admits a temp file **or** a directory. + + Two claims are stated as constraints rather than capabilities, because + neither capability exists today. Rule 1 does not route + `CLAUDE_CODE_TMPDIR` into the temp primitive: that variable overrides + the temp directory Claude Code uses for its own internal files, and + the env-var reference states that unsandboxed Bash commands inherit + the shell's `$TMPDIR` unchanged, so a plugin shelling out to `mktemp` + never observes it. Rule 5 fixes the form an override takes *if* a + plugin offers one; neither current adopter declares `userConfig`, and + the rule no longer reads as a promise that one is available. + + Minor, not major: no tier moves, no `topic-docs.yaml` key is renamed, + the slug spec is untouched, and no visibility guarantee changes — the + ephemeral row is slug-less and invisible to every other execution + context by construction, so it takes no row in the visibility matrix. + The eight bindings need no synchronized adoption wave. + ## 2.3.0 — 2026-07-26 - **The `.worktreeinclude` template carries sub-slices** (additive). Its diff --git a/docs/conventions/topic-docs/README.md b/docs/conventions/topic-docs/README.md index 40a143d063..8e55f62d45 100644 --- a/docs/conventions/topic-docs/README.md +++ b/docs/conventions/topic-docs/README.md @@ -23,16 +23,26 @@ were persisted forever. ## The two tiers (and their neighbors) -Placement follows document **nature**, decided by one question: does -anything downstream *enforce against* this document? +Placement follows document **nature**, decided by two questions in order. +First: does anything downstream *enforce against* this document? Yes puts +it in the contract tier while the task runs, and the durable tier once it +outlives the task. Second, for everything else: once this run ends, does +anything read the document again — a later session, another checkout, a +reviewer, or the producer itself on resume? **No** is the ephemeral row, +and it is the only row that answers no. **Yes** is the memory tier when +that reader is scoped to this checkout, and machine state when it is +scoped to the machine across projects. Membership answers the second +question, not frequency: a file inside a slice a later session reopens is +read again even if that session rarely looks at the file itself. | Tier | Location (default) | Git | Holds | |---|---|---|---| +| Ephemeral | An OS-API-created temp file or directory, one per run | Never in the repo | Files nothing downstream reads: a rendered HTML view, a spill file, a throwaway | | Memory | `.work//` | Never committed (self-ignoring) | `EXPLORE.md`, `RESEARCH.md`, `-checklist.md`, `baselines/`, raw captures and scratch | | Memory, concern-scoped | `.work/handoffs/`, `.work/reviews//` | Never committed | session handoffs; review reports — their axes are session and branch, so they sit outside topic slices | | Contract | `docs/topics//` | Committed **on the task branch only**; pruned before merge | `PLAN.md` (Brief + Plan), `PRD.md`, `design/` (incl. the `design-threads.md` / `design-resolution.md` gate files), `verification/` (the distilled manifest) | | Durable | knowledge-vault seam — default backend `docs/adr/`, `docs/specs/` | Committed, permanent | promotion targets | -| Machine state | `${CLAUDE_PLUGIN_DATA}`; `.claude/observability/` | Never committed | telemetry, caches | +| Machine state | `${CLAUDE_PLUGIN_DATA}`; `.claude/observability/` | Never committed | telemetry; caches; durable machine-scoped state a later session reopens across projects | Locations are the documented defaults; the tracked concern file's `contract_dir` / `memory_dir` keys override the memory and contract @@ -53,6 +63,110 @@ log — git log, PR threads, and tracker comments provide this natively for tracked contracts) and a default-persisted `brainstorm.md` (ideation is conversation output; persisting is opt-in, into the memory tier). +### The ephemeral tier + +The memory tier's one cell conflated two kinds with opposite +requirements: state that must SURVIVE the session as a read input +(resume artifacts, ledgers, captures) and files nothing downstream ever +reads again. The ephemeral row names the second. It is slug-less and +path-less by design — a run creates its own file or directory through +the platform's temp primitive — so it is invisible to every other +execution context by construction and takes no row in the visibility +matrix. + +Five rules hold at this row: + +1. **Resolve one deterministic path.** Never branch on whether a harness + injected a scratchpad path or set `CLAUDE_JOB_DIR`: those surfaces + are disjoint by session kind (`CLAUDE_JOB_DIR` is set for background + sessions only), so branching makes file placement depend on how the + session was launched, which is invisible from inside the plugin. Use + the platform's standard temp primitive and **name the temp root in the + template**: on Unix `mktemp "${TMPDIR:-/tmp}/-XXXXXX"` (add + `-d` for a directory), the positional-template form both GNU and BSD + `mktemp` accept identically; on Windows a user-scoped temp under + `%LOCALAPPDATA%\Temp`. The `XXXXXX` placeholders must be **trailing**: + BSD `mktemp` (macOS) substitutes only trailing Xs, so a template that + appends an extension after them — `-XXXXXX.html` — is not + portable. A producer that wants a meaningful filename takes the `-d` + form and writes a fixed name inside the run directory, which is why + the row above admits a temp file **or** a directory. A bare relative + template does **not** reach the temp tree — `mktemp report-XXXXXX` + creates the file in the current working directory, which is the + consumer's repository (reproduced against GNU coreutils 8.32, + 2026-07-27) — and the flags + that would fix it are not portable (`--tmpdir` is GNU-only, `-t` is + deprecated there). That root is the ambient `$TMPDIR` or system + default — **not** `CLAUDE_CODE_TMPDIR`, which overrides the temp + directory Claude Code uses for its *own internal* files: the env-var + reference states that "Unsandboxed Bash commands inherit your shell's + `$TMPDIR` unchanged" (verified 2026-07-27). A plugin shelling out to + `mktemp` therefore never observes that override, and no plugin should + claim it does. +2. **The lifetime outlives the call.** A path handed back to the user + must still be readable when they open it, so a producer that RETURNS + a path does not delete the file in a `finally` — that races the + reader and hands back a dead path. `finally` cleanup is correct only + for a file the producer itself consumes and hands to no one. How long + a returned file actually lives is the platform's decision, not this + contract's: it sits in the OS temp tree until something reclaims that + tree, and nothing documented does (see below). Size the footprint for + a file that OUTLIVES the session, not one that vanishes with it. +3. **Never the session scratchpad.** Plugins never require it, publish + pointers to it, or change semantics based on its presence. +4. **Nothing durable lands here.** If a later session, another checkout, + or a reviewer must read the file, it belongs in the memory or + contract tier — this row is not a shortcut past their rules. +5. **If a plugin exposes a temp-root override, its form is a manifest + `userConfig` typed `directory`, defaulting to empty** — never a + `.claude/topic-docs.yaml` key. A temp root is machine scope; a + tracked key would imply a team decision about a location no teammate + can observe. This constrains the FORM of an override, and does not + oblige any plugin to offer one — no implementer declares one today, so + the ambient temp root is currently the only root in play. Per the + configuration ownership table in `docs/PLUGIN-PHILOSOPHY.md`. + +**Keep the footprint small.** Nothing reclaims this tree on a schedule: +verified 2026-07-26 against the full Claude Code docs corpus, no +documented cleanup, retention, TTL, or pruning mechanism covers the temp +tree Claude Code writes under, and the one documented retention setting, +`cleanupPeriodDays`, is scoped to `~/.claude/` application data — a +different tree. That is precisely why rule 2 refuses to promise the file +dies with the session, and why the footprint rule is load-bearing rather +than tidy-minded: a producer writes one file, or one directory, per run +— never an accumulating tree — and rule 4 does real work, since anything +worth keeping belongs in a tier that is actually managed. + +**Why not the session scratchpad.** Verified 2026-07-26 against primary +sources: zero occurrences of "scratchpad" in the full Claude Code docs +corpus (`https://code.claude.com/docs/llms-full.txt`) — it is +system-prompt-injected only. It is keyed by working directory, so every +worktree gets a distinct root, and scoped by session UUID. Measured on +one machine: 230 directories, 31,260 files, 2.96 GB accumulated in ten +days with no pruning observed. Three upstream requests to make it a +supported surface are all closed as not-planned +([#45745](https://github.com/anthropics/claude-code/issues/45745), +[#17936](https://github.com/anthropics/claude-code/issues/17936), +[#21248](https://github.com/anthropics/claude-code/issues/21248)) — +upstream has not merely failed to document it, it has declined three +times to support it. + +**Re-derivation trigger.** An upstream versioned interface for the +scratchpad that guarantees injection, lifecycle, ownership, quota, and +cleanup semantics reopens rule 2, and the change lands here as a +recorded changelog entry. The dated verification above is an as-of +record, never standing authority. + +**Why the other three axes needed no change.** The placement question +was re-derived across four axes and only lifetime was uncovered: +git-visibility is already the tier table's own organizing question; +promotion-stage is already carried by the contract-slice lifecycle and +the two graduation edges; and write-contention is already solved at the +work-item tracker seam +([`plugins/work-items/reference/tracker-seam.md`](../../../plugins/work-items/reference/tracker-seam.md)), +whose race-safe claim-and-lease is provider-neutral. Recorded so the +analysis is not re-run. + ### The single-home rule Every fact has exactly one home. Any other surface — a handoff, a @@ -368,9 +482,10 @@ relationship to the contract is fully stated by their table row. | Plugin | Writes | Tier(s) | Binding | |---|---|---|---| +| adhd | rendered decision-table HTML view | ephemeral | by reference — the ephemeral row's five rules are its entire relationship | | discovery | `EXPLORE.md`, `RESEARCH.md` | memory | delta doc | -| architecture | `deepening-candidates-.md` (per-lens candidate ledgers) | memory | delta doc | -| planning | `PRD.md`, `PLAN.md` (Brief), `design/`, opt-in brainstorm persist | contract + memory | delta doc | +| architecture | `deepening-candidates-.md` (per-lens candidate ledgers); deepening HTML report | memory + ephemeral | delta doc | +| planning | `PRD.md`, `PLAN.md` (Brief), `design/`, opt-in brainstorm persist; five optional rendered HTML views (dense-round decision table, PRD pitch, brainstorm reaction page, plan view, design topology) | contract + memory + ephemeral | delta doc | | implementation | `PLAN.md` (Plan/progress), `DEVIATIONS.md`, status summaries | contract + memory | delta doc | | verification | `verification/` manifest; baselines, raw captures | contract + memory | delta doc | | session-flow | handoffs; running-retro ledgers | memory (`handoffs/`, `running-retros/`) | delta doc | @@ -379,6 +494,7 @@ relationship to the contract is fully stated by their table row. | toolchain | nothing of its own — its setup skill offers the concern file | — | delta doc | | knowledge | ingest trees — **formal carve-out**: its work root resolves through its own `library_dir` seam, not `memory_dir`; slug conformance is form-only (charset/reserved names), and its nested `//` sub-slices are sanctioned | memory (carved out) | by reference — the carve-out above is its entire delta | | claude-ops | telemetry | machine state | by reference — machine state resolves no contract paths | +| education | per-concept `lesson` / `reference` / `exercise` slices; `quiz-me` report library (`recall` reads it back); `primer` vocabulary-ladder HTML | machine state + ephemeral | by reference — its workspace and report library are its own `${CLAUDE_PLUGIN_DATA}` layouts, and only the workspace-less `primer` render resolves a path this contract owns | | docs-hygiene | (reader) audit-noise detector recognizes these shapes | — | by reference — reads shapes, writes nothing | ### Implementers restate the rules; they do not share a source diff --git a/plugins/adhd/.claude-plugin/plugin.json b/plugins/adhd/.claude-plugin/plugin.json index dc1a3c6afa..f6ac17adef 100644 --- a/plugins/adhd/.claude-plugin/plugin.json +++ b/plugins/adhd/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "adhd", - "version": "0.3.0", + "version": "0.3.1", "description": "Shape and restructure the assistant's output for a reader with ADHD — action-first, low-friction, and digestible. adhd:shape is a standing session posture: lead with the concrete next action, number multi-step work, restate state across turns, cap and rank lists, give concrete time estimates, make wins visible, and cut preamble, recap, and closers. adhd:clarify is a one-shot reshape of a dense, decision-heavy artifact already on screen — chunk it one-decision-at-a-time, define the session's own jargon, and surface exactly what you must decide, faithfully (operative terms quoted verbatim, no altitude loss), rendered as an HTML decision table for big content. Reauthored in part from ayghri/i-have-adhd (MIT). Deliberately mutually exclusive with terse-for-tokens output shapers like caveman — opposite objectives.", "author": { "name": "Melodic Software", diff --git a/plugins/adhd/CHANGELOG.md b/plugins/adhd/CHANGELOG.md index ef63cf8e9c..22c5824339 100644 --- a/plugins/adhd/CHANGELOG.md +++ b/plugins/adhd/CHANGELOG.md @@ -3,6 +3,20 @@ All notable changes to the `adhd` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.3.1] + +### Changed + +- `clarify` places its local HTML file in the topic-docs **ephemeral tier** + instead of preferring the session scratchpad. The old wording branched on + whether the harness injected a scratchpad path, which made placement depend + on how the session was launched — invisible from inside the skill — and + depended on an undocumented surface upstream has declined three times to + support. The skill now resolves one temp path deterministically, and states + explicitly that the file is not deleted before returning — the path is the + delivery mechanism, so it must stay readable when the reader opens it. See + `docs/conventions/topic-docs/README.md` §"The ephemeral tier". + ## [0.3.0] Fixes every finding from the 2026-07-23 live audit (handoff item diff --git a/plugins/adhd/skills/clarify/SKILL.md b/plugins/adhd/skills/clarify/SKILL.md index 5d27998a9d..181b644092 100644 --- a/plugins/adhd/skills/clarify/SKILL.md +++ b/plugins/adhd/skills/clarify/SKILL.md @@ -127,16 +127,23 @@ sessions and absent in others. Detect it: if the Artifact tool is available, tha is the top rung; otherwise degrade down the ladder. Never claim a decision table was rendered when only prose was produced. -Write any local HTML file to an **OS temp path** (the session's scratchpad -directory when the harness provides one, else the platform temp dir) and hand -back that path — a clarified view is transient generated state, so it never -lands in the consumer's repository tree. Do not rely on a plugin-data -substitution variable for this location: skill-body substitution is documented -only for a fixed set of variables, and an undocumented token can substitute -unpredictably (including to the wrong plugin's directory when the token -travels through another skill's arguments). If no writable temp location nor -the Artifact surface is available, drop to the terminal-markdown rung rather -than writing into the repo. +Write any local HTML file to the **ephemeral tier** — one file created through +the platform's temp API — and hand back that path. A clarified view is +transient generated state, so it never lands in the consumer's repository tree. +Do **not** delete the file before returning: the path is the delivery +mechanism, so it must still be readable when the reader opens it. It outlives +this invocation, and nothing documented reclaims the OS temp tree on a +schedule — so write one file per run and never an accumulating tree. Resolve +that one path deterministically: never +branch on whether the harness injected a scratchpad path or set +`CLAUDE_JOB_DIR`, and never depend on the session scratchpad, which is an +undocumented surface upstream has declined to support. Do not rely on a +plugin-data substitution variable for this location either: skill-body +substitution is documented only for a fixed set of variables, and an +undocumented token can substitute unpredictably (including to the wrong +plugin's directory when the token travels through another skill's arguments). +If no writable temp location nor the Artifact surface is available, drop to the +terminal-markdown rung rather than writing into the repo. The fidelity rules hold in **every** medium — verbatim terms, original-number back-links, omissions, lens line — table or prose. diff --git a/plugins/architecture/.claude-plugin/plugin.json b/plugins/architecture/.claude-plugin/plugin.json index 3a21c59044..2d71a8e147 100644 --- a/plugins/architecture/.claude-plugin/plugin.json +++ b/plugins/architecture/.claude-plugin/plugin.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", "name": "architecture", - "version": "0.4.1", + "version": "0.4.2", "description": "Scans an existing codebase for module-level architecture friction — shallow modules, seam leaks, and locality gaps — using Ousterhout's deep-module lens, presents candidates as a self-contained HTML report, and runs an interview loop on the selected candidate before handing off for planning.", "author": { "name": "Melodic Software", diff --git a/plugins/architecture/CHANGELOG.md b/plugins/architecture/CHANGELOG.md index bbd2e05665..f6287b46b7 100644 --- a/plugins/architecture/CHANGELOG.md +++ b/plugins/architecture/CHANGELOG.md @@ -3,6 +3,29 @@ All notable changes to the `architecture` plugin are documented here. Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this plugin uses semantic versioning. +## [0.4.2] + +### Changed + +- The deepening HTML report resolves its output location through the + topic-docs **ephemeral tier** rather than an unqualified "OS temp + directory": one file per run through the platform's temp API, resolved + deterministically and never branched on an injected scratchpad path or + `CLAUDE_JOB_DIR`. The executable Phase 2 step in `actions/deepening.md` + carries the rule, not only the format reference — that step is what + `/architecture:improve deepening` actually follows. Its `mktemp` form is + now a positional absolute template naming a run **directory** + (`mktemp -d "${TMPDIR:-/tmp}/deepening-review-XXXXXX"`, with `report.html` + written inside), replacing the `--tmpdir` / `-t` examples: `--tmpdir` is + GNU-only, `-t` is deprecated there, and a bare relative template creates + the report in the current working directory — the consumer's repository. + The directory form is required rather than cosmetic: BSD `mktemp` on + macOS substitutes only **trailing** `XXXXXX`, so the file template that + appended `.html` after the placeholders could not create the report on + macOS at all. A directory keeps the placeholders trailing while still + giving the report a meaningful filename. See + `docs/conventions/topic-docs/README.md` §"The ephemeral tier". + ## [0.4.1] ### Added diff --git a/plugins/architecture/reference/topic-docs.md b/plugins/architecture/reference/topic-docs.md index 671f364ea2..ad7622244e 100644 --- a/plugins/architecture/reference/topic-docs.md +++ b/plugins/architecture/reference/topic-docs.md @@ -13,14 +13,17 @@ deltas. | Artifact (writer) | Tier | Location (default) | |---|---|---| | `deepening-candidates-.md` (`/architecture:improve deepening`) | Memory | `.work//` — never committed | +| Deepening HTML report (`/architecture:improve deepening`) | Ephemeral | One file per run, created through the platform's temp API; handed back as a path and never deleted before returning | The candidate list is a cross-stage handoff: a planning step consumes its `agreed-shape` entry (see the deepening playbook's Handoff section). It stays in the memory tier because nothing downstream *enforces against* it — the agreed shape graduates into planning's own contract-tier artifacts (`PLAN.md`), which is where enforcement begins. -The HTML report is a human-readable companion written to a secure temp file — deliberately -ephemeral, outside this convention. +The HTML report is a human-readable companion that nothing downstream reads again, so it lands in +the contract's ephemeral tier. Its rules are the contract's — one deterministic path, never the +session scratchpad, no delete-before-return because the path is the delivery mechanism, and one +file per run because nothing documented reclaims the temp tree — not a delta of this plugin's. ## Slug derivation diff --git a/plugins/architecture/skills/improve/actions/deepening.md b/plugins/architecture/skills/improve/actions/deepening.md index 80cc80a76d..d47a25d452 100644 --- a/plugins/architecture/skills/improve/actions/deepening.md +++ b/plugins/architecture/skills/improve/actions/deepening.md @@ -36,7 +36,7 @@ Verification can be a second cheap read-only subagent pass or inline reproductio **Re-badge first.** Before rendering, map each surviving candidate's scan `confidence` to its `recommendation` badge (`strong` → `Strong`, `worth-exploring` → `Worth exploring`, `speculative` → `Speculative`), then re-badge against the two acceptance heuristics below (deletion-test acceptance form, two-adapter rule) and the Phase 1.5 verification result — scan-time confidence is an input, not the final badge. A candidate whose `shallow-signal` failed to reproduce, or whose value rests on a one-adapter abstraction, cannot carry `Strong`. **Promotion closes the same gate:** if re-badging lifts a candidate the scan rated below `strong` up to `Strong`, apply the Phase 1.5 reproduction to its `shallow-signal` *before* it carries the badge — a `Strong` claim reaches the report reproduced no matter which way the badge was reached, so the Phase 1.5 guarantee holds across both the original strong set and any promotions. -Write a self-contained HTML file via a secure temp-file primitive so the path is unpredictable and permissions are restrictive. On Unix/Linux, create it with `mktemp` (e.g. `mktemp --tmpdir deepening-review-XXXXXX.html` or `mktemp -t deepening-review.XXXXXX.html`); on Windows, use a user-scoped temp under `%LOCALAPPDATA%\Temp` or equivalent. Open for user: `start ` on Windows, `open ` on macOS, `xdg-open ` on Linux. Report the absolute path. +Write a self-contained HTML file to the topic-docs **ephemeral tier** (see [../../../reference/topic-docs.md](../../../reference/topic-docs.md)): one file per run, via a secure temp-file primitive so the path is unpredictable and permissions are restrictive. On Unix/Linux, create a run directory with `mktemp -d "${TMPDIR:-/tmp}/deepening-review-XXXXXX"` and write `report.html` inside it. The `XXXXXX` placeholders must be **trailing** — BSD `mktemp` (macOS) substitutes only trailing Xs, so a template like `…-XXXXXX.html` is not portable, and a directory plus a fixed filename is the form that keeps the Xs trailing while still yielding a meaningful `.html` name. Naming the temp root in the template is what reliably leaves the working directory: a bare relative template creates the file in the current directory, and the flags that would fix it are not portable (`--tmpdir` is GNU-only, `-t` is deprecated there). On Windows, use a user-scoped temp under `%LOCALAPPDATA%\Temp` or equivalent. Resolve that one path deterministically — never branch on an injected scratchpad path or `CLAUDE_JOB_DIR`. Open for user: `start ` on Windows, `open ` on macOS, `xdg-open ` on Linux. Report the absolute path. Do **not** delete the file after reporting: the path is the delivery mechanism and must stay readable for the user to open. It outlives the invocation and nothing documented reclaims the OS temp tree on a schedule, which is why one run writes one file and never an accumulating tree. Report is **self-contained — inline `