@natjswenson/devlog 0.10.0 → 0.11.1

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/README.md ADDED
@@ -0,0 +1,272 @@
1
+ # devlog
2
+
3
+ [![npm](https://img.shields.io/npm/v/@natjswenson/devlog?color=blue)](https://www.npmjs.com/package/@natjswenson/devlog)
4
+ [![license](https://img.shields.io/npm/l/@natjswenson/devlog)](./LICENSE)
5
+ [![security](https://img.shields.io/badge/security-audited-green)](./SECURITY.md)
6
+ [![vulnerabilities](https://img.shields.io/badge/npm%20audit-0%20issues-brightgreen)](#security)
7
+
8
+ A Claude Code skill that turns each version release (a git tag) into a published how-to guide, written in your own voice — and a React example for displaying it on your site.
9
+
10
+ > **Build in public, by release.** Tag a release like you always do. Run `/devlog`. Each new version shows up on your site as a polished, end-to-end implementation guide — in your voice, with cited sources and the gotchas you actually hit — not raw commit messages.
11
+
12
+ ## Live example
13
+
14
+ The skill is in production at [natejswenson.com/devlog](https://natejswenson.com/devlog), publishing to [github.com/natejswenson/daily-dev-log](https://github.com/natejswenson/daily-dev-log). What you see on that page is exactly what `npx @natjswenson/devlog preview` renders for you locally.
15
+
16
+ ## How it works
17
+
18
+ 1. **You ship a release** — tag it (e.g. `git tag v0.3.0`), like you already do.
19
+ 2. **Run `/devlog` in Claude Code.** The skill runs `devlog scan` to find tags that don't yet have an entry, researches the engineering topic behind each release, and writes a full setup → build → use → verify how-to guide (with a required `## Gotchas` section mined from your real fix commits, and 3+ cited sources). Every draft passes a deterministic lint (`devlog lint-post`) plus a quality self-review before it publishes. It's idempotent — re-running does nothing until you cut a new release, and a published entry is never overwritten (`devlog publish-entry` refuses).
20
+ 3. **Your site fetches it.** Static `manifest.json` + per-release markdown files served from `raw.githubusercontent.com` — no backend needed.
21
+
22
+ You can also manage configuration conversationally in Claude Code: "add this repo to devlog", "stop tracking X", "set min sources to 4", or `/devlog status` to see what would be generated without writing anything.
23
+
24
+ **In your voice.** Entries are written using a voice profile, resolved in this order: your `config.voicePath` → [ghostwriter](../ghostwriter)'s `voice/` dir if installed → a bundled default. devlog reads `voice-profile.md` (and `voice-notes.md` overrides) — never ghostwriter's `algorithm.md`, since LinkedIn reach tuning doesn't apply to a dev log.
25
+
26
+ ## Quick start
27
+
28
+ ```sh
29
+ npx @natjswenson/devlog init
30
+ ```
31
+
32
+ That command:
33
+ - Creates `<your-username>/daily-dev-log` on GitHub (or uses an existing one)
34
+ - Installs the skill at `~/.claude/skills/devlog/`
35
+ - Writes `~/.claude/skills/devlog/config.json` with your answers
36
+ - Lets you register one or more projects in a single run
37
+
38
+ Then:
39
+
40
+ ```sh
41
+ npx @natjswenson/devlog preview
42
+ ```
43
+
44
+ to see your dev log rendered locally at `http://localhost:5173`.
45
+
46
+ ## Prerequisites
47
+
48
+ - **Node 18+** — for the CLI and preview app
49
+ - **GitHub CLI** (`gh`), authenticated with `gh auth login` — used to create your dev-log repo and push entries
50
+ - **Claude Code** — to run the `/devlog` skill
51
+
52
+ ## Commands
53
+
54
+ | Command | What it does |
55
+ |---|---|
56
+ | `npx @natjswenson/devlog init` | One-time setup: create dev-log repo, install skill, write config |
57
+ | `npx @natjswenson/devlog add-project` | Register a project (interactive; `--yes --path <p>` for non-interactive/agent use) |
58
+ | `npx @natjswenson/devlog remove-project <key> --yes` | Unregister a project (published entries stay) |
59
+ | `npx @natjswenson/devlog set <field> <value>` | Update one config field (`targetRepo`, `branch`, `gitAuthor`, `githubUser`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
60
+ | `npx @natjswenson/devlog config [--json]` | Show your current config with validation status |
61
+ | `npx @natjswenson/devlog scan [--project <key>]` | JSON plan of new releases needing entries (used by the skill) |
62
+ | `npx @natjswenson/devlog lint-post <file>` | Deterministic post-contract check (used by the skill) |
63
+ | `npx @natjswenson/devlog publish-entry ...` | Copy a drafted entry into a clone + update the manifest; never overwrites (used by the skill) |
64
+ | `npx @natjswenson/devlog preview` | Run a local preview at `http://localhost:5173` |
65
+ | `npx @natjswenson/devlog --help` | Usage |
66
+ | `npx @natjswenson/devlog --version` | Version |
67
+
68
+ > **Tip:** run from any directory *outside* a clone of this repo. Running inside the repo causes a `package.json` name collision and `npx` fails with `command not found`.
69
+
70
+ ## What you end up with
71
+
72
+ ```
73
+ ~/.claude/skills/devlog/
74
+ ├── SKILL.md # The /devlog slash-command instructions
75
+ ├── config.json # Your settings (mode 0600)
76
+ └── voice/ # Bundled fallback voice profile (last resort)
77
+ ├── voice-profile.md
78
+ └── voice-notes.md
79
+
80
+ github.com/<you>/daily-dev-log/ # Created by init, populated by /devlog
81
+ ├── myproject/
82
+ │ ├── manifest.json
83
+ │ ├── v0.3.0.md
84
+ │ ├── v0.2.0.md
85
+ │ └── ...
86
+ └── ...
87
+ ```
88
+
89
+ ## Manual setup (if you prefer)
90
+
91
+ ```sh
92
+ gh repo create <you>/daily-dev-log --public --add-readme
93
+ mkdir -p ~/.claude/skills/devlog/voice
94
+ curl -o ~/.claude/skills/devlog/SKILL.md https://raw.githubusercontent.com/natejswenson/devlog/main/SKILL.md
95
+ # Optional fallback voice profile (used when voicePath and ghostwriter are both absent):
96
+ curl -o ~/.claude/skills/devlog/voice/voice-profile.md https://raw.githubusercontent.com/natejswenson/devlog/main/voice/voice-profile.example.md
97
+ curl -o ~/.claude/skills/devlog/voice/voice-notes.md https://raw.githubusercontent.com/natejswenson/devlog/main/voice/voice-notes.example.md
98
+ # Then copy config.example.json → ~/.claude/skills/devlog/config.json and fill it in
99
+ ```
100
+
101
+ ## Add to your site
102
+
103
+ ### React (drop-in)
104
+
105
+ ```sh
106
+ cp -r examples/react/ your-site/src/devlog/
107
+ ```
108
+
109
+ Edit `your-site/src/devlog/devlog-config.js` to point at your repo, then:
110
+
111
+ ```jsx
112
+ import DevLogPage from './devlog/DevLogPage.jsx';
113
+
114
+ <DevLogPage project="myproject" />
115
+ ```
116
+
117
+ Full instructions: [`examples/react/README.md`](./examples/react/README.md).
118
+
119
+ ### No site yet?
120
+
121
+ The `preview/` directory is a complete deployable Vite app. Set `VITE_DEVLOG_OWNER` / `VITE_DEVLOG_REPO` / `VITE_DEVLOG_PROJECTS` env vars on Vercel, Netlify, or Cloudflare Pages, build with `vite build`, deploy `dist/`. See [`preview/README.md`](./preview/README.md).
122
+
123
+ ### Other stacks (Next, Astro, plain HTML)
124
+
125
+ It's static JSON and Markdown on GitHub. Build whatever UI you want — see the **Data contract** below.
126
+
127
+ ## Data contract
128
+
129
+ The dev-log repo has this layout, all served as raw files from `https://raw.githubusercontent.com/<owner>/<repo>/<branch>/`:
130
+
131
+ ```
132
+ <repo>/
133
+ └── <project-key>/
134
+ ├── manifest.json # Index of all entries (newest first)
135
+ ├── v0.3.0.md # One entry per release (named by version)
136
+ ├── v0.2.0.md
137
+ └── ...
138
+ ```
139
+
140
+ **`manifest.json`:**
141
+ ```json
142
+ {
143
+ "entries": [
144
+ { "date": "2026-06-08", "file": "v0.2.0.md", "title": "...", "summary": "...", "version": "v0.2.0" }
145
+ ]
146
+ }
147
+ ```
148
+
149
+ Strict validation rules (entries that don't match are silently dropped by the React example):
150
+ - `date` matches `YYYY-MM-DD` (the release/tag date)
151
+ - `file` matches `^[a-zA-Z0-9._-]+\.md$`
152
+ - `title` and `summary` are non-empty strings
153
+ - `version` (optional) matches `^[a-zA-Z0-9._-]+$`
154
+
155
+ **Entry markdown:**
156
+
157
+ ```markdown
158
+ ---
159
+ title: "Essay-style title (never 'release vX.Y.Z')"
160
+ date: 2026-06-08
161
+ project: myproject
162
+ version: v0.2.0
163
+ tags: [reliability, python]
164
+ summary: "1-2 sentence hook framing the how-to"
165
+ ---
166
+
167
+ ## Shipped
168
+ What the release delivered, then the pivot to the topic the guide teaches.
169
+
170
+ ## <Setup / build / use-and-verify sections>
171
+ An end-to-end implementation guide a reader can follow without your repo:
172
+ complete, language-tagged code blocks; reader-side verification steps.
173
+
174
+ ## Gotchas
175
+ Real traps from the release's history, each: trap → symptom → escape.
176
+
177
+ ## Sources
178
+ - [Title](https://...) — what it supports (3+ distinct URLs)
179
+
180
+ ## Changelog
181
+ - commit message ([abc1234](https://github.com/.../commit/abc1234567...))
182
+ ```
183
+
184
+ That's the entire contract — `npx @natjswenson/devlog lint-post <file>` checks the mechanical parts of it.
185
+
186
+ ## Configuration reference
187
+
188
+ `~/.claude/skills/devlog/config.json`:
189
+
190
+ | Field | Type | Description |
191
+ |---|---|---|
192
+ | `targetRepo` | `"<owner>/<repo>"` | Repo where dev log entries are published. Must match `^[a-z0-9][a-z0-9._-]*\/[a-z0-9][a-z0-9._-]*$`. |
193
+ | `branch` | string (optional) | Branch in the dev-log repo. Defaults to `main`. Must not contain `..` or start with `-`. |
194
+ | `gitAuthor` | string | Your name. Retained for backward compatibility; it is **not** currently rendered on entries (the author filter was removed, and release notes summarize all commits in a tag range). Still **required** by config validation — it must be present and non-empty (don't drop it). Whitespace OK; no shell metacharacters. |
195
+ | `githubUser` | string | Your GitHub username. |
196
+ | `voicePath` | string (optional) | Directory holding `voice-profile.md` (and optionally `voice-notes.md`) used to write entries in your voice. A leading `~` is expanded. If unset, devlog uses ghostwriter's `voice/` if installed, else the bundled default. Read only — never shell-interpolated. |
197
+ | `projects` | array | One entry per project you want dev logs for. |
198
+ | `projects[].key` | string | Subdirectory name in the dev-log repo. Strict token: `^[a-z0-9][a-z0-9._-]*$`, no `..`. |
199
+ | `projects[].label` | string (optional) | Display name for the tab. Defaults to `key`. |
200
+ | `projects[].path` | string | Local filesystem path to the project. Whitespace OK. |
201
+ | `projects[].remote` | `"<owner>/<repo>"` | The project's GitHub remote. Used to mark public commits and link them. |
202
+ | `projects[].pathFilter` | string (optional) | Repo-relative subdir scoping this project's commits in a monorepo (e.g. `skills/devlog`). |
203
+ | `projects[].tagPrefix` | string (optional) | Prefix of the git tags that mark this project's releases (e.g. `devlog-v`). Defaults to `v`. Used in `git tag --list '<tagPrefix>*'`. |
204
+ | `deepDive` | object (optional) | Controls the researched how-to. `topicDomains` (array, default `["AI", "DevOps/SRE", "software engineering"]`) steers topic selection; `minSources` (integer 1-10, default 3) is the floor of **distinct** cited source URLs per post. |
205
+
206
+ See [`config.example.json`](./config.example.json) for a complete template, or run `npx @natjswenson/devlog config` to inspect your current config with validation.
207
+
208
+ ## Security
209
+
210
+ The package is designed to be safe to install on a developer's machine and have ambient gh/git credentials. See [SECURITY.md](./SECURITY.md) for the full threat model and audit history.
211
+
212
+ **At a glance:**
213
+ - ✓ All shell calls in the CLI use `spawnSync` with argv arrays (no shell, no injection surface)
214
+ - ✓ The skill validates every config field against an allowlist before interpolating into shell commands; instructs the LLM to single-quote interpolated values
215
+ - ✓ Markdown rendering uses `react-markdown` with `skipHtml` and an allowlist `urlTransform` (only http(s)/mailto allowed; data:, javascript:, vbscript:, file:, blob: all neutralized)
216
+ - ✓ Frontmatter parser uses `Object.create(null)` + key allowlist (no prototype pollution)
217
+ - ✓ All external JSON is schema-validated before use (manifest, env-var projects array)
218
+ - ✓ Vite dev server bound to `localhost`, CORS off
219
+ - ✓ Demo-mode `window.fetch` override gated to dev builds only
220
+ - ✓ `config.json` written atomically (tmp + rename), mode 0600
221
+ - ✓ All dependencies pinned to exact versions
222
+ - ✓ `npm audit`: 0 known vulnerabilities
223
+
224
+ **To report a vulnerability:** open a [GitHub security advisory](https://github.com/natejswenson/devlog/security/advisories/new). Do not open a public issue.
225
+
226
+ ## Customization
227
+
228
+ - **Tweak the entry template:** edit `~/.claude/skills/devlog/SKILL.md` (Generate mode, Step 3 — the how-to contract).
229
+ - **Tweak your voice:** edit the `voice-profile.md` / `voice-notes.md` in your `voicePath` (or `~/.claude/skills/devlog/voice/`).
230
+ - **Tune research depth:** `npx @natjswenson/devlog set deepDive.minSources 4`, or `set deepDive.topicDomains "security, platform engineering"`.
231
+ - **Tweak the UI:** override the `--devlog-*` CSS variables in `examples/react/DevLogPage.css` to match your theme.
232
+ - **Add or remove projects:** `npx @natjswenson/devlog add-project` / `remove-project <key> --yes` — or just tell Claude ("add this repo to devlog").
233
+
234
+ ## Testing & evals
235
+
236
+ The deterministic core (`lib/`, the CLI) is covered by `npm test` (node:test; scan tests
237
+ run against real throwaway git repos). The non-deterministic half — post quality — is
238
+ covered by a cost-capped eval harness:
239
+
240
+ ```sh
241
+ node evals/run_eval.mjs --mock # $0, runs in CI; deterministic layer only
242
+ node evals/run_eval.mjs --live # LLM judge on golden fixtures; quotes spend, hard cap $0.50
243
+ ```
244
+
245
+ The judge scores reproducibility, code completeness, gotcha quality, citations, voice,
246
+ and scope honesty (pass ≥ 7/10). Without `ANTHROPIC_API_KEY` in the environment, mock
247
+ mode is forced — CI can never spend money.
248
+
249
+ ## Troubleshooting
250
+
251
+ **`sh: devlog: command not found` when running `npx`:** you're inside a checkout of this repo. The local `package.json` name collides with the published one. Run `npx` from somewhere else, e.g. `cd ~ && npx @natjswenson/devlog ...`.
252
+
253
+ **Init prompts show `78` after placeholder text:** that's an artifact of how some output capture tools render `\x1b7`/`\x1b8` (cursor save/restore) escape sequences. In a real interactive terminal, you won't see it.
254
+
255
+ **Preview is blank / no tabs / no entries:** check the browser console. If you see CJS interop errors related to `react-dom/client`, `style-to-js`, or `react-markdown`, you're on a pre-0.1.6 release — upgrade with `npm cache clean --force && rm -rf ~/.npm/_npx && npx --yes @natjswenson/devlog@latest preview`.
256
+
257
+ **Init can't find `gh`:** install via [cli.github.com](https://cli.github.com/) and run `gh auth login`.
258
+
259
+ **`Config validation failed: ...`** — the validator rejected something in your `config.json`. Run `npx @natjswenson/devlog config` for a detailed diagnosis. Common causes:
260
+ - A field has shell metacharacters (`;` `&` `|` `` ` `` `$` etc) — see SKILL.md for the full list
261
+ - A project key contains `..` or `/`
262
+ - A path doesn't exist on disk
263
+
264
+ **Preview shows "Setup required":** you deployed the preview app standalone but didn't set the env vars. Set `VITE_DEVLOG_OWNER`, `VITE_DEVLOG_REPO`, `VITE_DEVLOG_PROJECTS` (JSON-stringified array) in your hosting environment.
265
+
266
+ ## Versioning
267
+
268
+ Releases are documented in [CHANGELOG.md](./CHANGELOG.md). The package follows semver — bug fixes/security patches in patch releases (0.1.x), behavior changes in minor (0.x.0).
269
+
270
+ ## License
271
+
272
+ MIT
package/SKILL.md CHANGED
@@ -16,7 +16,7 @@ actual work, backed by cited reputable sources, and including **gotchas earned f
16
16
  experience**. The release is the springboard; the teaching is the point.
17
17
 
18
18
  The deterministic work (release discovery, post linting, manifest updates, config edits)
19
- is done by the `@natjswenson/devlog` CLI — invoke it as `npx -y @natjswenson/devlog <cmd>`.
19
+ is done by the `@natjswenson/devlog` CLI — invoke it as `npx -y @natjswenson/devlog@latest <cmd>`.
20
20
  Every agent-facing command prints JSON.
21
21
 
22
22
  ## Decide which mode you're in
@@ -38,10 +38,10 @@ Map the user's request onto the CLI — never hand-edit `config.json`:
38
38
 
39
39
  | Intent | Command |
40
40
  |---|---|
41
- | Show config | `npx -y @natjswenson/devlog config --json` |
42
- | Add a project | `npx -y @natjswenson/devlog add-project --yes --path <abs-path> [--key K] [--remote O/R] [--label L] [--tag-prefix P] [--path-filter F] [--private]` |
43
- | Remove a project | `npx -y @natjswenson/devlog remove-project <key> --yes` |
44
- | Change a setting | `npx -y @natjswenson/devlog set <field> <value>` (settable: `targetRepo`, `branch`, `targetDir`, `gitAuthor`, `githubUser`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
41
+ | Show config | `npx -y @natjswenson/devlog@latest config --json` |
42
+ | Add a project | `npx -y @natjswenson/devlog@latest add-project --yes --path <abs-path> [--key K] [--remote O/R] [--label L] [--tag-prefix P] [--path-filter F] [--private]` |
43
+ | Remove a project | `npx -y @natjswenson/devlog@latest remove-project <key> --yes` |
44
+ | Change a setting | `npx -y @natjswenson/devlog@latest set <field> <value>` (settable: `targetRepo`, `branch`, `targetDir`, `gitAuthor`, `githubUser`, `voicePath`, `deepDive.minSources`, `deepDive.topicDomains`) |
45
45
 
46
46
  `targetDir` is the subdirectory of `targetRepo` that holds the devlog content tree
47
47
  (e.g. `content/devlog` when the target is the site repo itself); unset/empty means the
@@ -71,26 +71,32 @@ fix the named field via `set`.
71
71
 
72
72
  ## Status mode
73
73
 
74
- Run `npx -y @natjswenson/devlog scan --json` and render a compact table: project, new
75
- releases (version + date + commit count), and skipped tags worth mentioning (reason
76
- `prerelease`, `empty-range`, etc. — omit `entry-exists` noise unless asked). Note
77
- `tagFetch: "failed"` ("using local tags only") and `existenceCheck: "failed"` ("couldn't
78
- confirm which entries exist; publish will still refuse overwrites"). Write nothing.
74
+ Run `npx -y @natjswenson/devlog@latest scan --json --summary` and render a compact table:
75
+ project, new releases (version + date + `commitCount`), and skipped-tag counts worth
76
+ mentioning (`prerelease`, `empty-range`, `entry-tombstoned`, etc. — omit `entry-exists`
77
+ noise unless asked). Note `tagFetch: "failed"` ("using local tags only") and
78
+ `existenceCheck: "failed"` ("couldn't confirm which entries exist; publish will still
79
+ refuse overwrites"). Write nothing.
79
80
 
80
81
  ## Generate mode
81
82
 
82
83
  ### Step 1: Scan for new releases
83
84
 
84
85
  ```bash
85
- npx -y @natjswenson/devlog scan --json # all projects
86
- npx -y @natjswenson/devlog scan --json --project '<key>' # one project
86
+ npx -y @natjswenson/devlog@latest scan --json --summary # the plan table
87
+ npx -y @natjswenson/devlog@latest scan --json --project '<key>' # full detail, one project
87
88
  ```
88
89
 
89
- The JSON plan contains everything discovery used to require: per project, the
90
- `newReleases` array (`tag`, `version`, `date`, `prevTag`, `commits[{hash, subject, date,
91
- public}]`, `diffstat`) plus `skippedTags` with reasons, and the resolved `deepDive`
92
- settings (`minSources`, `topicDomains`). Handle the edges:
90
+ Start with `--summary`: per project it gives `newReleases` (`tag`, `version`, `date`,
91
+ `commitCount`), per-reason `skippedTags` counts, `publishedEntries` (every live entry's
92
+ `version`/`title`/`tags` the topic-dedup input for 3b), and the resolved `deepDive`
93
+ settings (`minSources`, `topicDomains`). When you start writing a project's posts, run
94
+ the full per-project scan for its `commits[{hash, subject, date, public}]` and
95
+ `diffstat`. Handle the edges:
93
96
 
97
+ - The output echoes `cliVersion` — the version npx actually ran. npx caches aggressively;
98
+ if it's older than the version this SKILL.md shipped with, say so and re-run (the
99
+ `@latest` pin usually prevents this).
94
100
  - `error: "unknown-project"` → list `availableKeys` and stop.
95
101
  - `totalNewReleases: 0` → tell the user nothing new was tagged (mention notable skipped
96
102
  tags) and stop. Do not create empty entries.
@@ -150,14 +156,28 @@ trivia or niche internals. If the obvious topic is repo-specific, step up one le
150
156
  the general pattern behind it — the test is "could a reader finish this how-to and have
151
157
  something working of their own?" When a release spans two candidate topics (or two
152
158
  releases in one run share one), don't write the same guide twice: give each post the
153
- most usable topic the run hasn't already covered. In a monorepo, one commit can appear
154
- in several projects' ranges it belongs to the post whose release story it is; other
155
- posts leave it out of their narrative and `## Changelog` (check what sibling entries
156
- already cover, across all projects).
159
+ most usable topic the run hasn't already covered. **The same rule applies against the
160
+ whole catalog**: before settling on a topic, check every project's `publishedEntries`
161
+ (titles + tags, in the scan output) if an existing entry already teaches this topic,
162
+ find the angle this release genuinely adds, or step to the next-most-usable topic.
163
+ Never publish a near-duplicate of a guide the catalog already has. Vary the surface
164
+ too: don't reuse the catalog's title shapes ("How to …" again and again) or repeat the
165
+ same section-heading skeleton post after post.
166
+
167
+ In a monorepo, one commit can appear in several projects' ranges — it belongs to the
168
+ post whose release story it is; other posts leave it out of their narrative and
169
+ `## Changelog` (`publish-entry` refuses a draft whose Changelog repeats a commit an
170
+ existing entry already lists). **Never force a second angle the history doesn't
171
+ support**: if the twin release has no story of its own beyond the shared commits, give
172
+ it a proportionally small post — or fold it into the sibling's `## Shipped` — rather
173
+ than inventing a premise to differentiate it.
157
174
 
158
175
  **3c. Research before writing.** Use web search/fetch to gather at least
159
176
  `deepDive.minSources` **distinct** reputable sources: official docs and release notes,
160
- standards bodies, primary research, well-regarded engineering writing. Avoid SEO farms.
177
+ standards bodies, primary research, well-regarded engineering writing. A content farm
178
+ or SEO-mill page never counts toward `minSources` — when a concept has a primary source
179
+ (the original paper, the official docs, the pattern's canonical text), cite that, not a
180
+ summary site that ranks for the keyword.
161
181
  Every specific external claim (a version, a behavior, a study, a definition) must be
162
182
  backed by a source you actually verified — if you can't source it, don't claim it. Don't
163
183
  lean on one URL for most claims. Keep a working `(claim, url)` list. Fetch tools can
@@ -277,16 +297,28 @@ Write each draft with the **Write tool** (never a bash heredoc) to a temp dir
277
297
  (`mktemp -d` once, reuse the absolute path — shell variables don't persist across bash
278
298
  calls). Name it `<version>.md`. Then:
279
299
 
280
- 1. **Lint:** `npx -y @natjswenson/devlog lint-post '<abs-draft-path>'` fix every
281
- finding (missing sections, thin gotchas, too few distinct sources, sources listed
282
- but never cited inline, untagged fences).
283
- 2. **Assemble-and-run check:** when the post's code is runnable without external
284
- services, copy its code blocks in order into a scratch dir and execute them exactly
285
- as a reader would. Anything undefined, out of order, or missing an entrypoint fails
286
- the stranger test mechanically fix the post, not just the scratch copy.
287
- 3. **Self-review against the how-to contract**, honestly, as a skeptical reader: walk
300
+ 1. **Ground-truth gate.** List every claim the draft makes about the user's own repo
301
+ a tag exists or doesn't, a count ("seven tests went red"), a timeline, an outcome —
302
+ and verify each one with a git command run NOW, in this session (`git tag -l`,
303
+ `git show`, `git log`), the same way 3c keeps a `(claim, url)` list for external
304
+ claims. A claim you can't verify gets removed, not softened. A specific number is
305
+ publishable only if it appears in a commit, a diff, or a fetched source — otherwise
306
+ drop the precision ("several", not "seven"). And never label output as real ("that's
307
+ the real output", "from my actual run") unless the command that produced it ran in
308
+ this session — unrun output is always framed as expectation ("you should see…").
309
+ 2. **Lint:** `npx -y @natjswenson/devlog@latest lint-post '<abs-draft-path>' --voice` —
310
+ fix every finding (missing sections, thin gotchas, too few distinct sources, sources
311
+ listed but never cited inline, untagged fences, voice violations).
312
+ 3. **Assemble-and-run check:**
313
+ `npx -y @natjswenson/devlog@latest assemble-post '<abs-draft-path>' --out '<scratch>/assemble/<version>'`
314
+ extracts the draft's code blocks in order as numbered files (`text` fences are
315
+ expected output, listed but not written). When the code is runnable without external
316
+ services, execute the blocks exactly as a reader would. Anything undefined, out of
317
+ order, or missing an entrypoint (or an install step the post never shows) fails the
318
+ stranger test mechanically — fix the post, not just the scratch copy.
319
+ 4. **Self-review against the how-to contract**, honestly, as a skeptical reader: walk
288
320
  points 1-8 above plus voice adherence. Revise the draft for any point that fails.
289
- 4. At most **two** revision passes; then proceed with the best version and carry any
321
+ 5. At most **two** revision passes; then proceed with the best version and carry any
290
322
  residual weakness into the final summary (e.g. "v0.5.0: only 2 gotchas had commit
291
323
  evidence").
292
324
 
@@ -312,57 +344,66 @@ mktemp -d # → record the absolute path, e.g. /var/folders/.../tmp.abc
312
344
  git -C '<abs-tmp>' clone --depth=1 'https://github.com/<targetRepo>.git'
313
345
  # <content-root> = '<abs-tmp>/<repo-name>/<targetDir>' if targetDir is set,
314
346
  # '<abs-tmp>/<repo-name>' otherwise.
347
+ ```
315
348
 
316
- # Per release (refuses to overwrite an existing entry on {"error": ...,
317
- # "message": "... immutable ..."} skip that release and note it):
318
-
319
- # 1. Style guide + up to 3 reference images of recently published covers.
320
- npx -y @natjswenson/devlog cover-context '<key>' '<version>' \
321
- --clone '<content-root>'
322
- # On {"error": "style-guide-missing", ...}: skip cover composition for this release
323
- # entirely — proceed straight to publish-entry with no --cover flag. Never block
324
- # publish on a missing style guide.
325
-
326
- # 2. Compose the cover using ONLY this release's title/tags/summary/`## Shipped` text
327
- # (never the raw draft file, never `## Changelog`) plus the returned style guide,
328
- # icon catalog, and reference images. A cover that just re-renders the title in large text is a failure —
329
- # find the one concrete mechanism this release is actually about
330
- # (not the project name, not "a bug fix") and draw ONE custom inline-SVG illustration
331
- # of it, sized as the dominant visual element of the canvas; title/kicker stay
332
- # secondary. Two different releases should never produce visually similar covers.
333
- #
334
- # Draw the illustration inside a `#hero-zone` container at exactly
335
- # `x:150 y:425 width:1300 height:400` (render-cover mechanically checks this box and
336
- # refuses to render otherwise) — pick ONE of two composition slots per post: single
337
- # centered hero (one freehand mechanism, nothing else), or two-node before/after (a
338
- # left node, a right node, a connecting line, all freehand). Snap interior key points
339
- # to a 25px coordinate grid. Catalog icons (image-style/icons.md) are never placed
340
- # inside `#hero-zone` they may only appear as an optional small accent glyph near
341
- # the kicker/title area, entirely outside the hero zone, its bottom edge no lower than
342
- # y:400. See the style guide's hero-zone grid contract section before composing.
343
- # Write the result with the Write tool to '<abs-scratch>/<key>/<version>.html' — a
344
- # full document starting with `<!DOCTYPE html>`, sized
345
- # `html, body { margin:0; width:1600px; height:900px; }`, referencing the bundled
346
- # font only as `font-family: 'DevlogCoverFont', sans-serif`.
347
-
348
- # 3. Rasterize it. On failure (render timeout / Chromium not installed / font missing /
349
- # a #hero-zone problem missing, duplicate, wrong position/size, or a catalog icon
350
- # overlapping it),
351
- # the .html is left in place for debugging retry composing once with the error text
352
- # fed back, or give up and proceed with no --cover flag.
353
- npx -y @natjswenson/devlog render-cover '<abs-scratch>/<key>/<version>.html' \
354
- --project '<key>' --slug '<version>' --out '<abs-scratch>'
355
- # Show the rendered <abs-scratch>/<key>/<version>.png in this session before continuing
356
- # this interactive review IS the quality gate for the cover, the same way Step 4 is for
357
- # the prose.
358
-
359
- npx -y @natjswenson/devlog publish-entry \
360
- --clone '<content-root>' --project '<key>' \
361
- --version '<version>' --entry '<abs-draft-path>' \
362
- --cover '<abs-scratch>/<key>/<version>.png'
363
- # Omit --cover entirely if no cover was produced for this release (missing style guide,
364
- # a render failure not worth a second attempt) publish still proceeds normally.
349
+ Right after the clone, **Write `<abs-tmp>/run-state.json`**: the clone path, the planned
350
+ releases, and a per-release status you update as each one drafts/lints/publishes. If the
351
+ session is compacted or interrupted mid-run, re-read it instead of re-deriving paths
352
+ from `/var/folders` archaeology.
353
+
354
+ Per release:
355
+
356
+ 1. **Cover context.**
357
+ `npx -y @natjswenson/devlog@latest cover-context '<key>' '<version>' --clone '<content-root>'`
358
+ returns the style guide, icon catalog, and up to 3 reference cover paths. **Read
359
+ only the single most recent reference image** (image reads are the expensive part;
360
+ open another only if you're genuinely unsure the new cover is distinct). On
361
+ `{"error": "style-guide-missing"}`: skip cover composition for this release entirely
362
+ and proceed straight to publish-entry with no `--cover` flag.
363
+ Never block publish on a missing style guide.
364
+ 2. **Compose** using ONLY this release's title/tags/summary/`## Shipped` text
365
+ (never the raw draft file, never `## Changelog`) plus the style guide and icon catalog. A
366
+ cover that just re-renders the title in large text is a failure — find the one
367
+ concrete mechanism this release is actually about (not the project name, not "a bug
368
+ fix") and draw ONE custom inline-SVG illustration of it as the dominant visual
369
+ element; title/kicker stay secondary; two releases should never produce visually
370
+ similar covers. Follow the style guide's hero-zone grid contract: the illustration
371
+ lives in a `#hero-zone` container at exactly `x:150 y:425 width:1300 height:400`
372
+ (render-cover mechanically enforces this), one of two slots (single centered hero,
373
+ or two-node before/after), interior points snapped to a 25px grid; catalog icons
374
+ never go inside `#hero-zone` (optional small accent glyph near the kicker only,
375
+ bottom edge above y:400). Write the document with the Write tool to
376
+ `'<abs-scratch>/<key>/<version>.html'`full `<!DOCTYPE html>` document, sized
377
+ `html, body { margin:0; width:1600px; height:900px; }`, font referenced only as
378
+ `font-family: 'DevlogCoverFont', sans-serif`.
379
+ 3. **Rasterize.**
380
+ `npx -y @natjswenson/devlog@latest render-cover '<abs-scratch>/<key>/<version>.html' --project '<key>' --slug '<version>' --out '<abs-scratch>'`
381
+ The HTML is the source of truth and **survives the render**: to fix a visual
382
+ problem, edit (or re-Writeif an Edit fails, re-Write the whole file) the same
383
+ .html and re-run render-cover; it re-renders whenever the HTML is present,
384
+ overwriting the old PNG. On `render-failed` (timeout / Chromium missing / font
385
+ missing / a `#hero-zone` geometry violation), retry composing once with the error
386
+ text fed back, or give up and proceed with no `--cover` flag. **Show the rendered
387
+ `<abs-scratch>/<key>/<version>.png` in this session before continuing —
388
+ this interactive review IS the quality gate for the cover**, the same way Step 4
389
+ is for the prose.
390
+ 4. **Publish.**
391
+ ```bash
392
+ npx -y @natjswenson/devlog@latest publish-entry \
393
+ --clone '<content-root>' --project '<key>' \
394
+ --version '<version>' --entry '<abs-draft-path>' \
395
+ --cover '<abs-scratch>/<key>/<version>.png'
396
+ ```
397
+ Omit `--cover` entirely if no cover was produced (missing style guide, a render
398
+ failure not worth a second attempt) — publish still proceeds normally. publish-entry
399
+ refuses three things; none is retried by workaround: an existing entry (`immutable`
400
+ — skip the release, note it), a tombstoned version (`tombstoned` — skip; that
401
+ identity was editorially retired), and a Changelog commit another entry already
402
+ lists (drop the commit from this draft's `## Changelog` and re-publish).
403
+
404
+ Then, once, after all releases:
365
405
 
406
+ ```bash
366
407
  git -C '<abs-tmp>/<repo-name>' add .
367
408
  git -C '<abs-tmp>/<repo-name>' commit -m 'devlog: add release entries'
368
409
  git -C '<abs-tmp>/<repo-name>' push --no-tags origin '<branch>'
@@ -415,5 +456,17 @@ rules:
415
456
  not an error.
416
457
  - **Entry already exists** (skipped as `entry-exists`, or `publish-entry` refuses): a cut
417
458
  release is immutable; never overwrite, never delete.
459
+ - **`entry-tombstoned`:** the release's entry was editorially retired (moved,
460
+ consolidated, or deleted on purpose) — skip it silently, never regenerate it.
461
+ - **The user moved/consolidated/deleted a published entry by hand:** tombstone the
462
+ identity it left behind so no later run resurrects it:
463
+ `npx -y @natjswenson/devlog@latest tombstone --clone '<content-root>' --project '<key>'
464
+ --version '<vX.Y.Z>' --reason '<where it went>'` (commit + push like a publish).
465
+ - **The user edited a published entry's prose/frontmatter:** resync its manifest row
466
+ (title/summary/date/tags — what the site index and RSS read) with
467
+ `npx -y @natjswenson/devlog@latest sync-entry --clone '<content-root>' --project '<key>'
468
+ --slug '<version>'`. If the result reports `coverStale: true` and the title/topic
469
+ changed, offer to recompose the cover (Step 5 flow, `--cover` via publish is not
470
+ needed — use the backfill commands or recompose+`render-cover`+commit).
418
471
  - **Unknown project argument:** list the available keys from the scan error.
419
472
  - **Config missing/invalid:** point at `npx @natjswenson/devlog init` / `set` and stop.