`ARCHITECTURE.md`) or improves an existing one (e.g. tightens
`RELIABILITY.md`), existing projects never notice. Only newly-bootstrapped
projects benefit.
2.**No refresh command.** A user who hears "SF has a better template now"
has no way to pull it.
3.**Pending content is frozen.** A file that is still the verbatim scaffold
stub — neither the user nor any agent has touched it — is treated the same
as a fully customized doc. SF refuses to refresh it because it cannot tell
the two apart.
4.**Only one file is versioned.**`PREFERENCES.md` carries
`last_synced_with_sf` in its frontmatter and gets a silent re-stamp via
`preferences-template-upgrade.ts` on drift. Every other scaffold file is
anonymous.
The user directive: **everything needs versioning, with upgrade paths for
documents that are not completed, perhaps with background agents.**
This ADR generalises the `preferences-template-upgrade.ts` pattern to all
scaffold-managed documents and defines a structured upgrade pipeline that can
distinguish *pending*, *editing*, and *completed* content.
## Decision
Adopt a per-document state model with explicit version markers, a
project-local manifest, drift detection, and an upgrade command. Existing
files are not stamped; they are migrated by content-hash match against an
archive of past template versions.
### 1. Document states
Every scaffold-managed file is in one of three states:
| State | Definition | SF action on drift |
|-------------|------------|--------------------|
| `pending` | Content equals (or hashes to) a known scaffold template version. Neither user nor agent has customised it. | Silent re-write to current template. Update marker. |
| `editing` | Marker present and stamped, content has drifted from the stamped template. Customisation in progress. | Do not overwrite. Write `<file>.proposed` with new template + diff. Optionally dispatch background merge agent. |
| `completed` | Marker absent, OR marker explicitly says `state=completed`. | Never modified by SF. |
#### Detection
For Markdown files, the marker is an HTML comment on the **first line**:
| Markdown docs | `AGENTS.md`, `ARCHITECTURE.md`, `docs/RELIABILITY.md`, `docs/SECURITY.md`, `docs/DESIGN.md`, `docs/QUALITY_SCORE.md`, `docs/RECORDS_KEEPER.md`, all `*/AGENTS.md` | HTML comment on line 1 |
| Frontmatter docs | `.sf/PREFERENCES.md` | Frontmatter fields: `last_synced_with_sf`, `sf_template_state`, `sf_template_hash` (extends prior art in `preferences-template-upgrade.ts`) |
| Reference slot text files | `docs/references/*-llms.txt` | HTML comment on line 1 (Markdown comments are valid in plain text consumed by LLMs) |
| `.siftignore` and similar non-Markdown configs | `.siftignore` | Skip versioning. Sibling file `.sf/scaffold-manifest.json` records the applied version. (Rationale: hash-based legacy match is sufficient; markers in dotfiles fight tooling.) |
#### User-content files SF must never stamp
These files are user-curated by intent (per ADR-001) and **must not** appear
in `SCAFFOLD_FILES` nor be touched by the upgrade path:
| (default) | Force the same operation that would run automatically. Useful when the user wants to refresh on demand without waiting for the next startup or milestone close. |
| `--dry-run` | Print the drift report and the planned actions. Make no filesystem changes. The primary diagnostic mode. |
| `--include-editing` | Synchronously merge `editing-drift` items inline (vs. the default async-via-subagent). Used when the user wants a definitive answer right now. |
| `--only=<glob>` | Restrict the sync to a path glob. Useful for tightly scoped refreshes (`--only=harness/**`) or for re-deriving a specific code-dependent doc (`--only=docs/RELIABILITY.md`). |
Exit code: 0 if no errors. Non-zero only if filesystem writes failed for
reasons unrelated to drift (permission, disk full).
## Implementation phases
| Phase | Scope |
|-------|-------|
| **A** | Stamp markers on all `SCAFFOLD_FILES` writes. Maintain `.sf/scaffold-manifest.json`. Extend `PREFERENCES.md` frontmatter with `sf_template_state` and `sf_template_hash`. Existing `agentic-docs-scaffold.ts` callsites unchanged externally. |
| **B** | Implement `detectScaffoldDrift`, `migrateLegacyScaffold`, and the initial `SCAFFOLD_VERSION_ARCHIVE` (empty). |
| **C** | **Automatic synchronous sync**: extend `ensureAgenticDocsScaffold` to apply `missing` + `upgradable` + legacy-migrated items in the same pass. No new command surface required for this; the existing callsites get the upgrade behaviour for free. `checkScaffoldFreshness` doctor finding for visibility. |
| **D** | **Automatic asynchronous sync via existing infrastructure**: after milestone completion (`auto-post-unit.ts`, `auto.ts:stopAuto`), dispatch a `scaffold-keeper` subagent (via the existing `subagent` extension) that runs the **`records-keeper`** skill against drifted docs. Code-as-fact verification: agent reads source and re-derives content. Surfaces results as `kind: "approval_request"` notifications using the structured-notification model from ADR-019/020. |
| **E***(escape hatch)* | `/sf scaffold sync` command for dry-run inspection, forced refresh, and scoped operations (`--only=<glob>`, `--include-editing`). |
Each phase is independently shippable and testable. Phase A alone unlocks
the architectural property: SF can tell *pending* from *completed* on every
project from now on. Phase C is what the user experiences as "automatic"
for the simple cases; Phase D is what makes records-keeper autonomous for
the code-derived cases.
## Consequences
### Becomes possible
- Continuous template evolution: SF can iterate scaffold content freely
knowing pending docs auto-upgrade.
- Visible signal for project staleness via doctor.
- Clean separation of *SF-managed* vs *user-owned* content per file, not per
directory.
- Foundation for background-agent merges of customised docs.
- Future templates (new harness specs, new ADR types) propagate without
manual project-by-project edits.
### Becomes harder
- Scaffold manager grows from "skip-if-exists" to a state machine. Test
matrix grows accordingly.
- Every template change must consider whether to bump the SF version archive
entry for the previous body. Forgotten archive entries cause legacy files
to be classified `customized` when they should be `pending`.
- Marker format becomes load-bearing: changing it is itself a versioning
problem (handled by versioning the marker schema in `schemaVersion`).
### Failure modes
| Failure | Behaviour |
|---------|-----------|
| Corrupt `scaffold-manifest.json` | Rebuilt by re-walking files; markers are source of truth. |
| Marker hash mismatch with stamped content (e.g. user hand-edited the marker) | File classified `editing`. SF will not overwrite. User can fix by running sync with `--include-editing` after reviewing the proposed file. |
| `SCAFFOLD_VERSION_ARCHIVE` missing an entry for a real prior version | Affected files classified `customized` and left alone. Recoverable by adding the archive entry in a later SF release; sync will then promote on next run. |
| Read-only filesystem | Each writer is wrapped in try/catch (same pattern as `upgradePreferencesFileIfDrifted`). Sync degrades to dry-run output. |
| Concurrent sync runs | First writer wins; second sees no drift on second pass. Manifest writes are atomic via temp+rename. |
## Alternatives Considered
| Alternative | Why rejected |
|-------------|--------------|
| Status quo (skip-and-create only) | No visibility, no refresh path, anonymous templates. The whole point of this ADR. |
| Aggressive refresh on every run (overwrite all scaffold files) | Destroys user customisations. Non-starter. |
| Git-based detection (compare repo HEAD against SF init commit) | Requires clean git state at sync time, breaks under merges/rebases, conflates user commits with SF state, fragile across worktrees (per ADR-001). |
| Per-file `.<file>.sf-meta` sidecar instead of inline marker | Doubles file count, easy to delete, harder to cite in human review. Marker travels with the file it describes. |
| Single global `SF_VERSION` stamp on the manifest only | Cannot distinguish pending vs editing per file; a single global re-stamp would either skip everything (current behaviour) or overwrite everything (rejected). Per-file state is the minimum useful granularity. |
| LLM-based "is this still pending?" classifier | Non-deterministic, expensive, unnecessary. Hash equality is the right primitive. |
## Migration
See [#7](#7-migration-strategy-for-existing-projects). One-shot, idempotent,
non-destructive. No project action required; first sync after upgrade