Skip to content

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

  1. docs/tags/README.md — which tags exist, in version order, with their release date, theme and source revision.
  2. docs/tags/<Tag>/SUMMARY.md — one bounded screen: theme, delivered mechanics, systems and scenes changed, verification verdict, known limitations, and links to everything else.
  3. docs/tags/<Tag>/README.md — the file-by-file map when you need to know what a given document holds before opening it.
  4. 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.md must 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 archive and index refuse 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

python tools/seal_tag.py reindex

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.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 root MEMORY.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's link_warnings and 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:

python tools/seal_tag.py backfill --all

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.