publish_design - manishiitg/agentworks GitHub Wiki
Status: Design (2026-06-24). Not implemented.
A workflow already produces HTML artifacts β the Pulse log (builder/improve.html)
and the reporting dashboard (reports/). We want to publish them to a public URL
on any static host (Netlify, Vercel, Cloudflare Pages, GitHub Pages, S3, a VPS, β¦), driven
by the builder agent β exactly the way Backup is agent-driven.
Same shape, pointed at public hosting instead of durable storage. The Backup scaffold
(workflow_backup.go, the Backup popup, the toolbar status dot, backup-strategy.md) is
the template; Publish mirrors each piece.
| Backup (exists) | Publish (new) | |
|---|---|---|
| Goal | "can I get it back?" β durable storage | "can I share it?" β a public URL |
| Config | workflow.json.backup |
workflow.json.publish |
| Status | backup/status.json |
publish/status.json (incl. the live URL) |
| Endpoint | handleRunWorkflowBackup |
handleRunWorkflowPublish (mirror it) |
| Reference doc | backup-strategy.md |
publish-strategy.md |
| UI | Backup popup + toolbar dot | Publish popup + toolbar button (shows URL + status dot) |
| Auth | creds / secret | a provider token via the secrets system |
| Agent prompt | buildWorkflowBackupAgentPrompt |
buildWorkflowPublishAgentPrompt |
The builder reads workflow.json.publish + read_skill(skills=[{"name":"builder-reference","path":"references/publish-strategy.md"}]),
deploys the HTML, and writes publish/status.json with the public URL.
The whole point: we do not enumerate providers in Go. Adding a host is a doc edit, not
a release β same philosophy as Backup (whose backup-strategy.md covers "any large-file
backend" via a matrix + rclone as the catch-all, with zero per-backend Go).
- Go stays generic:
workflow.json.publishcarries a free-formproviderstring, the deploy details, an auth secret name, and the resulting URL. Go never knows what "Netlify" is. -
publish-strategy.mdholds the knowledge β a general static-deploy method + specifics for common hosts. - The agent has shell + the user's token, so it runs whatever the host needs and reads back the URL.
The doc gives the agent these, and it picks whichever the named provider supports:
-
Provider CLI β
netlify deploy,vercel,wrangler pages deploy,gh-pages,surge,firebase deploy, β¦ - Git-push-to-deploy β push the static files to a repo the host auto-builds (Netlify / Vercel / Pages / Render). Reuses the git skill Backup already uses.
-
Object-store / file sync β
aws s3 sync/rclone/rsyncto a static bucket or a server+nginx. The true catch-all: any host that serves files from a bucket or directory.
Between these, "any provider with static hosting" is covered with no provider-specific code. New host you've never configured? Set it up conversationally with the builder ("publish to my Cloudflare Pages project X") β it writes the config and deploys.
The two artifacts differ:
-
Pulse log (
improve.html) β self-contained HTML. Publishes as-is. β -
Reporting dashboard (
reports/) β live: it callswindow.report.query(sql)againstdb/db.sqliteinside the app. Static hosting has neither the bridge nor the DB, so it would render empty.
Decided (2026-06-24): Option A β static HTML generation (snapshot). The agent bakes the dashboard to static HTML at publish time; no live DB, no client-side DB. B and C below are recorded for context but are not in scope (sql.js may be a future opt-in if interactivity is ever needed).
-
A. Snapshot / bake β static HTML (the approach). At publish time the agent runs every
query the dashboard uses, inlines the results as JSON, and injects a shim so the page reads
baked data:
Fully static, works on any host. Snapshot-as-of-publish β auto-republish after each run keeps it fresh. Also the privacy-safest: only the baked query results are exposed, not the whole DB. Handles the common case (a fixed set of queries).
window.__REPORT_DATA__ = { "<normalized sql>": [...rows] } window.report = { query: (sql) => window.__REPORT_DATA__[normalize(sql)] ?? [] }
-
B. sql.js in the browser (out of scope β future opt-in). Ship
db.sqlite+ sql.js (SQLiteβWASM) for a genuinely interactive dashboard, at the cost of exposing the whole DB. - C. Serverless function (out of scope). A Netlify/Vercel function queries the DB to keep data server-side; not pure static, most setup.
The snapshot is an agentic transform the agent performs at publish time per
publish-strategy.md β read the report's queries, run them against db.sqlite, inline the
results + shim, deploy the static bundle.
Publishing a dashboard publishes its data. Snapshot or sql.js, whatever's in the queries/ DB goes to a public URL. So:
-
workflow.json.publishshould let you scope what's published (which artifacts; for the dashboard, which views/queries, or a sanitized/filtered DB). -
publish-strategy.mdmakes the agent warn before exposing raw rows and confirm the scope, especially for sql.js (whole-DB) publishes.
Generic and open-ended (no provider enum):
{
"enabled": true,
"mode": "agent",
"targets": ["pulse", "report"], // which HTML artifacts to publish
"dashboard_mode": "snapshot", // snapshot (static HTML) β the decided approach
"destinations": [
{
"id": "main-site",
"provider": "<free string: netlify|vercel|cloudflare-pages|github-pages|s3|...>",
"method": "cli|git|sync", // which universal path
"site": "<project/site/bucket/repo identifier>",
"secret_name": "<global secret holding the token>",
"public_base_url": "<filled in by the agent after first deploy>"
}
],
"triggers": ["manual", "post_run"]
}{
"version": 1,
"state": "not_configured | configured_not_verified | publishing | published | stale | failed",
"url": "<public URL of the latest publish>",
"last_published_at": "<ISO>",
"last_attempt_at": "<ISO>",
"last_source_hash": "<so 'stale' = artifacts changed since last publish>",
"destinations": [ { "id": "...", "provider": "...", "url": "...", "state": "...", "error": "" } ],
"last_error": "",
"updated_at": "<ISO>"
}stale reuses Backup's source-hash trick: the artifacts changed since the last publish, so
the public URL is behind.
-
handleRunWorkflowPublishβ acceptsaction: "publish" | "configure"(+ laterunpublish). Spawns a builder session with the publish prompt; writespublish/status.json. -
buildWorkflowPublishAgentPromptβ the agentic contract: readworkflow.json.publish+read_skill(skills=[{"name":"builder-reference","path":"references/publish-strategy.md"}]); for each destination, prepare the static artifacts (bake the dashboard perdashboard_mode), deploy via the destination's method, capture the URL, write status. Confirm scope before exposing data. - Route under
/workflow/publish,/workflow/publish/run,/workflow/publish/config. -
publish-strategy.mdregistered inguidance.go(Modes: workshop/run β it's a workflow builder capability, like backup-strategy).
-
WorkflowPublishPopup.tsxβ public URL (with copy/open), per-destination status, "Publish now" / "Set up", scope (which artifacts), the configured destinations, and an illustrative "common hosts" hint list (suggestions, not a gate). ReusebackupStatus.tspatterns for the stateβvisual mapping. -
Toolbar button β a "Publish" button (Globe/Upload icon) with a status dot
(
publishedgreen,staleamber,failedred,not_configuredgrey) and the URL in the tooltip. Sits next to the Backup button. - AI-driven only (no manual config editor), consistent with Backup.
Publishing follows Backup's set-up-then-verify flow β the user configures and tests it manually before anything runs unattended:
-
Configure (
action: "configure") β the builder sets upworkflow.json.publishwith the provider/destination/token. State βconfigured_not_verified. Nothing is public yet. -
Test publish (manual "Publish now") β the first real deploy. The agent publishes, gets
a working URL back, and confirms it loads. State β
published(verified). -
Then auto-republish is allowed. Until a destination has had at least one successful
(verified) publish, the post-run trigger does not fire for it β so we never push an
unproven config to a public URL unattended. This matches Backup's
configured_not_verifiedβhealthygate.
Publish rides the same cron-driven post-run Pulse pass as Backup β it's one more step in the post-run steward, not a separate hook:
triage β fix β back up β publish β notify
- Auto (cron): on a scheduled run, if publish is on (and verified, and artifacts changed), the Pulse pass publishes β so the public dashboard/Pulse log stay current with no manual action. Gated three ways: publish on, destination verified (a prior successful manual publish), and source-hash changed (skip an unchanged artifact set).
- Manual: if publish is off (or for an unscheduled workflow), it's a manual "Publish now" via the builder β exactly like Backup's manual run. The manual publish is also the verification step above.
So: publish on + cron β auto-publish via Pulse; otherwise β manual via the builder, the same on/off-then-manual shape Backup has.
Publish isn't workflow-only. The Chief of Staff's Org Pulse produces its own HTML
(pulse/org-pulse.html β the org heartbeat log), and it can be published the same way so the
whole org's status is shareable at a public URL.
- Same provider-agnostic agentic mechanism β one
publish-strategy.mdserves both; only the source HTML and config location differ. -
Workflow publish: config in
workflow.json.publish; targetsimprove.html+reports/; runs in the workflow Pulse post-run pass. -
Org publish: config in the CoS config (
multiagent-config.json); targetpulse/org-pulse.html; runs as a step in the Org Pulse daily pass (org-pulse.md), same on/verified/changed gating. - Same setupβverifyβauto-via-cron flow on both.
So the Publish build should treat the "what HTML + where's the config" as parameters, not bake in "workflow." Reference doc and deploy logic are shared; the two Pulse passes each call it with their own artifact + config.
-
Backend scaffold β β
Done (2026-06-24).
WorkflowPublishConfig/WorkflowPublishDestinationinworkflow_manifest.go;workflow_publish.go(status types,publish/status.json, source-hash over improve.html + reports/ + db.sqlite,handleGetWorkflowPublish/handleUpdateWorkflowPublishConfig/handleRunWorkflowPublishwithconfigure/publishactions,buildWorkflowPublishAgentPrompt); routes/workflow/publish[/config|/run]. Mirrorsworkflow_backup.go. Build + vet clean. Manual publish path works end-to-end. -
publish-strategy.mdβ β Done (2026-06-24). The agentic playbook, registered asread_skill(skills=[{"name":"builder-reference","path":"references/publish-strategy.md"}])(workshop/run/multi-agent). 3 universal deploy paths + static-snapshot procedure + privacy/scope + setupβverify + status contract. Serves both workflow and org publish. -
UI β β
Done (2026-06-24).
publishStatus.ts(stateβvisual + dot),WorkflowPublishPopup.tsx(state header, prominent public URL with open/copy, Publish now/Set up, destinations, common-hosts hint), toolbar Publish button (Globe + status dot) inWorkflowToolbar.tsx, API methods + types. tsc clean. -
Auto-republish via Pulse β β
Done (2026-06-24). Added step 4 PUBLISH to the
post-run Pulse pass (
scheduler.gorunPostRunMonitoris now 5 steps: triage β fix β back up β publish β notify) and a matching 4b section inpost-run-monitor.md: re-publish only an already-verified destination, only when artifacts changed; never the first unattended publish. -
Org publish β β
Done (2026-06-24). Added step 6 to
org-pulse.md: publishpulse/org-pulse.htmlperpublish-strategy.md, same verified+changed gate, opt-in via a CoSpublishconfig /pulse/publish.json. Fully agentic (no new Go) β the Org Pulse agent deploys via shell per the shared reference doc.
- Dashboard: Option A β static HTML generation (snapshot). The agent bakes query results into static HTML at publish time. sql.js/serverless out of scope.
-
Provider model: provider-agnostic + agentic β no per-provider Go;
publish-strategy.mdholds the 3 universal deploy paths. - Trigger: manual + auto-republish, both from v1. Auto-republish is a step in the cron-driven Pulse post-run pass (triage β fix β back up β publish β notify), gated on publish-on + verified + source-hash-changed. Off β manual via the builder, like Backup.
-
Setup/verify: configure β
configured_not_verifiedβ manual test publish βpublished; auto only after verified (same as Backup). -
URL: host's default URL (e.g.
*.netlify.app,*.pages.dev) for v1; custom domains deferred.
(No open questions β design is settled; ready to build.)
- No per-provider Go adapters β providers live in the reference doc.
- No manual JSON config editor (AI-driven, like Backup).
- Not a general web host / CMS β only deploying the workflow's existing HTML artifacts.