Tag archives¶
seal_tag.py is what /gm-finalize calls to turn a finished tag into an
immutable archive under docs/tags/<Tag>/. You rarely run it by hand — the
exception is backfill, which retrofits the index files onto archives sealed
by an older GodotMaker release.
What a sealed archive looks like¶
docs/tags/
├── README.md parent index of every sealed tag
└── v0.1.0/
├── README.md navigation page for this tag
├── SUMMARY.md bounded retrieval summary
├── CHANGELOG.md what shipped, written by /gm-finalize
├── GDD-snapshot.md
├── PLAN.md
├── STRUCTURE.md
├── DESIGN.md
├── SCENES.md
├── MEMORY.md
├── memory/ architecture files MEMORY.md indexes
├── evaluation-final.json
└── evidence/
├── manifest.json every archived file: path, category, bytes, SHA-256
├── e2e/
└── screenshots/
The flat document paths (PLAN.md, STRUCTURE.md, SCENES.md, …) are
unchanged from earlier releases, so anything that already reads
docs/tags/<Tag>/PLAN.md keeps working.
An archive mirrors the project as it stands when archive runs, deletions
included: if memory/ or e2e/ is gone by the time the command runs, the
previous run's copy is removed rather than carried forward into the new
snapshot.
Reading an archive¶
docs/tags/README.md— which tags exist, in version order, with their release date, theme and source revision.docs/tags/<Tag>/SUMMARY.md— one bounded screen: theme, delivered mechanics, systems and scenes changed, verification verdict, known limitations, and links to everything else.docs/tags/<Tag>/README.md— the file-by-file map when you need to know what a given document holds before opening it.- The canonical documents and
evidence/— complete, but expensive to read. Open them only when the summary is not enough.
SUMMARY.md is a retrieval index, not a source of truth. It restates facts
from CHANGELOG.md, PLAN.md, evaluation-final.json and final_report.json
and nothing else — worker traces, exploration notes and MEMORY.md never
reach it. When the summary and a canonical document disagree,
the canonical document wins.
It links only what the archive actually holds. A project with no memory/
directory, or a backfilled archive missing a canonical document, gets a summary
without that pointer rather than one advertising a link that 404s.
Immutability¶
evidence/manifest.json carries "sealed": true once a tag is fully sealed.
From then on archive and index refuse to touch the directory and exit 3.
That is what stops a repeated /gm-finalize from silently rewriting history.
An archive whose manifest is missing or says "sealed": false is a
half-finished finalize — re-running archive is the documented recovery path,
and no flag is needed. --force exists for a deliberate reseal only.
Write order under failure¶
Two rules pull in opposite directions:
- A.
docs/tags/README.mdmust never list a tag that is not sealed. It is the retrieval entry point, and an unsealed archive is still an overwritable partial snapshot. - B. A failed run must never leave a tag sealed-but-incomplete, because
archiveandindexrefuse to touch a sealed tag.
index writes in this order: SUMMARY.md and the tag README.md, then one
atomic manifest write committing "sealed": true, then the parent index —
which is pure derived state, rendered only from manifests already on disk.
| Where it fails | Result |
|---|---|
| before or at the seal commit | tag unsealed, parent index untouched. Re-run index <Tag>. |
| at the parent index | tag sealed and correct; the index merely omits it. Run reindex. |
Every path that rewrites an archive — archive, index --force,
backfill --force — claims it before replacing anything: it drops the tag
from the parent index if it was sealed, then replaces the manifest with an
in-flight marker, then writes. A write that fails partway therefore leaves an
archive that plainly reads as unsealed and unlisted, rather than one still
carrying sealed: true over hashes that no longer describe its files. If
claiming the archive is itself what fails, nothing has been rewritten and the
archive is untouched.
Neither branch can produce an index entry for an unsealed tag, and neither
strands a sealed archive with no way back in. backfill follows the same
order: it seals target by target and writes the derived index once at the end,
so a mid-run failure leaves the earlier targets sealed, the rest untouched, and
the index listing only what genuinely sealed — a plain re-run finishes the job
because sealed targets are skipped.
Every generated file is written to a same-directory temp file and renamed into place, so an interrupted write never truncates the previous version.
/gm-finalize's completion gate checks the same markers: the archive must have
a parseable evidence/manifest.json with "sealed": true, and the parent
index must list the tag. Present-but-unsealed files do not pass.
Repairing a stale parent index¶
Regenerates docs/tags/README.md from the sealed archives on disk and touches
nothing else. This is the repair for the one failure index cannot finish
itself — the seal landed, the index refresh after it did not. Re-running
index there is the wrong move: the tag is legitimately sealed, so it exits 3.
Subcommands¶
| Command | What it does |
|---|---|
python tools/seal_tag.py archive <Tag> |
Copies the working docs, the memory/ subtree and e2e/ evidence into docs/tags/<Tag>/, then link-checks the archived MEMORY.md. Writes an unsealed manifest. |
python tools/seal_tag.py index <Tag> |
Generates SUMMARY.md, the tag README.md, the sealed manifest and the parent docs/tags/README.md. This is the step that seals the tag. |
python tools/seal_tag.py backfill <Tag> / --all |
Retrofits README.md, SUMMARY.md and a manifest onto archives sealed by an older release. Already-sealed archives are skipped unless --force. |
python tools/seal_tag.py reindex |
Regenerates docs/tags/README.md from the sealed archives on disk. Touches no archive. |
python tools/seal_tag.py bundle <Tag> |
Emits the JSON /gm-finalize uses to write the CHANGELOG. |
python tools/seal_tag.py reset |
Truncates .godotmaker/stage.jsonl and deletes metrics_current.jsonl. |
Exit codes: 0 success · 1 filesystem or runtime failure · 2 missing
sources, unresolvable memory links, or bad usage · 3 the tag is already
sealed.
Memory link checking¶
MEMORY.md indexes detailed architecture and constraints under memory/. Archiving the index
without those files would freeze a set of broken links, so archive copies
memory/ too and then verifies the archived MEMORY.md:
- A
memory/…link with no archived target, an absolute path, or a link that escapes the archive directory blocks the seal (exit 2). Fix the rootMEMORY.md, or restore the missing file, and re-run. - A link to a live project file that is deliberately not archived
(
src/player.gd,assets/…) is recorded in the manifest'slink_warningsand does not block.
Links inside HTML comments and fenced code blocks are ignored, so the example
entries shipped in the MEMORY.md template never block a first finalize.
Backfilling older archives¶
Archives created before this layout existed have the flat documents but no
README.md, SUMMARY.md or manifest, so they are invisible to the parent
index. Retrofit them once:
Backfill only adds index files. It never rewrites a canonical document — it
hashes them before and after and fails if any changed. It also does not
copy today's memory/ into a historical tag: that would inject present-day
content into a past snapshot. The missing subtree is recorded as a manifest
warning instead, and the tag README reports completeness as partial.
Archives that already carry a sealed manifest are skipped — re-indexing one
would replace its recorded seal revision with whatever this run resolves. Pass
--force to re-index them anyway.
An archive belonging to a finalize that is still in flight — written by
archive but not yet sealed by index — is skipped too, and --force does
not override that. It has the same shape as a legacy archive (PLAN.md
present, unsealed, no CHANGELOG.md), but sealing it from those incomplete
inputs would lock the real finalize out at exit 3. Finish it with
seal_tag.py index <Tag> instead. Naming such a tag explicitly is an error
rather than a silent skip.
That holds even for a run that died mid-copy. Before its first write every
command replaces the manifest with an in-flight marker naming its stage
(archiving, indexing, backfilling), so an interrupted archive stays
identifiable instead of decaying into the legacy shape. backfill resumes its
own interrupted work and keeps away from a finalize's.
Warnings already on the archive — a failed E2E or screenshot copy, say — are
carried into the manifest backfill writes, so a rewrite cannot quietly relabel
a partial archive as complete. Re-running --force re-derives the same notes
without stacking duplicates.
For the archives it does index, the source revision comes from git tag <Tag>
(the commit /gm-finalize tagged), not today's HEAD; an untagged archive
records null rather than a fabricated revision.
/gm-finalize never runs backfill on its own. Rewriting history is always an
explicit, user-invoked action.