When to use: Robin wants to write a long-form article and run it through the full pipeline —
outline → draft → higher-level improvement → a configurable chain of cross-model improvement
passes (Grok et al., editable per run) → two humanizer passes → MDX output. Point it at a
topic. The publish destination is driven by an optional RUN_DIR/publish-target.json; if absent,
the workflow produces RUN_DIR/article.mdx only with no build or push.
When NOT to use: a quick edit to an already-published article (edit the .mdx directly);
content that isn't a long-form article — a blog post, a tweet, a release note, an email
(wrong shape; use copy-review/message-framing for short user-facing copy); or when the
cross-model stage should be skipped entirely (the spend isn't worth it for the piece — just
run The Scribe + The Counselor directly, no workflow).
Type: mixed — it spawns drafting agents (The Scribe, The Counselor) AND runs a dispatch
script (scripts/model-pass.sh) plus a cross-repo publish into devweb. It borrows
operational-build.md's machinery (a runbook with action steps behind a gate), but note
operational-build is Type: execute; write-article is a machinery cousin, not the same type.
Inputs:
- A topic description — Robin provides it inline in the spawn or as a file path.
- Optionally a per-run
RUN_DIR/article-passes.json. If present, it replacesconfig/article-passes.jsonentirely for this run (not a merge — predictable and explicit). - Optionally a
RUN_DIR/publish-target.json. If present, it drives: the category/section enum (step 2), allowed MDX components (step 12), and the publish destination — repo path, content dir, build command, and live URL (step 14). If absent, the workflow producesRUN_DIR/article.mdxonly; step 14 becomes article-only output with no build or push. Pre-built targets live inconfig/publish-targets/; copy the relevant one toRUN_DIR/publish-target.jsonbefore starting.config/publish-targets/devweb.jsoncovers devweb.org. RUN_DIRis set by the Conductor perdocs/run-protocol.md.- The OpenRouter key, supplied as
OPENROUTER_API_KEYor throughOPENROUTER_KEYSTORE, consumed byscripts/model-pass.sh. The script loud-fails (exit 4) if absent. - Out of scope: any live-web fact-check (figure-grounding is tool-free, against the draft's own
cited sources); GPT/Gemini direct provider arms (v1 reaches them only via
openrouter:). - In scope (standing-authorized): when a publish-target is configured, the workflow publishes live —
it commits and pushes to
mainautomatically once the proofreader clears and the build is green. See the Automation policy below. The dev→prod boundary is crossed under Robin's recorded standing authorization, gated by the proofreader, not left as a manual release step.
Outputs:
RUN_DIR/angle.md— the angle, working title, proposed pillar.RUN_DIR/versions/NN-<stage>.md— the versioned article line. Every stage writes a NEW immutable, zero-padded file; no stage ever overwrites another.NNis creation order (00,01, …); the stage name disambiguates. The "current draft" at any point is the highest-numberedversions/file. This is what makes the run auditable end to end — diff any two stages forever. Typical sequence:00-outline.md,01-draft.md,02-revise.md,03-grounding.md(only if the figure gate triggers),04-reconcile.md,05-humanize-1.md,06-humanize-2.md,07-article.mdx.RUN_DIR/figure-check.md— per-claim grounding record (only if the figure gate triggers).RUN_DIR/passes/NN-<id>.md— one durable candidate per cleared cross-model pass (the cross-model perspectives; kept in their own dir because the resume-skip predicate keys on them).RUN_DIR/manifest.md— auto-written at close-out: one row perversions/stage (file, word count, one-line "what changed"). The audit index.RUN_DIR/proofread.md— the step-13 proofreader verdict (CLEAR|HOLD) + any concerns.RUN_DIR/article.mdx— a copy of the finalversions/NN-article.mdx, for the publish step.- Cross-repo (when a publish-target is configured):
<repo>/<content_dir>/<slug>.mdx— written + committed + pushed live in step 14, automatically when the step-13 proofreader returnsCLEARand the build (ifbuild_cmdis set) is green. Omitted when no publish-target is present. RUN_DIR/log.md,RUN_DIR/state.json— run narrative and close-out (step 15).
Leans on skills: humanizer and spiral-dynamics — both loaded by The Counselor in its
frame mode (step 1) and review mode (steps 10–11). The Scribe loads the lite house-voice rules
from ~/.claude/CLAUDE.md for drafting.
Reuses workflows: message-framing (step 1 — The Counselor's frame mode) and
copy-review (steps 10–11 — The Counselor's review mode). These are reused inline: the
Conductor spawns The Counselor in the named mode and that mode IS the mechanism. There is no
workflow-nesting primitive in the Bureau — steps 1, 10, and 11 do NOT nest the other workflows
as sub-workflows; they reuse the same Counselor mode the other workflow runs.
Automation policy — RESOLVED standing authorizations (Robin, 2026-06-24). "Write an article" runs to completion autonomously and ships live — it is automation, not a relay of approval gates. The only thing that halts a run is a real problem, never a preference:
- Cross-model spend — standing-authorized. The cross-model stage (step 6) fires the configured
passes automatically; no per-run
[EXTERNAL-ACTION CHECKPOINT]. Each call is still logged toRUN_DIR/log.mdwith an[EXTERNAL-ACTION]line (model, bytes, cost-signal, exit) for audit — the principal pre-authorized the class (Grok/OpenRouter passes on his own drafts), every firing is logged. This is how a recurring gated action is automated legitimately; it is NOT an un-gated external action. - Data custody / disclosure — resolved. (a) Sending pre-publication drafts to OpenRouter/xAI is accepted. (b) The published article does NOT disclose cross-model editing.
- Live publish — standing-authorized (when a publish-target is configured). The run commits and
pushes to
mainautomatically once the proofreader clears and the build (ifbuild_cmdis set) is green. This is an explicit, recorded standing authorization to cross the dev→prod boundary FOR THIS WORKFLOW — not a silent boundary breach. Safeguards: the proofreader step (a cold automated check that HOLDS on any real concern), the green-build gate, and easy reversibility (git revert+ push, or the target platform's rollback). When no publish-target is present, no boundary is crossed — the deliverable isRUN_DIR/article.mdxonly. - The only stops are problems, not preferences: (1) the proofreader finds a ship-blocking
concern → hold just before commit + alert Robin; (2)
npm run buildfails → hold + surface; (3) config validation fails → hold + name the file. Nothing else waits for a human.
The run is complete when ALL hold (the success path — proofreader CLEAR):
RUN_DIR/article.mdxexists with valid frontmatter.- The step-13 proofreader returned
CLEAR(RUN_DIR/proofread.md). - If a publish-target is configured:
build_cmd(if set) exits 0 with any target-specific checks passing — e.g. for devweb, all-static routes (every route○or●, noƒ). The article is committed and pushed tomain— live at the target'slive_url(step 14c). - If no publish-target:
RUN_DIR/article.mdxis the deliverable; step 14 logs article-only output and completes without build or push. RUN_DIR/log.mdcarries: the figure-gate decision; the logged cross-model pass list; one[EXTERNAL-ACTION]line per cross-model call that actually fired; the proofreader verdict; the push (if applicable); and the step-15 close-out with the count of paid passes.RUN_DIR/state.json#accountingis set (statusavailableor, on failure,unavailable).
Held (not complete): if the proofreader returned HOLD, the run is paused just before commit
with Robin alerted (nothing written to the target repo, nothing pushed) — resolve the named
concerns, then re-run from step 13. A build failure (14b) halts the same way.
- Figure gate skipped (no numbers). If the latest version carries no quantitative claims, step
6 logs
grounding: not-triggeredtoRUN_DIR/log.mdand the run continues at step 7 without entering 6a (noNN-grounding.mdversion is created). This is a branch, not a failure. - All cross-model passes fail. The step-8 batch may produce zero candidates (every pass
errored, was skipped, or failed integrity). Step 9 then runs as a Claude-only final revision of
the latest version. Every prior version is preserved —
model-pass.shwrites nothing on failure. - Resume after interruption mid-batch. Any cleared candidate already in
RUN_DIR/passes/is reused — step 8 skips that pass without re-charging the paid call. The workflow re-reads the effective config and skips every pass whose candidate exists. Candidate identity is keyed on the passid(the skip predicate matches*-<id>.md), not on list positionNN, so reordering or disabling a pass in a per-run config between interruption and resume cannot cause a paid call to be re-charged. - Per-run config override. If
RUN_DIR/article-passes.jsonexists it replacesconfig/article-passes.jsonentirely (not a merge). The effective config is one or the other, never a blend. - A draft that is almost entirely a data table. The figure gate triggers (6a grounds the
numbers); the length-delta integrity bound in
model-pass.shmay legitimately reject a cross-model pass that compresses the table — that is the partial-failure policy working, not a bug. Format step emits a<RunTable>only if the frontmatter carries arunobject.
- If a step fails (non-zero exit, agent error), the Conductor logs the failure to
RUN_DIR/log.mdperdocs/conventions/failure-signatures.md § Failure signature formatand raises a[CHECKPOINT]. Do not auto-retry silently. - The article's recoverable state at any point is the latest clean
RUN_DIR/versions/file. Every prior stage is preserved (immutable, numbered), so a bad step never destroys earlier work — roll back by reading an earlier version. The draft is never corrupted by a failed cross-model pass — the script fails closed and writes only topasses/. - If config validation (step 7) fails, stop and name the file — never POST against a malformed config.
- If
npm run build(step 14) fails, the article is NOT staged for release; surface the build error and[CHECKPOINT]— Robin or a follow-up run fixes the MDX.
Everything that spends money or branches is logged to RUN_DIR/log.md:
- Every cross-model call (step 8) writes an
[EXTERNAL-ACTION]line (model, bytes in/out,finish_reason, status, exit) — written bymodel-pass.sh --run-dir. These audit lines, not a per-run humango, are the control on the standing-authorized spend. - The planned cross-model pass list (step 7) is logged before any POST (no
goawaited). - The figure-gate decision (
grounding: not-triggered, or the 6a grounding record) is logged. - The proofreader verdict (step 13:
CLEAR/HOLD+ concerns) is logged; aHOLDalso fires an alert to Robin and logs the hold. - The publish (step 14c): the commit + push to
mainis logged. - The step-15 close-out surfaces the count of paid passes fired this run, read back from the
[EXTERNAL-ACTION]lines. A human inspects the run by readingRUN_DIR/log.md,RUN_DIR/proofread.md, theRUN_DIR/versions/+RUN_DIR/passes/files, and the published article on devweb.
Run these as spawned subagents (see "How to spawn an agent" and "Model routing" in
agents/orchestrator.md). Sequential — wait for each handoff before the next. Pass RUN_DIR
as an absolute path in every spawn prompt.
Versioned spine (never overwrite). The article advances through RUN_DIR/versions/NN-<stage>.md
files. Each step READS the current draft (the highest-numbered versions/ file) and WRITES its
output to the NEXT number with a stage-named file — it never edits an existing version in place.
The Conductor assigns NN (creation order) and passes both the input path (latest version) and the
output path (next version) in the spawn prompt. This preserves every stage for audit: at close-out
the versions/ dir + manifest.md IS the audit trail, and any two stages can be diffed. Each
cross-model pass still writes its candidate to RUN_DIR/passes/NN-<id>.md (its own dir, for the
resume-skip predicate); step 9 reconciles those candidates into the next versions/ file.
-
The Counselor (Voice, standard, mode: frame) — angle + house framing →
angle.mdReuses the Counselor frame mode (seeworkflows/message-framing.md) inline — spawned, not nested. The Counselor classifies the audience's value system (spiral-dynamics), chooses the angle and working title in the house voice, and proposes a pillar. Writes the angle, working title, and proposed pillar toRUN_DIR/angle.mdfor Robin's approval at the next gate. Voice calibration (anti-homogenization): from the audience read here, the Conductor selects the voice-calibration exemplar fromconfig/calibration-exemplars/index.md— match the bucket to the Counselor's value-system read, rotate within the bucket across runs — and passes that exemplar's PATH to the Scribe at steps 3-4 and the Counselor review at steps 10-11. Do NOT hardcode one calibration article for every run; a single fixed exemplar is the main source of cross-article uniformity and feeds a self-imitation loop. -
Action — proceed automatically (no approval wait). The Counselor's
angle.mdrecords the chosen angle, working title, and category/section for visibility. IfRUN_DIR/publish-target.jsonis present and has acategoriesarray, the category must be one of those values (e.g. devweb'sframeworks | memory | engineering). If nocategoriesarray is present, the Counselor proposes freely. The title + category carry forward to the format/publish steps. (If the angle is wrong, Robin redirects after seeing the result — cheap to redo; not a reason to hold the pipeline.) -
The Scribe (Outline, standard) → next version
NN-outline.mdGivenangle.md+ the approved working title + pillar. Produces a section-level outline — heading structure (h2/h3) in order, the key claim per section, and any figures/examples to gather before drafting. Outline only, no prose. -
The Scribe (Draft, strong) — reads the outline → next version
NN-draft.mdWrites the full article body end-to-end in the house voice (loads the lite voice rules from~/.claude/CLAUDE.md). Escalated to strong (Opus) — this is the piece's first real prose. -
The Scribe (Revise, strong) — higher-level improvement → next version
NN-revise.mdReads the latest version (the draft); standard revision sub-mode: argument structure, evidence quality, section balance, transitions. Structural work, not a line-edit. Writes the improved article as a NEW version file — the prior draft version is preserved untouched. -
Gate — figure check (conditional): the Conductor reads the latest version for real numbers or quantitative claims. If present, proceed to step 6a; else log
grounding: not-triggeredtoRUN_DIR/log.mdand skip to step 7. "Real numbers" means specific figures, percentages, dates, measurements — not vague qualitative statements ("most teams", "a lot faster").- 6a. The Scribe (Revise, strong, sub-mode: ground) — figure grounding → next version
NN-grounding.md+figure-check.mdReads the latest version. Re-examine every quantitative claim against the source the draft itself cites or the run's own inputs. For each number: confirm it against the cited source, correct it if the source disagrees, or — if it cannot be grounded against any source the draft names or any run input — mark it[unverified]so Robin decides. No live-web fact-check (out of v1; the Scribe does not invent sources). Writes the grounded article as a NEW version file and a separateRUN_DIR/figure-check.mdlisting each claim, its source, and its status.
- 6a. The Scribe (Revise, strong, sub-mode: ground) — figure grounding → next version
-
Action — cross-model stage prep (standing-authorized — NO human stop) The cross-model spend is pre-authorized (see Automation policy). Do NOT raise a per-run
[EXTERNAL-ACTION CHECKPOINT]; the run proceeds straight into the passes. The only check here is config validation, which IS a hard error-stop — never POST against a malformed config. Validate the effective config (the per-runRUN_DIR/article-passes.jsonif present, elseconfig/article-passes.json) with thesejq -eguards (pattern mirrorsaccount-run.shandscripts/model-pass.sh's contract); on any failure, halt and name the file:jq -e '.passes | type == "array"' <config>—.passesis an array.jq -e '[.passes[] | select(.enabled == true) | .model | startswith("openrouter:")] | all' <config>— every enabled pass'smodelcarries the routableopenrouter:provider prefix (v1's only arm). A non-openrouter:enabled pass fails loud — it is never silently skipped.- For each enabled pass, confirm its
instructionpath resolves to a real file under the bureau root:test -f "<bureau-root>/<instruction>". A missing instruction file fails before the API call, not as an empty-instruction POST. Log the ordered enabled-pass list (model IDs + planned call count) toRUN_DIR/log.mdfor the record, then proceed to step 8. (Nogois awaited — the standing authorization + the per-call[EXTERNAL-ACTION]audit lines are the control.)
-
Action — cross-model stage (post-approval) Precondition (before dispatching any pass): the Conductor confirms
RUN_DIR/andRUN_DIR/passes/exist (createpasses/if missing).model-pass.shonly writes its[EXTERNAL-ACTION]audit line when--run-dirpoints at an existing dir (silent no-op otherwise) — without this, a fired paid call goes unlogged and step 15's count under-reports. Iterate the effective passes config in order. For each pass withenabled: true, where<id>is its stableidandNNis the pass's zero-padded position in the list (01,02, …):- Resume check (keyed on the pass
id, not list position) — if a cleared candidate for pass<id>already exists inRUN_DIR/passes/(match the glob*-<id>.md), skip this pass (resume-idempotent — a completed candidate is never re-charged; the file existing means it cleared every integrity check). The skip predicate keys on<id>, never onNN: if a per-run config is reordered or a pass disabled between an interruption and the resume,NNshifts but<id>is stable, so matching on<id>prevents a re-charge. - Otherwise run (input is the latest
versions/file — the current draft; the instruction path resolves relative to the bureau root):The script makes the POST, runs its integrity checks (HTTP 2xx, nobash scripts/model-pass.sh <model> "$RUN_DIR/versions/<latest>.md" \ "<bureau-root>/<instruction>" \ "$RUN_DIR/passes/NN-<id>.md" --run-dir "$RUN_DIR".error,finish_reason == "stop", output within 50%–300% of input bytes), and writes the candidate ONLY on full success. Its exit-code contract: 0 candidate written · 1 bad args / missing input · 2 provider error · 3 integrity-check failed · 4 keystore key missing. - Partial-failure policy — if
model-pass.shexits non-zero (1/2/3/4), log the failure toRUN_DIR/log.md, skip that pass, and continue with the next. Noversions/file is written in this step (candidates land only inpasses/; step 9 writes the next version). The batch continues with whatever cleared.
- Resume check (keyed on the pass
-
The Scribe (Revise, strong, generous integration) — reconcile the cross-model passes → next version
NN-reconcile.mdGiven the latestversions/file (the current draft) + every cleared candidate inRUN_DIR/passes/(if any). The point of the cross-model stage is that other models' perspectives improve the piece and let it evolve — so integrate GENEROUSLY: adopt the candidates' edits by default. This is NOT a gate that defends the original wording, and it is NOT a "promotion authority" with editorial veto. The Scribe reverts a candidate's change to its own prior wording ONLY when one of these hard guards genuinely applies (and names which, per change):- (a) Facts — the change breaks a grounded fact or introduces a number/claim not in the source.
- (b) Known no-go framing — the change reintroduces something the run already corrected (e.g. a figure-gate fix); a corrected fact stays corrected.
- (c) Voice floor — AI-slop vocabulary, em dashes, curly quotes (the house voice baseline).
- (d) Concrete specifics — the change drops a real name or load-bearing technical detail
(e.g. a system name) for a vaguer word.
Everything else — phrasing, structure, tightening, rhythm — let the other model win where its
version is as good or better. Do NOT preserve the original just because it is the original or
because it "carries a nuance you prefer." Write the reconciled article as a NEW version file.
Mechanical corruption is NOT this step's job — truncation, refusals, and garbage are already
caught upstream by
model-pass.sh's integrity checks (only cleared candidates reach this step). If no candidates exist (all passes failed or were skipped), the Scribe does a Claude-only final revision of the latest version.
-
The Counselor (Voice, standard, mode: review) — humanizer pass 1: AI-tells + vocabulary scrub → next version
NN-humanize-1.mdReuses the Counselor review mode (seeworkflows/copy-review.md) inline — spawned, not nested. Reads the latest version. Loads thehumanizerskill. Objective: strip AI tells, inflated vocabulary, chatbot artifacts, banned words. Writes the cleaned article as a NEW version file. -
The Counselor (Voice, standard, mode: review) — humanizer pass 2: read-aloud rhythm + human texture → next version
NN-humanize-2.mdReuses the Counselor review mode inline; reads the latest version. Two objectives, both distinct from pass 1's de-slop: (a) sentence rhythm, paragraph flow, read-aloud cadence; and (b) human texture — apply thehumanizerskill's TEXTURE LENS to put back the unevenness a single thoughtful person leaves in (a long sentence beside short ones, a sentence-initial conjunction, a brief aside, not always an aphoristic closer), and vary the texture device from recent articles so the corpus does not converge on one cadence. Light touch: a few real moves, not a rewrite. Hard scope: texture touches cadence/phrasing/voice ONLY, never a fact, number, date, name, code, quote, or figure-grounded claim, and it never reintroduces slop (pass 1's de-slop floor still holds). A different objective on the now-de-slopped text, not a redundant re-run. Writes the final prose as a NEW version file. (The cold proofreader at step 13 still gates everything that follows.) -
The Scribe (Format, standard) — MDX + frontmatter → next version
NN-article.mdxGiven the latest version (the final prose) + the approved slug + category. A mechanical transform (no content edits): emits the next version asNN-article.mdxwith correct frontmatter (title,dek,dateISO,slugmatching^[a-z0-9-]+$; acategory/pillarfield using the value from step 2; optionalreadas an integer,draft,run). For allowed MDX components: ifRUN_DIR/publish-target.jsonhas anmdx_componentsarray, only those components may appear as JSX — any others will break the target's build. If nomdx_componentsis set, avoid custom JSX components entirely and emit plain MDX. The Conductor copies this final version toRUN_DIR/article.mdxfor the publish step. -
The Challenger (Critic, strong, fresh context — proofreader / publish-concern) — the safety gate →
proofread.md, verdictCLEAR|HOLDReads ONLY the finalarticle.mdx(the thing about to ship), cold. This is not a style or voice review (the humanizer already owns that) — it scans for ship-blocking concerns that would make publishing this to a public site a mistake:- Sensitive / non-public material — secrets, credentials, internal-only system details, private information about real people or clients, security-exploitable specifics.
- Factual / legal / reputational risk — claims that are wrong or unsupportable, defamation, unverified accusations, anything that could embarrass or expose Robin / Novadiem.
- Catastrophic failures — a broken or self-contradictory thesis, an obvious howler, a number
the figure-gate missed.
It returns
CLEAR(nothing ship-blocking — proceed to publish) orHOLDwith the named concerns. Nitpicks and preferences are NOTHOLDs — only a real concern holds. WritesRUN_DIR/proofread.md(the verdict + any concerns). CLEAR→ continue to step 14 automatically; no human stop.HOLD→ the run stops here, just before commit, and alerts Robin. The Conductor fires an alert (scripts/notify-escalation.sh, or thenotify_robinMCP tool) with the named concerns, logs the hold toRUN_DIR/log.md, and waits. Nothing is written into devweb and nothing is pushed until Robin resolves it. This is the ONE concern-driven stop in the workflow.
-
Action — publish (write → build → commit → push live). Runs automatically only when step 13 returned
CLEAR. ReadRUN_DIR/publish-target.json. If it does not exist, this step is article-only output: logpublish: article-only — no publish-target configuredtoRUN_DIR/log.mdand complete.RUN_DIR/article.mdxis the deliverable. Skip 14a–c. If it exists: a. CopyRUN_DIR/article.mdx→<repo>/<content_dir>/<slug>.mdx(values from the publish-target config). b. Ifbuild_cmdis set: run it in<repo>. Apply any target-specific build checks (build_check; for"all-static"this means every route must be○or●, noƒ). If the build fails, HALT — surface the error and hold (do not commit). A broken build is an error-stop, not a preference. Ifbuild_cmdis null/absent, skip this sub-step. c. On a green build (or if no build step), commit and push tomain(standing-authorized live publish — see Automation policy):git -C <repo> add <content_dir>/<slug>.mdx git -C <repo> commit -m "content: add '<title>' (<category> pillar)" git -C <repo> push origin main # → live at <live_url>The article is now live. (Reversible via
git revert+ push or the target platform's rollback if needed.) -
The Conductor (standard) — close out + write the audit manifest + run accounting last →
manifest.md,log.md,state.jsonWriteRUN_DIR/manifest.md— the audit index: one row perversions/stage in order (NN-<stage>→ word count → one-line "what changed from the prior version"), plus thepasses/cross-model candidates. This is the end-state audit: theversions/dir holds every stage immutably andmanifest.mdis its table of contents. Surface the count of paid external passes fired this run, read back from the[EXTERNAL-ACTION]entries inRUN_DIR/log.md. Summarize what ran and what was staged; flag anything deferred. As the final close-out action — after the manifest, the summary, and the finalstate.json/log.mdupdates — runscripts/account-run.sh <RUN_DIR>soaccounting.jsonreflects the terminal state, then setstate.json#accountingperdocs/run-accounting.md. Note:account-run.shhas no external-API cost source — it records the note; per-pass dollar capture is a registered v2 deferral, so v1 surfaces the count of paid passes plus thelog.mdbytes-in/out lines.