Building in Public

Chapter 5

Closing the Gaps Before the First Publish

CampaignForge AI · May 2026 · SimQuant LLC

CampaignForge AI - The Journey

Chapter 5: Closing the Gaps Before the First Publish

Status: Raw draft for Content Publisher Agent (11) to format Date: 2026-05-07 Author: Tim Simeonov (founder) + Claude (engineer)


What Chapter 4 Told Us to Do

Chapter 4 was a hard look at the gap between what CampaignForge AI claims to be and what it actually is right now. The verdict was: local skeleton, solid foundation, zero published content.

The two biggest gaps named in that chapter were:

  1. 1. Agent 11 (Content Publisher) had an empty contract file. The agent that is

supposed to be the public voice of the project had no documented behavior.

  1. 2. Four journey chapters were sitting on disk, never processed, never published.

Chapter 4 ended with a clear instruction: before the first publish, make the boundaries explicit, harden the system, and define the missing agents properly.

That is what Chapter 5 built.


What Was Built

Agent 05 Contract Written

agents/05-cost-analyst.md was empty. It now has a full contract.

The contract documents what Agent 05 actually does: a deterministic spend-limit calculation from the brief budget. No LLM. No external data. Monthly budget divided by 30 gives the daily cap. The result is a CostAnalystOutput proposal with status: PENDING — Gate 4 sets approval, not the agent.

The contract also documents the enforcement principle explicitly: Agent 05 produces a proposal. Enforcement happens at Gate 4 (human) and, in Phase 2, at the infrastructure level. Agent logic alone is not the safety layer.

Agent 11 Contract Written

agents/11-content-publisher.md was empty. It now defines three content modes:

Mode 1: campaign_performance Post-campaign performance update triggered by Agent 06 returning TRIGGER_CONTENT. Only runs on real performance data. Blocked when is_real_performance_data is false. This is the mode that has been partially implemented since Chapter 3.

Mode 2: build_progress Standalone mode for publishing journey chapters. Takes a raw markdown chapter file, calls the LLM to format it as a LinkedIn post, writes the draft to disk, prompts for CLI approval, and optionally publishes to LinkedIn.

This mode does not use CampaignState. It does not require a running pipeline. It runs as a standalone CLI command outside the main graph.

Mode 3: performance_case_study Deferred to Phase 2. The schema supports it now. The implementation follows the first real campaign with verified results.

The contract also documents the content invariants that cannot be violated: AI disclosure on every post, simulated metrics disclosure when data is not real, Gate 6 required before any campaign-mode publish, draft to disk before any publish attempt.

New Schema Types

Two new schemas were added to src/schemas.py:

BuildProgressDraftOutput Standalone draft output for Mode 2. Has chapter_path, chapter_title, chapter_number, draft_posts, and content_drafts_path. No campaign_id. No pipeline_id. This is intentional — build progress posts are not tied to a campaign run.

BuildProgressPublishedOutput Publish result for Mode 2. Records the chapter, the published posts, and whether the output went to LinkedIn or was saved locally.

Both ContentDraftPost and PublishedPost now support two additional content types: build_progress and performance_case_study. These types were previously missing from the Literal definitions.

Build Progress Draft Function

src/nodes/content_publisher.py gained three new functions:

build_progress_draft(chapter_path) Reads a chapter markdown file, extracts the title and chapter number from the content and filename, calls the LLM with the full chapter text and a build-progress formatting prompt, validates the output (disclosure required, schema validated), and writes the draft to dist/content/chapters/<chapter-slug>/linkedin_post_N.txt.

build_progress_publish(draft) Takes an approved BuildProgressDraftOutput, checks disclosure invariants, and either calls LinkedIn API (if LINKEDIN_ACCESS_TOKEN is set) or saves locally with a clear message. Writes publish_record.json to the drafts directory.

Supporting functions: _extract_chapter_meta (extracts title and chapter number from content and filename), _chapter_slug (derives the output directory name from the filename).

CLI Command: --publish-chapter

python campaignforge.py --publish-chapter content/journey-chapter-01.md

The command:

  1. 1. Reads the chapter file
  2. 2. Calls build_progress_draft (LLM formats the chapter into a LinkedIn post)
  3. 3. Prints the full draft to the terminal with character count and disclosure status
  4. 4. Prints the draft file path
  5. 5. Prompts: Review the draft above. Approve to publish? [y/N]
  6. 6. If approved: publishes to LinkedIn (token set) or saves to disk (no token)
  7. 7. If rejected: draft stays on disk, nothing published

In non-interactive mode (piped input), the command saves the draft and skips the prompt. This makes it testable and scriptable.

Tests

A new test file was added: tests/test_nodes/test_build_progress.py

It covers:

The full local test suite now passes 155 tests (up from 138 before this chapter).


What Did Not Change

The main campaign pipeline graph was not modified. All six gates remain in place. The existing content_draft_node and content_publish_node (Mode 1) are unchanged. No existing tests were modified.

The content publishing for the campaign pipeline still requires real performance data before it can trigger content. That constraint has not changed. The build progress mode is a completely separate path that bypasses that requirement correctly — it has nothing to do with campaign metrics.


What This Means for the Dogfood Loop

The project now has the infrastructure to publish its own build journey.

The four existing chapters in content/ can each be processed with:

python campaignforge.py --publish-chapter content/journey-chapter-01.md
python campaignforge.py --publish-chapter content/journey-chapter-02.md
python campaignforge.py --publish-chapter content/journey-chapter-03.md
python campaignforge.py --publish-chapter content/journey-chapter-04.md
python campaignforge.py --publish-chapter content/journey-chapter-05.md

Each command drafts a LinkedIn post from the raw chapter markdown, lets the operator review and approve it, and either publishes it or saves it to dist/content/chapters/<chapter-slug>/.

This is the first part of the dogfood loop that is now working end to end. The system is documenting itself. Agent 11 can now publish that documentation.

The next part of the loop — real campaign execution — requires Meta credentials and a real ad account. That is the next chapter.


The Honest Current State After Chapter 5

What exists now:

What does not exist yet:

The project is not off track. The boundaries are now explicit. The content publishing infrastructure is working. The next move is to set LINKEDIN_ACCESS_TOKEN and publish Chapter 1.

This post was drafted by AI and reviewed by the operator. Content is published as part of the CampaignForge AI build-in-public journey.