A focused hardening release.
No new features — just fixes for the rough edges reported since 0.8.7 shipped.
Every change here is either a regression caught by users, a CI workflow bug, or a build-time reliability issue.

🐛 Bug fixes

Docker: volume mount permissions fixed

docmd init and other file-writing commands failed with “Permission denied” when /docs was a host-mounted volume owned by a uid other than the container’s built-in docmd user (uid 1001).
The image now ships with su-exec and the entrypoint detects the host uid:gid of the mounted directory and re-execs as that identity before running any command — same pattern used by the official postgres and redis images.

# All of these now work without -u flags on a host volume
docker run -v $(pwd):/docs ghcr.io/docmd-io/docmd:latest init
docker run -v $(pwd):/docs -p 3000:3000 ghcr.io/docmd-io/docmd:latest

The cp -a (archive) mode that triggered “Operation not permitted” warnings on ownership preservation is gone.
The -u $(id -u):$(id -g) workaround is no longer required and has been removed from all READMEs and the Docker deployment guide.

Docker: init now non-blocking in CI

The interactive “Do you want to override these files?” prompt hung in non-TTY environments (CI, docker run without -it, piped input).
The CLI now detects non-interactive mode and skips the prompt automatically — keeping existing files by default (safe), or accepting -y/--yes to opt into overwrite.
A new --force flag also works in any environment for explicit override.

# Force overwrite in CI / Docker / piped input
docker run --rm -v $(pwd):/docs ghcr.io/docmd:latest init --force

# Or use -y
docker run --rm -v $(pwd):/docs ghcr.io/docmd:latest init -y

docmd add on npx: false “npm not found” on Windows

npx @docmd/core add <plugin> on Windows PowerShell printed “The package manager ‘npm’ was not found on your system PATH” even though npm --version worked fine in the same shell.
Root cause: when @docmd/core is launched through npx, the spawned child process inherits a stripped PATH on Windows.
The installer now resolves npm/pnpm/yarn/bun relative to process.execPath (Node’s own location), which always works regardless of how @docmd/core was launched.

The error message also now detects the specific case (Node reachable + binary missing) and suggests installing the plugin via the host package manager directly.

Template registry lookup for @docmd/template-*

Setting theme.template to an official template like summer or @docmd/template-summer printed:

⬢ Plugin "template-summer" not found in official registry - manual installation required

Root cause: the auto-install registry lookup stripped @docmd/plugin- from the package name to derive the registry key, but didn’t strip @docmd/template-.
So @docmd/template-summer resolved to key template-summer instead of summer, which doesn’t exist in the registry.
Now both prefixes are stripped correctly.
Custom/third-party templates were never affected.

Semantic search peer dependency handling

When plugins.search.semantic = true was set and the ML peer dependencies (@huggingface/transformers, onnxruntime-node) weren’t present, the build silently fell back to keyword search with a confusing error buried deep inside docmd-search:

⬢ Semantic indexing failed: Embedding failed: Failed to load model "Xenova/all-MiniLM-L6-v2": Cannot find package '@huggingface/transformers'

Two improvements:

  1. Clear pre-flight check — the plugin now verifies the peer dependencies are resolvable after auto-install and emits an actionable error naming the missing package, instead of letting the failure surface from inside docmd-search.
  2. Stale module cache fix — after auto-installing docmd-search mid-build, the current Node process’s module cache couldn’t see the new packages.
    Indexing now runs in a child process so it starts with a clean module resolution.
    npm install auto-install also passes --foreground-scripts so onnxruntime-node’s postinstall (native binary download) actually runs in CI environments with npm’s allow-scripts security feature enabled.

npm-publish: packages/templates/* now in the publish loop

The npm-publish.yml workflow was iterating packages/*, packages/legacy/*, packages/plugins/*, packages/engines/* but missing packages/templates/*.
Templates like @docmd/template-summer and @docmd/template-rain were not being released.
Now included.

docmd init no longer makes a network call by default

Previously docmd init always fetched SKILL.md from raw.githubusercontent.com/docmd-io/docmd-skills on every run.
The fetched content overrode the bundled default if it succeeded.
Two changes:

  • Default is now local-only — the bundled SKILL.md content ships with the package and is written without any network call.
  • Opt in via --with-skill flag or DOCMD_FETCH_REMOTE_SKILL=1 env var to fetch the latest SKILL.md from the docmd-skills repo (CI environments and offline workstations can keep using the bundled default).
docmd init --with-skill    # fetch latest SKILL.md from docmd-skills
DOCMD_FETCH_REMOTE_SKILL=1 docmd init   # same, via env var

docker-publish: stopped firing on every push to main

The docker-publish.yml workflow had a push: branches: [main] trigger that caused it to run on every commit to main, not just on release events. Removed — it now only fires on v* tag pushes, published releases, and manual dispatch.

Dockerfile: pnpm install made reliable in CI

The production stage was using corepack prepare pnpm@10.33.2 --activate (with explicit version), but the builder stage’s corepack prepare --activate (no version) failed in GitHub Actions CI because it required a package.json in the current working directory.
Both stages now use npm install -g pnpm@10.33.2 directly — simpler, no corepack resolution steps, and works identically in CI and local.
On a clean build, pnpm also refused to remove the existing node_modules directory without interactive confirmation in the non-TTY Docker environment.
The install command now passes --config.confirmModulesPurge=false, which is pnpm’s targeted flag for this case.
The broader ENV CI=true route was considered and rejected because it leaks into every subsequent RUN.

UI translation keys render as literal text in published package

builtWith, copyContext, copiedToClipboard and other template-translated strings rendered as literal key text in built output when using the published @docmd/ui package.
The monorepo worked fine because packages/ui/translations/ existed alongside dist/, but the published package’s files array didn’t include the translations/ directory, so pnpm pack stripped it from the tarball.
Added translations to the files array; loadTranslations now also emits a clear warning when the directory is missing so future regressions don’t surface as silent literal-key output.

@docmd/template-* registry key mismatch

Setting theme.template to summer or @docmd/template-summer printed “Plugin ‘template-summer’ not found in official registry — manual installation required” even though the package was installed.
Root cause: the auto-install registry lookup stripped @docmd/plugin- from the package name to derive the registry key, but didn’t strip @docmd/template-.
So @docmd/template-summer resolved to the key template-summer instead of summer.
Both prefixes are now stripped correctly.
Custom/third-party templates were never affected — they bypass the registry lookup entirely.

Clicking a navigation sidebar child link in an --offline build only toggled the parent’s collapse state instead of opening the target page.
Root cause: the data-spa-enabled flag stayed true for offline builds, so the SPA click handler bound and consumed clicks with preventDefault().
But file:// has no fetch for the SPA path-fetch, so the navigation never happened.
Fixed in two places:

  • data-spa-enabled is now false when isOfflineMode is true, so the SPA handler never binds and the browser does normal full-page navigation.
  • A safety-net check in the main SPA click handler bails out for file:// protocol, so the click isn’t consumed even if the flag is wrong.
docmd build --offline   # SPA now disabled automatically
# Workaround on 0.8.7: open via local server, or set layout.spa: false

--offline mode: comprehensive file:// robustness pass

While fixing #164, audited every other place the client-side JS touches localStorage, fetch, XMLHttpRequest, or relies on SPA/network.
All localStorage writes (sidebar collapse, theme toggle, locale preference, cookie consent) are now wrapped in try/catch so file:// builds don’t throw in Chromium-based browsers where local storage is blocked.
The version switcher’s HEAD fetch check is skipped under file:// (browsers block it anyway), going straight to direct navigation.
XHR in the noStyle i18n loader is wrapped with onerror handling.
URL builder already appended /index.html for offline mode (unchanged).
Result: opening a built site via file:// now works end-to-end with no JS errors, no broken navigation, and no silent feature loss.

📚 Documentation updates

The following docs were updated to reflect the fixes above:

  • Deployment → Docker — Image Details table updated to reflect the root + su-exec model, and the “Custom working directory and file ownership” section no longer recommends the -u $(id -u):$(id -g) workaround (no longer needed).
  • docker/DOCKER.md — Permission Issues section rewritten.
  • All six READMEs (en, de, es, fr, ja, zh) — removed the -u $(id -u):$(id -g) note.

The Docker docs will be extended in a follow-up to explicitly cover init --force, the -y flag, and the non-TTY behaviour.

🔒 Security foundation (Phase 0 — internal)

This release lands the shared security primitives that the rest of the security work in 0.8.8 builds on. These changes are non-breaking and purely additive — no public API change yet.

New safePath home and UserPath brand

safePath(root, relativePath) — the path-traversal guard required by DEVELOPMENT-BENCHMARK.md S1 — has moved from @docmd/api to @docmd/utils so it’s available everywhere without a dependency on the API package. @docmd/api continues to re-export it for backward compatibility.

Alongside it: a new UserPath TypeScript branded type. Any string cast via asUserPath() is marked as user-controlled input; downstream code is expected to resolve it through safePath() before any fs.* call. The brand is a runtime no-op (zero cost) and a type-system signal.

Centralised escape helpers

Four helpers now ship from @docmd/utils:

  • escHtml — HTML text and inline interpolation
  • attrEsc — semantic alias for HTML attribute values
  • jsonInjectJSON.stringify wrapper for <script> injection
  • scriptLiteralJSON.stringify wrapper for inline JS string contexts

Every escape helper is unit-tested with XSS canary payloads (19 assertions). One canonical path means future contributors don’t reinvent .replace(/&/g, '&amp;') patterns.

Two new internal ESLint rules

  • docmd/no-unsafe-fs-read — flags fs.readFile, fs.writeFile, fs.unlinkSync, etc. where the path argument wasn’t resolved through safePath(). Catches potential CWE-22 violations at lint time. Currently warn severity; promoted to error after Phase 1 fixes the surface.
  • docmd/require-verify-client — flags every new WebSocketServer({...}) without a verifyClient callback. Set to error immediately so the two existing dev-server WebSocket instances (the dev server and the workspace dev server) fail CI until they get origin checks. Phase 1 PR 1.D adds the verifyClient callbacks.

The initial lint pass surfaces 141 path-traversal warnings and 2 verify-client errors across the monorepo — those become the Phase 1 cleanup backlog.

New security.html config key (default flipped to escape)

The markdown parser now respects a security.html config key with three values:

Value Behaviour Use case
'escape' (default in 0.8.8) Raw HTML in markdown is HTML-escaped and shown as text Default — safe for user-generated content
'allow' Raw HTML passes through to the rendered output Documentation sites that intentionally use raw HTML like <details> blocks
'strip' Raw HTML is removed from the rendered output entirely Locked-down content (corporate wikis, regulated industries)

Behaviour change: the pre-0.8.8 default was effectively 'allow'. If your docs rely on raw HTML (e.g. <details>, <summary>, embedded <iframe>), add security: { html: 'allow' } to your config to keep the old behaviour. Container blocks (::: callout, ::: tabs, ::: steps, ::: cards, etc.) are unaffected — they’re parsed by custom rules, not raw HTML.

End-to-end coverage lives in scripts/brute-test-security.js (22 assertions across all three policies plus fallback behaviour).

Path-traversal hardening (private disclosure)

Three internal file-access paths were hardened against path traversal. No public API change. See the security advisory for affected versions.

The fixes land in packages/core/src/commands/mcp.ts, packages/api/src/hooks.ts, and packages/plugins/openapi/src/index.ts.
All three go through the centralised safePath helper from @docmd/utils so any future code path is gated by the same primitive.

End-to-end coverage in scripts/brute-test-security.js scenarios S7 through S10 (15 assertions including a deliberate canary file outside the project root and a malicious local-path plugin fixture).

Dev-server hardening (private disclosure)

The dev-server WebSocket handlers and the init command were hardened.
The dev server now binds to 127.0.0.1 by default (use DOCMD_HOST=0.0.0.0 to expose on LAN) and both WebSocketServer instances validate the Origin header against a loopback allowlist before accepting the handshake.
The init command no longer makes any network call on every run — the bundled SKILL.md content is written locally; users install the full agent skill set via npx docmd-skills [dir].

End-to-end coverage in scripts/brute-test-security.js scenarios S11 and S12 (16 assertions including a snapshot of intercepted fetch calls proving the zero-network property).

HTML escape hardening (private disclosure)

User-controllable values interpolated into meta tags and plugin head/body injection points are now escaped via the centralised attrEsc / sanitizeHeadInjection helpers from @docmd/utils.
The og/twitter meta tags, link href schemes, and plugin-generated head HTML all go through the same canonical escape path.
Container blocks (::: callout, ::: tabs, etc.) were already protected by Phase 0’s default html: 'escape' markdown-it policy and require no further change.

End-to-end coverage in scripts/brute-test-security.js scenarios S13 through S15 (13 assertions including an in-project plugin whose generateMetaTags returns <script> and javascript: payloads).

Plugin input validation (private disclosure)

The analytics plugin now format-validates measurementId (must match G-XXXXXXXX) and trackingId (must match UA-XXXXXXX-Y) before injecting the Google Analytics script.
Invalid IDs are skipped with a console warning instead of being interpolated raw.
The PWA plugin now format-validates themeColor against a CSS color regex (#hex, rgb(), hsl(), or a CSS named color) and falls back to the default #1e293b on invalid input.
Both validators use scriptLiteral() / attrEsc() from @docmd/utils for defence-in-depth.

End-to-end coverage in scripts/brute-test-security.js scenarios S16 and S17 (15 assertions: invalid GA4 / UA / themeColor payloads, valid case, default-fallback path).

MCP and bundled SKILL.md are now docmd-skills-aware

docmd-skills is published as a standalone npm package (https://www.npmjs.com/package/docmd-skills). The bundled SKILL.md written by docmd init and the MCP get_skill fallback both now point agents at the single-line install command:

npx docmd-skills ~/.claude/skills   # or ~/.cursor/skills, ./.skills, etc.

The full agent skill set (docmd-skills, docmd-dev, docmd-writer) is installed by that one command. The bundled SKILL.md is sufficient for MCP get_skill consumption; npx docmd-skills is the path to the rich, multi-file skill set for interactive agent use.

🛡️ Security hardening (Phase 1 — 13 CVEs closed)

The Battle Test 0.8.6 audit found 13 CVE-class issues in core + plugins.
All are fixed in 0.8.8.
CI pipelines that gated on docmd <cmd> exit codes were silently passing broken builds before these fixes.

Highlights

  • Cross-Site WebSocket Hijacking (CSWSH) — N-S1: dev-server WebSocket had no verifyClient callback, allowing any origin to open a connection and trigger threads:add-thread file writes.
    New verifyClient factory (packages/core/src/utils/ws-origin-guard.ts) allows loopback by default and any explicit extra host.
    Both WebSocket call sites in dev.ts and workspace.ts now reject non-allowlisted origins with exit code 1.

  • MCP read_doc path traversal — T-S1 / S-3: path.resolve(cwd, route) at packages/core/src/commands/mcp.ts accepted absolute paths like /etc/passwd and ../ escapes.
    Replaced with safePath(cwd, asUserPath(route)) which throws on traversal.
    The safePath primitive lives in @docmd/utils and is the single source of truth for all file-resolution calls.

  • Plugin RCE via local-subfolder — T-S8: require.resolve(name, { paths: resolvePaths }) at packages/api/src/hooks.ts searched parent directories, loading arbitrary npm packages from sibling folders.
    Local-path plugins are now resolved via safePath(projectRoot, asUserPath(name)) which restricts to the project root.

  • Plugin head XSS — T-S7: a plugin returning <script>alert(1)</script> from generateMetaTags injected live script into every page.
    New sanitizeHeadInjection() in @docmd/utils strips <script> / <style> and neutralises javascript: / vbscript: URIs.
    Wired into generator.ts for injectHead / injectBody.

  • OpenAPI / Analytics / PWA / Threads XSS — T-S3 / S-4 / S-5 / S-7: all four plugins now format-validate their untrusted config inputs (GA4 measurementId, UA trackingId, PWA themeColor) and escape via attrEsc() / scriptLiteral() from @docmd/utils before interpolating into the HTML.

  • Init makes a network call to GitHub — T-S5: removed.
    docmd-skills is now a standalone npm package — the bundled SKILL.md is the default; opt into the network call only via the legacy --with-skill flag (kept for back-compat).

  • Dev server binds to 0.0.0.0 by default — T-S6: default flipped to 127.0.0.1.
    The DOCMD_HOST env var opts into a LAN bind.
    The TUI warns when binding to a non-loopback address.

New helpers in @docmd/utils

Function Purpose
escHtml(s) HTML-escape text and <script> blocks
attrEsc(s) HTML-escape attribute values
jsonInject(s) Safely inject into JSON context
scriptLiteral(s) Safely inject into JS string literal
safePath(root, userPath) Path-traversal-safe file resolution
asUserPath(s) Branded UserPath type
normalisePath(s) POSIX path normalisation
sanitizeHeadInjection(html) Strip <script>, <style>, neutralise javascript: / vbscript:

Test coverage

scripts/brute-test-security.js88 assertions across 13 scenarios (S-1, S-3, S-4, S-5, S-6, S-7, S-9, S-10, S-12, S-13, S-15, S-16, S-17) covering every documented CVE path.
All 13 issues are regression-tested at the CLI surface — a CI run catches any regression automatically.

📦 Container parser (Phase 2 — 5 bugs fixed)

The 0.8.6 audit found five silent container-parser bugs (F1–F5) that produced wrong output with no error and no exit-code change.
The parser is now deterministic, brace-aware, and indentation-aware.

What was fixed

  • F1 — nested grids: ::: grids + N× ::: grid + one ::: per card used to dump the whole block as raw <p>::: grids<br>::: grid<br>...<br>:::</p> text.
    The normaliser adds explicit ::: closes so the grids rule matches.
  • F2 — self-closing tag corruption: ::: tag + orphan ::: corrupted the depth counter for every container that followed.
    Self-closing names (button, tag, embed) are honoured; the orphan ::: is removed with a [normaliser] WARNING.
  • F3 — mismatched close types: ::: callout ... ::: card ... ::: silently re-rooted the parser.
    The callout is auto-closed at EOF with a [normaliser] ERROR.
  • F4 — triple close erases content: bare ::: lines leaked into the page as <p>:::</p> paragraphs.
    Orphans are removed with warnings.
  • F5 — 5-level nested callouts collapse: nested callouts rendered only the outer level.
    The normaliser inserts implicit closes for the inner levels so each callout rule matches recursively.

Implementation

New packages/parser/src/utils/container-normaliser.ts — pure TypeScript port of the legacy robust-parser-shim/ (146 lines).
Linear scan, indentation-aware stack, no module-level mutable state.
Wired into packages/parser/src/markdown-processor.ts in BOTH processContent and processContentAsync BEFORE user plugins’ onBeforeParse and BEFORE markdown-it’s render.
Re-runs inside createDepthTrackingContainer BEFORE smartDedent so the inner recursive render sees balanced input.

Determinism contract

The normaliser is a pure function of its input — no Date.now, no Math.random, no module-level mutable state.
Three layers guard the contract:

  1. DeclarativeDETERMINISM AUDIT block in the file header documents the four non-deterministic primitives the module does NOT use.
  2. Empiricalpackages/parser/test/container-normaliser.test.js has 60 assertions including 100-way concurrent parse AND a real node:worker_threads cross-worker parse.
  3. Regression-proofverifyDeterminismAtBoot() in packages/core/src/engine/worker-parser.ts parses a known input at worker init and asserts the output matches a frozen snapshot.
    Any future non-determinism crashes the worker at boot.

🔧 CLI contracts (Phase 3 — 7 bugs fixed)

Five documented CLI failure paths silently exited 0; two validate paths returned wrong results.
All seven are fixed.

What was fixed

Command Failure case Pre-fix exit Post-fix exit
docmd build Unknown plugin in config 0 1
docmd migrate No --docusaurus/etc. flag 0 1
docmd migrate --help (it’s a help print) 0 0 (unchanged)
docmd remove <nonexistent> Plugin not in registry 0 1
docmd validate --json Broken links 0 1
docmd add <plugin> on TS/MJS/CJS Config file untouched 0 1 + fixed
docmd remove <plugin> Plugin entry left in config 0 1 + fixed
docmd build (workspace) Raw stack trace, exit 0 0 1 + clean TUI
docmd init example Taught F2 trap pattern OK cleaned up
docmd validate (trailing slash) False-positive on /page/ 1 0

Implementation

  • New packages/core/src/utils/exit.ts exports exitCodeFor(err), exitWithError(err, opts?), failWith(message, opts?) — the single source of truth for the exit-code contract.
  • New getPluginLoadErrors() in @docmd/api tracks plugin LOAD failures separately from RUNTIME hook errors.
    build.ts checks it after loadPlugins() and throws, which the existing catch block turns into exit 1.
  • New packages/plugins/installer/src/config-editor.ts — a brace-balanced scanner that handles all five supported config formats (docmd.config.{json,js,mjs,cjs,ts}) uniformly.
    The legacy regex-based injector only matched module.exports = {...} and silently no-op’d for export default defineConfig({...}).
    resolveConfigPath() was checking only .json/.js and would have silently scaffolded a new file next to a project’s .ts.

Test coverage

scripts/brute-test.js22 new assertions across three Phase 3 sections (test 26: exit codes, test 27: plugin add/remove, test 29: validate + workspace + init).
All under tests/cli-contracts/.

🤖 OKF plugin (0.8.8 — new core plugin)

A new core plugin generates an Open Knowledge Format (Google’s OKF spec) bundle for AI-agent consumption.
The bundle sits at site/okf/ and contains a typed manifest (okf.yaml), an interactive graph viewer, and one markdown file per page.

What you get out of the box

site/okf/
├── okf.yaml              ← typed manifest (bundle summary)
├── index.md              ← Karpathy-style catalog grouped by type
├── graph.html            ← interactive force-directed viewer
├── graph.json            ← graph data (nodes + edges)
├── graph.js              ← viewer runtime (vanilla, no CDN deps)
├── graph.css             ← viewer styles (theme-aware)
├── concepts/<slug>.md    ← one markdown file per page
└── _meta/
    ├── bundle.json       ← JSON mirror of okf.yaml
    └── lint-report.txt   ← warnings produced during generation

Default behaviour (0.8.8)

  • Default-enabled — OKF is a core plugin like llms and seo.
    The build auto-loads it and generates the bundle even when the user hasn’t added plugins.okf to their config.
    Three opt-out paths are supported (false, enabled: false, or capability filter).
  • Single-locale by default — the bundle contains pages in the default locale only.
    The default locale’s files sit at the bundle root (no en/ subdirectory).
  • Type inference — pages under /api/, /guides/, etc. are auto-classified; everything else falls back to concept.

Multi-locale opt-in

{
  "plugins": {
    "okf":  { "localeStrategy": "folders" },
    "llms": { "i18n": true }
  }
}

With both flags set, the default-locale files stay at the bundle root (so existing consumers don’t break) and non-default locales get a <locale>/ subdirectory (OKF) or .<locale> suffix (LLMS):

site/okf/okf.yamldefault locale
site/okf/concepts/...mddefault locale concepts
site/okf/ja/okf.yaml     ← Japanese
site/okf/ja/concepts/...md

site/llms.txtdefault locale (en)
site/llms.ja.txt         ← Japanese

Test coverage

packages/plugins/okf/tests/index.test.ts (6 tests) + packages/plugins/okf/tests/brute.test.ts (15 tests) including 12 real-fs scenarios.
21/21 pass.
New test exercises the default-enabled contract (a project with no plugins.okf entry still gets a bundle generated).

🧪 Test infrastructure (categorised)

The brute tests used to be 1000+ lines in two scripts/ files with duplicate test numbers (Phase 3 PR 3.A reused 26, 27, 29).
They are now categorised under tests/:

tests/
├── shared.js                                ← fixture helpers
├── runner.js                                ← orchestrator with TUI sections
├── cli-contracts/
│   ├── exit-codes.test.js                   ← F6, M-12
│   ├── plugin-add-remove.test.js            ← F7, M-3
│   └── validate-workspace.test.js           ← M-1, F8, F9
└── (per-package tests stay in place under packages/*/test/)

The runner is invoked by pnpm prep (or directly via node tests/runner.js) and runs each suite in its own TUI section.
Filter by --only=exit-codes,container-normaliser to run just the named suites.

Result

342 assertions across 9 categorised files, all green (with per-package unit tests adding 89 more across 4 packages, totalling 431 assertions / 13 files):

Suite Result Coverage
cli-contracts/exit-codes 7 / 7 F6, M-12
cli-contracts/plugin-add-remove 9 / 9 F7, M-3 (TS, MJS, JSON, CJS, JS)
cli-contracts/validate-workspace 6 / 6 M-1, F8, F9
packages/parser/test/container-normaliser 60 / 60 F1–F5 + determinism
packages/utils/test/{path,html-escape} 31 / 31 safePath, escHtml, etc.
scripts/brute-test-security 88 / 88 Phase 1 CVE suite (13 scenarios)
scripts/brute-test 114 / 114 29 feature-integration scenarios
packages/plugins/okf 23 / 23 Unit + 12 real-fs scenarios + graph opt-in/deprecation
packages/plugins/llms 6 / 6 Default-locale + i18n opt-in (NEW)

Updates — pnpm prep pipeline hardened

The release pipeline gained three operational improvements:

  • Per-package unit tests now participate in the gate. pnpm -r run test --if-present runs after tests/runner.js, so a regression in @docmd/parser, @docmd/utils, @docmd/plugin-okf, or @docmd/plugin-mermaid fails the release pipeline the same way a categorised-runner regression does. No package-level test suite can drift silently anymore.
  • --verbose / --full flag. Default output stays collapsed (each test step is one line) so a clean run is readable at a glance. Pass --verbose to stream the raw test output inline — useful when iterating on a test that just started failing and you want the assertion text in context.
  • Trailing Summary block. On a clean run, every section contributes a stat (lint counts, package totals, test counts, Docker version) that renders as one green block at the end. On failure, the same slot becomes a red Issues block listing every failure with file:line detail and capped overflow. Single place to look for the verdict.

🔌 Plugin author note

Both @docmd/plugin-llms and @docmd/plugin-okf are default-enabled core plugins in 0.8.8.
The build auto-loads them.
To opt out, use the enabled: false shape that already works for every other core plugin:

{
  "plugins": {
    "okf":  { "enabled": false },
    "llms": { "enabled": false }
  }
}

Looking ahead — Node 20+ in v0.9.0

The next minor release raises the floor to Node.js 20+ (from 18+), bringing the engine requirement, shipped Docker runtime, and CI environment all onto Node 22.
Node 18 reached end-of-life in April 2025; this keeps docmd on a supported runtime with room to spare.

This release (0.8.8) is unaffected — the >=18 floor stays until 0.9.0 ships.

Now (0.8.x) Next (0.9.0)
Minimum Node 18+ 20+
Docker base 20-alpine 20-alpine
Type Node ^24 ^24
CI target 24 24

Affected versions

All fixes in this release target 0.8.7. There are no breaking changes — every fix is additive or pure bug correction.