Skip to Content
DocumentationOperate a meshLifecycle & reconcile

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:

  1. What you changed — a body input, the render projection, a slug, an identity.
  2. 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 changedUnbuilt row (catalogued)Built row (sealed/published)
Nothingnoopnoop
Body inputs — primary/secondary keywords, intent, tier, section, template, content-determining columns (the content_hash)recomputerewrite — 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)recomputerestamp — 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)recomputerewrite (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)recomputerecompute
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:

ActionWho acts
rewriteThe writer (the one LLM step) reruns the body
restamp / relink / recompute / catalogue / createThe engine — deterministic, no LLM
redirect / pruneHuman-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 → published
  • planned / backlog — catalogued, not yet authorised to build.
  • queued — chosen to build (see promote below).
  • 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 backlog

Selectors compose with AND; --under is a graph walk down the mesh:

SelectorSelects
<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 cluster

seal — 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 hashes

publish — review → published

gtmesh publish <slug> # after the human PR review

A 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 images

A 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.

Last updated on