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.
Yioop is developed against a two-clone discipline:
confirmed-head tag marks the latest confirmed commit.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.
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/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/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.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 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.
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.
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).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.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.
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.
src/configs/PublicHelpPages.php, are the
exception).if, else,
and loop body./* ... */); never
//.Config.php for an existing one to reuse first.i,
j, k, in both PHP and JavaScript._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.)tl(); extract the key to a variable first.
The locale extractor only matches tl('literal_key').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).src/configs/Createdb.php; positional seeds break on a
fresh install.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.htmlspecialchars-escaped text; that breaks functional use
and double-escapes at display. Controllers escape for the view at
render time.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.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. 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."
Run these as separate checks and report PASS or FAIL for each. The CodeTool commands used are noted where they apply.
php -l on every touched file.CodeTool.php needsdocs <file> returns 0 —
every class and method has a docblock.i/j/k.// comments.i/j/k.CodeTool.php longlines <file> clean — no line
over 80 columns in a touched file.devlog/check_deprecated.sh clean on every touched file
— nothing this PHP passes in silence that Chris's PHP reports.if body is braced.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).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.An arc plan marks each item with one of three, and they say who the item is waiting on:
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.
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.
<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.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.<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.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.
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.
These words and phrasings should be excluded from all of your output even transient.
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.
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/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.
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.
!).
timeout -s
KILL 12 php index.php ... </dev/null >/tmp/log 2>&1
&, and give curl a --max-time.fuser -k
8080/tcp; a stale server on the port answers with old code and
hides the change under test.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.
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 envirnoment 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:
setUp assigning $this->whatever needs
public $whatever; on the class with a docblock.null to an internal function's parameter that
wants a string or an int (8.1).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.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.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.
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/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.