adapto:content-upload
The pipeline's gated writer (content-pipeline.md). It takes the
Markdown drafts you've approved and pushes them to Adapto: body md→HTML, create-or-update via the
ledger's local↔CMS id-map (one-way push — local is the source of truth), the SEO metadata mirrored into the
reserved _adapto_seo collection, everything landing as draft. It is schema-gated (every target
type must exist first) and drift-guarded (it won't silently overwrite backoffice edits).
When to use
- "Upload the drafts", "push the approved content to Adapto", "update the content in Adapto".
- After you've reviewed
.adapto/drafts/ and want the approved pieces in the CMS.
When not to use
- Writing/revising the drafts →
adapto:content-create.
- Taking CMS drafts live →
adapto:publish (upload lands drafts; publish flips them to published).
- Rendering metadata on the site →
adapto:seo-wire.
Inputs
- The approved drafts in
.adapto/drafts/ (frontmatter per content-pipeline.md §2). The user names which
to upload, or "all reviewed".
- The ledger (
.adapto/ledger.json) — the local↔CMS id-map (create vs update) + drift fingerprints.
.adapto/schema.json — collection + _adapto_seo ids (the schema gate).
inventory.md — to resolve internal_links / category_slugs.
- The working tenant + language (confirm the tenant).
Outputs
- Created/updated Articles / Pages / collection items in Adapto (
draft), provenance-tagged on Articles.
- A mirrored
_adapto_seo item per piece (meta/OG/JSON-LD), json_ld stringified.
- The ledger advanced to
status: uploaded with cms.content_id, cms.seo_id, last_push_hash,
cms_updated_at; calendar.md refreshed.
- Next step:
adapto:seo-wire (if the render layer isn't wired yet) so the metadata shows on the site;
then review on the dev server and adapto:publish to take it live; adapto:translate to localize.
Preconditions
- Preflight with the
adapto:doctor checks.
- Hard-block on an authenticated CLI (
adapto auth me) and a selected tenant (this skill writes);
confirm the working tenant.
.adapto/schema.json must exist with the targets the drafts need (else the schema gate routes you to
adapto:schema-design / adapto:schema-apply).
adapto CLI >= 0.1.3.
Plan phase
Build + validate; no writes yet. Print a machine-parseable plan and ask as a pickable question
(Approve / Change something / Discuss this):
- Per draft: create vs update (from the ledger id-map), its
type/slug/language, and the
_adapto_seo item it will write.
- Schema gate: confirm every draft's target type/collection exists in
.adapto/schema.json. Any missing →
STOP and route to adapto:schema-design / adapto:schema-apply; don't upload that piece.
- Drift check: for each already-uploaded piece, compare the live CMS item's
updated_at to the ledger's
cms_updated_at. If it changed, flag it drifted and ask per piece: overwrite / skip / import-CMS — never
silently clobber.
- Unresolved links: list any
internal_links/category_slugs not in inventory.md (written but flagged).
- The provenance session id that will tag Article writes. No cost/token figures. Nothing to upload →
say so and stop.
Apply phase
Runs only after approval. Deterministic CLI calls (--json on each).
SESSION_ID="agent_$(date -u +%Y-%m-%dT%H-%MZ)_$(printf '%04x' $RANDOM)" # for Article --source
For each approved draft:
- Body md→HTML. Convert the Markdown body to HTML (the CMS
content renders HTML — cheatsheet §3).
Resolve internal_links + category_slugs against inventory.md; warn on unresolved (write anyway).
- Create or update the content via the ledger id-map:
- Article —
articles create (new) / articles update <id> (known), with
--source '{"type":"ai_generated","name":"'"$SESSION_ID"'"}', --status draft, --author <from draft>;
then categories add-article <cat_id> <article_id> per resolved category.
- Page —
pages create / pages update <id> (no --source).
- Collection item —
collections items create <cid> / items update <cid> <item_id>, --data-json
keyed to the collection's fields, --status draft.
- Mirror
_adapto_seo. Upsert one _adapto_seo item for the piece (keyed target_slug + content_type,
content_id once known; json_ld stringified) via items create/items update on the _adapto_seo
collection id from schema.json.
- Update the ledger row →
status: uploaded with content_id, seo_id, last_push_hash (sha256 of the
draft body), last_push_at, cms_updated_at (the item's returned updated_at); refresh calendar.md.
- Drift: if the Plan flagged a piece as drifted and the user didn't choose overwrite, skip it and report.
Loop cleanly — judge success from each call's --json, not the shell exit code; end the loop exit 0 on
success so a created batch never surfaces as a red Error: Exit code 1 (conventions.md §8). Then restart the
dev server (stop→start) and keep it running so the new content appears — never kill it (starters sync at
startup — §14). Finally point to adapto:seo-wire / review + adapto:publish / adapto:translate.
Errors and recovery
- Schema gate fails (target type/collection missing) → stop that piece; route to
adapto:schema-design /
adapto:schema-apply; upload the rest.
- Drift detected → ask per piece (overwrite / skip / import); never silently overwrite backoffice edits.
- Item
data doesn't match collection fields → surface which field; don't send unknown keys.
- Partial failure mid-loop (Articles/Pages have no batch) → report what uploaded, update those ledger rows,
then stop; re-running is safe (the id-map makes it an update, not a duplicate).
_adapto_seo write fails but content succeeded → report the content id, mark the seo mirror pending, and
retry the metadata only (don't re-create the content).
- Not authenticated / no tenant → stop; offer both auth paths —
Log in or Register (conventions §11) — then tenant selection.
Forbidden actions
- Never write without an approved plan (plan-then-apply); never assume the working tenant.
- Never omit
--source on an Article write — it mislabels content as internal/CLI (forbidden-actions.md).
- Never silently overwrite a drifted CMS item — drift-guard and ask.
- Never publish — everything lands
draft (draft-first); the user reviews then adapto:publish.
- Never invent collection-item
data keys, _adapto_seo fields, or article/page fields not in the schema.
- Never modify the read-client; never blind-retry a non-idempotent write (use the ledger id-map).
1---2name: adapto-content-upload3description: Push approved content drafts to Adapto — convert each reviewed Markdown draft to HTML, create or update the Article/Page/collection item (one-way push via the ledger id-map), mirror its SEO metadata into _adapto_seo, and drift-guard against out-of-band CMS edits. Schema-gated; everything lands as draft. Plan-then-apply.4---56# adapto:content-upload78The pipeline's **gated writer** ([content-pipeline.md](../../shared/content-pipeline.md)). It takes the9Markdown drafts you've approved and pushes them to Adapto: body **md→HTML**, **create-or-update** via the10ledger's local↔CMS id-map (one-way push — local is the source of truth), the SEO metadata mirrored into the11reserved **`_adapto_seo`** collection, everything landing as **draft**. It is **schema-gated** (every target12type must exist first) and **drift-guarded** (it won't silently overwrite backoffice edits).1314## When to use15- "Upload the drafts", "push the approved content to Adapto", "update the content in Adapto".16- After you've reviewed `.adapto/drafts/` and want the approved pieces in the CMS.1718## When not to use19- Writing/revising the drafts → `adapto:content-create`.20- Taking CMS drafts **live** → `adapto:publish` (upload lands drafts; publish flips them to published).21- Rendering metadata on the site → `adapto:seo-wire`.2223## Inputs24- The **approved drafts** in `.adapto/drafts/` (frontmatter per content-pipeline.md §2). The user names which25 to upload, or "all reviewed".26- **The ledger** (`.adapto/ledger.json`) — the local↔CMS id-map (create vs update) + drift fingerprints.27- **`.adapto/schema.json`** — collection + `_adapto_seo` ids (the schema gate).28- **`inventory.md`** — to resolve `internal_links` / `category_slugs`.29- The working **tenant** + language (confirm the tenant).3031## Outputs32- Created/updated **Articles / Pages / collection items** in Adapto (`draft`), provenance-tagged on Articles.33- A mirrored **`_adapto_seo`** item per piece (meta/OG/JSON-LD), `json_ld` stringified.34- The **ledger** advanced to `status: uploaded` with `cms.content_id`, `cms.seo_id`, `last_push_hash`,35 `cms_updated_at`; `calendar.md` refreshed.36- **Next step:** `adapto:seo-wire` (if the render layer isn't wired yet) so the metadata shows on the site;37 then review on the dev server and `adapto:publish` to take it live; `adapto:translate` to localize.3839## Preconditions40- **Preflight** with the `adapto:doctor` checks.41- **Hard-block** on an authenticated CLI (`adapto auth me`) **and** a selected tenant (this skill writes);42 confirm the **working tenant**.43- `.adapto/schema.json` must exist with the targets the drafts need (else the schema gate routes you to44 `adapto:schema-design` / `adapto:schema-apply`).45- `adapto` CLI `>= 0.1.3`.4647## Plan phase48Build + validate; **no writes yet**. Print a machine-parseable plan and ask as a **pickable question**49(`Approve` / `Change something` / `Discuss this`):50- Per draft: **create** vs **update** (from the ledger id-map), its `type`/`slug`/`language`, and the51 `_adapto_seo` item it will write.52- **Schema gate:** confirm every draft's target type/collection exists in `.adapto/schema.json`. Any missing →53 **STOP and route** to `adapto:schema-design` / `adapto:schema-apply`; don't upload that piece.54- **Drift check:** for each already-uploaded piece, compare the live CMS item's `updated_at` to the ledger's55 `cms_updated_at`. If it changed, flag it **drifted** and ask per piece: overwrite / skip / import-CMS — never56 silently clobber.57- **Unresolved links:** list any `internal_links`/`category_slugs` not in `inventory.md` (written but flagged).58- The provenance **session id** that will tag Article writes. No cost/token figures. Nothing to upload →59 say so and stop.6061## Apply phase62Runs only after approval. Deterministic CLI calls (`--json` on each).6364```bash65SESSION_ID="agent_$(date -u +%Y-%m-%dT%H-%MZ)_$(printf '%04x' $RANDOM)" # for Article --source66```6768For each approved draft:691. **Body md→HTML.** Convert the Markdown body to HTML (the CMS `content` renders HTML — cheatsheet §3).70 Resolve `internal_links` + `category_slugs` against `inventory.md`; warn on unresolved (write anyway).712. **Create or update** the content via the ledger id-map:72 - **Article** — `articles create` (new) / `articles update <id>` (known), with73 `--source '{"type":"ai_generated","name":"'"$SESSION_ID"'"}'`, `--status draft`, `--author <from draft>`;74 then `categories add-article <cat_id> <article_id>` per resolved category.75 - **Page** — `pages create` / `pages update <id>` (no `--source`).76 - **Collection item** — `collections items create <cid>` / `items update <cid> <item_id>`, `--data-json`77 keyed to the collection's fields, `--status draft`.783. **Mirror `_adapto_seo`.** Upsert one `_adapto_seo` item for the piece (keyed `target_slug` + `content_type`,79 `content_id` once known; `json_ld` stringified) via `items create`/`items update` on the `_adapto_seo`80 collection id from `schema.json`.814. **Update the ledger** row → `status: uploaded` with `content_id`, `seo_id`, `last_push_hash` (sha256 of the82 draft body), `last_push_at`, `cms_updated_at` (the item's returned `updated_at`); refresh `calendar.md`.835. **Drift:** if the Plan flagged a piece as drifted and the user didn't choose overwrite, **skip** it and report.8485**Loop cleanly** — judge success from each call's `--json`, not the shell exit code; end the loop exit 0 on86success so a created batch never surfaces as a red `Error: Exit code 1` ([conventions.md](../../shared/conventions.md) §8). **Then restart the87dev server (stop→start) and keep it running** so the new content appears — **never kill it** (starters sync at88startup — §14). Finally point to `adapto:seo-wire` / review + `adapto:publish` / `adapto:translate`.8990## Errors and recovery91- **Schema gate fails** (target type/collection missing) → stop that piece; route to `adapto:schema-design` /92 `adapto:schema-apply`; upload the rest.93- **Drift detected** → ask per piece (overwrite / skip / import); never silently overwrite backoffice edits.94- **Item `data` doesn't match collection fields** → surface which field; don't send unknown keys.95- **Partial failure mid-loop** (Articles/Pages have no batch) → report what uploaded, update those ledger rows,96 then stop; re-running is safe (the id-map makes it an update, not a duplicate).97- **`_adapto_seo` write fails but content succeeded** → report the content id, mark the seo mirror pending, and98 retry the metadata only (don't re-create the content).99- **Not authenticated / no tenant** → stop; offer both auth paths — `Log in` or `Register` (conventions §11) — then tenant selection.100101## Forbidden actions102- Never write without an approved plan (plan-then-apply); never assume the working tenant.103- Never omit `--source` on an Article write — it mislabels content as `internal`/`CLI` (forbidden-actions.md).104- Never **silently overwrite** a drifted CMS item — drift-guard and ask.105- Never publish — everything lands `draft` (draft-first); the user reviews then `adapto:publish`.106- Never invent collection-item `data` keys, `_adapto_seo` fields, or article/page fields not in the schema.107- Never modify the read-client; never blind-retry a non-idempotent write (use the ledger id-map).