Publish content (gated)
- Input: a brief item with status "drafted" whose draft file sits next to it
(same folder, .draft.md produced by ce-produce).
- Gate: the brief's status must be "drafted" (step 1 already checks) AND
core.contracts.require_approval_lineage(brief_path) must pass; abort on
ContractError. A rejected item must never reach create_post. The profile
user must also confirm the final draft in-session or via channel.
Refuse a draft whose frontmatter lacks capsule: verified (ce-produce's
handoff note) - the human can override in-session, and the override is
recorded in the outcome record.
Read ce-editor's oversight block from the draft notes: a score at or
above the profile's editorial.oversight_threshold (default 7) adds
a prominent line to the approval-channel message - "editor recommends
a human line-edit: ". Publishing remains gated by the
same approval either way: the score informs the human, it does not
add a second gate. Rationale: scaled, unedited AI content correlates
with deindexation
(https://www.rankability.com/data/does-google-penalize-ai-content/).
- Create the post via the CMS adapter (
onsite.cms.adapter_for;
wordpress or git-static per cms.type - the git-static branch below
replaces steps 3-4): create_post (status draft by default; status
"publish" only when site-profile sets publishing: direct), then
update_seo_meta with title/description/focus keyword from the draft's
frontmatter, and schema if the draft includes it. (update_rankmath
remains as the WordPress adapter's alias for update_seo_meta.)
- Verify with
get_rendered_head; on success PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -m core status <brief-path> published --actor agent and
write the outcome record with measurement dates; on failure leave the post
in draft and record the post id + failure reason in the outcome record.
IndexNow: when site-profile.yaml has indexnow: {enabled: true, key: ...}
(additive key) AND the post actually went live (status "publish" - a WP
draft has no public URL to submit), call hoo.indexnow.submit(host, key, [post_url]) after the successful verify and record the returned status
in the outcome record; a non-200 is recorded, never retried in-run.
- If a Canva image brief exists next to the draft (from ce-image), record its
path in the outcome record for manual upload (media upload is not in v1).
- Outcome summary - the approver always hears what happened. Compose ONE
message for the whole run and deliver it through the configured
approval channel (same channel-neutral delivery as skills/onsite-apply
step 4; in-session prints it). One line per item: each published post
with its live URL; each post left in draft, held in a PR, or failed,
with the reason and what happens next. No per-item message spam. A run
that published nothing and failed nothing sends nothing.
Git-static sites (cms.type git-static)
Steps 1-2 are identical: the drafted status check and
require_approval_lineage run BEFORE anything is written anywhere, and
the human confirms the final draft the same way. Then the PR replaces the
REST write; the adapter only writes files in the local clone
(cms.repo_root), and this skill runs every git/gh command:
- Create the content file via the adapter:
create_post(slug, title, content) - draft: true frontmatter by default, status="publish"
(draft: false) only when site-profile sets publishing: direct. Then
update_seo_meta with description/canonical from the draft's
frontmatter, and schema_jsonld if the draft includes schema - it
lands in the jsonld frontmatter field, which the site's layout must
render into the head.
- Deliver as a PR:
git checkout -b organic-os/<brief-id>, add, commit,
push, gh pr create with the draft's summary and the brief's id and
approval lineage in the body. The brief STAYS drafted while the PR
is open (drafted -> published is its only legal exit; there is no
partial state for briefs) - record the PR url in the outcome record.
Merging is the human's final act: no publish without it.
- Merge detection, next run (
gh pr view <n> --json state,mergedBy):
merged -> PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -m core status <brief-path> published --actor <merger>; finish the outcome record
with merge time and measurement dates. Closed unmerged -> the brief
stays drafted; record the closure and leave the retry decision to a
human. On a pr-merge-channel site the merge doubles as the human
sign-off; the merge commit and the outcome record carry it.
- Verify, honestly:
rendered_head_verify is False - step 4's rendered
head check does not exist here. If the profile has cms.deploy_url,
fetch the expected live URL after merge detection and record a
best-effort post-deploy check, labeled exactly that. IndexNow only
when the post actually went live (draft: false and the site deployed),
after merge detection, same recording rules as step 4.
Dry-run: with onsite: {dry_run: true} in site-profile.yaml, run the same
gated flow with the adapter constructed dry_run=True - nothing is
created, the brief stays drafted, and the outcome record is marked
dry-run listing every write from the adapter's dry_run_log (see
skills/onsite-apply). With git-static, nothing written to the clone means
nothing to commit: no branch, no PR.
Never edit brain frontmatter directly. The contract CLI is the only write
path for status and approvals.
1---2name: onsite-publish3description: Use to publish an APPROVED, drafted content item via the CMS adapter (WordPress or git-static) - "publish the draft", /organic-os:publish. Refuses unapproved items.4---56# Publish content (gated)781. Input: a brief item with status "drafted" whose draft file sits next to it9 (same folder, <brief-name>.draft.md produced by ce-produce).102. Gate: the brief's status must be "drafted" (step 1 already checks) AND11 `core.contracts.require_approval_lineage(brief_path)` must pass; abort on12 ContractError. A rejected item must never reach `create_post`. The profile13 user must also confirm the final draft in-session or via channel.14 Refuse a draft whose frontmatter lacks `capsule: verified` (ce-produce's15 handoff note) - the human can override in-session, and the override is16 recorded in the outcome record.17 Read ce-editor's oversight block from the draft notes: a score at or18 above the profile's `editorial.oversight_threshold` (default 7) adds19 a prominent line to the approval-channel message - "editor recommends20 a human line-edit: <top factor>". Publishing remains gated by the21 same approval either way: the score informs the human, it does not22 add a second gate. Rationale: scaled, unedited AI content correlates23 with deindexation24 (https://www.rankability.com/data/does-google-penalize-ai-content/).253. Create the post via the CMS adapter (`onsite.cms.adapter_for`;26 wordpress or git-static per `cms.type` - the git-static branch below27 replaces steps 3-4): `create_post` (status draft by default; status28 "publish" only when site-profile sets publishing: direct), then29 `update_seo_meta` with title/description/focus keyword from the draft's30 frontmatter, and schema if the draft includes it. (`update_rankmath`31 remains as the WordPress adapter's alias for `update_seo_meta`.)324. Verify with `get_rendered_head`; on success `PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib"33 python3 -m core status <brief-path> published --actor agent` and34 write the outcome record with measurement dates; on failure leave the post35 in draft and record the post id + failure reason in the outcome record.36 IndexNow: when site-profile.yaml has `indexnow: {enabled: true, key: ...}`37 (additive key) AND the post actually went live (status "publish" - a WP38 draft has no public URL to submit), call `hoo.indexnow.submit(host, key,39 [post_url])` after the successful verify and record the returned status40 in the outcome record; a non-200 is recorded, never retried in-run.415. If a Canva image brief exists next to the draft (from ce-image), record its42 path in the outcome record for manual upload (media upload is not in v1).436. Outcome summary - the approver always hears what happened. Compose ONE44 message for the whole run and deliver it through the configured45 approval channel (same channel-neutral delivery as skills/onsite-apply46 step 4; in-session prints it). One line per item: each published post47 with its live URL; each post left in draft, held in a PR, or failed,48 with the reason and what happens next. No per-item message spam. A run49 that published nothing and failed nothing sends nothing.5051## Git-static sites (cms.type git-static)5253Steps 1-2 are identical: the drafted status check and54`require_approval_lineage` run BEFORE anything is written anywhere, and55the human confirms the final draft the same way. Then the PR replaces the56REST write; the adapter only writes files in the local clone57(`cms.repo_root`), and this skill runs every git/gh command:58593. Create the content file via the adapter: `create_post(slug, title,60 content)` - `draft: true` frontmatter by default, `status="publish"`61 (draft: false) only when site-profile sets publishing: direct. Then62 `update_seo_meta` with description/canonical from the draft's63 frontmatter, and `schema_jsonld` if the draft includes schema - it64 lands in the `jsonld` frontmatter field, which the site's layout must65 render into the head.664. Deliver as a PR: `git checkout -b organic-os/<brief-id>`, add, commit,67 push, `gh pr create` with the draft's summary and the brief's id and68 approval lineage in the body. The brief STAYS `drafted` while the PR69 is open (drafted -> published is its only legal exit; there is no70 partial state for briefs) - record the PR url in the outcome record.71 Merging is the human's final act: no publish without it.725. Merge detection, next run (`gh pr view <n> --json state,mergedBy`):73 merged -> `PYTHONPATH="$CLAUDE_PLUGIN_ROOT/lib" python3 -m core status74 <brief-path> published --actor <merger>`; finish the outcome record75 with merge time and measurement dates. Closed unmerged -> the brief76 stays drafted; record the closure and leave the retry decision to a77 human. On a pr-merge-channel site the merge doubles as the human78 sign-off; the merge commit and the outcome record carry it.796. Verify, honestly: `rendered_head_verify` is False - step 4's rendered80 head check does not exist here. If the profile has `cms.deploy_url`,81 fetch the expected live URL after merge detection and record a82 best-effort post-deploy check, labeled exactly that. IndexNow only83 when the post actually went live (draft: false and the site deployed),84 after merge detection, same recording rules as step 4.8586Dry-run: with `onsite: {dry_run: true}` in site-profile.yaml, run the same87gated flow with the adapter constructed `dry_run=True` - nothing is88created, the brief stays `drafted`, and the outcome record is marked89dry-run listing every write from the adapter's `dry_run_log` (see90skills/onsite-apply). With git-static, nothing written to the clone means91nothing to commit: no branch, no PR.9293Never edit brain frontmatter directly. The contract CLI is the only write94path for status and approvals.