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 graph (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 graph-to-graph: GoToMesh compares hashes stored in the graph, 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 page is sealed (it has its built_* provenance hashes). An unbuilt page is catalogued — it exists in the graph but has no written body yet.

The reconcile signals

For each kind of change, the action differs depending on whether the page is unbuilt or built:

What you changedUnbuilt page (catalogued)Built page (sealed/published)
Nothingnoopnoop
Body inputs — primary/secondary keywords, intent, section, template, content-determining columns, and the resolved entity’s grounding facts (the brief_hash)recomputerewrite — the writer reruns the body. If published, the old body keeps serving until the revision ships
Render projection — slug, mesh links, cardsrecomputenone — re-derives into site.manifest.json (a zero-rewrite manifest change; no writer, page file untouched)
Per-type schema (schemas/<type>.schema.yaml)recomputerewrite (soft) + flagged schema-dirty (confirm with gtmesh page validate)
Other derived — a page’s domain relationships (HAS_<AXIS> edges), priority, volumes (e.g. an entity fact edit, or a re-pull)recomputerecompute
Slug (a rename, or a parent_topic change)redirect (human-gated)redirect (human-gated)

A brand-new identity — a keyword or seed that wasn’t in the graph — 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 graph, 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
recompute / catalogue / createThe engine — deterministic, no LLM
redirectHuman-gated — not auto-applied; you have to ask for it

A rewrite reaches the writer for one of three reasons: spec-change (the brief moved), schema-change (the page type’s shape moved), or reopened — you asked for a re-author with recreate. A create is only ever a page that has never been built; a re-opened page already exists on disk, so it plans as a rewrite.

So: the link mesh changing is a cheap, automatic re-projection of the manifest — no writer, no page rewrite, and not even an action on a built page. Only a real content change pulls the expensive writer step back in. Mesh links live in the graph, not in the page body, so when an edge moves the engine just re-derives site.manifest.json and the site re-renders — the prose is untouched.

A body change (brief_hash) implies a projection change too, but only the body change reruns the writer — the projection re-derives for free into the manifest. You’ll never get a silent projection tweak 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 mints a url node with an ALIAS_OF edge to the live page. Old URLs keep resolving forever: the SSG emits 301 redirects from the manifest’s redirect table. Because the alias targets the page node, chain-collapse is intrinsic (rename twice and both old URLs resolve to the newest slug) and no redirect is ever lost.

Removals. When an identity’s input disappears entirely, its node is carried forward (a tombstone), not silently dropped, so the id is never reused and authored edges never dangle. To actually retire it, edit the graph and let the manifest re-derive. There is no --prune flag.

The status lifecycle

Reconcile actions are input-driven. Layered on top is the status of each page — 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 page promote --section glossary --dry-run # preview which pages would move planned → queued gtmesh page promote --section glossary # commit the promotion gtmesh page 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>pages in a section
--has <id|axis>pages holding a relationship to that node — the owner hub (categories/rotary-lobe-pumps) or a bare value node (application/cip); a bare axis (application) takes every relationship on it
--topic <value>pages that belong to that topic — dosing or topics/dosing
--page-type <id>pages of a page type
--slug-prefix <str>pages whose slug starts with the prefix
--status <state>scope to pages 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 page promote /integrations/slack # exactly the hub gtmesh page promote --page-type comparison # every comparison page gtmesh page promote --slug-prefix /apps/ # everything under /apps/ gtmesh page promote --under /integrations/slack # launch a hub and its whole cluster

seal — validate and record (writing → review)

gtmesh page 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 provenance hashes (built_brief_hash / built_schema_hash / body_hash) that make later diffs precise. A page that fails schema or an editorial lint cannot seal.

gtmesh page validate <slug> # schema (ajv) + deterministic editorial lint — the writer's self-check gtmesh page seal <slug> # only if valid AND lint-clean: → review, stamps the built hashes

publish — review → published

gtmesh page 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 — re-open the page for a fresh body (a full re-author). Status → writing; the existing prose stays on disk until the writer replaces it.
  • amend — a light re-seal for a body edit that isn’t a prose rewrite. The classic case is placing images.

For the full “I want to change a page — which move?” decision (input change vs. hand edit, writing vs. sealed, amend vs. recreate), see Editing a page.

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, then hands the slugs back) gtmesh page amend <slug> # body changed (assets placed) → needs_update gtmesh page seal <slug> # re-validates with assets present, re-stamps the body hash → review gtmesh page 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.

One writer at a time

Every graph writer — apply and all seven lifecycle verbs (seal, publish, recreate, amend, retire, unretire, promote/demote) — reads the committed graph and rewrites graph/nodes.jsonl whole. So a writer takes an advisory lock, .gtmesh/lock.json, for the whole read-and-write, and a second writer that arrives mid-flight stops instead of overwriting:

another gtmesh command is writing the committed graph (pid 4821 on studio.local, held since 2026-07-30T09:12:44.108Z) — wait for it to finish and run this again, or delete .gtmesh/lock.json if that process is gone

That is the whole handling: let the first command finish, then run yours again. The command exits non-zero and the committed graph is untouched, so nothing is half-committed. Batching slugs into one command (gtmesh page seal a b c) avoids the collision entirely and is faster besides.

A refused apply is the one case that leaves anything behind: it scaffolds page bundles before it writes the graph, so some content/<slug>/index.yaml skeletons may already exist. They are empty stubs plus a _brief that nothing in the graph points at yet — re-running apply reuses them and finishes the job.

The lock is created and removed per command, and it is local — .gtmesh/lock.json is git-ignored, so it never reaches a commit. If a writer is killed (Ctrl-C, a crash) the lock file survives it, but the next command sees the recorded process is gone and clears it automatically: there is nothing to unstick by hand. Deleting the file yourself is only ever needed for a holder gtmesh can’t probe — a stale lock left by another machine on a shared filesystem, which it also clears on its own after 15 minutes.

For the deeper “why” behind the brief hash, projection-as-manifest, and the graph-as-state model, see mental models. For the full set of commands, see the CLI reference.

Last updated on