| name | post-publish |
| description | Close the feedback loop. Canonize to Supermemory, update world-data, write ratings to sheets, grade reporters, update criteria files, update newsroom memory. Type-aware — edition, interview, supplemental, dispatch all converge here. |
| version | 1.12 |
| updated | "2026-07-11T00:00:00.000Z" |
| tags | ["media","active"] |
| effort | high |
| disable-model-invocation | true |
| argument-hint | [--type edition|interview|supplemental|dispatch] [--cycle XX] [--source <path>] |
/post-publish — Feedback Loop Closer
Purpose
The published artifact is canon-on-disk but not yet canon-in-memory. This skill canonizes it (bay-tribune, world-data), writes feedback (sheets, grades), and updates the files that train future cycles.
Everything this skill does makes the next cycle smarter than the last one.
This is the convergence point for editions and non-editions. /write-edition, /interview, /dispatch, /write-supplemental all converge here via --type. The format contract ([[EDITION_PIPELINE]] §Published .txt Format Contract) is the API: every type emits the same .txt shape, and post-publish reads from that uniformly.
These steps are base concepts. Each cycle exposes new gaps — a new ingest need, a feedback source we didn't account for, a step that needs splitting or combining. Evaluate after every run and update. Not done — compounding.
This is currently a teamwork skill — Mags and Mike walk through it together. As steps prove reliable, they automate one at a time.
Usage
/post-publish --type <type> --cycle <XX> --source <path>
Flags:
--type {edition|interview|supplemental|dispatch} — defaults to edition. Required for non-edition artifacts.
--cycle <XX> — engine cycle number. Required.
--source <path> — path to the published .txt. Defaults to editions/cycle_pulse_<type>_<cycle>_<slug>.txt for non-edition; editions/cycle_pulse_edition_<cycle>.txt for edition.
--skip-<name> — explicit opt-out for any substep that the type matrix marks ✓. Silent omission of a ✓ substep is a skill failure.
Per-type substep matrix
| Step | edition | interview | supplemental | dispatch |
|---|
| 0 staleness gate | — | — | — | — |
| 1a wiki ingest | ✓ | ✓ | ✓ | ✓ |
| 1b text ingest | ✓ | ✓ (transcript only — see §Step 1) | ✓ | ✓ |
| 1c NotebookLM push | ✓ (with --audio) | ✓ (source only) | ✓ (source only) | ✓ (source only) |
| 2a citizen cards | ✓ | ✓ | ✓ | ✓ |
| 2a-cul cultural cards | — | ✓ (when CUL-IDs present) | ✓ (when CUL-IDs present) | ✓ (when CUL-IDs present) |
| 2e voice reflection ingest | — | ✓ (citizen-voice subjects only) | — | — |
| 2b new businesses | ✓ | ✓ | ✓ | ✓ |
| 2d truesource sweep | ✓ | — | ✓ | — |
| 2c world summary | ✓ | — | — | — |
| 3 civic wiki | ✓ | — | — | — |
| 4 coverage ratings | ✓ | — (C93-gated) | — (C93-gated) | — (C93-gated) |
| 5 citizen+business intake | ✓ | ✓ | ✓ | ✓ |
| 5-bis wd-card rebuild (new POPIDs) | ✓ | ✓ | ✓ | ✓ |
| 6 grade | ✓ | — | — | — |
| 7 grade history | ✓ | — | — | — |
| 8 exemplars | ✓ | — | — | — |
| 9 newsroom memory | ✓ | ✓ | ✓ | ✓ |
| 10 criteria files | ✓ | — | — | — |
| 10b archive briefing bundle | ✓ | — | — |
— = skipped by default for that type (no flag needed; declared by the matrix).
C93-gated = skipped pending the C93 observation outcome (plan [[plans/2026-04-26-non-edition-publishing-pipeline]] T8). The --skip-<name> flag is only required to opt out of substeps the matrix marks ✓ for the chosen type.
Prerequisites
Verify these exist before starting (path varies by --type):
<source> — published .txt (the canonical artifact)
output/production_log_c{XX}.md — consolidated per-cycle production log (S195 convention; merges city-hall-prep + city-hall-run + sift + write-edition + post-publish into one log per cycle)
output/world_summary_c{XX}.md — world summary
For --type interview: <source> IS the transcript at editions/cycle_pulse_interview-transcript_{XX}_<slug>.txt (capture-only since /interview v2.0, S233 pipeline.30 — the legacy bare article cycle_pulse_interview_*.txt is retired). No companion artifact; single canonical transcript.
Parallelization Notes (S215, closes G-P5)
Several substeps are independent and can run concurrently to compress wall time. Default to running these in parallel via separate background invocations:
| Group | Substeps | Constraint |
|---|
| Parallel-OK | 2a citizen cards, 2c world summary, 4 coverage ratings, 1c NotebookLM push | No interdependency. Each writes a different surface (world-data citizens, world-data summary, sheets, NotebookLM). Run concurrently. 1c's audio wait (~2-6 min) hides entirely inside 2a's 30-40 min wall time. |
| Sequential after parallel group | Step 5 (citizen+business intake) | Depends on Step 2a completion via the verification gate (NAMES INDEX presence + parsed count). |
| Sequential after Step 5 | Step 5-bis (wd-card rebuild for newly-appended POPIDs) | Depends on Step 5 output JSON for appended[].popid range. Closes G-P32 one-cycle card lag per [[../../../docs/adr/0007-cross-layer-canon-authority-precedence |
| Sequential after Step 5-bis | Steps 6 (grade), 7 (grade history), 8 (exemplars) | Depend on Step 5's intake JSON for entity attribution; Step 5-bis must complete first so newly-appended citizens have wd-cards for grade lookups. |
| Parallel-OK with the grade group | Step 9 (newsroom memory), Step 10 (criteria files) | Independent of the grade-chain output; can run alongside Steps 6-8. |
| Sequential at end | Step 11 (filing + bot), Step 12 (production log), Step 13 (checklist) | Final state-of-the-skill steps; run after all prior substeps complete. |
Wall-time saving on edition runs: ~60-90s (Step 2a citizen cards is the slowest substep; running 2c + 4 against its wall time recovers their duration).
Steps
Step 0: Pre-flight Staleness Gate — RETIRED S225 (pipeline.23, closes G-P28)
Retired. The S215 staleness gate modeled the wrong dependency direction. scripts/checkPostPublishStaleness.js compared world_summary + engine_audit mtimes against the /city-hall production log, but world_summary is INPUT to /city-hall (built before it), not output — so the gate fired STALE every cycle by design flaw (G-P28, G-S11). The advisory line "Re-run /build-world-summary before continuing" misled the operator into spurious rebuilds; Mike spent cycles escalating "should we rebuild?" with no real signal underneath.
If real sequencing concerns arise (e.g., engine produced new state but world_summary wasn't rebuilt), compare output/world_summary_c<XX>.md mtime against output/engine_audit_c<XX>.json manually — that's the correct upstream baseline. Engine-sheet may retire scripts/checkPostPublishStaleness.js + lib/staleness.js in a future sweep (vestigial after this retirement).
Step 1: Bay-Tribune Ingest — Canon
1a. Wiki ingest — per-entity records (PRIMARY)
node scripts/ingestEditionWiki.js <source> --type <type> --apply
Extracts per-citizen, per-initiative, per-business, per-storyline records from the standardized sections (NAMES INDEX, BUSINESSES NAMED) into bay-tribune. Each entity gets its own searchable record. Log entity count.
Verification gate: stdout reports entity count ≥ 1 OR explicit "0 entities — body had no named citizens/businesses" (acceptable for pure-atmosphere dispatches). Fail otherwise.
Review: spot-check 2-3 records against the article body.
1b. Text ingest (BACKUP) — single canonical home (S215, closes G-P1)
Step 1b is the only home for edition text ingest. As of S215, /write-edition Step 6 no longer runs ingestEdition.js at close — running it both there and here produced duplicate bay-tribune text records (different doc IDs, same content) that polluted future sift queries. If /write-edition close already ingested text for some reason (rare emergency backfill), skip this substep and document in the production log.
node scripts/ingestEdition.js <source> --type <type> --cycle <XX>
Full text chunked to bay-tribune. Wiki records rank higher for entity searches; text chunks are backup for full-text queries. Log doc IDs.
For --type interview (post /interview v2.0, S233 pipeline.30): the <source> IS the transcript — no companion re-run. Step 1b runs once against the transcript .txt. The legacy two-invocation pattern (article + companion transcript) is retired; /interview v2.0 emits only the transcript.
Verification gate: bay-tribune doc ID returned (single ID — for interview, the transcript .txt is the canonical artifact, no companion article exists post /interview v2.0). Fail if the API call returns no ID or an error string.
Note: Podcast runs AFTER post-publish as a separate creative process. Podcast handles its own bay-tribune ingest.
1c. NotebookLM push — Mike's ingest surface (S310, research.23)
node scripts/notebooklmPush.js --file <source> --cycle <XX> --audio
node scripts/notebooklmPush.js --file <source> --cycle <XX>
Pushes the artifact into the Bay Tribune notebook (Mike's actual reading/listening path). Edition runs additionally generate the audio overview, deliver it to Drive + Discord (both drops, Mike-direct S310), and capture the notebook's own summary to output/nlm_summary_c<XX>.md.
NON-BLOCKING BY CONTRACT: the script exits 0 on every runtime failure (auth expiry — NotebookLM cookies rot every 2–4 weeks; rate limits; internal-API drift) and prints NOTEBOOKLM ... (non-blocking): warnings. A warning here never halts /post-publish — log it in the production log and continue. If auth expired: Mike refreshes the cookie string (DevTools → notebooklm.google.com → copy cookie: header value → /root/.nlm/cookies.txt), then nlm login --manual -f /root/.nlm/cookies.txt.
Verification gate: stdout ends with NotebookLM push complete OR a (non-blocking) warning — both pass. Anything else (non-zero exit) is a real script defect, not a bridge failure.
Parallel-OK: independent of the citizen/grade chain — run alongside Step 2a.
Step 2: World-Data Updates — City State
2a. Refresh citizen cards
node scripts/buildCitizenCards.js --apply
Citizens who appeared get updated profiles in world-data. New citizens get cards built. Verify new citizens have gender from ledger.
Why --apply: the script defaults to dry-run (writes nothing). Without the flag the substep silently does no real work. (G-P12, S195 — caught on E93 when --apply surfaced the 401 retry storm; previous cycles likely shipped dry-runs masquerading as success.)
Verification gate (strengthened S230 per canon.3 / ADR-0007): ALL THREE conditions must pass — (a) stdout reports refreshed/created card count ≥ 1 when NAMES INDEX is non-empty (or "0 citizens to refresh — empty NAMES INDEX" if empty); (b) stdout reports Errors: 0; (c) script exit code is 0. Any failing condition aborts /post-publish at Step 2a with a pointer to output/citizen_card_failures_c<XX>.json for engine-sheet diagnosis. The failure-list dump + non-zero exit ship via engine-sheet T6 (canon.3 plan); pre-T6 the script still exits 0 on partial failure, so the gate is partially load-bearing until T6 lands. Closes G-P34 lineage (S195 G-P12 → S222 G-P34 → S223 chunking fix → S230 gate strengthening).
Why this gate strengthening matters (per ADR-0007): wd-citizens is the derived lookup layer that reporters + Mara + sift Step 4 enrichment hit via MCP lookup_citizen. A 19% partial-write failure (S222 C94 empirical) means wd-citizens has a silent gap per cycle that compounds — same shape as the bay-tribune-only-citizens drift the ADR governs. Non-zero exit + failure dump make the gap visible immediately instead of catching it three cycles later when a reporter notices a missing card.
2a-cul. Refresh cultural cards (--type {dispatch|interview|supplemental} — only when NAMES INDEX contains CUL-IDs)
node scripts/buildCulturalCards.js --apply --cul <CUL-ID>
buildCitizenCards.js is Sim_Ledger-only — it cannot refresh cultural-only entities (musicians, artists, public figures registered in wd-cultural without a Sim_Ledger row). When the artifact's NAMES INDEX names cultural figures (e.g., S188 KONO Second Song dispatch named Marin Tao POP-00537 + Brody Kale CUL-905CBDE8), this substep refreshes their wd-cultural cards so the appearance count increments. Citizens with both POP- and CUL- IDs (Beverly Hayes is both citizen AND cultural figure) get both cards refreshed — Step 2a handles the citizen card via POPID; this substep handles the cultural card via CUL-ID.
Skipped for --type edition by default — edition NAMES INDEX entries are typically Sim_Ledger citizens with established cards. If a future edition surfaces CUL-only entries, the substep activates the same way (matrix flip + run).
Verification gate: for each CUL-ID in source NAMES INDEX, lookup_cultural(name) returns a card with last_appeared reflecting this cycle. If no CUL-IDs in NAMES INDEX, substep skipped with stdout "0 cultural-only entries — substep N/A this artifact." Plan reference: [[archive/plans/2026-04-30-dispatch-gap-followups]] Task E6 (closes the Brody Kale unrefreshed gap from S188).
2e. Voice reflection ingest (--type interview — only when an interview subject is a citizen-voice agent, i.e. their POPID appears in a .claude/agents/citizen-voice-*/IDENTITY.md)
engine.43 T5: an interview becomes real lived history for a voiced citizen — their answers drain into the same Reflection_Intake pipeline the 24/7 wake loop writes, so the next cycle's drain nudges their dials and their tension register carries across surfaces (a tension opened at a wake can resolve in an interview, and vice versa).
For each citizen-voice subject:
- Extract that citizen's OWN answers from the transcript (never the reporter's questions, never another subject's words — S298: the other party's words must not color the dial nudge) into a scratch file, answers only, in order.
- Drain (dry-run first, then live):
node -e "require('/root/GodWorld/lib/env');require('/root/GodWorld/lib/personaProvider').drainConversationReflection_('<POPID>', require('fs').readFileSync('<answers-file>','utf8'), 'INTERVIEW', {dryRun:true}).then(r=>console.log(JSON.stringify(r)))"
One Reflection_Intake row per citizen interviewed (daypart='INTERVIEW', applied='no', gated — the cycle drain picks it up like any WAKE row). Tension open/resolve mirrors wake step 3 (cap 3, typed page lines). Re-running the same transcript is a no-op (duplicate: true — snippet-matched idempotency guard). Skipped with "0 citizen-voice subjects — substep N/A" when no subject is a voiced agent.
Voice-packet dedup (pipeline.43 Task 5). Citizens whose published quotes came through the /write-edition quote-supply pre-pass already own their intake row — citizenVoice.js --record wrote the page doc (daypart='PRESS') + gated Reflection_Intake row at speak time. If output/voices/voices_c{XX}.json exists and lists the citizen (non-fallback entry), 2e must NOT write a second reflection row for the same quote — log 2e skip: quote-supplied via voice packet per citizen. Interview-format subjects (premium /interview path) keep the 2e backfill unchanged. Citizen usage tracking (NAMES INDEX / usage log / fame) is publication-side and stays untouched — it records printing, not speaking.
Verification gate: live run returns {"drained":true,...} per citizen-voice subject (or duplicate:true on re-run); no page-prose writes beyond typed tension lines (ADR-0014 — conversational prose never lands on the Supermemory citizen page).
2b. New businesses
Flagged here for visibility; actual writes happen in Step 5 via ingestPublishedEntities.js (which reads BUSINESSES NAMED and appends new entries to Business_Ledger).
Verification gate: BUSINESSES NAMED parsed; NEW rows count logged to production log Step 12 — actual append verification lives in Step 5's output JSON.
2d. Refresh A's player truesource — incremental sweep (--type edition and --type supplemental)
node scripts/ingestPlayerTrueSource.js --apply --skip-subfolder --include-flat --include-prospects
Walks MLB_Roster_Data_Cards/ (top-level flat files) + Top_Prospects_Data_Cards/ Drive folders for new TrueSource v1.0 attribute sheets, POP-ID DataPages, or per-season intake docs. Ingests each as a wd-player-truesource-tagged supplemental memory in world-data keyed by POPID. MCP lookup_citizen retrieves these for elite Tier-1/Tier-2 A's players alongside the standard wd-citizen card.
Why --skip-subfolder: Pass A (the heavy True_Source/ subfolder walk per player) was bootstrapped S180 (commit c15050f — 9/10 elite players ingested). Subsequent runs use this lighter flag set to pick up only newly-added flat-files + new Top_Prospects passes; avoids re-ingesting Pass A docs every cycle.
Why two paths: flat MLB_Roster_Data_Cards/*.txt covers established roster updates; Top_Prospects_Data_Cards/ covers minor-league prospects who get promoted. --include-flat --include-prospects activates both.
Skipped for interview + dispatch. Those types rarely surface MLB roster changes; if a future dispatch covers a roster move (trade, call-up), Mike can re-run the script manually with the same flags or extend the matrix.
Verification gate: stdout reports per-player ingest count + POPID resolution table OR "0 new truesource files — no Drive updates this run" when nothing has changed. output/intake_player_truesource.json written with full resolution detail. Failure modes (Drive auth, Supermemory 401, POPID resolution gaps) surface to production log Step 12.
2c. World summary ingest (--type edition only)
source ~/.bashrc && curl -s -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_CC_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--rawfile content output/world_summary_c{XX}.md \
--arg cycle "<XX>" \
'{content: $content, containerTags: ["world-data", "wd-summary"], metadata: {type: "cycle_summary", cycle: $cycle}}')"
World summary is per-cycle, not per-artifact. Non-edition types skip this without flag. Tag pair ["world-data", "wd-summary"] (S184) — the broad world-data tag keeps existing search_world queries working; the wd-summary tag enables filtered retrieval (find me only the summaries, not the entity cards). See [[../../../docs/SUPERMEMORY|SUPERMEMORY]] §Search/save matrix.
Then ingest the one-line snapshot as its own memory (v1.12 / S313). buildWorldSummary.js v1.2.0 emits a stable Snapshot: Cycle {XX} | Pop … | Illness … | … line in the summary header; grep extracts it deterministically — do NOT compose the snapshot by hand:
source ~/.bashrc && curl -s -X POST "https://api.supermemory.ai/v3/documents" \
-H "Authorization: Bearer $SUPERMEMORY_CC_API_KEY" \
-H "Content-Type: application/json" \
-d "$(jq -n \
--arg content "$(grep -m1 '^Snapshot: Cycle ' output/world_summary_c{XX}.md)" \
--arg cycle "<XX>" \
'{content: $content, containerTags: ["world-data", "wd-snapshot"], metadata: {type: "cycle_snapshot", cycle: $cycle}}')"
One compact memory per cycle — the cheap "where are we now" anchor for the Discord bot, citizen-loop, and agents that don't need a full summary chunk (Sonnet-agent Supermemory advice, S313). Tag wd-snapshot keeps it out of wd-summary filtered retrieval; the broad world-data tag keeps search_world working. Skip (with a production-log note) if the grep returns empty — that means the summary predates writer v1.2.0.
Verification gate: doc ID returned for both POSTs (summary + snapshot; snapshot gate waived only on the pre-v1.2.0 grep-empty skip).
Step 3: Civic Wiki — Per-Official Records (--type edition only)
node scripts/ingestCivicWiki.js --cycle <XX> --apply
Shipped S215 (closes pipeline.22 / G-P4). Reads output/civic-voice/<office>_c<XX>.json (per-voice statement arrays — Mayor, factions, projects, Clerk, Chief, DA) + output/city-civic-database/initiatives/*/decisions_c<XX>.json (per-initiative consolidated decision) and emits per-statement + per-decision wiki records to bay-tribune. Each statement memory carries speaker / office / topic / position / quote / reasoning / related initiatives in content body for full-text indexing; metadata carries recordType: civic-statement (or civic-decision), cycle, statementId, office, popId, topic, relatedInitiatives for filtered retrieval.
Use this so next-cycle sift can ask "what has Mayor Santana said about Health Center across cycles" via bay-tribune search instead of re-reading every civic voice JSON. Default is --dry-run; pass --apply to write.
Verification gate: stdout reports [DONE] Written: N, Errors: 0 AND N matches the pre-write Total memories: N line. Non-edition types skip this step (civic wiki is per-cycle, edition-aligned). Skip annotated in production log Step 12.
Step 4: Coverage Ratings to Sheet
node scripts/rateEditionCoverage.js <source> --type <type> --apply
For --type edition: per-domain ratings (-5 to +5) written to Edition_Coverage_Ratings sheet. Engine reads these in Phase 2 next cycle — the city reacts to what the newspaper published.
For non-edition types: gated on the C93 observation outcome (plan T8). Default behavior: skip with stdout "non-edition coverage rating gated on C93 observation — see plans/2026-04-26-non-edition-publishing-pipeline.md T8." If the C93 observation determines non-edition coverage is needed, this gate flips to active.
Verification gate: edition: sheet row count increased OR stdout reports per-domain rating list. Non-edition: explicit gate-skip message logged.
Step 5: Citizen + Business Intake to Sheets
node scripts/ingestPublishedEntities.js editions/cycle_pulse_<type>_<cycle>_<slug>.txt --type <type> --cycle <XX> --apply
Reads NAMES INDEX + BUSINESSES NAMED sections from the published .txt and emits entities to canonical engine sheets:
- Intake tab (citizens, engine.51 T8) — genuinely-NEW extracted citizens emit lean 9-col
Intake rows (First/Last/Notes only), NOT direct Simulation_Ledger writes; the engine's processIntake_ mints them (POPID + demographics + income) deterministically next cycle. Existing citizens are NOT emitted — their UsageCount is already bumped by the cycle-path processMediaUsage_ off the same media paste the edition is compiled from, so re-emitting here would double-count fame. Canon-drift (bay-tribune-known, SL-missing) stays deferred to engine-sheet backfill per [[../../../docs/adr/0007-cross-layer-canon-authority-precedence|ADR-0007]] — not routed to Intake. Consequence: new-citizen POPIDs + wd-cards lag one cycle (POPID assigned at engine mint, not at extraction). Existing-citizen card refresh (Step 2a) is unaffected.
- Business_Ledger — new businesses land direct with cols A–D populated, E–I blank for engine fill (not a citizen path; outside the "zero SL writes" invariant).
Handles two NAMES INDEX formats: T1 strict (POP-NNNNN | Name | Role) and pre-T1 freeform (Name — Description). Plus a CITIZEN USAGE LOG fallback (S197 BUNDLE-A): when NAMES INDEX is absent (pre-S197 editions, or any cycle that skipped /write-edition Step 3a inject), the script derives entity rows from the rich-prose CITIZEN USAGE LOG so backfill replay still lands the canonical sheets correctly. BUSINESSES NAMED also fallback-resolves from CITIZEN USAGE LOG (NEW CANON) sub-sections; absent strict section + no fallback returns 0 gracefully.
Default mode is --dry-run; pass --apply to write. Output: output/intake_published_entities_c<XX>_<slug>.json with full resolution detail (matched / candidates / canon-drift / ambiguous / phantom) plus intakeRows (planned lean rows, inspectable in dry-run) and intakeVerified (readback, populated on --apply).
Canon-drift is a HANDOFF, not a log line (S345, C101-Abraham precedent). The canon-drift bucket writes output/canon_drift_c<XX>.json — but nothing downstream reads that file. If the bucket is non-empty, this run must do BOTH: (1) list the drift names + suggested actions in the publish close report shown to the builder, and (2) append them to the NEXT[engine-sheet] handoff line (or say plainly "N canon-drift citizens need engine-sheet backfill — names in canon_drift_c.json"). Writing the JSON alone is a silent parking lot: Elliot Abraham was flagged backfill at C101 publish and sat unminted until S345; a C95–C101 sweep found 9 more names parked the same way (ENGINE_REPAIR row R-CANON-DRIFT). Name-variant check before treating a drift name as missing: accent/hyphen forms (Tran-Munoz vs Tran-Muñoz, Soria Dominguez vs Soria-Dominguez) resolve to existing SL rows.
Verification gate (defense-in-depth, S189 dispatch gap E8 + S197 BUNDLE-A --strict): after ingestPublishedEntities.js finishes, cross-check its parsed entity count against the source .txt directly:
node scripts/verifyNamesIndexParse.js <source> --expected <N> --strict
Where <N> = the parser's total parsed-row count that ingestPublishedEntities.js reports — every NAMES INDEX row it parsed. Use that total directly; do NOT reconstruct it by summing the resolution buckets (matched + candidates + ambiguous + phantom + appended). That sum omits the canon-drift bucket, which IS a parsed row (an existing-in-bay-tribune / missing-in-Sim_Ledger citizen, per [[../../../docs/adr/0007-cross-layer-canon-authority-precedence|ADR-0007]]); bucket-summing under-counts by the number of canon-drift rows and the gate then false-fails on the off-by-one against the independent NAMES INDEX row count. (RB-4, C98 G-P-C98-2.) The helper independently counts NAMES INDEX rows in the source .txt. Exit code 0 = counts match AND section present; exit code 1 = mismatch OR --strict and NAMES INDEX section absent (FAIL LOUDLY, block publish).
Why --strict exists (S197 BUNDLE-A): the C93 silent-no-op pattern was missing-section + zero-expected resolved to exit 0. /write-edition emitted CITIZEN USAGE LOG only (no NAMES INDEX, no BUSINESSES NAMED), ingestPublishedEntities silently returned 0 entities, verifyNamesIndexParse said [OK] no mismatch against expected=0 — and 5 entities (3 new citizens + Atlas Bay Architects + Greater Hope Pentecostal) silently dropped from canonical sheets. --strict requires NAMES INDEX section to be present; if absent the gate fails with a pointer to run emitFormatContractSections.js --inject upstream.
Why the count-match gate exists (S189 dispatch gap E8): S188 KONO Second Song dispatch run had ingestEditionWiki.js + ingestPublishedEntities.js both silently return 0 entities despite valid POP-00537 + CUL-905CBDE8 NAMES INDEX rows (false "pure-atmosphere artifact" success). Engine-sheet's E1+E2 fixed the parsers; this gate prevents future parser regressions from re-introducing silent data loss. Plan reference: [[archive/plans/2026-04-30-dispatch-gap-followups]] Task E8.
Combined gate:
output/intake_published_entities_c<XX>_<slug>.json written; if --apply, intakeVerified rows show all verified-by-readback (ok: true). appended is intentionally empty — the media lane no longer writes Simulation_Ledger (engine.51 T8).
verifyNamesIndexParse.js --expected <N> --strict exits 0 (source NAMES INDEX present AND row count matches parser-reported count).
If gate 2 fails with "NAMES INDEX section absent": run node scripts/emitFormatContractSections.js <source> --inject to derive the strict sections from CITIZEN USAGE LOG, then re-run /post-publish Step 5. If gate 2 fails with "mismatch": investigate the parser regression before continuing — silent zero-entity ingest poisons bay-tribune retrieval.
Step 5-bis: Build wd-cards for newly-appended POPIDs (all types, when Step 5 appended new citizens)
engine.51 T8 note (S305): under Intake-tab routing, Step 5 no longer appends new POPIDs — new citizens are minted by the engine's processIntake_ NEXT cycle, so appended is now always empty for the media citizen lane and this substep always self-skips. New-citizen wd-cards therefore build one cycle later (at the next cycle's Step 2a, after the engine mint). This re-opens the one-cycle card lag that G-P32 previously closed same-session — an accepted, unavoidable tradeoff of the deterministic intake front door (POPIDs are engine-assigned, not extraction-assigned). Existing-citizen cards are refreshed normally in Step 2a. The substep is retained for the (non-citizen) edge case where appended is ever non-empty.
Original purpose (pre-T8): closes G-P32 one-cycle card lag per [[../../../docs/adr/0007-cross-layer-canon-authority-precedence|ADR-0007]] §How to Apply. Step 2a's buildCitizenCards.js ran BEFORE Step 5 appended new POPIDs; without this substep, newly-published citizens had NO wd-card until next cycle's Step 2a runs.
Reads Step 5 output JSON (output/intake_published_entities_c<XX>_<slug>.json) for appended[].popid. If appended is empty, substep is skipped with stdout "0 new POPIDs — Step 5-bis N/A this artifact." (Post-T8: this is the normal path.)
If appended has 1+ POPIDs:
node scripts/buildCitizenCards.js --apply --popid-range POP-XXXXX:POP-YYYYY
Where XXXXX:YYYYY is the min/max of appended POPIDs. The --popid-range flag shipped S223 (engine.18) for surgical rebuilds.
Verification gate (same shape as Step 2a strengthened gate): ALL THREE conditions — (a) stdout reports Written: N matching the appended POPID count; (b) stdout reports Errors: 0; (c) script exit code 0. Per ADR-0007 §How to Apply: this substep + Step 2a together enforce that every Sim_Ledger row has a wd-card within the same /post-publish run.
Why this runs after Step 5 (not folded into Step 2a): Step 2a runs before the canonical artifact has been ingested to bay-tribune; new citizens aren't in Sim_Ledger yet so there's nothing to build. Step 5 appends to Sim_Ledger; Step 5-bis builds the matching wd-cards. Sequencing is structural, not optional.
Pre-T6 caveat (canon.3 plan): until engine-sheet T6 ships the Errors-gate non-zero exit on buildCitizenCards.js, this substep can silent-partial-fail the same way Step 2a can. Production log Step 12 must log the appended POPID list + the substep's stdout summary so operator can manually inspect.
Step 5b: Refresh base_context.json + desk packets (all types)
node scripts/buildDeskPackets.js <XX>
Rebuilds output/desk-packets/base_context.json (the cycle source-of-truth that lib/mags.js loadWorldState() reads for the Discord bot's hourly system-prompt rebuild) plus the 9 desk packets. Without this step, the Discord bot reports a stale cycle until the next manual cycle run.
Side effect: takes ~60 seconds. Harmless — desk packets get rebuilt anyway pre-cycle, and the bot's worldview catches up on its next hourly tick.
Plan reference: [[../../../docs/plans/2026-04-26-discord-bot-edition-currency]] Task 1 (S180 surfaced; S184 wired here).
Verification gate: output/desk-packets/base_context.json mtime updated; the cycle field matches <XX> — the field is nested, so check jq '.baseContext.cycle' output/desk-packets/base_context.json, NOT jq '.cycle' (top-level .cycle returns null and false-alarms; the top-level keys are baseContext / bondStats / canon / householdStats). (G-P-C99-1.)
Step 6: Grade Edition (--type edition only)
node scripts/gradeEdition.js editions/cycle_pulse_edition_<XX>.txt <XX>
Per-desk and per-reporter grades from edition text + errata + Mara audit.
Why the path arg (G-P13, S195): script signature is <edition-file-path> [cycle]. Passing only <XX> makes it try to open /root/GodWorld/93 and exit "Edition file not found." Always pass the full path.
Verification gate: output/grades/grades_c<XX>.json written.
Step 7: Update Grade History (--type edition only)
node scripts/gradeHistory.js
Rolling averages (5-edition window), trends, roster recommendations.
Verification gate: output/grades/grade_history.json mtime updated.
Step 8: Extract Exemplars (--type edition only)
node scripts/extractExemplars.js editions/cycle_pulse_edition_<XX>.txt <XX>
A-grade articles become desk workspace exemplars for future cycles.
Why the path arg (G-P19, S195): same signature pattern as gradeEdition.js — script wants <edition-file-path> [cycle]. Passing only <XX> bombs identically.
Verification gate: stdout reports exemplar count ≥ 0; if A-grade articles existed in grades_c<XX>.json, exemplar count > 0.
Step 9: Update Newsroom Memory
Read the consolidated production log as source (S195 convention; single per-cycle log):
output/production_log_c<XX>.md — story picks, editorial review, voice decisions, tracker updates, what was dramatic, Mara corrections, Rhea flags. All cycle phases merged.
Distill into docs/mags-corliss/NEWSROOM_MEMORY.md:
- Errata from this artifact (what Mara caught, Rhea caught, Mags caught in review)
- Coverage patterns (which sections ran, which didn't, what was thin)
- Character continuity (which citizens appeared, what was established about them)
- Active arcs (what advanced, opened, closed, was promised)
- Previous artifact summary (so next sift knows what was covered)
Verification gate: NEWSROOM_MEMORY.md mtime updated; new section added for this cycle/artifact.
Step 10: Update Criteria Files (--type edition only)
Grade-conditional /skill-check gate (G-P-NEW4, S246 — token-burn). /skill-check is a model-reasoning task per target; running it ×5 every cycle is high-cost / low-marginal-value when the edition already graded clean and the direct-evidence sources (grades, production log, Mara corrections, this conversation) already carry the editorial findings. Gate on the edition's Final Arbiter verdict (output/final_arbiter_c<XX>.json):
- Verdict A (PROCEED, weighted ≥ 0.75) → SKIP /skill-check ×5 by default. The structural assertions are assumed-pass on a clean edition; write the criteria-file updates directly from the direct-evidence sources below. Log "skill-check skipped — edition graded A (Arbiter PROCEED {score}); criteria updated from direct evidence" in the production log.
- Verdict A with PROCEED-WITH-NOTES (0.60–0.75) or Verdict B (HALT/below) → RUN the full /skill-check ×5 suite. A sub-clean grade is exactly when the structural assertion pass earns its cost — it surfaces which skill mechanically drifted.
- Operator override either way: if a specific skill felt off this cycle regardless of grade, run /skill-check on that one target. Don't run all five to check one.
(Alternative consolidation if you do run them: batch into a single multi-skill model call rather than 5 separate invocations — same structural value, lower token cost.)
When the gate says RUN — /skill-check for each skill that produced a cycle artifact — S216 governance.2 extended skill-check from 2 targets (write-edition + sift) to 5 (added city-hall + dispatch + interview). For each target, check whether the cycle has the relevant artifact; if yes, run /skill-check; if no, skip with one-line "no artifact this cycle" note.
| Skill | Artifact check (run /skill-check if exists) | Writes |
|---|
write-edition | output/production_log_c<XX>.md has a ## /write-edition section (legacy fallback: production_log_edition_c<XX>.md exists) | output/skill_check_write-edition_c<XX>.json |
sift | output/sift_proposals_c<XX>.json exists | output/skill_check_sift_c<XX>.json |
city-hall | output/production_log_c<XX>.md has a ## /city-hall section (legacy fallback: production_log_city_hall_c<XX>.md exists) | output/skill_check_city-hall_c<XX>.json |
dispatch | editions/cycle_pulse_dispatch_c<XX>_*.txt exists | output/skill_check_dispatch_c<XX>.json |
interview | editions/cycle_pulse_interview-transcript_c<XX>_*.txt exists | output/skill_check_interview_c<XX>.json |
Each /skill-check JSON is a feedback source: its assertions, reviewerOverlap, and evalFeedback tell you exactly which criteria held and which need sharpening.
Read these feedback sources:
- Each
output/skill_check_<skill>_c<XX>.json produced this step — structural assertion pass/fail + eval-feedback suggestions
output/grades/grades_c<XX>.json — per-reporter scores
output/production_log_c<XX>.md — Mara corrections, Rhea flags, editorial review
- Mike's feedback — capture in conversation
Update each criteria file with specific findings based on its /skill-check output:
docs/media/story_evaluation.md (write-edition + sift): which priority signals predicted strong articles, did front-page scoring match the actual best article, weak story indicators to add.
docs/media/brief_template.md: which briefs produced articles that followed closely, which produced drift, word-count tuning.
docs/media/citizen_selection.md: any citizen misrepresented (role, neighborhood, gender, history), freshness balance, any reporter invented details contradicting canon.
docs/media/city_hall_evaluation.md (city-hall, S216): which criteria held / failed for this cycle's civic source-material run; any new failure pattern to encode as a criterion.
docs/media/dispatch_evaluation.md (dispatch, S216): word-count band tuning (criterion 4 is intentionally provisional), location-specificity examples, identity-through-action vs data-dump patterns observed.
docs/media/interview_evaluation.md (interview, S216): voice-differentiation sharpening (criterion 9 is intentionally subjective), world-altering canon flagging patterns, archetype-match observations.
Pattern from skill-creator eval framework (S141): set assertions, grade each assertion against the actual edition, update criteria with what passed and failed. Over cycles the criteria files become the trained editorial standard.
Each updated criteria file gets a changelog entry at the bottom:
## Changelog
- C<XX>: [what changed] — [why, based on what evidence]
Verification gate: for each skill whose artifact existed this cycle, a corresponding /skill-check JSON exists in output/ AND its assertion file has a new changelog entry dated this cycle. Skills whose artifact didn't exist this cycle are skipped (one-line skip note in the post-publish production log).
Step 10b: Archive Briefing Bundle (--type edition only — pipeline.11, briefing versioning)
node scripts/archiveBriefingBundle.js <XX>
Copies each desk's live bundle (output/desks/{desk}/current/*) to a cycle-keyed
archive (output/desks/{desk}/archive/c<XX>_bundle/) so retro audits can ask
"did the briefing cause the grade?" across cycles. Pure additive copy — no live-data
mutation; idempotent (re-running replaces that cycle's bundle dir). --dry-run previews.
Verification gate: stdout reports N desk(s) archived (expect 6: business / chicago /
civic / culture / letters / sports) with 0 skipped, and output/desks/business/archive/c<XX>_bundle/
contains briefing.md + packet.json + summary.json. A desk skipped means its
current/ bundle was missing this cycle — note it in the Step 12 production log.
Step 11: Filing + Cleanup
node scripts/postRunFiling.js <XX>
Verify all pipeline outputs exist and names are correct.
Known stale manifest (G-P22, S195 — engine-sheet handoff Wave 3 BUNDLE-H): the script's required-files manifest reflects pre-pipeline-v2 state and reports INCOMPLETE every cycle on artifacts that don't exist anymore (output/desk-output/{civic,sports,...}_c<XX>.md from before S134 + output/photos/e<XX> and PDF paths that require /edition-print to have run separately). False-alarm fatigue — every cycle reports failing files. Until the manifest is updated, treat the INCOMPLETE message as advisory; the actual pipeline-v2 outputs (output/reporters/*/articles/c<XX>_*.md, the consolidated production log, the published .txt) are what matter.
Bot restart deferred to /session-end (G-P23, S195). The previous version of this step ran pm2 restart mags-bot here, but that conflicts with the boot/session-end RAM lifecycle (boot stops bot+dashboard; session-end restarts them). Bot doesn't need to be up until a citizen messages it on the next hourly tick — restart timing is functionally equivalent. Single canonical restart home: /session-end.
Verification gate: postRunFiling.js exit 0. (Bot restart no longer in this step — verified at /session-end.)
Step 12: Finalize Production Log — Wiki Pattern
Append a Post-Publish section to output/production_log_c<XX>.md with inline Supermemory doc IDs. Each canonized artifact gets a doc ID on its line so next cycle queries the record directly instead of re-reading files.
## Post-Publish C<XX> — <TYPE> COMPLETE
### Source
- Artifact: <source path>
- Type: <type>
### Bay-Tribune Ingest
- Wiki entities: {count}
- Article text: {doc ID} (edition / supplemental / dispatch)
- Transcript text: {doc ID} (interview type — single canonical artifact post /interview v2.0)
- Civic wiki: {doc ID} (when 1c is built)
### World-Data Ingest
- World summary: {doc ID} (edition only)
- Citizen cards refreshed: {count}
- New businesses flagged: {count}
- Pending engine-sheet intake: {citizen/business rows}
### Sheet Writes
- Edition_Coverage_Ratings: {domains + ratings written, OR "C93-gated skip" for non-edition}
### Grading (edition only)
- Grades: output/grades/grades_c<XX>.json
- Grade history updated
- Exemplars extracted: {count}
### Criteria Files Updated (edition only)
- story_evaluation.md: {changelog entry added}
- brief_template.md: {changelog entry added}
- citizen_selection.md: {changelog entry added}
### Newsroom Memory
- Updated with errata, coverage patterns, active arcs
The doc IDs embedded here are the query keys. Next cycle's sift reads this log first, uses the doc IDs for direct Supermemory queries, never needs to re-parse the artifact text.
Verification gate: production log mtime updated; new "Post-Publish C" section present and matches the type-applicable rows from the matrix.
Step 13: Completion Checklist
Per-type checklist applicability follows the matrix in §Usage. Edition runs all rows; non-edition types skip the — rows.
Verification gate: all matrix-✓ rows checked OR an explicit --skip-<name> invocation recorded.
Time Budget (G-P25, S195)
Substeps vary widely in wall time. Surface the most expensive ones up front so operators don't underestimate run-time.
| Substep | Wall time | Notes |
|---|
| 1a wiki ingest | ~30s | Fast, single API call per chunk |
| 1b text ingest | ~30s | Same |
| 2a citizen cards (--apply) | ~30-40 min | Loops 800+ ledger rows; the largest single substep. The --apply path batch-pauses 60s every 200 writes (rate-limit guard), so wall time scales with ledger size — C99 ran ~36 min, not the ~10 min an unpaused loop would suggest. ~4 min in dry-run (no writes, no pauses). (G-P-C99-2a.) |
| 2a-cul cultural cards | ~10s per CUL-ID | Negligible at typical 1-3 IDs |
| 2c world summary | ~5s | Single API write |
| 4 coverage ratings | ~30s | Sheet write |
| 5 citizen+business intake | ~1-2 min | Sheet append + verify |
| 5b desk packet refresh | ~60s | Rebuilds 9 packets + base_context |
| 6 grade edition | ~2 min | Per-article model passes |
| 8 extract exemplars | ~30s | Filesystem walk |
| 10 criteria files (with /skill-check) | 5-15 min | Most token-heavy step. /skill-check is a model-reasoning task. |
| 11 filing + bot | ~10s | (Bot restart now at /session-end) |
Whole-skill budget: ~30-45 min for an edition end-to-end. ~10-15 min for non-edition (no Step 6/7/8/10).
If you're tempted to "wait it out" on Step 2a or Step 10, set expectation accordingly — they are real wall time, not stuck.
Output
| Output | Feeds into |
|---|
| bay-tribune (wiki + text) | Next sift canon search |
| world-data (citizen cards + world summary) | Next sift MCP lookups |
| Edition_Coverage_Ratings sheet | Engine Phase 2 next cycle |
output/grades/grades_c<XX>.json | Next cycle desk workspaces |
output/grades/grade_history.json | Next cycle desk workspaces |
output/grade-examples/ | Next cycle desk workspaces |
docs/media/story_evaluation.md | Next sift Step 2 |
docs/media/brief_template.md | Next sift Step 5 |
docs/media/citizen_selection.md | Next sift Step 4 |
docs/mags-corliss/NEWSROOM_MEMORY.md | Next sift Step 1 |
Gap log (S212 — see [[plans/GAP_LOG_TEMPLATE]])
At skill close, capture friction observed during post-publish as a gap log. /post-publish is a heavy skill at the media generator terminal; sidecar gap logs catch inefficiency the skill couldn't catch while running. Type-aware: same gap log structure for edition / interview / supplemental / dispatch publishes.
Output path: output/production_log_c<XX>_post_publish_gaps.md (sidecar to the unified output/production_log_c<XX>.md; pipeline.34 convention production_log_c{XX}_<skill>_gaps.md).
Gap prefix: G-P* (e.g., G-P1, G-P14).
Common categories for /post-publish gaps:
- format-contract (CITIZEN USAGE LOG vs NAMES INDEX, parser silent no-ops, BUSINESSES NAMED absence)
- env-auth (env vars missing, Supermemory 401 retries, pre-flight gaps)
- grading-pipeline (Mara grade not found, hallucinated counts, reporter-name normalization)
- sequencing (early-ingest duplicates, bot-restart conflict with /session-end)
- stale-derivative (world-summary/manifest reflecting pre-pipeline-v2 state)
Discipline: write the gap log even on clean runs. File a ROLLOUT row in pipeline.<n> pointing at the gap log per ADR-0005 §How to add work. Promote bundles (e.g., BUNDLE-A format-contract enforcement, BUNDLE-D pre-flight env+auth) when multiple gaps share root cause.
Where This Sits
After /write-edition (edition path) or after /interview, /dispatch, /write-supplemental (alternate paths). Parallel with /edition-print. Closes the loop.
↩ Loop: /post-publish writes ratings + updates memory → next /pre-flight reads ratings → engine reads them in Phase 2 → next cycle reflects what was published.
Changelog
- 2026-07-11 — v1.12 (S313, engine-sheet). Step 2c snapshot ingest added: second POST per edition cycle — the
Snapshot: Cycle {XX} | … one-liner grep-extracted from world_summary_c{XX}.md (writer v1.2.0 emits it), tagged ["world-data","wd-snapshot"], metadata type: cycle_snapshot. Cheap "where are we now" anchor per Sonnet-agent Supermemory advice (Drive file, S313). Grep-empty (pre-v1.2.0 summary) skips with production-log note.
- 2026-07-11 — v1.11 (S312, engine-sheet). pipeline.43 Task 5 — Step 2e voice-packet dedup: citizens whose quotes came through the
/write-edition quote-supply pre-pass (output/voices/voices_c{XX}.json, non-fallback) already own their Reflection_Intake row from citizenVoice.js --record; 2e must not write a second reflection row. Commit cba2cfad. (Changelog entry backfilled at v1.12 — frontmatter was bumped without one.)
- 2026-07-10 — v1.10 (S310, research-build). research.23 Task 7 — Step 1c NotebookLM push added (all types;
--audio on edition only). Matrix row + step block + Parallel-OK grouping updated. Non-blocking by contract: scripts/notebooklmPush.js exits 0 on runtime bridge failures (auth rot / rate limits / API drift); verification gate accepts complete OR (non-blocking) warning. Plan: [[plans/2026-07-10-notebooklm-bridge-deploy]].
- 2026-06-22 — v1.9 (S267, research-build). governance.42 RB-6. Step 5b verification gate path fix: the base_context.json cycle field is nested — gate now checks
jq '.baseContext.cycle', not the top-level jq '.cycle' (which returns null and false-alarms; verified live — top-level keys are baseContext/bondStats/canon/householdStats). Closes G-P-C99-1. Step 2a Time Budget bump: ~10+ min → ~30-40 min, with the --apply batch-pause cadence (60s every 200 writes) noted as the reason wall time scales with ledger size (C99 ran ~36 min). Closes G-P-C99-2a. Doc-text only, no code change.
- 2026-04-17 — Initial 13-step skill (S156, post-publish formalized).
- 2026-04-26 — v1.1 (S180, research-build). Type-aware:
--type {edition|interview|supplemental|dispatch} flag added. Per-type substep matrix encodes default skips; required only for matrix-✓ opt-outs. Verification gate declared on every substep. Coverage ratings (Step 4) explicitly C93-gated for non-edition. Convergence point for the unified non-edition publishing pipeline (plan [[plans/2026-04-26-non-edition-publishing-pipeline]] T3).