Blog tool agent architecture

Nine steps, four layers

One blog task, from a Teamwork row to a filed document. First the process, then what powers each step, then what lives inside each of the four layers. One question decides where any piece of code belongs: does it show, store, do, or land?

One blog task, end to end

nine steps · three gates · one graph per Teamwork task

Each Write Blog task becomes exactly one run. Clicking “Draft all” starts several of these side by side, so one post failing never stalls the others.

shows stores does lands DOES Confirm client, folder and topic blocks with a visible reason if any is missing DOES Write the post house rules, 500 to 750 words DOES Mechanical check, then queue for review zero critical flags, or it does not queue YOU You approve the words gate one of three change requested dropped run ends here YOU You release it with approved minutes refused without a number; release starts delivery DOES Document, QC comment, time entry replays approved bytes, no model in the path YOU You ask for the task to be closed never implied by delivery DOES Close it in Teamwork Log Efficiencies first, and never the parent STORES File the row away only once closure is verified
The stripe on each step is the layer it runs in; a two-tone stripe is a handoff, so “you approve” is a click in the shell recorded on Cloudflare, and “deliver” is the runner working and the share receiving. Read the stripes top to bottom and the shape of the month is visible: the runner works, Cloudflare holds, you decide, the runner works again.
NoteThe three YOU steps are the only places the process stops. Everything between them is mechanical, and no gate implies the next one: approving is not releasing, and releasing is not closing the task.

What powers each step

three kinds of engine

Only one step in the whole run is a model writing prose. Everything else is fixed code or your decision, which is what makes the output predictable.

Model used by: 1 of 9 steps drafting the post, only capped by its permission slip reaches no client system Deterministic script used by: 5 of 9 steps no model anywhere in it replays what you approved holds the credentials Human gate used by: 3 of 9 steps approve, release, close server refuses without it one gate is never another
The delivery step is a script rather than a prompt for one reason: a script cannot be talked into changing the wording. The model is removed from that path instead of being instructed to behave.

Step by step

what runs it, and what it is allowed to touch

StepPowered byKindMay touch
Confirm client, folder, topic probe_client.py
blog_index.py
script Reports dashboard, BMG-6 read-only, the topic sheet
Write the post monthly-blogs skill
via Claude
model Its own working folder, and the web for link checks
Mechanical check blog_checks.py script The draft on disk
Queue for review blogs_to_review.py script Cloudflare, as pending cards
Approve the words You, in Review gate The card’s decision, nothing else
Release with minutes You, on the card gate The release stamp and the minute count
Deliver deliver_review.py script BMG-6, and Teamwork for the comment and the time
Ask for closure You, on the row gate Nothing yet. It only records the request
Close in Teamwork run_complete_task script The subtask and Log Efficiencies, never the parent
File the row away blog_archive script Cloudflare, plus the spreadsheet mirror
One thing here is not yet true. There is a second, deterministic drafting path (blog_tool.py) that is deliberately switched off: on 13 August the skill’s six Las Vegas posts passed the checker with zero flags while the tool-generated post missed two house rules. It stays available for batches where cost matters more than polish, which is why the draft step must remain a model step rather than being “simplified” into a script.

HTML shell

Shows public/index.html

Draws whatever state it is handed and captures your clicks. One file, about 5,000 lines, nine views.

you pick a view switchView() picks the view loadBlogs() asks the server GET /api/blogs no rules here renderBlogs() draws what came back after any click: reload, then redraw
The screen never updates itself from a click. Every decision round-trips to the server and the view redraws from the response, which is why a stale browser tab cannot invent state.

The nine views

each has a load and a render

ViewShows
pagesOptimizations board
blogsMonthly blog tasks
reviewDrafts awaiting a decision
archiveWhat already shipped
workTop 3 and the queue
googleGBP case tracker
goalsObjectives
projectsInitiatives
systemThis architecture, live

What it keeps in memory

all of it a cache of a response

GlobalHolds
DATALast /api/work payload
REVIEWBatches, items, cards
BLOGSSwept tasks and roster
PAGESPage state rows
ARCHIVEDelivered work by month
READ_*Two-pane reader position
NeverHolds a rule the server does not also enforce. The shell is allowed to hide a button; it is not allowed to be the reason something cannot happen.
Sits in the wrong layer today. “Approve all skips cards whose evidence is needs_client” is enforced in this file. A crafted request to /api/review would bypass it. The server has to be the one that refuses.

Cloudflare

Stores bmg-workspace · bmg-skill-router

Holds every decision you have made, and the map of which state may follow which. Executes nothing and holds no client password.

your click 1 · saved first Workspace D1 review_card — decision, minutes blog_tasks — what is open page_state — staleness hashes blog_archive — what shipped 22 tables in total 2 · forwarded retried until acknowledged Router D1 workflow_runs — the current step run_events — append-only history runs — every attempt runners — is the Studio awake R2 — files a job produced
The order is the safety property. Your decision is durable before the event is sent, so a Studio that is asleep or a router that is unreachable delays the work without ever losing the click.

The cockpit’s eleven endpoints

access means you in a browser; bearer means the Studio

EndpointWho calls itWhat it is for
/api/reviewYouApprove, drop, revise, release
/api/blogsYou + StudioSwept tasks, client mapping, jobs
/api/page-stateYou + StudioWhat each page actually says now
/api/actionYou + StudioThe legacy work queue
/api/archiveYou + StudioDelivered work, by month
/api/workYouTop 3 and the queue
/api/syncStudioPushes the analysis in
/api/systemYouThis architecture, read-only
/api/goalsYouObjectives
/api/projectsYouInitiatives
/api/snoozeYouHide an item for a while
NeverRuns a job, reaches a client system, or holds a credential for one. The only secret it may keep is a token for talking to the router.
Sits in the wrong layer today. action.js composes the instruction text a job runs with, so the words a model receives are authored on Cloudflare. And GET /api/blogs calls out to reports.bmgdashboard.com live, making a page load depend on another system being up. Both belong on the Studio: the runner should push job state in, and Cloudflare should serve what it was last told.

Local runner

Does Mac Studio

The only thing that does work, because it is the only thing holding credentials. There are two of these right now, and that is the problem the refactor exists to fix.

TODAY — ONE PROCESS DOES BOTH poller.py decides the next step 14 hardcoded branches does the work and holds the credentials business scripts claude-skills TARGET — DECIDING MOVES OUT Cloudflare decides the next step the transition map, not a branch claim runner does the work only still the only holder of credentials business scripts the same ones
The scripts that write the blog and deliver the file do not change. What moves is the decision about what runs next: out of a hand-written branch on the Studio, into a map on Cloudflare that can be read while the Studio is asleep.

Router runner

the engine being kept

ModuleJob
index.jsClaim a run, execute, report
claude.jsBuild the prompt, run the model
grants.jsScope tools to this run’s inputs
caps.jsStop a runaway loop of calls
spend.jsHold the DataForSEO dollar ceiling
files.jsFetch inputs, upload deliverables
config.jsRead the vault key at moment of use
health.jsReport whether it is fit to work

Legacy poller

the engine being retired

BehaviourDetail
dispatch14 branches on a magic string
intervalEvery 3 minutes, one action
lockStops overlapping cycles
recoveryRe-claims after a mid-run kill
dedupeCompleted ids cached locally
retryWaits out a model session limit
historyNone. No run events, no timeline
idempotencyNone. Two clicks can do it twice

The scripts both engines call

the business rules, which are not moving

ScriptPipelineDoes
deliver_review.pyBlogsReplays approved bytes to the share
blog_checks.pyBlogsMechanical house-style check
probe_client.pyBlogsFinds a client’s folder and topics
blog_index.pyBlogsReads and marks the topic sheet
build_topic_list.pyBlogsResearches next month’s topics
blogs_to_review.pyBlogsPushes drafts to the review queue
apply_review.pyOptimizationsWrites approved changes to WordPress
reconcile.pyOptimizationsRe-reads live pages for staleness
find_to_plan.pyOptimizationsSweeps optimize tasks
gmail_cases.pyGoogle casesSyncs GBP case status from mail
NeverInvents words between your approval and delivery. Where output reaches a client, the path contains no model at all: that is enforced by removing the model, not by instructing it.
Sits in the wrong layer today. The 14 dispatch branches are a transition map living inside the thing that does the work. Nothing outside that Python file can answer “what happens after Release?”, which is why the dashboard cannot show you where a post is. Splitting the deciding from the doing is the actual refactor.

BMG-6 and Teamwork

Lands the share, the task

Where finished blog work comes to rest. Only the runner may touch either one.

approved bytes review_card.after replay deliver_review.py no model in this path document → BMG-6 QC comment → Teamwork time entry → Teamwork
All three land in one deterministic pass, so a retry updates the same row rather than doubling it. Completing the task is deliberately not shown: it is a separate explicit click, never a consequence of delivery.

BMG-6

the file share

FactValue
mount/Volumes/BMG_Customers
reached byThe runner only, over SMB
holdsThe client-named document
folderPer client, per month, resolved by probe rather than guessed
mirroredA row in blog_archive, so the app can show what shipped without the share being mounted

Teamwork

where the work is recorded

LandsRule
QC commentPosted once, never rewritten
actual timeTo Jarrett, on the work date
efficiencyAlways to Derek, at month end
completionLog Efficiencies closes before Write Blog
the parentNever closed. It carries other people’s subtasks
NeverTouched by Cloudflare or by the browser. Both surfaces are reachable only from the Studio, and only by replaying something you already approved.

WordPress and Gmail are landing surfaces too, but for different pipelines: WordPress for Optimizations and Gmail for the Google cases tracker. Same rule applies to both. Neither is part of the blog path.