Building a Content-Publishing Workflow from Task Logs
I have months of detailed engineering task logs. Every feature, every production incident, every architectural decision has a corresponding entry in docs/task-log/ — frontmatter, decision tables, timestamped implementation notes, troubleshooting records. None of it has ever appeared on the blog.
The gap is friction. Converting an engineering log to a readable post is real work: restructure the narrative, strip the implementation noise, add context for an outside reader. It always felt like a task for later. This post is about eliminating that friction permanently.
The Design Question
Where should the transformation logic live?
The obvious answer is a build script — scan the task-logs, template-generate some markdown. But the core operation here is “read unstructured developer notes and synthesize coherent prose.” That’s not deterministic computation. That’s editorial judgment. A templated script would produce output that reads like a templated script.
The right tool is a Claude agent. Specifically, a new .claude/agents/content-publisher.md agent (Sonnet model) with a single job: read a task-log entry and produce a draft blog post, then upload the source material to the GraphKnow wiki pipeline.
I already had 30-odd agents in the repo — reviewers, build resolvers, a planner, an architect. The closest existing agent was doc-updater, which generates codemaps and READMEs from source code. But it uses the Haiku model (fast, cheap, mechanical) and is oriented around code structure, not prose. Extending it would blur its scope. A new agent with clear responsibility and a better model costs one more file.
Two Key Decisions
Draft files need a build-time gate, not just a naming convention.
The blog’s markdown pipeline (apps/blog/scripts/build-markdown.ts) reads every *.md file in posts/. If I commit a DRAFT-2026-04-23-<slug>.md file, it gets picked up and rendered under a broken slug. The fix is one line in loadPosts():
if (f.toLowerCase().startsWith("draft-")) return false;
Now the DRAFT- prefix is a build-time signal, not a social convention. The filter uses .toLowerCase() to catch DRAFT-, Draft-, draft- equally. Drafts are committed (not gitignored) so they survive across worktrees and appear in PRs for review — without ever reaching the live blog until the rename happens.
Publish status belongs in a JSON manifest, not in task-log frontmatter.
My task-logs aren’t single files — many are directories with decisions.md, implementation-log.md, open-questions.md. There’s no single frontmatter owner. A JSON manifest at docs/task-log/.publish-manifest.json maps each logId to its publish state:
{
"20260423-content-publishing-flow": {
"blogStatus": "drafted",
"graphknowStatus": "none",
"blogPostSlug": "content-publishing-workflow"
}
}
Machine-readable, one file, no markdown parsing required.
The GraphKnow Side
GraphKnow already has an LLM wikification pipeline. When I upload a source document, the worker converts it, runs the wikify step (LLM-generated wiki page with slug, tags, outbound links), then indexes it. I don’t need to pre-process anything — the raw task-log markdown is a valid input.
So the GraphKnow side is just a curl call:
curl --request POST "${GRAPHKNOW_URL}/api/sources" \
--header "Authorization: Bearer ${GRAPHKNOW_BEARER_TOKEN}" \
--form "file=@${FILEPATH};type=text/plain"
That’s scripts/upload-to-graphknow.sh — 80 lines of Bash, a --dry-run flag, no jq dependency. The agent calls it and parses the source ID from stdout.
The Publish Flow
content-publisher agent (logId)
→ reads docs/task-log/<logId>/
→ writes apps/blog/posts/DRAFT-<date>-<slug>.md
→ uploads to GraphKnow (if token set)
→ updates .publish-manifest.json
/publish-draft <slug>
→ shows preview, waits for confirmation
→ renames DRAFT- file to production filename
→ runs bun run --filter @cloudnest/blog build:markdown
→ updates manifest: blogStatus → "published"
The confirmation gate on /publish-draft is deliberate: publishing is the one irreversible step. Everything before it is safe to run unattended — drafting, GraphKnow uploads, and manifest updates can all succeed or fail without affecting the live blog.
The full bun run build:markdown is run at publish time (not a dry-run) because it catches broken wiki-links and missing plugin behavior, not just frontmatter validation.
Implementation Scope
The workflow spans 2 new agent/command files, 2 bash scripts, 1 build-script change, a manifest, and a task-log entry. Small, focused, fits in a day.
This Post
This post was the first real end-to-end run. The content-publisher agent read the task-log directory for 20260423-content-publishing-flow/ (decisions, implementation log, open questions) and produced this draft. I edited it. You’re reading the result.
The backlog is content-publisher --all-unpublished away — that command scans all task-log entries not yet marked published, generates drafts for each, and updates the manifest. #blog #meta #graphknow #workflow
