---
phase: 15-ci-skip-api-harness-jobs-for-doc-only-prs
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
  - .markdownlint-cli2.jsonc
  - package.json
  - pnpm-lock.yaml
  - .gitea/workflows/ci.yml
  - README.md
  - apps/api/README.md
  - apps/pwa/README.md
  - apps/pwa/e2e/README.md
  - docs/API.md
  - docs/ARCHITECTURE.md
  - docs/DEVELOPMENT.md
  - docs/GETTING-STARTED.md
autonomous: true
requirements: []
must_haves:
  truths:
    - "pnpm md:lint exits 0 on the current repo (13 baseline violations fixed)"
    - "Introducing a bare fenced code block (MD040) makes pnpm md:lint exit non-zero"
    - "The fast-checks CI job runs a Markdown lint step between Format check and Typecheck"
    - "pnpm format:check still exits 0 after the MD031 blank-line fixes (no Prettier conflict)"
    - "markdownlint never scans .planning/** (excluded by config ignores)"
  artifacts:
    - path: ".markdownlint-cli2.jsonc"
      provides: "markdownlint-cli2 config (extends prettier preset; content rules; globs + ignores)"
      contains: "markdownlint/style/prettier"
    - path: "package.json"
      provides: "md:lint script + markdownlint-cli2 devDependency"
      contains: "md:lint"
    - path: ".gitea/workflows/ci.yml"
      provides: "Markdown lint step in fast-checks job"
      contains: "pnpm md:lint"
  key_links:
    - from: ".gitea/workflows/ci.yml"
      to: "package.json md:lint script"
      via: "fast-checks step runs pnpm md:lint"
      pattern: "pnpm md:lint"
    - from: "package.json md:lint script"
      to: ".markdownlint-cli2.jsonc"
      via: "markdownlint-cli2 auto-discovers root config (globs + ignores)"
      pattern: "markdownlint-cli2"
---

<objective>
Add a real markdown lint gate to the existing `fast-checks` CI job: install `markdownlint-cli2`, create the root `.markdownlint-cli2.jsonc` config (Prettier-compatible preset + content rules + scoped globs that exclude `.planning/**`), add a root `md:lint` script, wire a "Markdown lint" step into `fast-checks`, and fix the 13 baseline violations so the gate starts GREEN.

This delivers Success Criterion 4: `fast-checks` runs markdownlint over docs; an introduced violation fails the gate; the existing baseline passes.

Purpose: Docs get a fast but real format+lint gate without a separate CI job, scoped so churny `.planning/**` bookkeeping is never linted.
Output: `.markdownlint-cli2.jsonc`, updated `package.json` (script + devDep), one new `fast-checks` step, 7 doc files fixed (13 violations), green `pnpm md:lint` + `pnpm format:check`.
</objective>

<execution_context>
@$HOME/.claude/gsd-core/workflows/execute-plan.md
@$HOME/.claude/gsd-core/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md
@.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md
</context>

<tasks>

<task type="execute">
  <name>Task 1: Install markdownlint-cli2, add md:lint script, create .markdownlint-cli2.jsonc</name>
  <read_first>
    - package.json (root — current scripts block lines 6-16 and devDependencies lines 17-25; mirror the existing `format:check` script + `prettier` devDep placement)
    - .prettierrc (root JSON tool-config structural analog: printWidth 100 — the reason MD013 must stay disabled)
    - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md (Pattern 5 + "Code Examples" → the exact `.markdownlint-cli2.jsonc` body and rule-decision table; Pitfall 5 `#` vs `!` negation; Pitfall 6 .pnpm-store)
    - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("`.markdownlint-cli2.jsonc` (new file)" and "package.json (root)" sections — exact insertion points)
  </read_first>
  <action>
    Install `markdownlint-cli2` at version `0.22.1` as a root-workspace devDependency: run `pnpm add -D markdownlint-cli2@0.22.1 --workspace-root` (this updates root package.json devDependencies and pnpm-lock.yaml). Add a root script `"md:lint": "markdownlint-cli2"` immediately after the existing `"format:check"` script — NO glob args on the CLI; globs/ignores live in the config file so `pnpm md:lint` and a bare `npx markdownlint-cli2` resolve identically (RESEARCH Open Question 3 resolved).

    Create `.markdownlint-cli2.jsonc` at the repo root using the exact body from RESEARCH "Code Examples → .markdownlint-cli2.jsonc" / PATTERNS "`.markdownlint-cli2.jsonc` (new file)". Required keys: `config.extends` = `"markdownlint/style/prettier"`; ENABLE `MD001`, `MD024`, `MD040`, `MD031`, `MD051`, `MD052` (all `true`); DISABLE `MD041`, `MD034`, `MD036` (all `false`) — rationale per the rule-decision table (MD041 because CLAUDE.md starts with `## Project`; MD034 because the version table uses `pkg@version` syntax; MD036 because docs/API.md uses bold response labels). Set `globs` to exactly `["docs/**/*.md", "*.md", "apps/**/*.md"]` and `ignores` to exactly `[".planning/**", "node_modules/**", "**/node_modules/**", ".pnpm-store/**"]`. Do NOT add MD013 (line-length) — it is disabled by the prettier preset and re-enabling it produces 700+ false violations against `.prettierrc` printWidth 100.

    Do NOT run md:lint to green yet — the 13 baseline violations are fixed in Task 2; this task may leave `pnpm md:lint` red.
  </action>
  <verify>
    <automated>test -f .markdownlint-cli2.jsonc && node -e "const p=require('./package.json'); if(!p.scripts['md:lint']) process.exit(1); if(!p.devDependencies['markdownlint-cli2']) process.exit(1); console.log('script+dep present')" && grep -q 'markdownlint/style/prettier' .markdownlint-cli2.jsonc && grep -q '.planning/' .markdownlint-cli2.jsonc && echo OK</automated>
  </verify>
  <acceptance_criteria>
    - package.json `scripts` contains `"md:lint": "markdownlint-cli2"` (no glob args)
    - package.json `devDependencies` contains `markdownlint-cli2` at `0.22.1`; pnpm-lock.yaml updated
    - `.markdownlint-cli2.jsonc` exists at repo root and `config.extends` is `"markdownlint/style/prettier"`
    - `.markdownlint-cli2.jsonc` enables MD001/MD024/MD040/MD031/MD051/MD052 and disables MD041/MD034/MD036
    - `.markdownlint-cli2.jsonc` `ignores` array includes `".planning/**"`, `"node_modules/**"`, `"**/node_modules/**"`, `".pnpm-store/**"`
    - `.markdownlint-cli2.jsonc` `globs` array is exactly `["docs/**/*.md", "*.md", "apps/**/*.md"]`
  </acceptance_criteria>
  <done>markdownlint-cli2@0.22.1 is a root devDep, `pnpm md:lint` is wired to the root config, and the config scopes the lint to docs/repo-root/app markdown while excluding `.planning/**`. md:lint may still be red (fixed next task).</done>
</task>

<task type="execute">
  <name>Task 2: Fix the 13 baseline markdown violations; green md:lint + format:check; wire the fast-checks step</name>
  <read_first>
    - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-RESEARCH.md ("Baseline Violation Assessment" table — exact file/rule/count; Pitfall 4 — verify MD031 fix does not break Prettier)
    - .planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-PATTERNS.md ("Docs content fixes" table + MD040/MD031 fix patterns; "Markdown lint step in fast-checks" insertion slot)
    - .gitea/workflows/ci.yml (fast-checks job lines 8-38 — confirm the real step names: "Format check" line 31-32, "Typecheck" line 34; insert the new step between them)
    - README.md (lines ~78, ~119 — the 2 bare fences)
    - docs/API.md (lines ~517, ~544), docs/ARCHITECTURE.md (lines ~64, ~171), docs/DEVELOPMENT.md (lines ~9, ~22) — 2 bare fences each
    - docs/GETTING-STARTED.md (lines ~52, ~54 — the MD031 fence pair needing blank lines)
    - apps/api/README.md (line ~19), apps/pwa/README.md (line ~45), apps/pwa/e2e/README.md (line ~76) — 1 bare fence each
  </read_first>
  <action>
    Fix all 13 violations confirmed at these exact locations (run `pnpm md:lint` first to re-confirm line numbers, then fix):
    - MD040 (11 bare fences — add a language tag matching the fence content; use `bash` for shell, `text` for plain output, `json`/`yaml`/`ts` as appropriate): apps/api/README.md:19, apps/pwa/e2e/README.md:76, apps/pwa/README.md:45, docs/API.md:517, docs/API.md:544, docs/ARCHITECTURE.md:64, docs/ARCHITECTURE.md:171, docs/DEVELOPMENT.md:9, docs/DEVELOPMENT.md:22, README.md:78, README.md:119.
    - MD031 (1 fence pair — add a blank line before the opening ```` ```bash ```` and after the closing ```` ``` ````): docs/GETTING-STARTED.md:52 and :54.

    Only change what each rule requires — add a language token after the opening backticks (MD040) or add surrounding blank lines (MD031); do NOT rewrite fence bodies or restructure docs. Do not "fix" any rule that the config disables (MD013/MD034/MD036/MD041) — those are intentionally off.

    After fixes, add the new step to the `fast-checks` job in ci.yml between the "Format check" step and the "Typecheck" step — a named step `Markdown lint` running `pnpm md:lint` (mirror the one-line `Format check` step pattern exactly). This is the only ci.yml change in this plan; do not touch the api/harness/changes/gate jobs (Plan 02 owns those).

    Then run BOTH `pnpm md:lint` (must exit 0) AND `pnpm format:check` (must exit 0) — per RESEARCH Pitfall 4, the MD031 blank-line additions must not introduce a Prettier conflict on docs/GETTING-STARTED.md. If format:check newly fails on a file you touched, run `pnpm format` on that file and re-confirm md:lint is still 0.
  </action>
  <verify>
    <automated>pnpm md:lint && pnpm format:check && grep -q 'pnpm md:lint' .gitea/workflows/ci.yml && echo OK</automated>
  </verify>
  <acceptance_criteria>
    - `pnpm md:lint` exits 0 (13 baseline violations resolved)
    - `pnpm format:check` exits 0 (no Prettier conflict introduced by the MD031 fix)
    - `.gitea/workflows/ci.yml` fast-checks job contains a step named `Markdown lint` running `pnpm md:lint`, positioned after the `Format check` step and before the `Typecheck` step
    - Re-running md:lint after temporarily inserting a bare ```` ``` ```` fence into any in-scope .md (then reverting) exits non-zero (gate can fail) — proven during execution, not left in the tree
    - No api/harness/changes/gate job was modified by this plan
  </acceptance_criteria>
  <done>The markdown baseline is clean, `fast-checks` runs `pnpm md:lint`, the gate can fail on a real violation, and Prettier and markdownlint do not conflict. SC-4 satisfied.</done>
</task>

</tasks>

<artifacts_produced>
This phase introduces the following new symbols (Plan 01 portion). The plan-review source-grounding pass must treat these as newly-created, not drift:

- `.markdownlint-cli2.jsonc` — new root config file
- `md:lint` — new root `package.json` script
- `markdownlint-cli2` — new root devDependency (0.22.1)
- `Markdown lint` — new step name in the `fast-checks` job
</artifacts_produced>

<threat_model>
## Trust Boundaries

| Boundary | Description |
|----------|-------------|
| npm registry → repo devDependencies | `markdownlint-cli2` is fetched and runs in CI and on developer machines |
| markdown content → lint tool | doc files are the input; markdownlint only reads, never executes content |

## STRIDE Threat Register

| Threat ID | Category | Component | Disposition | Mitigation Plan |
|-----------|----------|-----------|-------------|-----------------|
| T-15-01 | Tampering (supply chain) | new `markdownlint-cli2` devDependency | mitigate | Pin to exact `0.22.1` (no `^`/`~`); RESEARCH Package Legitimacy Audit verdict OK (~4 yrs, ~3M/wk, DavidAnson/markdownlint-cli2). Audit table present in RESEARCH; no `[ASSUMED]`/`[SUS]` packages so no blocking-human checkpoint required. |
| T-15-02 | Denial of Service (gate noise) | lint glob accidentally scanning `.planning/**` or `node_modules` | accept→mitigate | `ignores` excludes `.planning/**`, `node_modules/**`, `**/node_modules/**`, `.pnpm-store/**`; verified the glob lints exactly 12 in-scope files, not bookkeeping churn. |
| T-15-03 | Tampering (false-green) | a disabled content rule silently hides a real doc defect | accept | Rule-disable decisions (MD034/MD036/MD041) are scoped to non-author-controlled patterns documented in RESEARCH; MD040/MD031/MD001/MD024/MD051/MD052 stay enabled to catch broken fences/links. |
</threat_model>

<verification>
- `pnpm md:lint` exits 0 on the clean tree; exits non-zero when a bare fence is introduced.
- `pnpm format:check` exits 0 (Prettier/markdownlint do not conflict).
- `.gitea/workflows/ci.yml` fast-checks job runs `pnpm md:lint` between Format check and Typecheck.
- markdownlint scans 12 in-scope files and never touches `.planning/**` (confirm "Finding:" line lists the negated ignores).
</verification>

<success_criteria>
Maps to Phase 15 Success Criterion 4: `fast-checks` runs markdownlint-cli2 over the docs glob; an introduced markdown-lint violation fails the gate; the existing markdown baseline passes (13 violations fixed, rules configured) so the gate starts green.
</success_criteria>

<output>
Create `.planning/phases/15-ci-skip-api-harness-jobs-for-doc-only-prs/15-01-SUMMARY.md` when done.
</output>
