Skip to Content

gtmesh page

Everything you do to a page: author it into the mesh, then move it through its states.

gtmesh page add --slug <slug> --keyword <kw> --section <s> --type <t> gtmesh page rehome <slug> --parent-topic <value> gtmesh page promote [slugs...] [selectors] gtmesh page demote [slugs...] [selectors] gtmesh page validate [slugs...] gtmesh page seal <slugs...> gtmesh page publish <slugs...> gtmesh page amend <slugs...> gtmesh page recreate [slugs...] [selectors] [--yes] gtmesh page retire <slugs...> gtmesh page unretire <slugs...>

The group splits in two. add and rehome author — they put a page (or a page’s topical relationship) into the committed graph, which is what the rest of the engine then reasons over. Everything else moves a page along the lifecycle — the status path from catalogued idea to published article, and back again when something needs redoing.

What every verb here has in common

Each one records a decision; none of them builds. A lifecycle verb writes a page’s status and stops there. The building — scaffolding a page bundle, baking its _brief — happens on the next apply, so the rhythm is always decide, then apply.

They are writers of committed state. The change lands in graph/nodes.jsonl, so it belongs in your next commit alongside whatever content it goes with. (validate is the exception — it is read-only.)

They name pages by slug, and three of them also by selector. The lifecycle verbs take exact slugs positionally; add names its page with --slug because the page doesn’t exist yet. promote, demote and recreate additionally take the selector flags below, so you can act on a section, a topic, or a whole cluster in one move.

The global options (--project, --json, --quiet) apply to every verb in the group.

The selectors

Field predicates compose with AND, and they compose with any positional slugs you also name. --under is a walk down the mesh from a hub.

Each flag takes one value, and repeating one is an error rather than a silent last-wins — --page-type home --page-type index used to match only index, quietly dropping the pages the first flag named. To cover several values, list the pages as positional slugs or run the command once per value.

SelectorSelects
[slugs...] (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 (categories/rotary-lobe-pumps, application/cip), or every relationship on an axis (application). Targets are node ids; gtmesh explain <slug> lists a page’s
--topic <value>Pages that belong to that topic — its members and its apex. Takes the value (dosing) or the node id (topics/dosing)
--page-type <id>Pages of a page type
--slug-prefix <str>Pages whose slug starts with this prefix
--status <state>Scope to pages currently in this status
--under <hub-slug>A hub slug + everything reachable down the mesh from it
--orphan(promote/demote only) Pages that belong to no topic — no MEMBER_OF or apex edge — excluding structural nav pages. Pairs with the topically-orphaned doctor check
--to <status>(promote/demote only) Target status. Default queued for promote, backlog for demote
--dry-runPreview the transition without writing

Author a page

page add

Authors a fixed (seed) page — a hand-authored input node that persists across plan/apply. It inserts the node, and the topic the page heads or joins, directly into the committed graph; the next apply builds it. This is how you stand up a new cluster — see Add a new category.

OptionDescription
--slug <slug>The page URL slug (required)
--keyword <kw>The primary keyword (required)
--section <section>The section / URL root (required)
--type <page_type>The page type (required, e.g. category-hub)
--parent-topic <t>The topic the page belongs to. topics/<t> is always ensured — a member joins it, an apex (--role) heads it
--role <hub|pillar>Make this page the apex of --parent-topic: it owns the value, minting topics/<t> plus a HUB_OF/PILLAR_OF edge. Omit for a member. One hub and one pillar per topic
--kind <k>The entity’s domain kind (category, manufacturer, …). Sets the topic’s axis and makes a new cluster resolvable, so members bind to it
--label <label>An extra node label (repeatable) — parity with graph upsert
--prop <key=value>An extra node prop, value JSON-parsed (repeatable) — parity with graph upsert
--intent <intent>Search intent (e.g. commercial)
--discriminator <d>Identity discriminator
# a category hub that heads its own topic, plus a member that joins it gtmesh page add --slug /categories/dosing-pumps --keyword "dosing pumps" \ --section categories --type category-hub --parent-topic dosing --role hub --kind category gtmesh page add --slug /guides/dosing-pump-sizing --keyword "dosing pump sizing" \ --section guides --type guide --parent-topic dosing

add authors a new page and errors if the slug already exists. To give an existing page a topic, reach for rehome.

page rehome

Homes an existing orphan page into a topic — the inverse of demote --orphan, and the actuation the curation loop drives over the backlog pool match recommends against. It repoints a page that is already committed: it authors the page’s topical edge onto the topic and mints topics/<value> if absent. No new page, no identity churn.

Once homed, the page has a topical edge, so it clears the topic gate and page promote / next can move it into the build set.

It is idempotent, and says which it did: a membership that isn’t there yet is authored; one that already exists is left alone and pinned as authored so apply’s rebuild keeps it; one already authored reports that nothing changed. A page can belong to several topics — rehome adds a membership, it never replaces one, so use explain to see every topic a page belongs to.

Argument / OptionDescription
<slug>The slug of the committed page to home (required, e.g. /glossary/effluent)
--parent-topic <value>The topic to home it into (required). topics/<value> is minted if it doesn’t exist
--role <hub|pillar>Elect this page the apex of the topic (HUB_OF/PILLAR_OF). Omit to home it as a member (MEMBER_OF). One hub and one pillar per topic
gtmesh match # the ranked shortlist gtmesh page rehome /glossary/effluent --parent-topic dosing # enact one of its suggestions

Move a page through the lifecycle

page promote / page demote

promote is the gate between the cheap catalogue loop and the expensive production loop. Deciding which catalogued pages to actually write is always an explicit, reviewable choice — intent-based, reversible with demote, and applied by default (--dry-run previews). It writes only the status of matched pages; the scaffolding happens on the next apply.

demote is the exact mirror: the same selectors, moving pages backward (default target backlog).

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 # a hub and its whole cluster gtmesh page demote --section blog # park a section back to backlog gtmesh page demote --orphan --to backlog # park the orphan cleanup set

Changing a page’s authority role (apex vs member) is a different thing entirely — that is driven by the apex edge, not by these verbs. See the mesh & link graph.

page validate

The read-only checker the writer leans on. Two passes per page:

  1. Schema — the page’s structured YAML is validated (via ajv) against its per-type schema in schemas/.
  2. Editorial lints — the deterministic, rule-based checks (banned words, structural rules, and so on) that don’t need a model.

The lint also enforces symbolic refs in prose: a literal internal URL in a body is an error — link with ref:page/<id> or ref:links/<scope>/<n> so the link re-resolves on rename or re-ownership, and an unresolvable ref is an error too. External URLs are untouched. See symbolic ref in the glossary.

Run it with no slugs and it does one extra thing: it also checks your committed inputs — reference tables and discovery seeds — against the engine’s column contracts. That catches a malformed CSV before it ever reaches plan/apply.

ArgumentDescriptionDefault
[slugs…]Page slugs to validateEvery scaffolded page (and the committed inputs)

Named slugs are matched against the committed graph, and a missing leading slash is added for you (as with graph get). A slug that matches no page is a hard error, not a quiet “0 pages checked” — so a typo can’t masquerade as a pass.

gtmesh page validate # every page + all committed inputs gtmesh page validate /glossary/water-pump # just one page gtmesh page validate --json # machine-readable results for CI

It writes nothing. It runs the same gate seal enforces, exposed so you can run it as often as you like: a page that doesn’t validate cannot seal.

page seal

The checkpoint at the end of writing. seal runs the validation above and, only if the page is schema-valid AND lint-clean, moves it from writing to review and stamps its provenance hashes (built_brief_hash / built_schema_hash / body_hash) — the hashes that make later graph-to-graph diffs precise.

It also requires the bundle to carry its _brief block. built_brief_hash is stamped from the node, so sealing a bundle whose block was dropped — hand-authored, or a writer that rewrote the whole file — would claim the body answers an assignment that isn’t on disk. gtmesh apply bakes a missing block unconditionally and moves no hash or status, so the fix is one command.

ArgumentDescription
<slugs...>Page slugs to seal (one or more)
gtmesh page validate /glossary/idempotency # the writer's self-check (no status move) gtmesh page seal /glossary/idempotency # only if valid AND lint-clean → review gtmesh page seal slug-a slug-b # several at once

Like validate and apply, seal warns once when the shared schema base (schemas/common.schema.yaml) is pending an upgrade — the gate then ran against a stale contract. gtmesh upgrade refreshes it.

page publish

The last deliberate human move in the authoring lifecycle: review (or ready) → published. A page reaches review once it’s sealed; after the human PR review, publish flips its status.

It sets status only — it builds nothing and rewrites no body. A page with only image briefs (art-direction stubs, no real image files yet) publishes fine: the briefs are schema-valid and your site renders placeholders. Generate the real images whenever you’re ready.

ArgumentDescription
<slugs...>Page slugs to publish (required)
gtmesh page publish /glossary/water-pump gtmesh page publish /apps/slack /apps/notion # several at once

Exit code (CI gate). publish takes explicit slugs, so every slug you name is a page you expect to go live. It exits 0 only when every named slug ends up published, and non-zero as soon as one doesn’t — the same contract seal holds. Each page that didn’t publish is named on stderr with its reason (typically: it isn’t in review/ready yet, so seal it first — or the slug is wrong), and that line survives --quiet.

A page that is already published is the state you asked for, so it counts as success and the run stays 0 — re-running a publish step doesn’t turn a pipeline red the second time. A mixed batch still does the work it can: the eligible pages publish, and the run is non-zero because the others didn’t. So gtmesh page seal <slug> && gtmesh page publish <slug> fails loudly in a script instead of reporting a silent no-op.

page amend

Handles a body edit that changes the page’s files but not its prose, by moving the page to needs_update — the status that says “this body changed; re-seal it.”

The canonical case is image placement: the writer authors art-direction briefs, and the image-director skill later generates and places the real asset files into the bundle. That’s a body change, so the page has to re-seal — but it isn’t a rewrite, because the prose is untouched.

ArgumentDescription
<slugs...>Page slugs to amend (required)
# (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 is a different path — that’s a rewrite action that reruns the writer, and it keeps placed assets, so image work is never undone. For a full re-author from scratch, see recreate.

page recreate

Re-opens built pages for a fresh body: status goes back to writing and the page’s content provenance (built_brief_hash) is cleared, which is what puts it back on the writer’s worklist. The writer then authors a fresh body against the current brief. Reach for it when a page’s prose is wrong enough that you’d rather start over than edit.

Your prose is still on disk. recreate changes the graph, not the page bundle — the existing body stays in content/<slug>/index.yaml until the writer replaces it, and placed image assets are preserved. Nothing is moved or deleted, so an accidental recreate costs you a status and a hash, not your content. (Run git diff after one: only graph/nodes.jsonl changes.)

You don’t need a plan/apply between the recreate and the writer — the page is already on the worklist.

It refuses when re-authoring would drop answers. A page keeps the _brief it was sealed against, so recreate can see the questions the brief has since stopped asking — folded onto another page, graduated to their own, or faded. If the body still answers one of them, the writer working from the current brief has no reason to write it again, and the answer is lost silently. So recreate refuses, names the pages and the questions, and stops. page amendseal re-stamps them with no re-write; gtmesh plan’s question-fold advisories name where each question went. Pass --drop-answers to re-author anyway.

It also refuses when the on-disk _brief isn’t the block seal stamped — an older release re-baked it, or it was hand-edited. Then there’s no record of what the body answers and nothing can be ruled out, which is the more dangerous case, not the safer one. git log -- content/<id>/index.yaml has the sealed version if you want to look before deciding.

--dry-run writes nothing, whether you named pages with a selector or listed them explicitly. (It used to be read only on the selector path, so recreate <slug> --dry-run re-opened the page while reporting a preview.)

Which pages are eligible. A page needs a body to replace, so planned and backlog pages are skipped — they’ve never been built. Retired pages are skipped too: a tombstone is a recorded decision to leave the build set, and reversing it is its own deliberate step (unretire). Skips are named in the output rather than silently dropped.

recreate takes the selectors minus --orphan and --to (its target is always writing), plus one flag of its own:

FlagDescription
--yesActuate a selector match (an explicit slug list needs no confirmation)

A selector needs --yes. Unlike promote/demote, a recreate isn’t cleanly reversible — the previous built_brief_hash is gone and a review/published status is lost. An explicit slug list is already a deliberate act and runs immediately; a selector prints the matched pages and waits for --yes, so a wider-than-intended match can’t sweep your mesh back into writing.

gtmesh page recreate /glossary/water-pump # one page gtmesh page recreate --section guides --dry-run # see what a selector would catch gtmesh page recreate --under /categories/rotary-lobe-pumps --yes gtmesh page recreate --topic dosing --status published --yes # a topic's cluster, published only

Then hand the batch to the article-writer skill and close it out as usual — gtmesh page sealgtmesh page publish.

page retire / page unretire

retire tombstones a page whose demand has faded or folded away (status → retired) — the way to actuate a propose-retire from gtmesh plan. It’s distinct from demote --to backlog: a backlogged page is a live candidate the match/curation loop should still reconsider; a retired page is deliberately gone. It leaves the build set and the curation pool, and — crucially — suppresses a re-minting demand draft in plan, so a still-present keyword can’t silently rebuild it on the next pull. The node is kept, so a redirect to a successor still resolves.

A tombstone is terminal. Once a page is retired, plan leaves it alone and never proposes its retirement again, so actuating a propose-retire clears it from the human gate for good.

The decline is recorded on the demand, not on one classification of it: the retired page’s head keyword is declined across identities, so adding a routing rule can’t re-mint the same keyword under a fresh identity. Only the exact head is declined, never the secondaries — retiring "gear pump" doesn’t block "gear pump vs screw pump" — and a head that a live page still owns is never declined, so retiring a duplicate leaves the survivor fully reconciled.

unretire reverses it: retiredbacklog, so the page re-enters the parked pool as a curation candidate and the re-mint suppression lifts.

ArgumentDescription
<slugs...>Page slugs to retire, or retired slugs to bring back (required)
gtmesh page retire /glossary/obsolete-term gtmesh page unretire /glossary/obsolete-term

Retiring a duplicate works, but merge is the verb you actually want — and it doesn’t exist yet. retire says this demand is gone; a duplicate is this demand belongs to that page instead, which should redirect the dupe onto the canonical page and fold its keywords in. Until that verb lands, retire is the right tool and behaves correctly here; the tombstone just records a weaker fact than the one you meant.

Because the tombstone is committed state, gtmesh ui keeps rendering it — dimmed and struck in the DB table, hollow and dashed in the Graph — and hides it from the DB build queue until you select retired in the status filter.

Reads & writes

  • Reads: the committed graph — the named or matched pages and their current status. seal and validate additionally read the page bundles, their per-type schemas in schemas/, and the editorial rules; validate with no slugs also reads the committed input CSVs.
  • Writes: graph/nodes.jsonl — the authored node or edge (add, rehome) or the moved status (everything else). seal also stamps the built hashes; recreate clears built_brief_hash. Page bundles are never rewritten by this group. validate writes nothing at all.
  • Commits: the graph change belongs in your next commit, alongside whatever content it goes with.
Last updated on