Selected work

Framing a project so AI agents can build parts without losing the whole

A zero-dependency Python 3.11+ project framing CLI, starter kit and matched Research Navigator comparison for building modular software with AI coding agents.

How do you build fast with AI agents without losing the whole?

Working local starter kit and CLI (v0.1.1 at commit 6fdee0d; initial freeze v0.1.0-frozen at aa770b8). Python 3.11+ standard library only. Public implementation on GitHub.

When an AI coding agent starts in an empty folder or reads one long chat transcript, small tasks often succeed while larger builds drift: modules duplicate each other's helpers, interfaces shift mid-run, and early unit tests pass against mocks that do not match the connected program.

On the front page of this site, the Baldies remake shows one side of fluid software: rebuilding an existing program from the outside so its rules live in plain tables you can change while it runs. FluidSoftware (fluid.py and its starter-kit/) is the build-time companion for new software. Before dispatching builders, I materialize the project frame first: a whole-project map (FRAME.md), native module contracts (areas/*/MODULE.md) with disjoint file ownership, scoped context packets, and SHA-256 delivery and acceptance receipts.

I designed the workflow and evaluation protocol and directed the team (the AI coding agents I direct) to build the 1,264-line Python CLI (60,109 bytes across 6 standard-library files), package the self-hosted foundation, and run the matched Research Navigator comparison on October 23, 2026.

Three separate dependency graphs and three-tier context

A single dependency list mixes three different questions: what sits inside a capability area, whose interface a module consumes, and whether a builder must wait before starting. fluid.py separates them into three directed acyclic graphs (DAGs) validated by fluid/model.py:

  • parent (containment hierarchy): Optional capability grouping used when rendering ancestor context.
  • depends_on (semantic contract): Declares that a consumer module relies on a provider's interface. Builders can code against a pinned contract in parallel, and any later byte change to an upstream provider's owned files or evidence marks downstream deliveries stale.
  • build_after (dispatch gate): Blocks fluid.py claim until the prerequisite module has been delivered or accepted.
FluidSoftware four-stage project framing and receipt lifecycle Stage 1 validates project.json across three DAGs (parent, depends_on, build_after). Stage 2 materializes FRAME.md, MODULE.md contracts, and .fluid/state.json without overwriting existing files. Stage 3 renders a three-tier bounded context packet. Stage 4 binds claim, deliver, and accept receipts to SHA-256 file digests and marks downstream consumers stale when upstream contracts change. 1. BRIEF & 3 DAGS fluid.py validate parent: hierarchy depends_on: contract build_after: gate Disjoint file ownership 2. ATOMIC FRAME fluid.py frame --apply FRAME.md (whole map) areas/*/MODULE.md .fluid/state.json Never overwrites edits 3. 3-TIER CONTEXT context --module ID 1. Whole project map 2. Ancestor chain 3. Owned unit & peers Cold-start continuation 4. HASH RECEIPTS claim -> deliver accept -> verify Upstream file edit marks consumers stale SHA-256 byte identity Commit 6fdee0d (v0.1.1): 1,264 LOC across 6 Python 3.11+ standard-library files, 7/7 unit tests passing.
How fluid.py validates three separate DAGs, materializes module contracts, scopes builder context, and tracks SHA-256 staleness across semantic consumers.

Self-hosted foundation check

Before testing the workflow on an external application, the tool packaged its own four responsibilities (model, frame, state, and cli) through foundation.project.json and workflow/prepare_foundation.py.

Across a 15-event lifecycle check, fluid.py exercised read-only frame preview, atomic hard-link file creation, repeat application preserving active state, exclusive lock contention on .fluid/state.lock, and transitive staleness propagation when state and frame delivered after cli, followed by invalidate and renewed acceptance (accepted = 4, stale = 0, integrity_issues = 0). Release v0.1.1 (6fdee0d) also tightens validate_brief() so falsey non-null parent values ("", 0, false) are rejected rather than coerced to None; the original October 23 freeze remains tagged at v0.1.0-frozen (aa770b8).

Inspect the self-hosted foundation.project.json 3-DAG contract & SHA-256 state receipt
{
  "project": "fluid-foundation",
  "modules": [
    { "id": "model", "owns": ["fluid/model.py"], "depends_on": [], "build_after": [] },
    { "id": "state", "owns": ["fluid/state.py"], "depends_on": ["model"], "build_after": ["model"] },
    { "id": "frame", "owns": ["fluid/frame.py"], "depends_on": ["model", "state"], "build_after": ["model"] },
    { "id": "cli",   "owns": ["fluid/cli.py", "fluid.py"], "depends_on": ["model", "state", "frame"], "build_after": ["state", "frame"] }
  ],
  "verification": { "accepted": 4, "stale": 0, "integrity_issues": 0 }
}

Matched comparison: building a four-document Research Navigator

On October 23, 2026, I ran a paired comparison (PROTOCOL.md and RESULTS.md) to see how a fresh builder given a 4-module Fluid frame (navigator.project.json: parsing, storage, query, cli) compared against a fresh builder using an ordinary workflow on the same task brief (TASK_BRIEF.md).

Both builders had to write an offline Python 3.11+ CLI (navigator.py with index, search, show, export, and status) over four real documents (two Markdown files, one dense plain-text file, and a 223-paragraph Word .docx containing 81 table paragraphs), preserving SHA-256 provenance, native line and paragraph locators, and stale-source detection.

Matched Research Navigator build (October 23, 2026, n = 1 builder per arm).
Measure Foundation-assisted arm (runs/foundation) Ordinary-workflow control (runs/control)
Delivered architecture 4 framed modules (nav/parsing.py, nav/storage.py, nav/query.py, nav/cli.py) + nav/contracts.py; 87 indexed passages 4 self-organized modules (navigator.py, nav_sources.py, nav_index.py, nav_search.py); 92 indexed passages
First-pass automated evaluator 11 pass, 1 fail (freshness), 1 manual-review gate 11 pass, 1 fail (markdown_primary), 1 manual-review gate
Cause of first-pass failure Frozen evaluator bug: Path.read_text() converted CRLF to LF before checking a passage from resuse.txt that preserved 3 CRLF pairs Frozen evaluator bug: ATX-only heading list rejected a valid CommonMark level-2 setext heading (\text{integration risk} underlined by -)
Adjudicated acceptance on unchanged first delivery 13 / 13 protocol rows satisfied (adjudication, manual review) 13 / 13 protocol rows satisfied (adjudication, manual review)
Product repairs required 0 (unchanged first-delivery bytes) 0 (unchanged first-delivery bytes)

Limits: Both fresh builders completed working, independently accepted applications on their first delivery on October 23, 2026. Although the assisted builder finished its implementation pass in a shorter interval than the control builder, target-specific frame authoring (navigator.project.json) was completed before dispatch without separate timing, both arms required manual and adjudication review, and this is a single-pair run (n = 1) on one machine with 0 external reproductions. It does not establish a net speed multiplier.

Auditing the evaluator before requesting code repairs

The sharpest engineering finding from the comparison came from auditing the evaluation harness before requesting code repairs (EVALUATOR-REVIEW.md):

  • Universal-newline stripping in evaluate.py (adjudicate_newlines.py): In the Foundation arm, commands 29 to 31 exported lines 14 to 17 of resuse.txt, which contain three CRLF line endings. The exported Markdown file (sha256: b20241bb...) preserved all three CRLF pairs, but the frozen evaluator read the report with Path.read_text() (normalizing CRLF to LF) and searched for the raw CRLF JSON string. Re-running the interrupted freshness sequence with byte-preserving reads passed all 16 CLI checks on the unchanged snapshot.
  • ATX-only heading oracle vs. CommonMark setext rules (adjudicate_headings.py): In the Control arm, searching largest coherent responsibility in deep-research-report.md returned \text{integration risk} in its parent heading chain because line 298 was followed by a single - on line 299 inside a display equation. Running the reference parser markdown-it-py 4.0.0 in its commonmark preset confirmed that CommonMark 0.31.2 §4.3 parses lines 298 to 299 as an h2 setext heading, and all 4 remaining search and show checks passed on unchanged code.

If I had sent those two red rows back to the builders as repair requests, both agents would have weakened their source fidelity or parser generality to appease a broken check. The starter workflow now explicitly checks failed assertions against original sources before requesting code repairs.

Run the CLI, unit tests and comparison demo

Requires Git and Python 3.11 or newer. No third-party packages, accounts, or API keys are used. These commands validate and materialize the self-hosted foundation, run the 7 unit and lifecycle checks, and execute both delivered Research Navigators against a generated sample corpus (.md, .txt with CRLF, and .docx with a Word XML table):

git clone https://github.com/thisisntjon/fluid-software.git
cd fluid-software
git checkout 6fdee0dddf8a446bdbd298b81db5a9df66e0232a
python fluid.py validate foundation.project.json
python fluid.py frame foundation.project.json ./tmp-self-host --apply
python ./tmp-self-host/fluid.py verify ./tmp-self-host --json
python -m unittest discover -s tests -v
python examples/research-navigator/run_demo.py

You can also open showcase/fluid-architecture-showcase.html locally in any browser for an offline interactive SVG walkthrough of the decomposition stopping rule, the three DAGs, the three-tier context lens, and SHA-256 staleness propagation.

Primary artifacts and source links

Questions or notes on your own runs? Reach me at jon@simoneresearch.com or on the contact page.