Lifecycle & reconcile
This is the heart of operating a mesh. When you change an input and run gtmesh apply, the engine
reconciles your registry (desired state) against what’s committed and emits
one action per identity — one decision per page that could exist. This page explains what each
edit triggers, who acts on it, and the deliberate human moves layered on top.
The diff is registry-to-registry: GoToMesh compares hashes stored in the registry, never by re-reading your pages. Two facts drive the action it picks:
- What you changed — a body input, the render projection, a slug, an identity.
- Whether the page is built or unbuilt. A built row is sealed (it has its
built_*provenance hashes). An unbuilt row is catalogued — it exists in the registry but has no written body yet.
The reconcile signals
For each kind of change, the action differs depending on whether the row is unbuilt or built:
| What you changed | Unbuilt row (catalogued) | Built row (sealed/published) |
|---|---|---|
| Nothing | noop | noop |
Body inputs — primary/secondary keywords, intent, tier, section, template, content-determining columns (the content_hash) | recompute | rewrite — the writer reruns the body. If published, the old body keeps serving until the revision ships |
Render projection — slug, mesh links, in-projection columns (the projection_hash) | recompute | restamp — the engine re-derives the deterministic meta. No writer, status unchanged. Reason relink when a mesh edge moved. The page file isn’t even rewritten |
Per-type schema (schemas/<type>.schema.yaml) | recompute | rewrite (soft) + flagged schema-dirty (confirm with gtmesh validate) |
Other derived — clusters, role, priority, volumes (persisted but not hashed; e.g. an entities.csv category edit, or a re-extract) | recompute | recompute |
Slug (a rename, or a parent_topic change) | redirect (human-gated) | redirect (human-gated) |
| Identity removed (its input vanished) | prune (human-gated) | prune (human-gated) |
A brand-new identity — a keyword or seed that wasn’t in the registry — is a create when
it’s already promoted to an actionable status (queued/writing/needs_update): it scaffolds the
page and moves it to writing. Otherwise it’s a catalogue — recorded in the registry, no
page yet.
Who acts
The single most important intuition here is what pulls the writer back in versus what the engine handles silently:
| Action | Who acts |
|---|---|
rewrite | The writer (the one LLM step) reruns the body |
restamp / relink / recompute / catalogue / create | The engine — deterministic, no LLM |
redirect / prune | Human-gated — not auto-applied; you have to ask for them |
So: the link mesh changing is a cheap, automatic restamp (reason relink) — no writer, no
page rewrite. Only a real content change pulls the expensive writer step back in. Mesh links
live in the registry, not in the page body, so when an edge moves the engine just re-derives the
projection and the site re-renders — the prose is untouched.
Body changes always imply a projection change too (the body hash is a subset of the projection
hash by construction), so when both could fire the higher-cost action — rewrite — wins. You’ll
never get a silent restamp that hides a real content change.
Slug changes & removals (detail)
These two are destructive or URL-affecting, so the engine never does them automatically — they stay human-gated.
Slug changes. When an identity’s slug changes, apply moves its bundle to the new path —
the writer’s body and its co-located assets follow it — and appends the hop to the durable,
cumulative ledger registry/redirects.csv. Old URLs keep resolving forever: the SSG emits 301
redirects from the ledger. The ledger collapses chains (if A→B and later B→C, it records A→C)
and never truncates, so no redirect is ever lost.
Removals. When an identity’s input disappears entirely, its row is carried forward, not silently dropped, and its bundle folder lingers. To actually remove it:
gtmesh apply --prune # removes the carried-forward row AND its bundle folder together--prune is destructive, which is exactly why it’s gated behind an explicit flag.
The status lifecycle
Reconcile actions are input-driven. Layered on top is the status of each row — and the deliberate human moves that change it. The path a page travels:
planned/backlog → queued → writing → review → publishedplanned/backlog— catalogued, not yet authorised to build.queued— chosen to build (seepromotebelow).writing— scaffolded; awaiting a body from the article-writer.review— sealed; valid and lint-clean, awaiting human PR review.published— live.
promote / demote — choose what to build
promote is the gate between the cheap catalogue loop and the expensive production loop. It
sets status and builds nothing — so deciding which catalogued pages to actually write is
always an explicit, reviewable choice. It’s intent-based, reversible (demote), applies by
default, and --dry-run previews:
gtmesh promote --section glossary --dry-run # preview which rows would move planned → queued
gtmesh promote --section glossary # commit the promotion
gtmesh demote --section blog # park a section back to backlogSelectors compose with AND; --under is a graph walk down the mesh:
| Selector | Selects |
|---|---|
<slug…> (positional) | exact slugs (e.g. just the hub, not the whole prefix below it) |
--section <name> | rows in a section |
--cluster <name> | rows whose any cluster axis = name |
--tier <id> | rows at a tier |
--page-type <id> | rows of a page type |
--slug-prefix <str> | rows whose slug starts with the prefix |
--status <state> | scope to rows currently in a status |
--under <hub-slug> | a hub and everything reachable down the mesh from it |
--to <status> | target status (default: queued for promote, backlog for demote) |
Examples:
gtmesh promote /integrations/slack # exactly the hub
gtmesh promote --page-type comparison # every comparison page
gtmesh promote --slug-prefix /apps/ # everything under /apps/
gtmesh promote --under /integrations/slack # launch a hub and its whole clusterseal — validate and record (writing → review)
gtmesh seal moves a drafted page from writing to review. It only succeeds if the page is
schema-valid AND lint-clean, and it stamps the three provenance hashes
(built_content_hash / built_projection_hash / built_schema_hash) that make later diffs
precise. A page that fails schema or an editorial lint cannot seal.
gtmesh validate <slug> # schema (ajv) + deterministic editorial lint — the writer's self-check
gtmesh seal <slug> # only if valid AND lint-clean: → review, stamps the built hashespublish — review → published
gtmesh publish <slug> # after the human PR reviewA page with only image briefs (art-direction stubs, no real image files yet) publishes fine — the briefs are schema-valid and the SSG renders placeholders. Generate the real images whenever you’re ready.
recreate / amend
These two re-open a page that was already shipped:
recreate— archive the current body and re-enter the write flow (rebuild the page from scratch).amend— a light re-seal for a body edit that isn’t a prose rewrite. The classic case is placing images.
The image re-seal seam
Image generation is networked and non-deterministic, so it lives in a skill, never in the CLI. The
article-writer authors art-direction briefs; the explicit image-director skill turns them into
placed files. Placing assets is a body edit, so the page re-enters a light re-seal via amend —
not a rewrite, because the prose is untouched:
# (the image-director skill generates and places the asset files into the bundle)
gtmesh amend <slug> # body changed (assets placed) → needs_update (the skill runs this)
gtmesh seal <slug> # re-validates with assets present, re-stamps the body hash → review
gtmesh publish <slug> # → published, now with imagesA prose rewrite (a real content change → rewrite) reruns the writer, which keeps placed
assets — so image generation is never undone by a later edit.
For the deeper “why” behind two hashes, restamp-vs-rewrite, and the registry-as-state model, see mental models. For the full set of commands, see the CLI reference.