Monday, December 01, 2025

From “Ask Codex” to AI Workers: Evolving ClubHub’s AI Work System

 


When I started ClubHub, “using AI” meant cracking open a terminal, pasting a prompt into Codex, and hoping it changed the right files.

That… sort of worked.

But pretty quickly I wanted something more structured:

  • AI that works like a developer on the team, not a magic autocomplete.

  • Clear tasks, a backlog, and a way to avoid collisions when multiple AI workers (and humans) touch the same codebase.

  • A paper trail: what was done, why, and what’s next.

This post walks through the evolution:

  1. A simple codex.sh script to spin up AI work sessions

  2. A real backlog documenting work for humans and AI

  3. Project rules that constrain what AI can do

  4. A locking system so multiple workers can collaborate safely

Lots of examples; steal anything useful.


1. The first step: a tiny Codex wrapper

The very first thing was a shell script to create a consistent context for AI work.

Instead of manually copying prompts, I added scripts/codex.sh:

#!/usr/bin/env bash set -euo pipefail # Simple Codex runner for ClubHub tasks. # Usage: ./scripts/codex.sh "Short task description" TASK_DESC="${1:-}" if [ -z "$TASK_DESC" ]; then echo "Usage: $0 \"Short task description\"" exit 1 fi SYSTEM_CONTEXT_FILE="prompts/system-project-context.md" cat <<EOF You are an expert Go, React, and DevOps engineer working on the ClubHub project. Project context: $(cat "$SYSTEM_CONTEXT_FILE") Task: $TASK_DESC Instructions: - Work in small, reviewable steps. - Prefer changing existing code over big rewrites. - When behaviour changes, update or add tests. - At the end, summarise what you changed and any follow-ups. EOF

The script didn’t talk to the API directly (Cursor / ChatGPT handles that), but it gave me:

  • A single source of truth for project context

  • A way to phrase tasks consistently

  • A habit: “spin up an AI worker with a specific mission”

The system context file

prompts/system-project-context.md looked roughly like this:

# ClubHub System Context - Backend: Go 1.21, chi router, Postgres - Frontend: React + TypeScript + Vite + Tailwind - CI: GitLab CI (.gitlab-ci.yml) - Goal: MVP for club membership, payments, events. General rules: - Keep changes small and focused on the current task. - Don’t introduce new dependencies without a clear reason. - For any new behaviour, add tests. - Prefer clarity over “clever” code.

This was “AI v0”: useful, but still essentially a very smart autocomplete pointed at the whole repo.


2. Adding a backlog AI can understand

Next I needed a backlog that both humans and AI could work from.

Enter docs/backlog.md.

Instead of vague TODOs, each item became a structured “story”:

## A – MVP Member + Subscription + Payments vertical slice (NOW) > Goal: A club admin can create a member, assign a membership plan, > record payments, and see them on the dashboard end to end. ### A1 - Membership subscription creation endpoint - **ID:** A1 - **Epic:** A - **Type:** Story - **Status:** Todo - **Priority:** High - **Area:** Backend - **Description:** Implement POST /api/membership-subscriptions to link a member to a membership plan, creating an active subscription. - **Acceptance Criteria:** - Creates subscription linking member_id to plan_id - Sets status to "active" - Sets start_date and next_billing_date - Validates member and plan belong to same club - Returns subscription details with status

This changed the AI relationship:

  • Instead of “fix whatever you see”, prompts became
    “Work on item A1, here are the acceptance criteria.”

  • Both of us were now staring at the same backlog.

For some stories (especially for Epic A/B) I started moving them into per-story files (docs/backlog/A.../A1-*.md), which made it easier to attach extra context and notes.


3. Teaching AI the rules of the repo

To stop AI from colouring outside the lines, I added project rules (Cursor / .cursorrules style).

Conceptually, they look like this:

# .cursorrules (conceptual example) - pattern: "internal/**.go" rules: - "Respect existing layering: handlers -> services -> db." - "Do not import new third-party libraries without explicit instruction." - "When changing behaviour, update or add tests in *_test.go." - pattern: "frontend/src/**.tsx" rules: - "Use existing UI primitives and Tailwind classes." - "Keep components small and focused." - "Add React Testing Library tests for new UI behaviour." - pattern: "docs/**.md" rules: - "Do not delete sections; deprecate with comments if needed." - "Backlog items must keep ID / Epic stable."

Now a worker started with:

  1. Shell script + system context

  2. Backlog item (A1 / B5 / etc.)

  3. Rules that say what’s allowed

Still missing: coordination. If three workers grabbed A1 at once, chaos.


4. Locking: treating AI as real team members

The next evolution was to assume there could be multiple workers, human and AI, and they mustn’t stomp on each other.

I added a lock to each story file’s front-matter.

Story file with lock

Example docs/backlog/A-mvp-member-subscriptions-payments/A1-membership-subscription-creation.md:

--- id: A1 epic: A title: Membership subscription creation endpoint status: todo # todo | in-progress | done | blocked priority: high area: backend # backend | frontend | ci-cd | docs | infra | ux | other lock: owner: "" # empty = no lock locked_at: "" # ISO8601 UTC, e.g. 2025-11-29T12:34:00Z expires_at: "" # ISO8601 UTC; empty = no expiry --- ## Summary Implement POST /api/membership-subscriptions to link a member to a plan and create an active subscription. ## Context - Multi-tenant: member and plan must belong to same club. - Plan interval defines next_billing_date. ## Acceptance criteria - Creates subscription linking member_id to plan_id - Sets status to "active" - Sets start_date and next_billing_date from plan interval - Validates member and plan belong to same club - Returns subscription details with status

Locking rules

Plain language for humans and prompts:

  • If lock.owner is non-empty and expires_at is in the futureLOCKED.

  • If lock.owner is empty, or expires_at is empty, or expires_at is in the pastUNLOCKED (missing or stale).

When a worker takes the task, they:

status: in-progress lock: owner: "ai-worker-codex-1" locked_at: "2025-11-30T11:05:00Z" expires_at: "2025-11-30T15:05:00Z"

When they finish:

status: done lock: owner: "" locked_at: "" expires_at: ""

If a lock expires, a “sweeper” (or a human) can reset it:

status: todo # or blocked, with explanation lock: owner: "" locked_at: "" expires_at: ""

5. Updating the AI worker guide

To make this stick, I created an AI worker guide in docs/ai-workers/README.md:

# AI Worker Guide: ClubHub ## 1. Before you start a story 1. Pick a story ID from docs/backlog.md (e.g. A1, A15, B5). 2. Open its story file under docs/backlog/** (e.g. docs/backlog/.../A1-*.md). 3. Read the YAML front-matter. 4. Check the lock: - If lock.owner is non-empty AND expires_at is in the future: - The task is LOCKED by someone else. Do not edit it. - Otherwise, the task is free/stale and you may take it. ## 2. Taking a lock - Update front-matter: - status: in-progress (unless done/blocked) - lock.owner: your worker ID (e.g. "daragh" or "ai-worker-codex-1") - lock.locked_at: current UTC time - lock.expires_at: current UTC time + 4 hours (or similar) Commit this metadata change first. ## 3. While working - At the start of each run, re-read the story file. - If lock.owner is different from your worker ID, stop – someone else took over. - Only modify files relevant to this story. ## 4. Finishing - When the work meets the acceptance criteria: - Update status to done (or blocked, with a note in the body). - Clear the lock fields (owner, locked_at, expires_at). - Append a short "Outcome" section summarising what changed and any follow-ups. ## 5. Recovering dropped tasks - If expires_at is in the past: - Treat the lock as stale. - Either: - Set status back to todo and clear the lock, or - Take the story yourself by setting lock.owner / locked_at / expires_at. - Optionally add a bullet in a "History" section explaining what happened.

Now every AI session can be told:

“Follow the AI Worker Guide in docs/ai-workers/README.md.”


6. Wiring locks into the rules

Finally, I wired the locking behaviour into the project rules so workers don’t “forget”.

Conceptual .cursorrules snippet:

- pattern: "docs/backlog/**.md" rules: - "Before editing, ALWAYS parse the YAML front-matter." - "If lock.owner is set and not expired and does not match you, do not edit." - "If taking the task, update status to in-progress and set lock.{owner,locked_at,expires_at} first." - "When finishing, set status to done or blocked and clear lock.*."

And a sweeper prompt for occasional cleanup:

# Sweeper: clear stale locks You are a maintenance worker for the ClubHub backlog. Tasks: - Scan all story files under docs/backlog/**.md. - For each file: - Parse front-matter. - If lock.expires_at is non-empty and in the past: - If status is not "done": - Set status to "todo". - Clear the lock block (owner, locked_at, expires_at). - Do not change any other fields or the body content.

Run that occasionally and the backlog never stays jammed with dead locks.


7. Why this feels different from “just using AI”

After this evolution, the AI stopped being “a fancy autocomplete” and started to feel like:

  • A team of junior/mid engineers with:

    • A backlog

    • Task IDs

    • Ownership and locks

    • A written working agreement

Key properties:

  • Repeatability: prompts and rules live in the repo, in version control.

  • Accountability: each story file shows status, lock owner, and a short history.

  • Safety: multiple workers can operate without stepping on the same story.

The tooling is simple — shell scripts, markdown, YAML — but it creates a strong frame for AI collaboration.


8. A short history of the AI system (from git)

One of the nice side-effects of keeping everything in the repo is that the

AI system itself has a clear history. You can literally see when each concept

arrived.


A simplified timeline (from git):


- **2025-11-10 – First Codex wrapper**

  - Added `scripts/codex.sh` and `prompts/system-project-context.md`.

  - Goal: stop copy-pasting giant prompts; give AI a stable project context.


- **2025-11-12 – Backlog as a first-class citizen**

  - Introduced `docs/backlog.md` with structured items (`ID`, `Epic`, `Status`,

    `Acceptance Criteria`).

  - AI tasks switched from “fix this file” to “implement story A1/B5/etc.”.


- **2025-11-15 – Project rules for AI**

  - Added project rules (e.g. `.cursorrules` / Cursor config).

  - Defined per-area constraints:

    - Backend: respect layering, add tests.

    - Frontend: keep components small, test with RTL.

    - Docs: don’t delete history, keep IDs stable.


- **2025-11-18 – Locking and AI Worker Guide**

  - Created per-story markdown files under `docs/backlog/**`.

  - Added YAML front-matter with `status`, `priority`, and a `lock` block:

    - `lock.owner`, `lock.locked_at`, `lock.expires_at`.

  - Wrote `docs/ai-workers/README.md` explaining how workers:

    - Check locks

    - Take a lock

    - Release or recover stale locks


- **2025-11-20 – Sweeper and hygiene**

  - Added a “sweeper” prompt in `docs/ai-workers/sweeper-prompt.md` to clear

    stale locks.

  - Ensured rules require AI workers to:

    - Honour locks

    - Update status when done

    - Keep backlog and docs in sync with work.


This is the bit I find most interesting: the AI system itself is just another

feature, evolving through small commits:


- Tiny script → shared context  

- Shared context → backlog the AI can read  

- Backlog → rules  

- Rules → locks  

- Locks → a team of humans + AI that can safely work in parallel

No comments:

🐌 From Codex CLI to OpenAI API: Building a Smarter AI Worker in 24 Hours

From Codex CLI to OpenAI API: Building a Smarter AI Worker in 24 Hours How throttling led to a complete rewrite, cost optimization, and a mo...