Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/workflows/validate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -58,3 +58,16 @@ jobs:

- name: Stamp templates and verify the module works
run: ./scripts/smoke_test_powershell_module.ps1

smoke-living-docs:
name: Smoke-test living-docs add-on (pwsh on Linux)
runs-on: ubuntu-latest
defaults:
run:
shell: pwsh
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Stamp the add-on and verify check-docs enforces
run: ./scripts/smoke_test_living_docs.ps1
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ This repo **dogfoods its own standard** (Core + Public tier).
| The standard itself (tiers, where-things-live, do/don't) | `plugins/repokit/skills/repo-standard/standard/the-standard.md` |
| Commit / changelog / version conventions | `plugins/repokit/skills/repo-standard/standard/commit-conventions.md` |
| Pre-commit / pre-PR checklists | `plugins/repokit/skills/repo-standard/standard/pre-commit-checklist.md`, `pre-pr-checklist.md` |
| Doc formatting & the living-docs pattern | `plugins/repokit/skills/repo-standard/standard/doc-style.md`, `living-docs.md` |
| What to test per repo type | `plugins/repokit/skills/repo-standard/standard/testing-matrix.md` |
| The scaffolding methodology (`/new-repo`) | `plugins/repokit/skills/new-repo/SKILL.md` |
| Templates stamped into new repos | `plugins/repokit/skills/new-repo/templates/` |
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- A **living-docs add-on** for `/new-repo` (opt-in, orthogonal to type and tier): volatile shared
facts live once in `docs/STATE.json`, `README.md`/`docs/**/*.md` render them via marker-delimited
state blocks, and a cross-platform `scripts/check-docs.ps1` (pwsh 7) enforces consistency —
blocks match the state file, no stale/future `as_of` dates, no superseded-content markers in the
runbook, one table-separator style — locally and in a Core-tier `docs.yml` workflow. Stamps a
current-state-only `docs/RUNBOOK.md` skeleton and adds living-docs ground rules to the scaffolded
`AGENTS.md` (ADR-0006).
- Two new `repo-standard` docs: `standard/living-docs.md` (the pattern, plus an
adopt-in-an-existing-repo recipe) and `standard/doc-style.md` (deterministic doc formatting —
one table style, ISO dates, fixed status words — so many sessions/models write like one author).
The pre-commit / pre-PR checklists gain matching doc-consistency steps.
- A smoke test for the add-on (`scripts/smoke_test_living_docs.ps1`): stamps the templates,
renders the state blocks, and proves the check *fails* on each enforced problem class (drifted
block, stale date, superseded marker, malformed table separator). CI runs it on `ubuntu-latest`.

- A cross-platform smoke test for the `powershell-module` templates
(`scripts/smoke_test_powershell_module.ps1`): stamps the core-tier templates with dummy values
and verifies the result parses (`Test-ModuleManifest`), imports, lints clean (PSScriptAnalyzer),
Expand Down
65 changes: 65 additions & 0 deletions docs/adr/0006-living-docs-state-file.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# ADR-0006: Living docs — a state file, rendered blocks, and a deterministic check

- **Status:** accepted
- **Date:** 2026-07-13

## Context

A RepoKit-scaffolded ops repo (44 commits in) showed what happens to docs that track live
operational state when the only defence is prose rules. Its `AGENTS.md` explicitly mandated
"update the README in the same commit as any state change" — yet the runbook was touched in 31
commits and the README in 18, with 15 runbook-only commits. The 1,271-line runbook accreted
history instead of state: three policy versions coexisting under "superseded by v2" annotations,
a "checked <date>, due later today" note contradicting a "PASSED" record three days younger, and
the same counts appearing with different values in different places (43,228 vs 43,229; 42 vs 43).
Two table styles alternated through the file, and it was wholesale-restructured three times in a
week. The symptoms were worse under some models than others — a rule that depends on per-session
model judgment doesn't hold.

RepoKit had nothing to prevent any of this: no runbook concept, no doc-sync rule, no doc
formatting standard, and no doc-consistency step in either checklist.

Rejected alternatives:

- **A hand-maintained standalone HTML runbook** (with a design system): the failures are content
problems, not rendering problems — they would exist identically in HTML, but harder to diff,
review, and edit reliably; formatting drift would move from table styles to diverging markup.
- **Generated HTML from markdown/state**: solves nothing the state file doesn't already solve,
and adds a render pipeline to every stamped repo.
- **Prose-only strengthening of the checklists**: that is the mechanism that already failed.
- **`STATE.yml`**: pwsh 7 has no built-in YAML parser; YAML would force a module dependency (or a
hand-rolled parser) into every stamped repo. JSON parses natively in pwsh, Python, node, `jq`.

## Decision

1. **Volatile shared facts live once, in `docs/STATE.json`** — statuses, live resources, counts,
sizes, each with an `as_of` date. Values are strings, so rendering is byte-deterministic.
2. **Docs render facts via marker-delimited state blocks** (`<!-- state:begin keys=... -->` /
`<!-- state:end -->`) in `README.md` and `docs/**/*.md`, rewritten only by
`scripts/check-docs.ps1 -Update` — never by hand.
3. **A deterministic pwsh 7 check script enforces it** (per ADR-0005's cross-platform pwsh rule
for stamped executable content): blocks match the state file, no stale or future `as_of`
(default 14 days, per-fact override, `0` = never stale), no superseded-content markers in the
runbook, one table-separator style. A `docs.yml` workflow runs it in CI.
4. **The runbook is current-state-only**: replaced content is deleted (git keeps history), dated
journal prose goes to `CHANGELOG.md`, and formatting follows the new `doc-style.md` (which
applies to all repos, not just living-docs ones).
5. **Packaged as an opt-in `/new-repo` add-on** (`templates/addons/living-docs/core/**`) — a
third, orthogonal axis beside type and tier, because operational state is independent of both.
Add-ons only add files, so the type-x-tier collision rules are untouched. Deliberate
exception: the add-on ships its CI workflow at the **core** tier even though CI is normally
Public-tier ceremony — deterministic enforcement is the add-on's entire point, especially in
private repos, where the motivating failure happened.

## Consequences

- Cross-file drift becomes structurally impossible for facts in the state file: they render from
one source, and CI fails when a block is out of date, a fact goes stale, or superseded prose
survives — independent of which model or session made the change.
- Stamped living-docs repos carry a small tooling footprint (one script, one workflow, one JSON
file) and require pwsh 7 to refresh blocks; the staleness check deliberately fails CI until a
human re-confirms an aged fact — a forcing function, not a bug.
- Facts not lifted into `STATE.json` are still unguarded; the checklists and `living-docs.md`
push toward lifting anything two docs both state.
- `scripts/smoke_test_living_docs.ps1` proves the enforcement in this repo's CI, negative tests
included (drifted block, stale date, superseded marker, malformed separator must each fail).
16 changes: 15 additions & 1 deletion plugins/repokit/skills/new-repo/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,10 @@ Gather these from the user's arguments / message, else ask — keep it to load-b
- **visibility** — `private` (= Core tier), `public` (= +Public), or `published` (= +Published).
- **author** — default `Peter Braun` (`PBNZ`).
- **license** — default `Apache-2.0`.
- **living-docs add-on** — yes/no, default **no**. Ask: *"Will this repo's docs track live
operational state (deployed resources, scheduled jobs, long-running migrations)?"* If yes, the
add-on stamps `docs/RUNBOOK.md`, `docs/STATE.json`, `scripts/check-docs.ps1`, and a `docs.yml`
check workflow (see `references/living-docs-rules.md`).
- For `powershell-module` only: **ModuleName** (PascalCase, e.g. `MyModule`).

If the chosen type is a **stub** (anything other than `powershell-module`, `docker-compose`, or `power-platform-connectors`), tell the user so: the
Expand All @@ -42,7 +46,9 @@ want to continue.
1. **Resolve the file set** (see `references/file-set-resolution.md`).
`active_tiers = [core] + ([public] if visibility in {public, published}) + ([published] if visibility == published)`.
The file set is the union, over each active tier `t`, of everything under `templates/<t>/` and
everything under `templates/types/<type>/<t>/`. On a path collision pick one winner: **higher
everything under `templates/types/<type>/<t>/` — plus, for each chosen add-on, everything
under `templates/addons/<addon>/<t>/` (add-ons only add files; they never collide). On a path
collision pick one winner: **higher
tier wins (`published` > `public` > `core`), then within a tier the type overlay wins**.
Produce an explicit list of `(template path → target path)` pairs: drop any trailing `.tmpl`,
and substitute filename placeholders (e.g. `{{ModuleName}}.psd1.tmpl` → `MyModule.psd1`).
Expand All @@ -62,6 +68,14 @@ want to continue.
into the `{{START_HERE_MAP}}` placeholder in the stamped `AGENTS.md`. Add a one-line pointer in
the README.

**Resolve `{{LIVING_DOCS_RULES}}`** in the stamped `AGENTS.md`: with the living-docs add-on
on, substitute the verbatim rules block from `references/living-docs-rules.md` and append that
file's `## Status` snippet to the stamped `README.md`; with the add-on off, delete the
placeholder line entirely. When the add-on is on, finish by running
`pwsh scripts/check-docs.ps1 -Update` then `pwsh scripts/check-docs.ps1` inside the new repo
(both must succeed); if pwsh 7 is missing on this host, say so in the summary — running it is
the user's first task.

**Self-check (gate — both must pass before you continue).** See `references/placeholders.md`:
(a) no enumerated placeholder tokens remain anywhere in the output; (b) every expected target
file exists and no `.tmpl` suffix survived. If either fails, fix and re-check.
Expand Down
17 changes: 17 additions & 0 deletions plugins/repokit/skills/new-repo/references/file-set-resolution.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,23 @@ So visibility decides **how many tiers** turn on; the type contributes an overla
active tier**. A `private` repo gets only `core` (base + type-core). A `published` repo gets all
three base tiers plus all three type-overlay tiers.

## Add-ons — orthogonal to type and tier

An **add-on** is an opt-in overlay chosen in the interview (currently only `living-docs`). For
each chosen add-on:

```
file_set ∪= templates/addons/<addon>/<t>/** for each active tier t
```

Add-ons **only add files** — an add-on template must never produce a target path that a base or
type template also produces, so add-ons need no precedence rule. (If you ever author one that
collides, that's a bug in the add-on, not a resolution question.)

Deliberate exception to "CI ships at the Public tier": the `living-docs` add-on ships its
`docs.yml` check workflow at **core**, because deterministic doc-consistency enforcement is the
add-on's entire point — including, especially, in a private repo (see ADR-0006).

## Precedence — higher tier wins, then type overlay wins

If the same target path is produced by more than one template, pick a single winner (you never
Expand Down
40 changes: 40 additions & 0 deletions plugins/repokit/skills/new-repo/references/living-docs-rules.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# living-docs add-on — AGENTS.md rules block and README snippet

Used by `/new-repo` when the **living-docs add-on** is selected. Two verbatim snippets:

1. Substitute the rules block below into the `{{LIVING_DOCS_RULES}}` placeholder in the stamped
`AGENTS.md`. When the add-on is **not** selected, delete the placeholder line entirely.
2. Append the `## Status` snippet to the stamped `README.md` (before any final "see AGENTS.md"
pointer), so the README and the runbook dashboard render from the same facts.

The same wording is what an existing repo adds when adopting the pattern by hand — see the
`repo-standard` skill's `standard/living-docs.md`, "Adopting in an existing repo".

## The `{{LIVING_DOCS_RULES}}` block

```markdown
- **Docs move together (living docs).** `docs/STATE.json` is the single source for volatile
shared facts (statuses, live resources, counts, as-of dates). A commit that changes anything a
doc states updates `docs/STATE.json` in the same commit; run
`pwsh scripts/check-docs.ps1 -Update` and include the re-rendered blocks. CI (`docs.yml`)
fails otherwise.
- **`docs/RUNBOOK.md` is current-state-only.** Replace outdated text instead of annotating it —
git keeps the history. Dated journal entries go to `CHANGELOG.md`, never the runbook. See the
`repo-standard` skill: `standard/living-docs.md` and `standard/doc-style.md`.
```

## The README `## Status` snippet

````markdown
## Status

<!-- state:begin keys=overall_status,live_resources -->
<!-- state:end -->
````

## After stamping

Run `pwsh scripts/check-docs.ps1 -Update` inside the new repo to render the empty state blocks,
then the plain check — it must pass before the scaffold commit. If pwsh 7 is not available on the
scaffolding host, leave the blocks empty and tell the user in the summary that running it is
their first task.
4 changes: 3 additions & 1 deletion plugins/repokit/skills/new-repo/references/placeholders.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,10 @@
| Token | How to compute |
|-------|----------------|
| `{{year}}` | the current year |
| `{{today}}` | the current date, `YYYY-MM-DD` |
| `{{Guid}}` | a **fresh** GUID per repo — use whatever the host offers: `uuidgen` (Linux/macOS), `[guid]::NewGuid()` (pwsh), or `python3 -c "import uuid; print(uuid.uuid4())"`. Never reuse a literal GUID; a hardcoded one would collide across every scaffolded module. |
| `{{START_HERE_MAP}}` | the where-things-live table you build in step 4 from the resolved file set |
| `{{LIVING_DOCS_RULES}}` | living-docs add-on **on**: the verbatim rules block from `references/living-docs-rules.md`; add-on **off**: delete the placeholder line entirely |

## Post-scaffold self-check

Expand All @@ -33,7 +35,7 @@ Do **not** grep for a bare `{{` — GitHub Actions expressions like `${{ secrets
workflow templates are legitimate and must not be flagged. Grep only for the known tokens:

```
rg -n '\{\{(name|description|author|year|license|type|tier|ModuleName|Guid|START_HERE_MAP)\}\}' <new-repo-dir>
rg -n '\{\{(name|description|author|year|today|license|type|tier|ModuleName|Guid|START_HERE_MAP|LIVING_DOCS_RULES)\}\}' <new-repo-dir>
```

Any hit is a failure — report the file and line, fix the substitution, and re-run.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: Docs

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:

jobs:
check-docs:
name: Living-docs consistency check
runs-on: ubuntu-latest
defaults:
run:
shell: pwsh
steps:
- name: Check out repository
uses: actions/checkout@v7

- name: Run check-docs
run: ./scripts/check-docs.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Runbook — {{name}}

> **Rules for this file** (living docs — see the `repo-standard` skill's `standard/living-docs.md`):
> current state only — when something changes, replace the old text; git keeps the history.
> Volatile facts come from `docs/STATE.json` via state blocks — edit the JSON, then run
> `pwsh scripts/check-docs.ps1 -Update`; never edit between the markers by hand. Dated journal
> entries go to `CHANGELOG.md`, not here. Formatting per `standard/doc-style.md`: one table
> style, `YYYY-MM-DD` dates, fixed status words (`PLANNED / IN PROGRESS / BLOCKED / DONE`,
> `OK / DEGRADED / DOWN`).

## Dashboard

<!-- state:begin keys=overall_status,live_resources -->
<!-- state:end -->

## Example area — current state

<!-- Copy this section once per operational area (a service, a pipeline, a migration wave).
Describe how it works NOW and how to operate it. Prefer a state block over a hand-written
date; at most one as-of date per section. Delete this example section once you have a
real one. -->

## Reference

<!-- Stable commands, queries, and links that rarely change. -->
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"stale_after_days": 14,
"facts": {
"overall_status": {
"value": "PLANNED",
"as_of": "{{today}}",
"note": "Overall project status"
},
"live_resources": {
"value": "none yet",
"as_of": "{{today}}",
"note": "Deployed resources",
"stale_after_days": 0
}
}
}
Loading