fix(plugins): bring five ephemeral-file producers onto the tier contract - #1708
Conversation
A marketplace-wide audit of nineteen ephemeral-file producers found
five that violate the topic-docs ephemeral tier. The other fourteen
were already conforming or correctly classified elsewhere.
firecrawl was the worst: every scrape, search, and interact call wrote
`/tmp/fc-<nonce>.<ext>` with a hardcoded literal `/tmp`, a hand-rolled
`date +%s%N` nonce instead of the platform primitive, and no Windows
branch at all. It now uses `mktemp`, which works on every platform
because the skill's commands run under `shell: bash`, resolving through
Git Bash's `/tmp` mount to `%TEMP%` on Windows. It also had no cleanup
anywhere, so a research-heavy session left one file per call behind --
the footprint rule's exact failure case. Spill files are self-consumed,
so they are now removed after the Read, with the one exception the
contract requires: when the user asked for the scraped file itself the
path is the deliverable and is never deleted.
prototype's explore-directions offered "an OS temp OR gitignored scratch
location" -- a non-deterministic branch whose second half also puts the
file inside the repository, against the tier's never-in-the-repo rule.
visualization wrote a local HTML file and handed back its path with no
placement rule at all; it now carries the same paragraph its sibling
skills already use.
event-storming's `{system_temp}` placeholder was never bound to a
platform primitive anywhere in the file. Its delete-vs-archive split
was already sound and is untouched.
context7's `> /tmp/nextjs-router.md` is the same class as firecrawl in
an illustrative pipe example rather than a mandated rule; fixed for
consistency.
Wording is taken from the marketplace's existing exemplars rather than
invented, so this adds no sixth phrasing of the same rules.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 22s —— View job PR Review
SummaryWell-scoped conformance fix. The audit rationale in the PR description is solid, the firecrawl overhaul is the most substantive change and looks correct, and all five plugins have matching CHANGELOG entries and patch-version bumps. One portability bug across two files, one nit. 🔴 Important —
|
|
Claude finished @kyle-sexton's task in 3m 31s —— View job Security Review — PR #1708
All 18 changed files are documentation/instruction markdown files and JSON manifests — no GitHub Actions workflows, no executable code, no secrets handling. The security surface is the shell patterns that these skill instructions direct Claude to generate and execute. No supply-chain, CI trigger, or permission-widening concerns in scope here. FindingsIMPORTANT —
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 30e76de71f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…shell The ephemeral-tier fix justified its Windows path with "this skill's commands run under `shell: bash` on every platform". That is false, and the marketplace's own record already said so. `shell:` governs only the shell used to evaluate `!` dynamic-context injections at skill-load time -- preprocessing whose result the model sees, never a constraint on the Bash tool calls a skill body issues later. The upstream frontmatter reference defines it that way; this repo's precompute-context convention scopes it to injections; the skill-quality gate only checks it when injections exist; and two sibling CHANGELOGs record that the key is inert on skills whose `!` lines were removed. So the sentence attributed a real portability outcome to a field that cannot produce it. Where the Bash tool is Git Bash the outcome happens to hold; on a Windows host without Git Bash the PowerShell tool runs and `mktemp` does not exist -- precisely the case `shell: bash` was being cited as insurance against. The gotcha now names what actually decides it and gives the fallback, and the core-pattern sentence points at that gotcha instead of re-deriving a shell claim. The frontmatter key stays: this skill has a genuine `!` injection using `command -v`, `/dev/null`, and `head`, so declaring it is required there. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 52s —— View job PR Review — Updated for
|
|
Claude finished @kyle-sexton's task in 5m 24s —— View job Security Review — PR #1708
All 18 changed files are documentation/instruction markdown and JSON manifests — no GitHub Actions workflows, no executable code, no secrets or credentials directly in scope. The security surface is the shell patterns these skill instructions direct Claude to generate and execute. Prior reviews already flagged IMPORTANT — Shell command injection via
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 9a31ce0569
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
AI-generated by an autonomous triage agent (instance Claiming the triage lane on this PR; a classification brief follows shortly. |
|
AI-generated by an autonomous triage agent (instance Triage — PR #1708Verdict: Linkage-check state
Claim-by-claim verification1. Scope and hygiene — claim holdsThe diff touches exactly the five plugins named, 18 files, and every plugin gets a patch bump with a matching
No files outside those five plugins. 2. The
|
| Plugin / file | Form introduced | Portability |
|---|---|---|
event-storming agentic-simulation.md |
${TMPDIR:-/tmp} / $env:TEMP path prefix (no flag) |
Conforms — this is the contract-prescribed shape |
firecrawl SKILL.md, context/commands.md, evals/evals.json, update/SKILL.md |
mktemp -t fc-*-XXXXXX |
Portable in practice, but GNU marks -t deprecated |
context7 lookup/context/cli.md |
mktemp -t ctx7-XXXXXX |
Same — deprecated |
prototype explore-directions/SKILL.md |
mktemp --tmpdir explore-directions-XXXXXX.html |
Hard break on BSD/macOS — --tmpdir is GNU-only |
visualization visualize/SKILL.md |
mktemp --tmpdir visualize-XXXXXX.html |
Hard break on BSD/macOS |
So: 1 of 5 plugins conforms; 2 inherit a deprecated flag; 2 introduce a flag that does not exist on macOS. The PR body concedes the shape of this itself — "Every invocation in this diff uses -t or --tmpdir" — which is precisely the pair the contract rules out (next section).
Both --tmpdir sites are hedged with e.g., but they are the only concrete invocation either skill offers, and both skills document open <path> for macOS in the same block — so macOS is explicitly in their support surface.
3. Issue #1709 — exists, open, and genuinely covers the gap
Open, titled "mktemp: no flag form is both non-deprecated and portable, and the bare form writes into the repo", labeled needs-human + status: needs-decision + priority: medium + work-class: scoped. Its scope is the marketplace-wide flag inheritance from architecture/skills/improve/actions/deepening.md, and its acceptance criteria require a form "verified by execution on both GNU and BSD." So the deferral is real and correctly targeted, not a parking-lot dodge.
One consequence worth naming: #1709 is itself labeled needs-human and awaiting a decision. A PR whose known limitation is parked on a needs-decision issue inherits that gating.
4. The tier contract — the rules exist, but NOT where cited and NOT on main
The contract is not under docs/topics/. It is docs/conventions/topic-docs/README.md, and it does not exist on origin/main (8a926ac) — it lives only in PR #1675, which is still OPEN and unmerged (branch fix/1635-topic-docs-ephemeral-tier, head 0279e058). Verified by git grep -iln ephemeral origin/main, which returns no such file.
Reading the file at 0279e058, the three cited rules do exist:
- Never in the repo — L40, table row:
| Ephemeral | An OS-API-created temp file or directory, one per run | Never in the repo | ... | - One file per run — L40 above, and the footprint paragraph: "a producer writes one file, or one directory, per run — never an accumulating tree"
- Self-consumed cleanup — rule 2, L100-108: "
finallycleanup is correct only for a file the producer itself consumes and hands to no one."
But rule 1 (L79-99) prescribes the exact opposite of this PR's flag choice:
Use the platform's standard temp primitive and name the temp root in the template: on Unix
mktemp "${TMPDIR:-/tmp}/<prefix>-XXXXXX"(add-dfor a directory), the positional-template form both GNU and BSDmktempaccept identically … and the flags that would fix it are not portable (--tmpdiris GNU-only,-tis deprecated there).
So the contract this PR is titled after now names both flag forms the PR uses as the wrong answer. Four of the five plugins here would land non-conformant the moment #1675 merges.
Chronology, in fairness to the author: rule 1's verification stamps are 2026-07-27 — the same day #1709 was filed and the first reviews landed. The contract was sharpened toward the portable form concurrently with this PR, so this reads as the contract moving under the PR, not the author ignoring a settled rule.
Unverified sub-claim, flagged rather than asserted: rule 1 calls the positional form "accepted identically" by GNU and BSD, but its reproduction evidence is GNU-only (coreutils 8.32). prototype and visualization need the .html suffix to survive after the X-block, and #1709's own body says "Confirm the .html suffix survives on BSD — BSD's behavior here needs checking, not assuming." None of the three reviewers who proposed mktemp "${TMPDIR:-/tmp}/…-XXXXXX.html" showed BSD execution evidence either. This is the open question that makes #1708 a decision rather than a mechanical flag swap. No assertion about BSD suffix behavior is made here.
5. The promised adversarial audit — it ran; the remediation half did not
The body says: "An independent adversarial verifier is auditing this branch … Findings will be addressed on this PR before merge."
- Audit: happened.
chatgpt-codex-connector[bot](different vendor) reviewed both commits —30e76deand9a31ce0— filing 1 P1 and 3 P2 inline findings. Two in-houseclaudereview and security-review bots also reported on both commits. - Remediation: did not happen. HEAD is still
9a31ce0— the same commit Codex reviewed. Unaddressed at HEAD:--tmpdiratprototype/…/SKILL.md:111andvisualization/…/SKILL.md:96— flagged by three independent reviewers across two commits, still in the diff.- Codex P1:
${TMPDIR:-/tmp}selects shared/tmpon a multi-user POSIX host; session dir lands mode 0755 and its markdown 0644, so other local users can read workshop context. Fix suggested:mktemp -dor explicit 0700. Confirmed unaddressed — grep of the diff forchmod,0700,umask,mktemp -dreturns nothing. - Codex P2: context7 example never echoes
$OUT, so the next tool call cannot locate the file. - Codex P2: firecrawl cleanup exception is worded for a "scraped file" only, so a user-requested
search/crawl/map/parsedeliverable can still be deleted — and that contradictsfirecrawl/skills/update/SKILL.mdin the same PR.
So the claim is half-true, and the unkept half is the half that mattered.
Classification — work-class: structural
Justification, on evidence rather than file count:
- The unit of work is a contract change, not five independent edits. The remedy for the two broken call sites is one marketplace-wide flag decision that is already parked on mktemp: no flag form is both non-deprecated and portable, and the bare form writes into the repo #1709 and explicitly scoped as "one decision recorded in an owner doc rather than five independent judgements."
- It is cross-cutting by construction: the same inherited form spans these five plugins plus
architecture/improveand every othermktempcall site underplugins/**. - Its correctness is coupled to an unmerged upstream contract (docs(topic-docs): add the ephemeral tier row so throwaway files stop reaching for the scratchpad #1675) whose rule 1 currently contradicts it.
Caveat, stated plainly: the label's "hard to reverse" clause does not fit — these are markdown instruction edits and trivially revertible. structural is chosen for the contract-change and cross-cutting clauses, not for irreversibility. scoped was rejected because the blast radius is not bounded by a brief; it is bounded by a decision nobody has made yet.
Decision-ready options
1. RECOMMENDED — hold #1708 behind #1675, then close the flag question
Merge #1675 first (it carries the normative form), resolve #1709's BSD-execution gap for the suffix case specifically, then update the four non-conforming call sites in this PR and re-request review. Also fold in the three unaddressed Codex findings.
Why: this is the only option under which #1708 is conformant on landing rather than non-conformant on landing. It also keeps the flag decision in the one place that owns it. Cost: #1708 blocks on two other items, one of which needs a human decision.
2. Merge now as a strict improvement, accepting a known macOS break
The pre-PR state (hardcoded /tmp + a hand-rolled date +%s%N nonce, no cleanup, no Windows branch) is worse than what this replaces it with, and 3 of 5 plugins improve unambiguously.
Cost: knowingly ships the exact regression three independent reviewers flagged, on two skills that advertise macOS support; lands instruction text that contradicts #1675 rule 1; and leaves the Codex P1 /tmp permissions finding open. Requires an explicit human decision to accept.
3. Split the PR
Land event-storming (already conformant), firecrawl, and context7 now; hold prototype + visualization until the BSD suffix question resolves.
Cost: extra PRs and version-bump churn, and it still leaves -t deprecation in two plugins to be swept later by #1709 anyway.
Recommended: option 1.
Labels applied
needs-human, work-class: structural.
|
AI-generated by an autonomous triage agent (instance |
…sion dir private
Four review findings on the ephemeral-tier conformance sweep, all fixed
here rather than deferred to a follow-up.
The session directory was named, not created. event-storming's
simulation reference composed `{system_temp}/eventstorming-session-{id}`
and created it normally. On a multi-user POSIX host with TMPDIR unset,
`${TMPDIR:-/tmp}` is the shared world-readable `/tmp`: a predictable
name exposes the persona and session Markdown to every local user (0755
directory, 0644 files) and lets any of them pre-create the path so the
workshop writes into a directory someone else owns. The directory is now
created by the primitive itself -- `mktemp -d` on POSIX/Git Bash, whose
`mkdtemp` POSIX mandates at mode 0700, and `New-Item` under a
`[IO.Path]::GetRandomFileName()` component in the per-user `$env:TEMP`
on PowerShell. The random component defeats pre-creation; the 0700 mode
gates traversal, so the file modes inside need no change. The two
downstream sites that restated the old path formula now refer to the
returned path.
The mktemp flag forms were not portable. `--tmpdir` is a GNU long option
BSD/macOS does not implement, and GNU marks `-t` deprecated -- so the
diff shipped a form that fails on a platform these same skills emit
`open <path>` for. Every call site now carries the temp root in the
positional TEMPLATE argument instead. That is not a new invention: it is
the form this repo already mandates for shell scripts, where
scripts/shell-portability-tokens.txt lints `-p` and `--tmpdir` out of
changed *.sh files and names the positional template as the replacement
both dialects accept identically (#1527). The gate scans .sh only, which
is how these skill documents drifted from a convention the repo was
otherwise enforcing.
The two skills handing back an HTML file create a private run directory
with `mktemp -d` and write a fixed-name page inside it, rather than
asking for a `.html` suffix after the XXXXXX. GNU auto-implies
`--suffix` there; whether BSD does is the open question #1709 raised,
and the directory form does not need the answer. GNU coreutils 8.32 was
exercised directly; BSD is not executable from here and is taken from
the man page, not from a run.
context7's dump-to-disk example emitted nothing. The docs output is
redirected and `OUT` dies with the shell invocation, so a following Read
had no way to locate the randomly named file. It now echoes the path in
the same call, as the firecrawl examples do.
firecrawl's cleanup exception was command-specific. It exempted only a
requested "scraped file", so a file the user explicitly asked for from
search, crawl, map, parse, interact, or agent could be deleted after
being read -- and it contradicted the command-agnostic invariant already
recorded in the update skill. The exception is now command-agnostic on
both sides.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 2m 45s —— View job PR Review — #1708 (HEAD
|
|
Claude finished @kyle-sexton's task in 4m 25s —— View job Security Review — PR #1708
All 18 changed files are documentation/instruction markdown files and JSON manifests — no GitHub Actions workflows, no executable code, no secrets in scope. The security surface is the shell patterns these skill instructions direct an agent to generate and execute. Prior reviews (three total across two commits) have flagged Resolved at HEAD — confirmed not re-reportingShell injection via user-derived World-readable
Firecrawl cleanup exception too narrow (Codex P2 on commit Context7 path not echoed (Codex P2 on commit SUGGESTION —
|
…reach it The portability fix moved three skills onto `mktemp -d`, whose directory name carries a random component. Shell state does not survive between Bash tool calls, so a snippet that only assigns the path to a variable leaves the following call -- the one that actually writes the HTML page or the persona files -- with no way to name the directory it must write into. That is the same defect the review already caught in the context7 example, reintroduced by the new snippets. All three now echo the created path in the same call and instruct the agent to carry the echoed literal, matching the firecrawl examples. The PowerShell branch returns `.FullName` rather than the DirectoryInfo object New-Item emits, and spells the accelerator `[System.IO.Path]` in full. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 2m 19s —— View job PR Review — #1708 (HEAD
|
| Finding | Status |
|---|---|
mktemp --tmpdir on BSD/macOS (three reviewers, two commits) |
Fixed in 91327a0: replaced with mktemp -d "${TMPDIR:-/tmp}/…" for dir-based delivery |
Event-storming session dir permissions P1 (predictable path, 0755 in shared /tmp) |
Fixed in 91327a0: mktemp -d + POSIX mkdtemp mandating 0700 |
Context7 pipe example not echoing $OUT (Codex P2) |
Fixed in 91327a0 |
| Firecrawl cleanup exception too narrow (Codex P2) | Fixed in 91327a0: now command-agnostic |
Firecrawl shell: bash misattribution |
Fixed in 9a31ce0 |
[IO.Path] accelerator shorthand in CHANGELOG and skill body |
Fixed in 9bf825f: expanded to [System.IO.Path] throughout |
🟡 Nit — CHANGELOG for prototype (0.3.3) and visualization (0.1.1) don't mention the echo requirement
The 0.3.3 entry and 0.1.1 entry describe the directory-based mktemp approach but omit the echo-and-carry requirement added in 9bf825f. The context7 CHANGELOG at 0.4.3 sets the precedent: it explicitly states "the example echoes the generated path in the same call." The agents who read the SKILL.md files get the correct instruction regardless; this is a consumer-facing documentation gap only. Non-blocking.
🟡 Nit (carry-over, PLAUSIBLE) — {session_id} constraint doesn't name shell metacharacters
agentic-simulation.md:333 forbids "spaces, slashes, or other path-unsafe characters" but doesn't name $, `, ;, |, or &. These are evaluated by Bash inside double-quoted strings and appear in the POSIX rm -rf "{session_dir}/" cleanup arm. Confidence stays at PLAUSIBLE: the {domain}-{date}-{random4} format substantially limits the practical risk, the PowerShell arm is injection-safe via -LiteralPath, and the phrase "other path-unsafe characters" may reasonably cover metacharacters. Optional improvement; not blocking.
Looks good overall
The four-commit arc is coherent: each commit addresses exactly what prior reviewers flagged, and 9bf825f's rationale (shell state doesn't persist between Bash calls; a random directory name is unrecoverable without an echo) is precisely correct and clearly stated. The PR is clean.
|
Claude finished @kyle-sexton's task in 3m 40s —— View job Security Review — PR #1708 (HEAD
|
…e's width The echo-path fix left both paragraphs ragged against their file's own column width -- 78 in visualize, 100 in explore-directions. Text only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 3m 2s —— View job PR Review — #1708 (HEAD
|
| Finding | Fixed in | Status |
|---|---|---|
mktemp --tmpdir absent on BSD/macOS (prototype, visualization) |
91327a0 |
Replaced with mktemp -d "${TMPDIR:-/tmp}/…-XXXXXX" — no flag, valid on both dialects |
Event-storming session dir: predictable path + world-readable 0755 in shared /tmp |
91327a0 |
mktemp -d mandates 0700 via POSIX mkdtemp; random component defeats pre-creation |
Context7 pipe example not echoing $OUT |
91327a0 |
Now echo "$OUT" in same call |
| Firecrawl cleanup exception too narrow (scrape-only) | 91327a0 |
Now command-agnostic, enumerates all six commands |
Firecrawl shell: bash misattributed to Bash-tool runtime |
9a31ce0 |
Corrected to runtime gotcha note |
Echo missing from prototype + visualization mktemp -d snippets |
9bf825f |
echo "$d" added; rationale stated inline |
[IO.Path] PS accelerator shorthand in event-storming |
9bf825f |
Expanded to [System.IO.Path] throughout |
Checked at HEAD:
prototype/skills/explore-directions/SKILL.md:112-113:d=$(mktemp -d "${TMPDIR:-/tmp}/explore-directions-XXXXXX"); echo "$d"✅visualization/skills/visualize/SKILL.md:97-98:d=$(mktemp -d "${TMPDIR:-/tmp}/visualize-XXXXXX"); echo "$d"✅event-storming/skills/simulation/reference/agentic-simulation.md:271:mktemp -d "${TMPDIR:-/tmp}/eventstorming-session-XXXXXX"with echo and carry instruction ✅firecrawl/skills/firecrawl/SKILL.md:87: Command-agnostic cleanup exception ✅context7/skills/lookup/context/cli.md:129:echo "$OUT"in same call ✅
🟡 Nit (carry-over) — prototype and visualization CHANGELOGs don't mention the echo requirement
The 9bf825f commit added echo "$d" to both SKILL.md files but did not touch their CHANGELOG entries. The echo is a normative instruction (without it the following write call has no recoverable path), so it's a meaningful behavioral detail for consumers reading changelogs.
Compare context7/CHANGELOG.md:14 — "the example echoes the generated path in the same call" — to the prototype 0.3.3 and visualization 0.1.1 entries, which describe the mktemp -d approach but omit the echo. Non-blocking; the SKILL.md files are authoritative for agents.
🟡 Nit (carry-over, PLAUSIBLE) — {session_id} constraint doesn't name shell metacharacters
agentic-simulation.md:333: "no spaces, slashes, or other path-unsafe characters" doesn't explicitly list $, `, ;, |, & — which Bash evaluates inside double-quoted strings and which appear in the archive commands at lines 339 and 652. The rm -rf cleanup path is now injection-safe (it uses the mktemp return value, not a recomposed path), so the risk surface is limited to the archive shell commands. Confidence remains PLAUSIBLE given the {domain}-{date}-{random4} format constraint. Non-blocking.
Looks good
1aca1f1is purely cosmetic: rewrapsexplore-directionsat 100 columns andvisualizeat 78 columns with no wording changes. No issues.- All five plugins carry correct patch bumps and matching CHANGELOG entries.
- The
mktemp -d+ fixed-name-file-inside approach for the two HTML-producing skills neatly sidesteps mktemp: no flag form is both non-deprecated and portable, and the bare form writes into the repo #1709's open suffix-portability question without waiting for the marketplace-wide decision. firecrawl/evals.jsonandfirecrawl/update/SKILL.mdare consistent with the core pattern.- Both downstream
{session_dir}reference sites in event-storming use the primitive's returned value, not a recomputed path.
…reaching for the scratchpad (#1675) ## Summary The topic-docs tier table sorts documents by one question — does anything downstream *enforce against* this? — which cannot express **lifetime**. Its finest-grained cell, the memory tier, conflated state that must SURVIVE the session (resume artifacts, ledgers, captures) with files that DIE with it. 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 6.4 MB docs corpus, keyed by working directory, declined three times upstream as a supported surface: anthropics/claude-code#45745, #17936, #21248, all closed as not-planned) — while `architecture:improve` had independently settled on a `mktemp` temp file. That divergence, not a shared mistake, is what the convention registry's trigger anticipates: an owner doc before a second plugin adopts. This adds one additive **Ephemeral** row plus a section stating its five rules, a re-derivation trigger, and a rationale recording why the other three candidate axes needed no change. **Minor, not major:** no tier moves, no `topic-docs.yaml` key renamed, slug spec untouched, and no visibility guarantee changed — 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. ### Scope beyond the original row Review enumerated further producers that the new row reclassifies. Rather than defer them, this PR places every producer in the plugins it already touched: - **`planning`** — the `/planning:interview` dense-round tables move out of the memory slice into one OS temp directory per run, and the plugin's four other undocumented HTML views (`prd` pitch, `brainstorm` reaction-capture, `plan` view, `design` topology) get a placement where they previously resolved to nowhere. Both the eagerly-loaded `SKILL.md` and the on-demand `context/loop.md` move together. - **`education`** — `primer`'s HTML had no resolvable path at all (it routed through a workspace placement while creating no workspace) and is now ephemeral-tier; the workspace lesson HTML is correctly **machine state**, and stops calling itself ephemeral while writing into persistent state. An HTML lesson also gained a canonical name (`lesson.html`, replacing `lesson.md`, never both) and a `<meta name="concept">` marker so the slug-collision guard keeps an identity source. The remaining five plugins are swept in #1708. ### Acceptance criteria - [x] `docs/conventions/topic-docs/README.md` carries an Ephemeral row with all rules stated - [x] The tier decision algorithm gained a lifetime discriminator, so the ephemeral row and the memory tier are distinguishable from the normative sentence alone - [x] `docs/conventions/topic-docs/CHANGELOG.md` records the change as MINOR (2.4.0) with the justification - [x] `plugins/adhd/skills/clarify/SKILL.md` no longer references the session scratchpad - [x] `plugins/architecture/skills/improve/actions/deepening.md` — the executable step, not only the format reference — resolves its temp path by the new row's rule - [x] Every ephemeral producer in the touched plugins uses a portable `mktemp` form - [x] The row carries a re-derivation trigger naming the observable change that invalidates it - [x] A rationale section records why the other three axes needed no change Beyond the stated criteria, `plugins/architecture/reference/topic-docs.md` said its HTML report was "deliberately ephemeral, **outside this convention**" — now stale, so it is corrected in the same change rather than left contradicting the new row. ## Test plan Run from the branch worktree against `origin/main`: - `npx markdownlint-cli2` over all changed markdown — **0 errors** - `scripts/check-changelog-parity.sh --check-bump origin/main` — **pass** - `scripts/check-contract-slice-prune.sh --check-diff origin/main` — **pass**, no path under `docs/topics/` - `scripts/check-skill-portability.sh origin/main` — **pass**, no unexcused coupling tokens The `mktemp` forms were verified against primary sources rather than assumed: FreeBSD and macOS `mktemp(1)` both document that only **trailing** `Xs` are substituted, so a `…-XXXXXX.html` template cannot create the file on macOS — while GNU coreutils 8.32 accepts it (reproduced locally). This repo's CI is `ubuntu-24.04` throughout, so no gate here would have caught it; every producer now takes the `-d` run-directory form, which keeps the placeholders trailing. ## Related Closes #1635 - #1700 — the ephemeral-producer sweep this PR's review enumerated; covered between this PR (planning, education) and #1708 (the remaining five plugins) - #1708 — sibling sweep, disjoint plugin set --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 3m 35s —— View job Security Review — PR #1708 (HEAD
|
… its meaning differs
Independent review caught a false premise this PR had propagated to six
sites. `--tmpdir` is NOT absent on BSD. Both FreeBSD mktemp(1) and the
macOS/Xcode page document `-p tmpdir, --tmpdir[=tmpdir]`. The claim was
inherited uncritically and repeated as if verified; it was not.
The real hazard is worse than absence, which is why the fix itself
stands. The flag exists in both dialects and means different things:
- GNU treats the positional template as relative to that directory and
lets the flag beat TMPDIR -- its own docs deprecate `-t` because
"-p without -t offers better defaults (by favoring the command line
over TMPDIR)".
- BSD/macOS consult it only as a fallback FOR THE `-t` FLAG when
TMPDIR is unset, so TMPDIR wins instead. With a bare positional
template and no `-t`, the flag does nothing at all and the template
resolves against the CURRENT DIRECTORY.
So `mktemp --tmpdir visualize-XXXXXX.html` does not fail on macOS; it
silently writes into the consumer's repository -- the precise outcome
the ephemeral tier's never-in-the-repo rule exists to prevent, and a
silent wrong answer rather than a loud one. BSD's `-t` also takes a
prefix rather than a template, so the two dialects produce different
filenames from the same argument.
An absolute path in the positional TEMPLATE is reinterpreted by neither,
which is what every call site now uses. The fix is unchanged; only the
justification was wrong.
Three further review findings:
The concurrent-session-safety bullet still asserted the retired
`eventstorming-session-{id}` formula as the anti-collision mechanism.
Non-overlap now rests on the primitive's random component, which is what
actually provides it.
The firecrawl cleanup exception enumerated all seven commands while
calling itself command-agnostic -- a closed list is the opposite of
command-agnostic and would have to be edited whenever a command is
added. The enumeration is gone.
The 0700 claim was stated flatly across both platforms. It is POSIX
`mkdtemp`'s guarantee specifically; the Windows branch does not inherit
it and relies on `$env:TEMP` being per-user and ACL-scoped. Scoped
accordingly.
`skills/update/scripts/update.sh` moves off `mktemp -d -t` to the same
positional form -- on BSD that argument is a prefix, so the run
directory was named differently there than on GNU. All 17 of its checks
still pass.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 2m 21s —— View job PR Review — #1708 (HEAD
|
|
Claude finished @kyle-sexton's task in 2m 58s —— View job Security Review — PR #1708 (HEAD
|
…ctually show The correction in d7d1be9 asserted that FreeBSD and macOS document `-p tmpdir, --tmpdir[=tmpdir]`. The verbatim SYNOPSIS on both pages shows only the short `[-p tmpdir]`; the long spelling appeared in a rendering of the options list, not in text quoted from the page. Since the whole point of that commit was to stop repeating unverified claims, the claim is narrowed to `-p` -- which both synopses do show -- with the long form attributed to GNU where it is certain. The argument and the fix are unchanged: `-p` exists in both dialects, means different things, and the positional absolute TEMPLATE is reinterpreted by neither. Also repairs the sentence three changelog entries lost while rewrapping ("and lets the" was dropped mid-clause), and corrects the PR description's test plan, which still claimed no executable code path changed after update.sh was modified. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 1m 57s —— View job PR Review — #1708 (HEAD
|
|
Claude finished @kyle-sexton's task in 6m 0s —— View job Security Review — PR #1708 (HEAD
|
…raph Cosmetic only -- the narrowed flag claim left a line past the file's 78-column wrap. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 2m 44s —— View job PR Review — #1708 (HEAD
|
| Finding | Fixed in |
|---|---|
mktemp --tmpdir breaks on BSD/macOS (three reviewers, two commits) |
91327a0: mktemp -d "${TMPDIR:-/tmp}/…-XXXXXX" — no flag, valid on both dialects |
| Event-storming session dir: predictable path, world-readable 0755 (Codex P1) | 91327a0: mkdtemp mandates 0700; random component defeats pre-creation |
Context7 pipe example not echoing $OUT (Codex P2) |
91327a0: echo "$OUT" added in same call |
| Firecrawl cleanup exception too narrow (Codex P2) | 91327a0: Now command-agnostic — "whichever command produced it" |
Firecrawl shell: bash misattribution |
9a31ce0 |
Missing echo in mktemp -d snippets for prototype/visualization/event-storming |
9bf825f: echo "$d" added; rationale stated inline |
[IO.Path] PS accelerator shorthand |
9bf825f: Expanded to [System.IO.Path] |
False "--tmpdir absent on BSD" premise at six sites |
d7d1be9 → narrowed further in aaeef9d |
Concurrent-session-safety bullet asserting retired {id} formula |
d7d1be9: Updated to attribute non-overlap to the primitive's random component |
| 0700 claim stated too broadly | d7d1be9: Scoped to POSIX mkdtemp specifically |
| Firecrawl exception enumerated a closed command list | d7d1be9: De-enumerated to "whichever command produced it" |
| Three changelog entries with dropped clause | aaeef9d |
Spot-checked at HEAD:
visualization/SKILL.md:97:d=$(mktemp -d "${TMPDIR:-/tmp}/visualize-XXXXXX"); echo "$d"✅prototype/SKILL.md:112:d=$(mktemp -d "${TMPDIR:-/tmp}/explore-directions-XXXXXX"); echo "$d"✅agentic-simulation.md:271:mktemp -d "${TMPDIR:-/tmp}/eventstorming-session-XXXXXX"with echo-and-carry ✅firecrawl/SKILL.md:87: Command-agnostic cleanup exception ✅context7/cli.md:129:echo "$OUT"in same call ✅update.sh:34:mktemp -d "${TMPDIR:-/tmp}/firecrawl-update-XXXXXX"✅
🟡 Carry-over nit — prototype and visualization CHANGELOGs omit the echo requirement
First raised two review passes ago and unchanged at HEAD. The 9bf825f commit added echo "$d" — the instruction that makes the randomly-named directory recoverable in the follow-up Bash call — but neither the [0.3.3] prototype entry nor the [0.1.1] visualization entry mention it. Compare the [0.4.3] context7 entry, which explicitly states "the example echoes the generated path in the same call." The SKILL.md files are authoritative for agents — this is a consumer-facing documentation gap only. Non-blocking.
🟡 Carry-over suggestion (PLAUSIBLE) — {session_id} constraint cites stale motivation and omits shell metacharacters
Carried through four prior review passes. agentic-simulation.md:333:
"no spaces, slashes, or other path-unsafe characters, since the ID is used in filesystem paths (including the
rm -rfcleanup)"
Two issues: the rm -rf parenthetical is stale (cleanup now operates on {session_dir}, the mktemp-returned path, not a path built from {session_id}), and $, `, ;, |, & — which Bash evaluates inside double-quoted strings — are not listed. The remaining live surface is the archive-copy at line 339 and line 653, where {session_id} appears in a path an agent would pass to a shell command. Confidence stays PLAUSIBLE: the {domain}-{date}-{random4} format substantially limits the attack surface, and the PowerShell arm is injection-safe via -LiteralPath. Non-blocking.
Looks good overall
The nine-commit arc is coherent and self-correcting — each commit addressed exactly what prior reviewers flagged, and the final two commits (aaeef9d, 7c707cb) improve citation precision and column formatting without reopening anything. All five plugins carry correct patch bumps and matching CHANGELOG entries. The mktemp -d + fixed-name-file-inside approach for the two HTML-producing skills sidesteps #1709's open suffix-portability question cleanly. The PR is ready for merge subject to the two non-blocking nits above, which can be addressed here or deferred to a follow-up at the author's discretion.
… now requires #1675's lane resolved the open suffix question and wrote the answer into `docs/conventions/topic-docs/README.md` rule 1: the `XXXXXX` placeholders must be TRAILING, because BSD `mktemp` on macOS substitutes only trailing Xs. So `<prefix>-XXXXXX.html` is not merely non-conforming -- the file cannot be created at all there. GNU coreutils accepts it and this repo's CI is ubuntu-24.04 throughout, so no gate here would have caught it. Both HTML producers already take the `-d`-plus-fixed-filename form the rule prescribes, so they conform as written and no invocation changes. What changes is the justification: the prose said we merely "do not depend on" the suffix working, hedging an unknown. The unknown is now settled, so it says plainly that the suffix form is broken on macOS and that the run directory is what preserves the `.html` name. Merges origin/main to pick up that rule. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TkLLDcB8Ht2tm3j688F5ZP
|
Claude finished @kyle-sexton's task in 5m 3s —— View job PR Review — #1708 (HEAD
|
|
Claude finished @kyle-sexton's task in 2m 50s —— View job Security Review — PR #1708 (HEAD
|
…oring time (#1751) No linked issue ## Summary The `pr-issue-linkage / pr-issue-linkage` check is a **required** merge gate, but nothing enforced its contract at the moment a PR body was written. A body missing a closing keyword or a `## Related` section was therefore only ever caught post-hoc — one CI round trip after the PR was already open — which is what happened on most PRs filed directly with `gh pr create` during the 2026-07-29 queue drain. This adds the missing authoring-time enforcement: a `PreToolUse` hook on the Bash tool, owned by the `source-control` plugin, that validates a `gh pr create` / `gh pr edit` body against the same contract **before** the call runs and blocks with the missing half named, so the authoring agent self-corrects in the same turn instead of on the next CI cycle. `/source-control:pull-request create` has always run the equivalent pre-create gate (`skills/pull-request/reference/create.md` §2.4.2). This hook covers the calls that never go through the skill; the skill's own path is unaffected, since its gate runs first and the hook then sees a body that already passes. ### Enforcement is keyed to the consumer's own policy The gate runs only when the repository root carries `.github/workflows/pr-issue-linkage.yml` (or `.yaml`). A repository that does not run the check is never gated, so the hook cannot drift away from what its consumer actually enforces. This is deliberately **not** the `pr_body_required_sections` seam (`docs/conventions/pr-body-convention/`). That key is the repo's configurable section scaffold, and its portable default excludes `Related` on purpose; the authority for *this* gate is the workflow file that defines the check. ### The validator is mirrored, not approximated Ported from the reusable `melodic-software/ci-workflows/.github/workflows/pr-issue-linkage.yml` `github-script` step, including the three places a hand port silently diverges: - **Both HTML-comment strips, in order** — every terminated comment span, then an unterminated comment opener swallowing the rest of the body. Without this an unedited PR template, whose instructional prose names the very markers the gate looks for, passes vacuously. - **Heading-level semantics** — only a heading at the same level or higher closes `## Related`, so a nested `### ...` subsection is that section's *content*. A naive "next line starting with `#`" reading calls such a section empty and false-blocks a compliant body. - **JavaScript word boundaries**, which POSIX ERE has no equivalent for, transcribed as explicit non-word characters around a newline-wrapped probe — so `Closes #12abc` and `unclosed #5` stay non-matches exactly as they are in CI. ### Fail-open on extraction, fail-closed on a determinable bad body Judged: a `--body`/`-b` literal, a readable `--body-file`/`-F` path, and the sole heredoc feeding `--body-file -` or a `--body "$(cat <<EOF ... EOF)"` substitution. Allowed: an unexpanded variable, several heredocs (which one reaches `gh` is not statically knowable), an unterminated heredoc, an unreadable body file, an absent body flag (`--fill`, `--template`, `--editor`, the interactive prompt), and any `--repo`-targeted invocation, whose target may not be the repository whose workflow file the scope guard read. Guessing at a body the hook cannot see would block compliant calls, which costs more than a miss. The PowerShell tool and direct `gh api .../pulls` calls are documented as out of scope at the hook's own site, alongside the `--repo` limit. ## Test plan - `plugins/source-control/hooks/pr-body-linkage-gate.test.sh` — 53 black-box cases, all passing: the scope guard, both halves independently, all nine closing keywords plus the colon and `owner/repo#N` forms, both no-issue markers, the two word-boundary non-matches, three comment-stripping cases, four section-boundary cases (including the deeper-subsection case), every body source and every undeterminable-body path, `gh pr edit`, env/`env(1)`/`sh -c` wrappers, `--repo`, and the kill switch. - Repo gates run locally, all green: `shellcheck` (with `.shellcheckrc`), `shfmt`, `check-silent-skips`, `check-hook-userconfig-argv`, `check-shell-portability` (vs `origin/main`), `check-cross-plugin-source-drift`, `sync-hook-utils --check`, `check-changelog-parity` (`--check` and `--check-bump`), `check-plugin-manifest-presence`, `validate-plugin-contracts`, `validate-plugins`, and `markdownlint-cli2` on every changed markdown file. - Dogfooded: this PR's own body was run through the hook before `gh pr create` fired — and the first draft was **blocked**, correctly. That draft spelled the comment delimiters out literally while describing the comment-stripping rule, so the strip ate everything after them, `## Related` included. CI would have rejected it identically. The hook caught it before the PR existed, which is the whole point. ## Related - Refs #1748, #1745, #1708 — PRs whose bodies failed `pr-issue-linkage` post-hoc during the 2026-07-29 queue drain, which is the recurring failure this hook removes at the source. - `docs/conventions/pr-body-convention/README.md` reserves the enforcement seam for the `pr_body_required_sections` key; this hook deliberately does not consume that key, for the reason given under "Enforcement is keyed to the consumer's own policy" above. - `plugins/guardrails/hooks/block-convention-violation.sh` gates the `gh pr create` **title** against the tracked team convention. Different field, different source of truth; the two hooks compose rather than overlap. --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…p form (#2437) Fixes #1709 ## Summary Completes the mktemp portability decision: the portable, non-deprecated form is `mktemp "${TMPDIR:-/tmp}/<name>-XXXXXX"` (trailing Xs, positional absolute template), verified by execution on both GNU and BSD with outputs recorded on the issue. This PR migrates the last two non-conforming call sites under `plugins/**`. ## Fix `plugins/claude-config/skills/audit/scripts/fix-plugin-drift.sh` moves its two `mktemp -t <name>-XXXXXX.json` scratch files to the convention form. GNU marks `-t` deprecated; BSD `-t` treats its argument as a prefix rather than a template; and the `.json` suffix was the silent macOS trap — BSD substitutes only trailing Xs, so a suffix template is created verbatim with no randomness (verified on a real macos-latest runner, evidence on the issue). The extension was cosmetic; both files are consumed via explicit paths. claude-config 0.37.2 with a CHANGELOG entry. ## Verification - GNU (coreutils 9.4, local) and BSD (macOS 26.5.2, GitHub Actions run 31594143233): candidate file and dir forms succeed identically; suffix template misbehaves on BSD; bare relative template lands in the CWD on both. Full outputs recorded on #1709. The throwaway macOS workflow was removed from this branch after the evidence was captured, so it nets to zero in this diff. - `scripts/affected-tests.sh --run` over the diff: all 26 selected suites passed (one unrelated goimports-absence skip). - Sweep: no `mktemp -t`/`--tmpdir`/bare-relative-template invocations remain under `plugins/**`. ## Related - `docs/conventions/topic-docs/README.md` ephemeral tier rule 1 — already names this exact form; unchanged here. - Refs #1708, #1414 (the no-macOS-CI structural gap this verification worked around). --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>

Summary
A marketplace-wide audit classified nineteen ephemeral-file producers against the topic-docs ephemeral tier. Fourteen were already conforming or correctly classified in another tier. Five violated the contract; this fixes them.
firecrawl— the worst of themEvery scrape, search, and interact call wrote
/tmp/fc-<nonce>.<ext>:/tmpwith a hand-rolleddate +%s%Nnonce instead of the platform primitive, and no Windows branch at allNow uses
mktemp "${TMPDIR:-/tmp}/fc-…-XXXXXX", which works on every platform — including Windows, where Git Bash resolves/tmpthrough its mount to%TEMP%(by default under%LOCALAPPDATA%\Temp). Spill files are self-consumed, so they are removed after theRead, with the one exception the contract requires: when the user asked for the file itself, the path is the deliverable and is never deleted.The other four
prototype/explore-directions— offered "an OS temp or gitignored scratch location": a non-deterministic branch whose second half also puts the file inside the repository, against the tier's never-in-the-repo rule.visualization/visualize— wrote a local HTML file and handed back its path with no placement rule at all, while its sibling skills all carry one.event-storming/simulation—{system_temp}was never bound to a platform primitive anywhere in the file. Its delete-vs-archive split was already sound and is untouched.context7/lookup—> /tmp/nextjs-router.md, the same class as firecrawl but in an illustrative pipe example rather than a mandated rule. Fixed for consistency, and the example now echoes the generated path in the same call: the docs output is redirected, so without the echo a followingReadhas nothing to locate the randomly named file with.On the wording, stated accurately
An earlier draft of this description claimed the new paragraphs were taken from existing exemplars and therefore added no new phrasing. That claim was wrong, and an exemplar-fidelity audit caught it. What the diff actually does:
visualizationandprototypeparagraphs borrow their mechanics fromarchitecture/improve/actions/deepening.md(the Windows path, the open-commands, "report the absolute path") and the phrase "never lands in the consumer's repository tree" fromadhd/clarify/SKILL.md— but they are near-duplicates of each other, i.e. one new phrasing used twice, not two independent derivations. They no longer inherit that exemplar'smktempflag choice; see the portability section below.event-stormingparagraph tracks no exemplar. Its framing is new phrasing, structurally justified because that file needs a whole session directory rather than a one-shot file.Session directory is created, not merely named
Review flagged this as a P1, and it is a real exposure rather than a style point.
event-storming/simulationcomposed its session directory path from the session id and created it normally. On a multi-user POSIX host withTMPDIRunset,${TMPDIR:-/tmp}falls back to the shared, world-readable/tmp— so a predictable name both exposes the persona and session Markdown to every local user (a normally-created directory lands at 0755 with 0644 files inside) and lets any of them pre-create the path, so the workshop writes into a directory someone else owns.The reference now creates the directory with a secure primitive instead of naming it:
mktemp -d "${TMPDIR:-/tmp}/eventstorming-session-XXXXXX"on POSIX/Git Bash, andNew-Item -ItemType Directoryunder a[IO.Path]::GetRandomFileName()component in the per-user$env:TEMPon Windows PowerShell. The random component defeats pre-creation and POSIXmkdtempmandates mode 0700, which gates traversal into the directory regardless of the modes of the files inside it — so the file modes need no separate change. The delete-vs-archive cleanup protocol is untouched, and the two downstream sites that restated the old path formula now refer to the path the primitive returned.Portability — fixed here, not deferred
An earlier draft of this description shipped
mktemp --tmpdirandmktemp -t, and recorded the gap as a known limitation to file a follow-up for. Review caught it. It is fixed in this PR rather than deferred.A second review pass then caught the justification being wrong, and that correction matters more than the first. This description — and six sites in the diff — asserted that
--tmpdiris "absent on BSD". That is false. The FreeBSDmktemp(1)synopsis and the macOS/Xcode page both readmktemp [-d] [-p tmpdir] [-q] [-t prefix] [-u] template ...— BSD has the directory flag. (The claims below are kept to the short-pspelling, which is what both synopses show verbatim; the long--tmpdirform is GNU's, and BSD spells it inconsistently across versions.) The "absent on BSD" claim was inherited from an existing comment in this repo and repeated as if verified; it was not.The real hazard is worse than absence, which is why the fix itself stands unchanged. The flag exists in both dialects and means different things:
TMPDIR. Its own manual deprecates-ton exactly that ground: "the use of-pwithout-toffers better defaults (by favoring the command line overTMPDIR)".-tflag whenTMPDIRis unset — "If the-poption is set, then the given tmpdir will be used if theTMPDIRenvironment variable is not set." SoTMPDIRwins instead, and with a bare positional template and no-tthe flag does nothing at all.So
mktemp --tmpdir visualize-XXXXXX.htmldoes not fail on macOS. It resolves the template against the current directory and silently writes into the consumer's repository — the precise outcome the ephemeral tier's never-in-the-repo rule exists to prevent, and a silent wrong answer rather than a loud one. BSD's-talso takes a prefix rather than a template, so the two dialects produce different filenames from the same argument.Every call site in this diff therefore carries the temp root in the positional TEMPLATE argument, which neither dialect reinterprets:
The form is still the one this repository already mandates for shell scripts —
scripts/shell-portability-tokens.txtlintsmktemp -pandmktemp --tmpdirout of changed**/*.shfiles and points at the positional TEMPLATE replacement (#1527) — and that gate scanning.shonly is exactly how these skill documents drifted from it. Note that the gate's own inline comment carries the same "BSD does not implement" error this PR just corrected in its own prose; fixing that comment is #1544's lane, not this PR's, so it is deliberately untouched here.The two skills that hand back an HTML file (
visualization/visualize,prototype/explore-directions) create a private run directory withmktemp -dand write a fixed-name page inside it, rather than askingmktempfor a template with a.htmlsuffix after theXXXXXX.That question is now settled — and had it gone the other way this PR would have shipped a macOS-broken instruction. When these producers were written the suffix form was an open risk (#1709 raised it). #1675's lane has since resolved it against primary sources and written the answer into the tier contract itself:
docs/conventions/topic-docs/README.mdnow requires theXXXXXXplaceholders to be trailing, because BSDmktempon macOS substitutes only trailing Xs — so<prefix>-XXXXXX.htmlis not merely non-conforming, the file cannot be created at all there. GNU coreutils accepts it and this repo's CI isubuntu-24.04throughout, so no gate here would ever have caught it.Both producers already take the
-d-plus-fixed-filename form that rule prescribes, so they conform as written. The surrounding prose is upgraded from "we do not depend on the suffix working" to stating plainly that it does not.On verification, stated plainly. GNU coreutils 8.32 under Git Bash, executed in this environment:
mktemp -d "${TMPDIR:-/tmp}/es-session-XXXXXX"→/tmp/es-session-cmjdnl, exit 0. (An earlier probe here also showedmktemp "${TMPDIR:-/tmp}/visualize-XXXXXX.html"succeeding on GNU — that success is exactly the trap the trailing-X rule now closes, since the same command fails on macOS.) The PowerShell branch was executed too (PowerShell 7.6.4, Windows 11): it creates the directory under%LOCALAPPDATA%\Tempand returns its full path. BSD/macOS is not executable from here, so every BSD claim above is quoted from the FreeBSD and macOS man pages rather than run — which is the discipline whose absence produced the "absent on BSD" error in the first place. The 0700 mode claim likewise rests on POSIXmkdtemp, not on this environment (Git Bash reports 0755 because MSYS emulates POSIX modes over Windows ACLs), and is now scoped to POSIX in the skill text rather than stated flatly across both platforms.What is still not safe, and is avoided everywhere in this diff: a bare
mktempwith a relative template resolves against the current directory, somktemp -u fc-test-XXXXXXrun inside a repo returnsfc-test-tdvDNS— a file in the consumer's tree, against the tier's own rule.Explicitly left alone
The audit confirmed these correct, and changing them would be regressions:
quiz-meandai-briefing(machine state),audit-pass(machine-state findings store),course-digestandyoutube-digest(correct Ephemeral/Memory/Machine-state split, verified against the actualos.tmpdir()library code),running-retro(machine state by design — the observer outlives the session),machine-health(durable report archive),pressure-testandprototype/context/discipline.md(in-repo throwaway source code, a deliberate pattern outside this contract), and thework-items/source-controlmktemp+rm -fspill-file pattern (rule 2's permitted self-consumed exception).Test plan
Run from the branch worktree against
origin/main:scripts/check-changelog-parity.sh --check-bump origin/main— pass (all five plugins bumped with matching## [<version>]entries)scripts/check-contract-slice-prune.sh --check-diff origin/main— pass, no path underdocs/topics/scripts/check-skill-portability.sh origin/main— pass, no unexcused coupling tokensmarkdownlint-cli2over every changed Markdown file — pass, 0 errorsmktempforms exercised directly in this environment (GNU results and the BSD caveat quoted above)plugins/firecrawl/skills/update/scripts/update.test.sh— 17/17 pass after theupdate.shtemp-dir changescripts/check-shell-portability.sh origin/main— pass on the changed shell fileOne executable path changed in this PR:
update.sh's run-directory creation, covered by the 17 checks above. Everything else is instruction text in skill bodies and changelog prose, where no test suite applies. CI confirms the full gate set.Review findings from the adversarial passes have been addressed on this PR, not deferred:
/tmp, now created bymktemp -d);mktempflag form across every touched call site, plusskills/update/scripts/update.sh, whose-d -tproduced a differently-named run directory on BSD (all 17 of its checks still pass);context7example — and the same defect in the newmktemp -dsnippets, whose randomly-named directory is unrecoverable in the following tool call without it;firecrawlcleanup exception, now genuinely command-agnostic rather than a closed seven-command list that would need editing whenever a command is added;--tmpdiris absent on BSD" premise at six sites, corrected above against the FreeBSD and macOS man pages;eventstorming-session-{id}formula as the anti-collision mechanism; andmkdtemprather than asserted across both platforms.Related
Closes #1700 — the ephemeral-producer sweep. This changed while the PR was open: #1700 named two producers (
planning:interviewround tables,education:teachlesson HTML), each gated behind an open design question, which is why an earlier revision of this description said the issue was deliberately not closed here. #1675's lane has since resolved both — verified in mergedmainrather than taken on report:planning/skills/interview/context/loop.mdnow creates onemktemp -d "${TMPDIR:-/tmp}/interview-XXXXXX"run directory per interview, andeducation/skills/teach/context/lessons.mdtook the outcome #1700 explicitly anticipated ("it was never ephemeral, fix the label") — workspace lesson HTML is now documented as machine state, with only theprimerpage migrated to the ephemeral tier. With those two answered and the five here migrated, the sweep the issue describes is complete.This is otherwise downstream conformance work from the ephemeral tier contract added in #1635 (PR #1675). #1635 is closed by that PR, not this one, so it carries no closing keyword here.
#1709 stays open and is deliberately not closed by this PR. Its portability question is answered for these five call sites, but its acceptance criteria also require the chosen form to be recorded normatively in
docs/conventions/topic-docs/README.mdand applied to every remainingplugins/**call site. At least two are outside this diff and still on the old forms:architecture/improve/actions/deepening.md(the origin of the--tmpdirchoice) andclaude-config/skills/audit/scripts/fix-plugin-drift.sh(mktemp -t). That migration and the normative record remain #1709's, and it carriesneeds-human/status: needs-decisionfor a reason.Closes #1700
Related
XXXXXXconstraint these producers already satisfy.scripts/shell-portability-tokens.txtcomment that still carries the "BSD does not implement" framing corrected here; deliberately untouched by this PR.