Disciplined AI Collaboration
Constraints, checks and skepticism for building software with AI.
Ship
A1One chain
Every tool sits on one chain. One command runs the whole gate in stages, and each stage runs the checks, the fixers, the generators and the validators it owns. A check that only runs when someone remembers its command is a convention, and a convention is not a check. The chain is what turns a collection of tools into a verdict.
A1.1Everything through one chain
A tool reached only by its own command enforces nothing. One chain, and everything in it. Tooling accumulates as separate commands, and the commands stop getting run.
Three linters, two of which nobody has run since spring, and a formatter that runs on some machines because of an editor plugin. A tool outside the chain depends on a person's memory, and memory is the one component in the system with no check on it.
Route every tool through the one entry point, and treat a tool outside it as enforcing nothing. Give the project one entry point whose default is the whole pipeline, and let arguments narrow it and never widen it. Order its stages by what each one needs from the one before, and state the dependency when a step produces an artifact another stage consumes. Put every check, fixer, generator and validator in a stage as a direct call, never as an alias the gate shells out to. Write the aggregate on every exit path. Let the gate govern its own tooling with the same rules it applies to the code.
List every check the project claims to have. Run the one command. Each check must appear in its output, or it is not a check the project has.
Narrowing is for local iteration speed only. A member, a step or a bypass answers a question faster; a completion claim requires one unbypassed whole-scope run, and a narrowed run never overwrites the one aggregate, because a document about a narrower subject under the aggregate's name is a document about a different subject wearing the same name.
Stage order encodes real dependencies, so it is load-bearing rather than tidy. A cleaning step runs before anything measures a file. A type check runs before any structural check reads a tree that may not compile. The fixer stage writes the closure graph the graph-aware rules read when they load, so it precedes linting, and a graph-aware rule fails closed when the graph is missing rather than passing over nothing. Several stages mutate the working tree, the comment stripper, the graph write, the codemods and every fixer, which is why an investigation that runs the gate has mutated under a read-only contract.
The rule index is re-derived from disk before the stages rather than inside them. The orchestrator loads the quality config at module load and the config loads the generated index, so a rule deleted from disk but still named in a stale index would kill the run before stage one. Re-deriving first makes deleting a rule a one-step operation, and it is the same move that makes adding a rule a dropped file. The chain checks its own registration contract and the shape of every finding, because the mechanism enforcing every other rule is the one most able to decay silently.
Where a host already has a toolchain, the chain hands off rather than duplicating. The host elects which concerns to hand over, its tools stay its own, and there is one chain instead of two. A verification slot the host cannot fill, a build, a runtime probe, a size cap, resolves absent, the step reading it does not run, and the claim it would have settled is carried as observed by a person rather than as verified.
The chain is data before it is a run. A pure planner takes the resolved scope, which members, which step, which bypasses, and returns the stage array, and the runner walks that array in order. A stage carries its slug from a closed set, whether it is bypassed by default, whether its steps may run in parallel, and its steps. A step carries a label, its command as a direct call, its scope, its tags, and where it produces or consumes an artifact another step reads, so the load-bearing order is stated on the step rather than remembered. A member carries whether it is gated and where its tests live, and a member with no tests declares null rather than an empty string, because the two are different claims. Everything a stage-control flag can do is a function over this data, and nothing a flag does can add a step the array does not hold.
A stage array · the chain as typed data a pure planner buildsexport const STAGE_SLUGS = ["prepare", "prune", "heal", "format", "lint", "test", "build", "validate"] as const; export type StageSlug = (typeof STAGE_SLUGS)[number]; export interface Member { readonly id: string; readonly dir: string; readonly gated: boolean; readonly tests: string | null; } export interface Step { readonly label: string; readonly command: readonly string[]; readonly scope: "wide" | "perMember" | "appOnly"; readonly tags: readonly ("generate" | "validate" | "build")[]; readonly produces?: string; readonly consumes?: string; } export interface Stage { readonly slug: StageSlug; readonly bypassedByDefault: boolean; readonly parallel: boolean; readonly steps: readonly Step[]; } export declare function stagesFor(scope: { readonly members: readonly Member[]; readonly only?: string; readonly bypass?: readonly StageSlug[] }): readonly Stage[];
flowchart TB
command["one command"]
rederive["the rule index is re-derived from disk before any stage"]
prepare["prepare · dependency integrity, one typecheck per member"]
unused["unused · dead code, dead exports, dead dependencies"]
fixers["fixers · strip comments, rebuild the graph, run the rewriters"]
format["format · the formatter, fix on"]
lint["lint · every linter, per member, fix on, reading the graph the fixers wrote"]
test["test · every suite, then the passing-test floor"]
build["build · the site, the diagrams, the chapters, every generator, every derivation"]
validate["validate · discovery, leaks, graphs, configs, documents"]
report["the one aggregate · written on every exit"]
command --> rederive --> prepare --> unused --> fixers --> format --> lint --> test --> build --> validate --> report
prepare -. fail fast .-> report
lint -. fail fast .-> report
test -. fail fast .-> reportflowchart TB
whole["No arguments · every stage, whole scope, healing on"]
member["A member · that member's steps, repo-wide steps reported as skipped"]
step["A step · only that step"]
bypass["A bypass · one stage skipped, local iteration only"]
claim["A completion claim"]
whole --> claim
member -. never satisfies .-> claim
step -. never satisfies .-> claim
bypass -. never satisfies .-> claimB1Scale follows from structure
Nobody chooses how many parties a body of work needs. The count follows from how the work partitions into concerns that must be able to contradict each other. The floor is the number of those concerns. The ceiling is set by the worst fan-in, the point where claims resting on one surface stop surviving. A count with no partition behind it is a preference, and the fan-in finds it out.
B1.1Floor and ceiling
Scale follows from structure and is never a choice. A system that scales carries a guide to its own seams. The number of agents or people on a task tends to come from a preference, and a preferred number is wrong in one of two directions.
Five agents work a task that has two concerns. Three of them wait, and the surface they all write to becomes the bottleneck. A count chosen independently of the partition either leaves a concern with no owner or gives one surface more claims than it can hold.
Derive the party count from the partition and the fan-in, never from a preference. Partition the work into concerns first. Count the partition to get the floor. Measure the fan-in on each shared surface to find the ceiling, from traffic the surface already records. Choose a count inside that range and write down the partition it came from, and keep a guide to the seams beside every system that scales.
Ask what partition the current count came from. A count with no partition behind it is a preference, and the fan-in will find it out.
Volume is the wrong operand. Quantity divides work and does not divide concerns, so a large task with two concerns needs two parties and a small task with five needs five, and the fan-in caps both.
Fewer parties than the floor and one concern has no owner, so it is decided by whoever happens to be nearest, which is the substitution every other chapter refuses. More parties than the ceiling and the shared surface becomes the bottleneck: a claim is stale by the time it lands more often than it is read, and the parties spend their rounds re-deriving each other's reads. The fan-in is measured, never declared, because a coordination surface already records who authored a claim, who it was addressed to and which surface it landed on, and a declared fan-in is a count someone typed.
Determinism is what makes scale free. A deterministic check costs the same on the thousandth change as on the first, and its verdict is the same whoever runs it, so adding a party adds no enforcement cost. A protocol resting on care multiplies its cost by the party count while its enforcement stays flat, which is why every coordination rule here is either a mechanism or a declared piece of conduct with its evidence written down.
flowchart TB
work["A body of work"]
partition["Partition it into concerns that must be able to contradict each other"]
floor["Floor · the number of concerns"]
fanin["Measure the fan-in on each shared surface"]
ceiling["Ceiling · the worst fan-in before claims stop surviving"]
count["The count · a choice inside the range, with the partition written down"]
work --> partition --> floor
partition --> fanin --> ceiling
floor --> count
ceiling --> countC1The deploy is a file operation
The deployable is derived from the routes the site serves. The deploy is a file operation with a rollback, and it never touches a process it does not own. Secrets stay outside the tree, and every served surface speaks encrypted transport, the local development server included, because development exercises the same transport as production or it exercises something else.
C1.1Derived from the routes
The deployable is what a page can reach, and the deploy is a file operation with a rollback. A deploy that ships a build folder ships whatever happened to be in it.
A page fails to pre-render, nobody notices because the old file is still in the build folder, and the stale page ships. A build folder accumulates whatever a tool once emitted, and a deploy that ships the folder ships all of it.
Derive the deployable from the routes, and make the deploy a file operation with one rollback. Start from the served routes and follow every path a text artefact names. Delete every file nothing reaches. Upload the whole of what is left. Keep one backup of what you replace, and restore it on any failure. Hand a command that touches a shared machine to the person who owns it rather than running it, and keep every secret in the one artifact declared to bear it.
List every file in the deployable and the route that reaches it. A file no route reaches is a file the discovery check should have refused.
A machine that hosts other people's processes is never touched beyond your own files. The deploy is a file operation because a file operation has a rollback and a process operation has a blast radius.
The discovery check holds four things before anything ships. Every route has a pre-rendered file with real text, its own title and its own canonical address. The sitemap lists exactly the routes and the robots file points at it. Every payload a machine reads is parsable and carries no inline markup. Every asset in the build is reached from a route, and the rest was pruned. A page that fails to render, or an id declared for a page no view registers, reds the gate before a deploy rather than after.
flowchart TB
routes["The served routes"]
follow["Follow every path a text artefact names"]
reached["What a page can reach"]
prune["Delete everything else"]
backup["Back up what is live"]
upload["Upload the whole of what is left"]
check{"Healthy?"}
done["Live"]
restore["Restore the backup"]
routes --> follow --> reached --> prune --> backup --> upload --> check
check -- yes --> done
check -- no --> restoreD1When not
A rule that stands in for a property is a proxy. A proxy is easy to state and easy to enforce for its own sake, and it ends up guarding the wrong thing. Four proxies come up often enough to name, with the property each one stands for and what holds that property instead.
D1.1Proxies give way
A rule that stands in for a property gives way once the property has a check. A rule that outlives its reason gets enforced for its own sake.
The team argues about whether a file may have one more line while the module it lives in has no boundary at all. A proxy is easier to state than the property, so it gets stated first and then outlives its reason.
Hold the property with a check, never the proxy with a rule. Ask what property a rule protects. Name the property. Check the property directly and let the proxy go.
For each rule you hold, name the property it protects. A rule with no property behind it is ceremony.
flowchart TB
subgraph proxies["The proxy rule"]
r1["Benchmark everything before any application logic"]
r2["Synchronous over asynchronous, always"]
r3["A fixed line count, repeated in every document"]
r4["A voice the AI keeps up all session"]
end
subgraph properties["The property it stands for"]
p1["A measurement precedes an optimisation"]
p2["One correct answer exists"]
p3["Bounded complexity"]
p4["Consistent behaviour"]
end
subgraph holds["What holds the property"]
h1["Measure a hot path when you have one, never as a rite"]
h2["Asynchronous by necessity, still one correct answer"]
h3["One value in the tool that enforces it, no document repeats it"]
h4["A role document and a check on the output, never a voice"]
end
r1 --> p1 --> h1
r2 --> p2 --> h2
r3 --> p3 --> h3
r4 --> p4 --> h4E1The honest gaps
The strength and the tension are the same thing. Nothing computes worth. I decide it. Nothing detects non-progress. I notice it. Confidence is a threshold, not a distribution. Several conduct rules have no artifact behind them yet. I state the gaps because a method that claims completeness is one whose gaps you find in production.
E1.1Declared absences
An absence gets measured and declared. Nobody infers it, and nobody papers over it. A method that hides its gaps hands them to the reader unannounced.
The method reads as complete. A reader relies on a guarantee it never gave, and finds out where it matters most. A guarantee nothing provides gets assumed by whoever needs it, and the assumption never fails where it began.
Declare every absent guarantee by name, where the reader would otherwise assume it. Keep a list of the guarantees the method claims and mark each as held, derived or absent. Fill an absence with a declaration in the adapter. Never fill it with an assumption in the core.
Read the declared absences. Each must name what would fill it. A method with no declared absences has stopped looking for them.
The absences are named in the adapter as slots nothing fills. Worth: whether a task is worth doing is a decision I make with a sentence written before the work, and no mechanism computes utility against cost over branches. Idempotence: no step compares a prior render against a fresh one to prove regeneration reached a fixed point, so the generators overwrite unconditionally and the drift check is the only signal. Non-progress: the gate has no detector for a run that thrashes, so it runs every step every time and cannot tell a converging run from an oscillating one. Confidence: a claim passes or fails a threshold, and nothing carries a distribution of how sure the verifier is. Relevance: no registry entry carries its own test of whether it applies, so every composition iterates everything. Counts: an authored number is no longer checked against a real count, because the predicate was withdrawn deliberately in favour of authoring no counts at all.
The adoption status follows from the slots. The naming standard is enforced. The reasoning ontology is adopting rather than wired in: its epistemic and structural predicates are real and running, and its conative layer, worth, stopping on non-progress and calibrated confidence, is the honest gap. Several conduct rules describe behaviour no artifact can witness, and they are listed as conduct with the evidence a check would need. The one habit that keeps this list honest is measuring an absence before reporting it: the search is re-run one scope wider and taken over every surface where the thing could be declared, because a capability nobody calls and one that does not exist read identically from inside a tree.