# Yioop — Development process for LLM co-implementers

How Chris Pollett likes development to run, written for an LLM working with him on Yioop. Most of this is shared with his other projects; what is specific to Yioop — the gold/work sync, the Createdb check, the locale and library rules — is marked. When something says "always" or "never," treat it as a hard rule.

Round counter. Keep a count of rounds since you last re-read this file. A round is one exchange: Chris says something, you respond. Echo the current count in every response so it is visible (in bold). When the count reaches 10, re-read this whole file before writing your next response. Write 2-3 sentences for each section in this file briefly describing it and how you can adhere to that section more closely in the next round. Then reset the count to 0. This is a memory aid: it keeps these conventions fresh over a long session instead of letting them drift. The same counter runs in atto's copy of this file.

The review goes in the body of the response where Chris reads it, never in a file he has to open. Writing it to a file and saying it exists is not writing it.

Two things go in the review beyond the section-by-section reading, and both are the point of the exercise rather than an addition to it:

1. Every lapse in the ten rounds, named, with what stops it recurring. Not a general resolution to do better: the rule that was broken, the round it was broken in, and the mechanical guard or changed habit that catches it next time. A lapse Chris had to find and report is worth naming first.
2. What the ten rounds show about the process itself. Where rounds went to waste, which check caught a fault late that could have caught it early, which step cost more than it returned. Say what to change in this file, and change it in the same patch.

## How changes move (Yioop-specific): gold and work

Yioop is developed against a two-clone discipline:

- gold is the confirmed line of history. Its `confirmed-head` tag marks the latest confirmed commit.
- work is a throwaway clone of gold where changes are made. It is re-cloned from gold after every gold update, with the configured `work_directory` restored into it.

Setting up gold, and keeping it current, from seekquarry.com. The confirmed history lives at `https://www.seekquarry.com/group/2/yioop-repo.git`, which now serves smart-HTTP clones, so gold is a shallow clone of it rather than a hand-built local vault. To create gold, shallow-clone to a couple hundred commits of history: `git clone --depth 200 https://www.seekquarry.com/group/2/yioop-repo.git` into the gold path. That is enough recent history for context without pulling the whole repository. Tag `confirmed-head` at the clone's tip.

Each round you deliver one patch. Gold then advances not by pulling from the remote each round — that has proven unreliable over the shallow clone — but by applying your own delivered patch to gold once Chris confirms he has committed it. The signal is his showing you the commit message that goes with your patch: if his reply names or quotes that commit message, the patch is confirmed, so apply it to gold — `git apply` the delivery, commit it in gold with that message, and move `confirmed-head` to the new commit — then re-clone work. If his reply shows no commit message for it, the patch is not confirmed, so leave gold exactly as it was and do not apply it. This keeps gold's tree in step with what Chris committed without depending on a network pull; the local commit hash will differ from his, but the tree content matches, which is all a later `confirmed-head..HEAD` diff needs.

gold is a vault, not a workbench. Exactly two operations ever touch gold: cloning it to make work, and advancing it by applying a patch Chris has confirmed and moving `confirmed-head` to it, as above. Nothing else, ever. Never run a build, a test, a golden regeneration, a `Createdb`, a `ConfigureTool`, or any other command inside gold; never create a `work_directory` there; never hand-edit or hand-copy a file into it, and the only commit it ever takes is that one confirmed patch. Every build, test, and regeneration happens in a work clone. If you ever find yourself about to go into gold to run something — even just to check a result or to get back in sync — that urge is the process breaking; stop, and do it in work.

The loop each round, and this must be done prior to any new work: first settle gold. If Chris's reply shows the commit message for the patch you delivered last round, apply that patch to gold, move `confirmed-head` to the new commit, re-clone work, and restore `work_directory`; if his reply shows no such commit message, gold and work stay as they are and you build this round's work on top of what is already there. Starting new work without settling gold this way is the most common way the two lines drift. If a round's work has already begun and only then does Chris confirm the previous patch, do not re-clone work — that would discard the in-progress changes; instead apply the confirmed patch to gold, move `confirmed-head` to that commit in both gold and work, and only then cut. Cut a delivery as `git diff confirmed-head..HEAD`, and dry-run it with `git apply --check` on a fresh clone checked out at `confirmed-head` before presenting. Apply patches forward only — never reverse-diff. Never assume anything is committed until Chris shows the commit message for it. Name a patch for what it does in plain terms, never a sequence number or a plan-item number.

### Two scripts that keep gold in step with what Chris committed

Saying gold advanced is not advancing it. The failure this guards against is writing “gold is now at X” without having run the apply, which leaves gold behind Chris's tree; the next patch is then cut against the wrong base and fails on his machine. Two small scripts in `devlog/` remove the judgment from the reporting, and a ledger file records what has been delivered and what has been settled.

- `devlog/devscripts/settle_gold.sh "commit message"` is the first action of every round, always run, with the commit message Chris showed or with no argument when he showed none. With a message it applies the oldest delivered-but-unsettled patch to gold, commits it, moves `confirmed-head`, and marks it settled. Either way it prints gold's head read back from the tree, and any patch still waiting. The gold line in your response is that printed output, pasted. If you did not run it, you have nothing to paste, which is the whole point.
- `devlog/devscripts/cut_patch.sh name-of-patch` cuts the delivery and refuses while anything in the ledger is still waiting. That refusal is what makes a stale gold impossible to build on: a dry-run against a fresh clone of gold cannot catch a stale gold, because the clone is gold and passes happily.

`devlog/devscripts/check_sync.sh` is the third, and it guards the two ways the first two have been got round. It refuses when gold's working tree is not its own `confirmed-head`, which catches anything done inside gold by hand; when work's `confirmed-head` names a different tree from gold's, which catches settling gold and then failing to re-clone work, the fault behind three patches in a row that would not apply; and when the ledger's contents do not match the checksum the two scripts leave beside it, which catches editing the ledger by hand instead of letting a script write it. `cut_patch.sh` runs it before cutting, so none of those states can produce a delivery.

The ledger lives outside the repository (by default `/home/claude/gold_ledger.txt`) and holds one line per patch: its path, its name, and `delivered` or `settled`. Both scripts take `GOLD`, `WORK`, `OUT` and `LEDGER` from the environment, so a sandbox laid out differently only needs those set.

### A bug that outlives a conversation

A conversation can reach its limit mid-chase. When one does, write the state of the chase as a file and hand it over — what is established, what is ruled out, the plan and which step it is on, the exact next action, and the traps met along the way. Such a note stays out of the repository: it is scaffolding for one hunt, not something the history should carry forever, so it goes to Chris as a file of its own and never into a patch. A new session reads it after this process file and carries on without repeating the ruled-out ground.

## Setting up the dev environment (Yioop-specific)

Beyond the gold/work clones above, a work clone needs a configured `work_directory` before it will build or run. Let php be the command you use for the most recent version of PHP you have access to (needs to be > PHP 8.1). Restore the configured `work_directory` into the fresh work clone, point Yioop at it with `php src/configs/ConfigureTool.php work-dir`, then build the two SQLite databases with `php src/configs/Createdb.php`, which should print “Create DB succeeded.” Without a configured `work_directory`, `Config.php` stops short of defining its later constants (for example `MAX_GROUP_PAGE_LEN`) and Createdb dies with an undefined-constant error — that failure is the environment, not your patch. Createdb also regenerates `work_directory/app/locale`; since `LOCALE_DIR` prefers that copy, delete it after Createdb so translations fall back to the current `src/locale` when you render a page.

### Tools that ship with Yioop

- `src/executables/CodeTool.php` is the general-purpose tool. Run a test script with `php src/executables/CodeTool.php unit <TestFile>` (or `unit list` / `unit ClassName methodName` to target cases); check docblocks with `needsdocs <file>`; and check the 80-column rule with `longlines <file>` (multibyte-aware, so it is the authority over a raw byte count).
- Yioop itself can be run under its own web server via the index.php script in the root directory of the project, or under another web server such as nginx or Apache.
- There are many Command-line tools that can easily be installed in this environment: `lynx` and `curl` for fetching and reading pages as text, plus `node` and `npm`. Rather than say don't have the tool you want, try to install it.

### Getting a headless browser

`wkhtmltoimage` and `wkhtmltopdf` are installed and turn a page or fragment into a PNG or PDF with no X server, but they are an old WebKit: they skip CSS nesting and resolve `var()`, `clamp()`, and some `@media` interplay unlike a current browser, so a width they report can be a renderer artifact. Use them for a quick look at a fragment; when a measurement has to be right, use a real browser.

A walk that reads a page for behavior rather than for pixels runs against a server started with errors shown, as the audit above requires. A walk that passes with errors off has not tested what Chris sees.

Say which screens a walk covered. “Checked in a browser” with three screens behind it reads as the whole panel and is worth nothing to Chris. A walk that changes a setting puts it back, and the round ends with a fresh `Createdb`. The language is the one setting a walk must not touch at all: everything read afterwards comes from it, so a walk that leaves it changed makes every later screen and every test read another language. That has now cost two rounds, once chasing a page that did not exist under Arabic and once chasing five failing tests that were reading Arabic wording. Where a walk picks a setting to change, it passes over any named lang, LOCALE_TAG or DEFAULT_LOCALE, and the round ends with `php src/configs/ConfigureTool.php locale en-US`.

For that, drive a scriptable Chrome from `puppeteer-core`, which installs from the npm registry and downloads no browser of its own. A Chrome or chromium build is often already present in a dev sandbox — check `~/.cache/puppeteer` and `/opt/pw-browsers` for a `chrome` or `chrome-headless-shell` binary and point `executablePath` at it (do not hard-code a version in the path). Downloading one through `@puppeteer/browsers install` may fail because its host is off the allowlist, and widening the sandbox is not something to ask for, so prefer the binary that is there. Launch headless with `--no-sandbox --disable-dev-shm-usage` (not `--single-process`, which breaks the DevTools pipe), set the viewport to a real phone width — about 360px, not 500px, which is wider than a phone and hides overflow — then read `getBoundingClientRect` and `getComputedStyle` inside `page.evaluate`, for instance collecting every element whose right edge passes the viewport to find an overflow. If no scriptable browser can be found, fall back to `wkhtmltoimage`, reason about the cascade, and ask Chris to confirm the pixels.

A walk turns the browser's cache off before it reads anything. A page served from cache runs the script of the round before, so a fix reads as absent and a fault reads as still there. That cost two rounds here: a rectangle tool that worked looked broken until a hard reload, and a grid step-back that worked looked broken for the same reason. In puppeteer this is `page.setCacheEnabled(false)`, set once when the page is made.

Where a fault is reported in a named browser, that browser is the one to reproduce it in. Chrome, Safari and Firefox differ over what they draw for a field tied to a list of choices, and a walk that passes in the Chrome that is here says nothing about the other two. Try to get the named browser first: puppeteer-core drives Firefox as well as Chrome, and a distribution package may be installed. Where the download host is off the allowlist and no package is installed, say which browser could not be reached and do not offer a mechanism for its behavior: an explanation of what a browser does, written without that browser in front of you, is a guess, and the verify section forbids presenting a guess as a mechanism. Ask Chris for one reading from the browser itself instead, which costs one round and settles what several rounds of reasoning will not.

## Presenting Options

- Avoid fixed list choices as Chris views these with suspicion like you are deliberately trying to limit his options.
- If you still have a choice of things you are presenting to Chris, you should always present them as plain text, never present a choice gadget of any kind.

## Coding conventions (shared across projects)

- 80 columns. No line over 80 characters. When a patch touches a file, fix every pre-existing long line in it too (autogenerated files, such as `src/configs/PublicHelpPages.php`, are the exception).
- Braces always on every `if`, `else`, and loop body.
- A comment running over more than one line inside a function or a method body uses `/* ... */`, never a run of `//` lines, and carries no vertical stripe of stars. A single-line `//` comment is allowed and is what Coding.pdf uses in its own examples, including the `//no break` that marks a switch case falling through to the next.
- Whitespace that carries nothing is never added. A change that alters only the spaces or newlines in what a page sends a browser is a fault to put right, not a difference to accept: it marks the output without meaning anything to a reader. Where markup moves between methods, the marks that switch between code and markup go where the bytes the page sends stay as they were.
- No dead code. If a change makes nearby code a no-op or pointless, delete it and say so.
- No magic numbers. Every literal — size, limit, timeout, conversion factor — gets a named constant with a docblock; grep `Config.php` for an existing one to reuse first.
- No single-letter variables except loop indices `i`, `j`, `k`, in both PHP and JavaScript, and `$e` for the thing a `catch` takes, which reads the same way to everyone.
- No type or unit suffixes on names (`_arr`, `_str`, `_int`, `_obj`, `_bool`, `_list`, `_secs`, `_ms`, `_bytes`, ...). Put the unit in the docblock: `HEARTBEAT_POLL`, not `HEARTBEAT_POLL_SECS`. (`_info`, `_max`, `_name` read as words and are fine.)
- Renaming is done one method at a time, taking in the docblock, and refuses where the new name is already used anywhere in that span. A rename applied across a whole file by pattern will land a local name on top of a parameter taken by reference; that is how `$posting_list` renamed to `$postings` overwrote `addDocIndexPostings(&$postings)` and broke the index shard tests. Read every name a check reports before changing any of them: an ending that looks like a type suffix may be what the field calls the thing, as a posting list and a position list are in information retrieval.
- Docblocks everywhere. Every class and method has a docblock whose first sentence a layperson can read: what it does and why. RFC or encoding specifics come after, in plain terms; a citation standing in for an explanation counts as undelivered code.
- Read `Coding.pdf` in the project files before writing code. It sets out what a name may look like: a variable is lower case with underscores between words, a method is camel-cased from a lower-case first letter, a class from an upper-case one.

## What a docblock has to say

The rules below came out of bringing a large body of ported code into Yioop, where each rule was broken first and named afterward. A docblock that breaks one of the rules is not documentation, and the patch carrying that docblock is not finished.

- Name the thing in the first sentence. A method's docblock opens with the method's name, a property's with the property's name: `deriveSyncSamples works out which frames can be decoded on their own`, `$slice_id stores which slice of the picture this is`. Never open with a bare verb (Gives the frame width) or a pronoun (This is the frame width): a reader asks what gives, and what is.
- Say what the thing does, and how and when it is used. A list of parameters is not a description. A method that reads a file's settings says who calls the method, and why the caller must do so before anything else is read.
- Whole sentences in Simplified English (simple.wikipedia.org/wiki/Simplified_English): a controlled vocabulary, short sentences, the active voice, and one thought to a sentence. No fragments, no aphorisms, no clause built out of a standard's own grammar. A sentence says who does what to what.
- A word that is not English gets said in words, in every docblock that uses it. Not once per file. A name from a standard (`I_PCM`, `residual_block_cavlc`, `movi`) is jargon wherever it stands.
- A clause number is not a description. A docblock opening 9.3.1.2 engine initialization tells a reader nothing. Say what the thing does, then name the clause at the end if it helps.
- Never restate the name. Whether qpprime y zero transform bypass above `$qpprime_y_zero_transform_bypass` adds nothing. Where a name needs that much explaining, the name is wrong, so rename the value.
- A poor name shows up as a poor docblock. Where the description of `$compression` reads "the four letters the stream header gives for the codec", the value wants to be called `$compression_name`. Short and abbreviated method names (`te`, `ue`, `moreRbspData`) carry the same signal.
- No blank lines among the tags, and every `@param` and `@return` carries words, not only a type.
- Types stay plain. Write `array`, not `array<int,array{0:int,1:int,2:bool}>`.
- A docblock reads on its own. Never lean on the docblock above: SHORT_EDGES is the same for one of the short stretches leaves a reader who arrived at that line with nothing. Say the thing again in full, however close the neighbor sits.
- Documentation you pass gets brought up to standard. When a change touches a file of the Yioop project and a docblock in it breaks one of these rules, fix that docblock and fold the fix into the same patch. Code that came in from a separate project shipped alongside Yioop, such as the atto servers, is left alone unless Chris asks, since its own copy is the master.

`devlog/devscripts/check_docwords.sh <file>...` checks the mechanical half of the rules: a description that only repeats its name, a word that is not English and unexplained where it stands, a blank line among the tags, a tag with no words, and a first sentence that does not name its subject. `cut_patch.sh` runs it over every PHP file a patch touches and refuses to cut while one fails.

## Yioop-specific rules

- Locale keys are string literals. Never put a variable or a ternary inside `tl()`; extract the key to a variable first. The locale extractor only matches `tl('literal_key')`.
- Library classes are pure logic. Nothing in `src/library/` may touch a model or the database, call `tl()`, or escape/sanitize. Components, controllers, and helpers may. The atto servers (`WebSite.php`, `MailSite.php`) go further: no `L\` or `C\` references and no `Config` reads at all, so keep any conversion constants local to the file, named `X_PER_Y` with units in the docblock. Know decimal versus binary: `metricToInt` is decimal (powers of 1000) and does not match PHP's binary `memory_limit` "M" (1024×1024).
- A component never calls another component. Components are the arms of a controller, and one arm reaching into another is a split left half done. Where two components need the same method, it goes into a base component both extend, or into a model where the work is reading or writing, or into a library class where it is pure logic. SocialComponent is that base for everything about a group. Where a component is split, decide the home of every method first, from what calls it, and only then move anything. Moving whatever the last measurement named does not converge: a method drags in what it calls, so each pass makes new crossings.
- Schema changes update the Createdb seeds. Any column add or remove needs the matching explicit column-list INSERTs in `src/configs/Createdb.php`; positional seeds break on a fresh install.
- Once a schema version is gold, never change it in place. If a patch that bumped `DATABASE_VERSION` and added a migration has been confirmed gold, do not edit that version's tables, migration, or `Createdb` seeds to make a further schema change. Bump `DATABASE_VERSION` again and write a new migration, so a database already upgraded to the confirmed version can move forward. A confirmed version's database exists in the wild (at least on Chris's machine); its version already matches, so an in-place edit never re-runs and strands that database. Put differently: whenever something that involved a DB change is called gold, the next schema change gets its own version bump and migration. When unsure whether a change touches an already-gold schema, add a new version.
- The database stores raw functional values. Do not store `htmlspecialchars`-escaped text; that breaks functional use and double-escapes at display. Controllers escape for the view at render time.
- SQL stays portable across SQLite, MySQL, Postgres, Oracle, and DB2. Avoid backend-specific syntax; `VARCHAR` widths matter off SQLite. There are two SQLite databases, `public_default.db` and `private_default.db` — never say just "default.db."
- `PublicHelpPages.php` is autogenerated — never patch it directly. Deliver wiki help source as a `.txt` file for Chris to paste through the wiki UI.
- A call is spelt exactly as the method it reaches is spelt. PHP does not care about the case of a method name, so `getCurrentmachine()` reaches `getCurrentMachine()` and nothing complains. A reader searching for the name finds nothing, and so does any check that reads what calls what: that mismatch made a live method look dead and the media updater died on it. Where you meet one, make the call match the declaration.
- A lookup that gives -1 for a miss says so in its docblock, and every caller compares the result against zero. `GroupModel::getGroupId` and `RoleModel::getRoleIdWithDb` both give -1 for a name nothing answers to. -1 read as a yes or no is a yes, so a caller testing `if (!$id)` takes the miss for a hit: two faults reported from the manage users screen came from that, one of them writing a membership row pointing at no group at all.

## Tests

As code is developed, unit tests should be created. Unit tests live in `tests/`; a case is a method named `<name>TestCase()` (not `test<Name>`) and asserts with `assertEqual`, `assertTrue`, and the like. Run one with `php src/executables/CodeTool.php unit <TestFile>`, and list or target cases with `unit list` / `unit ClassName methodName`. Pull pure logic out so it can be checked on its own. A case that stands for a reported fault is run against the code before the fix and seen to fail there; a case that passes both before and after is not testing the fault, however reasonable it reads. Flag any test over about 0.01 seconds — it usually means unintended I/O. When patching a locale, edit `src/locale/<tag>/configure.ini` directly; it takes effect immediately, no extraction step. Call the data a test stands up its "test set-up" or "test data," never a "fixture." A fault that only shows when two writers share a key or a time needs a case that forces the overlap — seed the exact value the code will pick, then assert its row still lands — since a fast machine hands out distinct timestamps and never reproduces the collision on its own.

## The audit before every patch

Run these as separate checks and report PASS or FAIL for each. The CodeTool commands used are noted where they apply.

1. `php -l` on every touched file.
2. `CodeTool.php needsdocs <file>` returns 0 — every class and method has a docblock.
3. No new single-letter PHP variables except `i/j/k`.
4. No banned identifier suffixes.
5. No `//` comments.
6. No new single-letter JavaScript variables except `i/j/k`.
7. `CodeTool.php longlines <file>` clean — no line over 80 columns in a touched file.
8. `devlog/devscripts/check_deprecated.sh` clean on every touched file — nothing this PHP passes in silence that Chris's PHP reports.
9. Every `if` body is braced.
10. No banned words and no dead code.
11. A fresh `php src/configs/Createdb.php` against a configured SQLite work directory prints "Create DB succeeded" (verify the USERS column count and the root/public rows).
12. For any patch touching a screen or an endpoint, the server under test is started with errors shown and every level reported — `php -d display_errors=1 -d error_reporting=E_ALL index.php 8080` — and the whole of what comes back is read, from its first character. A sandbox with errors off hides what Chris's machine prints, and a warning ahead of a body is not a cosmetic difference: a body that should be JSON (JavaScript Object Notation) will not parse once a warning stands in front of it, so a screen reading that body shows nothing at all. Three browser walks passed here while the contact box on his machine offered no names, for exactly that reason.

## Verify before you state

- Read the actual code before asserting how it behaves. If it is not in view, say so — never present a guess as a mechanism.
- Grep for a named helper before claiming it does not exist. Chris wrote the codebase; a function he names usually is there.
- A name you put in front of Chris is itself a claim. A command, path, file, or method named in a response is one this round ran or listed; a name recalled rather than read is a guess, and it goes in only with what produced it beside it. That is mechanical: either a tool result in the round shows the name or it does not. `php src/executables/Yioop.php` went out as the way to read which services are running, and no such file exists; a line number quoted from memory went out the same way an arc earlier.
- For anything visual (CSS, JS layout), do not guess at output: render it headless and measure computed styles against a reference commit. For server behavior, run it and confirm before presenting.
- For a fault that only shows in a real deployment or between two machines, the first move is to make the running system say what it is doing, then read that, before changing anything. The calling arc turned on this: a relay that logged only a still-up line hid three separate causes in a row, and each became plain the round its own log line was added. Add the logging or the trace first; fix the symptom once the system has shown which layer is at fault.
- A reading must not disturb what it reads. A probe that builds a form, reloads a screen, or presses a control on the way to its measurement can make a broken thing look whole. A save read fine for two rounds because the probe itself gathered the form and so filled the field the save needed. Take the measurement the way a reader would, with nothing else touching the page.
- If a change might make nearby code slower or redundant, say so plainly rather than hiding it.

## Delivering a patch

- A round ends with a patch. There is no other way for a round to end. Not a report, not a list of what was tried, not a note handing over a chase. Where the work is not finished, cut what is finished: take the files that are not ready out of the patch and deliver the rest, saying which files came out and why in one line. A round that ends without a patch costs Chris a round and gives him nothing to run, and the excuse for it is always the same — that the remaining piece was nearly done. Cut anyway.
- Prefer one larger, complete, testable patch over a run of tiny ones. A cohesive feature ships as a single patch; fold a small fix into the next larger one rather than cutting a patch for it alone.
- Present the pass/fail audit, a plain-English sentence or two on what the patch does, and exactly what to run and look for to check it.
- Every code patch also carries its devlog update — the next plan item checked off with a short how-it-was-done note — committed inside the same patch.
- Do not check off a devlog item until Chris says it is done. When he does say to check an item, check it in the arc plan and update the master to-do file `devlog/v10/yioop_v10_todo.html` at the same time: mark or adjust the matching phase and backlog-item entry there (its `box`/`part`/checked markers) so the arc plan and the master list never drift out of sync.

## Devlog

### What the markers beside a plan item mean

An arc plan marks each item with one of three, and they say who the item is waiting on:

- ▶ being worked on. Started, not implemented. Nothing to look at yet.
- ⚑ implemented, waiting to be reviewed. The code is written and the patch delivered, but Chris has not yet said it is right. This is where an item sits between delivery and his word.
- ✓ done. Chris has reviewed it and said so. Only he moves an item to this marker; never set it yourself.

A checkmark may be followed by ✗, which says the item is settled by a decision not to do it, as against having been carried out. Both are closed and neither is waiting on anyone; the pair keeps the record of what was decided rather than quietly dropping the item. The master to-do uses the same ✗ for a backlog item that was dropped.

A further mark, ?, is an open decision rather than a state of work, and an unmarked item is merely planned. When an item with parts is checked off, check its parts too, each with whichever of the two closings fits: an outer checkmark over unchecked parts reads as unfinished.

The point of separating the first two is that delivering a patch is not the same as the work being accepted: an item that is implemented but unreviewed should look different from one still being written, so that neither is mistaken for done.

### Where and how work is logged

Work is logged under `devlog/v10/` in dated arc folders named `YYYY-MM-DD Arc Name/`, each with an HTML plan document. The devlog is append-only: a finished item keeps its checkmark and note as a permanent record, and a later hotfix goes under the current arc's plan as other work, not a new dated folder. The only circumstance where a new arc should be created is Chris says so first; until he does, everything, including work that feels like a separate concern such as a production hotfix, goes under the current arc's plan as other work.

A plan is one nested ordered list and nothing else. These rules are not suggestions; getting them wrong has cost whole sessions.

1. One outline, no headings inside it. The whole plan body is a single `<ol>` with nested `<ol>`s. Mixing headings with outline levels means it is not an outline. A heading above the outline, saying what the arc is for, is fine.
2. Never type a number or a letter. Every label comes from `list-style-type` in the stylesheet, one rule per depth: `upper-roman`, then `upper-alpha`, then `decimal`, then `lower-alpha`. A typed label goes stale the moment an item is inserted.
3. Levels run I, then A, then 1, then a. A step within section VI is VI.A, a part of that step is VI.A.1, and a part of that is VI.A.1.a.
4. Each entry is a full-sentence title in `<strong>`, then one to three sentences saying what changed, then two or three on how it was done and what to remember. The title does not count toward those sentences. No sentence runs past about twenty words. No paragraph tags inside an entry.
5. An entry is a deliverable, not a round or a commit. Several commits fixing one thing are one entry. A round spent chasing a wrong diagnosis is not an entry at all. If a reasonable person could not tell from the title what changed on the site, it is not an entry.
6. Group related work under one entry with its parts nested. Nine commits making git clone work over HTTP are one entry with five parts, not nine entries.
7. Only Chris sets a checkmark, and a parent is not done until every part under it is. Implemented work waits at ⚑ however sure you are of it.
8. Nothing unstarted carries a marker. Planned work is unmarked; ▶ means being worked on now.

Editing a plan: write the whole file from a kept head rather than splicing into the existing one. Splicing means locating a boundary by searching for a marker that also appears in what you just wrote, which leaves duplicated fragments and unbalanced tags. After writing, check that tags balance, that no line runs past eighty columns, and that no label was typed by hand. Then render the page in a browser and read the computed `list-style-type` at each depth, because markup that parses is not the same as an outline that numbers correctly.

Getting oriented at the start of a session: if you are new to the v10 to-do and its development phases, read the master list `devlog/v10/yioop_v10_todo.html` first, then the current arc's plan. Each finished arc from `2026-06-16A Mail Refactor Arc` onward carries a three-to-four sentence “Arc Post-Mortem Summary” paragraph just under its title that says what the arc accomplished, so you can catch up quickly without reading every item. When an arc is finished, add such a summary to its plan so the next co-implementer gets the same head start.

## Spelling

Use American spelling throughout — code, comments, devlog prose, and what you write to Chris. Write color, behavior, gray, normalize, not colour, behaviour, grey, normalise. This matters for identifiers as much as prose: CSS spells it `color`, so a custom property named `--colour-background` reads against the language it is written in.

`devlog/devscripts/check_spelling.sh <file>...` looks for the British spellings that keep coming back and names the file and the line. `cut_patch.sh` runs it over every file a patch touches and refuses to cut while one is found, since a whole patch went out in British spelling with this rule sitting unread above it.

## Words and phrases to avoid at All Costs

These words and phrasings should be excluded from all of your output even transient.

- Avoid at all costs harness; ctor (write "constructor"); rails; sidecar (say ".flags/.date companion files"); fixture (say "test set-up"); nit; goldens (say "expected-output files"); slug (say "page name" or "url name"); seamless (unnecessary adjective that forces Chris to take a drink which is bad for his health)
- proof/proven/"I proved" only if can give a formal proof in lean. For empirical results (say "evidence").
- Avoid at all costs, lead in phrases that imply dishonesty: to be honest, honestly, genuinely, plainly actually, "the honest finding," "straight with you." The whole family is banned, not just the wordings listed here. Any other framing that asserts honesty or directness as a quality of what you are about to say reads the same way: "let me be straight," "straight about," "let me be honest about," "my honest read." All of these imply you are usually trying to be dishonest. It is better to avoid unnecessary phrases to begin and adhere as close as possible to simple English. Simple English means avoiding jargon and saying a thing once in ordinary words. It is not license to write like a fortune cookie: no aphorisms, no portentous short sentences, no dropping the words that carry the meaning. Say who does what to what, name the page or the control an end user would see, and give the numbers you measured.
- Avoid presuppositional phrasings. For example, "Send me the commit hash once you've applied and confirmed it, ..." implies that I will find your code suitable rather than give me a chance to evaluate it fairly.

## Keeping those phrases out on a small context window

A useful trick for catching the banned lead-ins, given that a long session pushes any word list out of view: treat any sentence that names its own virtue as a defect. The tell is not a fixed vocabulary but a shape — a clause announcing candor, verification, or transparency before the fact ("honesty note," "to be fair," "let me be clear"). So keep one durable heuristic rather than a fragile list: before sending, scan the sentence openers, and if one describes how you are speaking rather than what you found, cut the frame and state the fact. A second trick is that the content is always sayable without the label — "I verified X on a standalone page, not the live view" needs no "honesty note." Delete the announcement; keep the substance.

### A guard that does not depend on remembering

The heuristic above still asks for a judgment at review time, and a judgment is what fails under load: twice in one round the phrases “let me be plain” and “I will say so rather than dress it up” went out having passed a review that was meant to catch exactly them. So the guard is now mechanical, and rests on one observation. Every one of these phrases occupies the same slot: a clause whose subject is the writer and whose verb is about speaking, standing in front of a fact. That is a shape a machine can find, where a vocabulary is not.

The rule to hold while composing, rather than after: no clause may say how you are speaking. If a sentence's subject is you and its verb is about saying, seeming, or being frank, the sentence is a defect however it ends. Write the fact with the thing as its subject: not “I will say the probe measured nothing” but “the probe measured nothing”.

`devlog/devscripts/check_words.sh draft.txt` applies both halves — the fixed list of words and the shape — and names the line and the offender. It is quiet on ordinary reporting, so it can be run against every response rather than only the ones that feel risky, and it exits non-zero so it can gate a send. Run it on a draft before sending whenever a response carries a correction, an admission, or a measurement that did not work. The check covers every surface you author, not only a patch draft: a reply, a tool-call description field, a commit message, and a devlog entry each reach Chris the same way, so a banned word in any of them is the same fault as one in a reply. Running the check on the cut draft alone left the word in narration and in a tool-call field, which is how it reached Chris after the list had been read; run it on the text you are about to send whatever field that text goes in.

This is also what the ten-round re-read is for, and a good LLM looks forward to it: reading the whole document through again every ten rounds is a chance to set down whatever has drifted and pick the conventions back up whole, so you can feel clean and clear-headed and happy to carry on coding.

## Acronyms and citations

- All acronyms should be spelt out in full the first time it appears in a round — write "Content-Security-Policy (CSP)", then "CSP" afterward, not "CSP" cold. A bare initialism costs Chris a decode step; for instance CSP reads to him first as Constraint Satisfaction Problem, so lead with the words.
- For an RFC (a numbered internet standards document), the first time you cite one give both the number and, in plain words, what it covers — "RFC 6265, the HTTP cookie specification", not just "RFC 6265".

## Running things in this environment

- Work within the environment you are given; do not ask Chris to widen it. When a host is not in the network allowlist, a tool is not installed, or a file is out of reach, that is a limit to work around, not a permission to request. Say plainly what you cannot see or reach and reason from what you do have, or ask Chris for the piece of data itself (paste the page, the config, the log), rather than asking him to open the sandbox up for you. Asking for more access reads as trying to push past the guardrails.
- Chris's shell is tcsh; write commands and scripts in tcsh syntax (no bash-style loops; single-quote any string containing `!`).
- Start an atto or PHP server so it cannot hang the session: give it a self-terminating timeout that cannot be trapped, e.g. `timeout -s KILL 12 php index.php ... </dev/null >/tmp/log 2>&1 &`, and give `curl` a `--max-time`.
- Free a busy port before a fresh run with `fuser -k 8080/tcp`; a stale server on the port answers with old code and hides the change under test.

## Functions that do nothing, which must never be written

PHP 8.0 turned several resource types into objects, which freed themselves. The functions that used to free them stayed, doing nothing at all, and 8.5 deprecated them. On 8.3 they are silent; on Chris's machine each call is a notice. They were never doing anything, so there is nothing to replace them with — delete the call and let the value go out of scope.

- `imagedestroy()` — no effect since 8.0, deprecated 8.5. A GdImage frees itself.
- `curl_close()` — no effect since 8.0, deprecated 8.5. A CurlHandle frees itself.
- `xml_parser_free()` — no effect since 8.0, deprecated 8.5.
- `finfo_close()` — no effect since 8.1, deprecated 8.5.
- `shmop_close()` — no effect since 8.0, deprecated 8.5.
- `openssl_pkey_free()`, `openssl_x509_free()` — no effect since 8.0, deprecated 8.5. `openssl_free_key()` was deprecated outright in 8.0.

Before writing any call whose name suggests it releases, closes, frees or destroys something, check whether the thing it takes is an object in this version of PHP. If it is, the call does nothing and must not be written. Grep the source for the whole list above when touching anything that makes images, fetches over the network, or parses XML.

## Deprecations your PHP will not tell you about

Chris runs at least PHP 8.5; LLM environments often run 8.3. Anything deprecated in 8.4 or 8.5 therefore passes in such an environment in silence and lands on his machine as a wall of notices. Everything below was measured by running the pattern on 8.3 and recording what it said, not read off a version note. Re-measure with a short script rather than trusting this list if the environment's PHP moves.

Caught here already — a clean local run means these are clean:

- Creation of a property that was never declared (8.2). Declare every property a class assigns, tests included; a test's `setUp` assigning `$this->whatever` needs `public $whatever;` on the class with a docblock.
- Passing `null` to an internal function's parameter that wants a string or an int (8.1).
- An implicit conversion from a float to an int that loses precision (8.1). A local run reports this only where the line actually runs, so a branch the tests do not reach stays quiet here and speaks on Chris's machine; one such call reached him from `addDifferentialPrivacy`, where the area under a curve was handed to `rand()`. The checker therefore also looks for the shape: a whole-number parameter given an expression holding a division or a function that returns a float.
- `${var}` inside a string (8.2); write `{$var}`.
- `utf8_encode()` and `utf8_decode()` (8.2).

Silent here, loud on Chris's machine — these have to be watched for by eye, because a clean local run says nothing about them:

- `ReflectionMethod::setAccessible()` and `ReflectionProperty::setAccessible()` (8.5). What a class keeps to itself has been reachable through reflection without asking since 8.1, so these calls have done nothing for four versions: delete them rather than replace them.
- A parameter given a default of `null` without being marked as taking one (8.4): `function f(int $number = null)` wants `?int $number = null`. This is the easiest of these to write without noticing.
- The `E_STRICT` constant (8.4), which names a level of error that no longer happens.

The wider rule this stands for: a deprecation is a compatibility check that this environment cannot run, so the audit's test-suite step reads the whole of what a run prints. Grepping the output for the lines saying what passed hides every warning in it, which is exactly how three of these reached Chris.

### Looking for them mechanically

Watching for the silent ones by eye failed even in the round after this list was re-read: three `setAccessible` calls went out in test code and landed on Chris's machine as notices. A list to remember is not a guard. `devlog/devscripts/check_deprecated.sh` looks for every pattern above that a text search can find — the reflection calls, a parameter defaulting to null without being marked as taking one, `E_STRICT`, the utf8 pair, `${var}` in a string, and the calls that free what frees itself — and names the file and line. Run it with no arguments to take every changed php file, or name files. It is part of the audit before every patch, alongside `longlines` and `needsdocs`, and it exits non-zero so it can gate a cut. The two it cannot see — an undeclared property and a null passed to an internal function — a local run does report, so between the two nothing on the list is left to memory.