Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,40 @@
- **fix(install): a failed Pi install exits nonzero, like a declined one.** The
script refused to call a DECLINED upgrade success — "a zero status would tell
a script that everything is ready when the thing that runs people is too old"
— and then reported a FAILED install as success anyway, through two `|| true`
sites. A caller got the happy outro and status 0 with the agent runtime
absent, which is strictly worse than the too-old Pi the decline path already
refuses. **The rationale proved more than the code implemented.** Both sites
now end in the same deliberate family as the decline, naming what did not
happen and the command that fixes it.

**And the rule now has an instrument, which it never had.** Every Pi branch
was reachable only by running the real installer against the real network, so
nothing checked the exit code any of them produced and the rule shipped as a
comment. `installer-pi-exit-codes.test.mjs` runs the script's OWN bytes —
extracted at a marker, executed against stub `pi` and `npm` — over five
behaviours, and carries a control that restores `|| true` in a copy and
asserts the block goes green, so the passing assertions are known to measure
the defect rather than merely to pass.

- **feat(install): the installer installs and upgrades Pi instead of printing a command and hoping.** An absent Pi is installed without asking — chief cannot run a single person without it, so there is nothing to weigh. A Pi that is merely too old is somebody's working tool, and replacing it is a different act, so that one asks: `Upgrade Pi to >= <floor>? [Y/n]`, default yes. Declining exits nonzero and says why, because telling a script that everything is ready when the thing that runs people is too old is worse than failing.
**The floor is read, never restated.** It has one definition in the Rust source, the release process already stamps it into the manifest, and the installer reads it from the manifest it has just unpacked. A version bump needs no edit to the installer, and the repository's single-definition guard stays satisfied — a number copied into a shell script would be a second definition wearing a copy's clothes.
**The prompt reads the terminal, not its own source.** This file is piped into `sh`, so stdin is the script text; reading stdin would eat the rest of the installer. Where there is no terminal at all — CI, a container build — the default is taken and SAID, because a silent choice made on somebody's behalf is what surprises them later. Missing `npm` refuses cleanly rather than half-installing, and a Pi that npm reports as installed is CHECKED against the floor rather than announced as ready.

- **feat(intercom): organization mail rides the same queue as the operator's own typing.** A busy person now reads an ordinary message at the next step boundary inside the running turn, instead of at the end of it. The operator typing mid-turn has always been submitted as a steering message and consumed within seconds; ordinary mail rode the follow-up queue, which Pi consumes only once the agent has no more tool calls or steering messages — so somebody an hour into a piece of work did not see a teammate's message until the hour was over, while the identical words typed by the operator arrived immediately.
**The digest is kept; only its consumption point moves.** Batching is still the answer to twenty messages arriving in one turn, and nothing about how a batch is built has changed.
**The idle and boot rows are byte-identical.** Mail arriving at somebody doing nothing still starts them; mail arriving inside the boot window is still parked. This changes WHEN a busy person reads a message, never whether an idle one is woken — and it touches nothing in the converge, activity or settle paths, so the operator wake lease is untouched by construction.
**The change was unpinnable as written, so it was made pinnable.** The delivery table was already covered, but which mode the mailbox passed into it was two string literals at two call sites — so the timing of every delivery in the product could change without one test noticing. It is one named decision now, and a test asserts the rule rather than the table.

- **fix(hire): the guidance told agents to omit the department id; the schema required it; the model obeyed the schema and guessed.** `org_hire`'s `departmentId` was a REQUIRED field whose own description opened "DEFAULT: the department YOU head". An agent read that, reasoned correctly that it should omit the field, met a schema that would not permit it, and improvised the most salient name in context — the company's. It obeyed the instrument over the claim, which is the right thing for it to do.
**The field is optional now and the default is real.** An omitted id resolves to the department the caller heads, or failing that the one they sit in — the resolver that already existed and is word-for-word what the description promised. The promise was always implementable; it simply was not implemented. The worked example shows the omitted form first, because an example that keeps passing the field re-teaches the habit the description is trying to correct.
**The company name is still refused on the override path**, and that refusal is now the only thing standing behind an explicitly-passed id, so it is pinned by a test that passes the field deliberately rather than omitting it. Accepting the name as an alias was rejected again and for a sharper reason than before: with a default in place the alias would exist only on the override path, where a mid-level head naming the company means their OWN team — so it would resolve to the impressive-sounding wrong target rather than the semantically right one.
**A sweep asked the general question of every parameter in the file** — does the schema permit what the prose promises? — across `DEFAULT`, `omit`, `omitted`, `optional` and `leave empty`. Exactly one leaf field disagreed, this one. One other hit reads "never omit this field" and is required, so prose and schema agree. A mechanical guard was therefore not added: an instrument for a class of one is furniture.
- **feat(cards): a refusal says "refused"; a crash still says "failed".** The word is a claim about whose fault the failure was, and the two invite opposite recoveries — "refused" invites a corrected call, "failed" invites a retry. So `Hiring teammate refused` when the caller named a department that does not exist, and `Hiring teammate failed` when something actually broke. This is only implementable because the classification exists to follow; the verb reads it rather than matching on message text, which this file bans.
**The split follows the classification, so a refusal added next month gets the right word without anyone remembering to add it.** One predicate, in one place, because three renderers build a failure title and a rule copied three times is three rules waiting to disagree — two of them were bespoke titles that would otherwise have kept saying "failed" for a refusal.
**The `(system fault)` tag reads the same marker.** It measured only the ABSENCE of a status, so the verb moved to the fault marker and the tag stayed on the old instrument — a partial batch wrapping a real crash said "failed" correctly and then dropped the crash marker, leaving somebody debugging it to read a list of people already hired and reasonably conclude they had passed bad input. One classification, two surfaces, and a test that fails if either moves without the other.
**One case turned out not to be what the classification said it was.** A partial hire carries a status so the card can name the people already hired, and a retry does not double-hire them — but the error it wraps may be a genuine crash. A status carried for CONTEXT is not a claim about fault, so that path now asks the wrapped error's own type, and a producer in the same position marks itself rather than being enumerated in the renderer. Without it, a real crash mid-batch would have been labelled a refusal and invited a correction to a call that was never wrong — the earlier defect pointed the other way, and worse for it.

- **fix(install): the installer no longer prints a curl error on the first line a stranger runs.** A clean install printed `curl: (23) Failure writing output to destination` immediately under "Resolving the latest chief release…", and then completed successfully. The install was never broken; the message was, and it appeared on the very first command anyone runs against this project — where a reader has no reason to read it as anything but a failure.
**The cause was a pipeline, not the request.** The release lookup piped curl into `grep -m1`, which exits on its first match; if curl is still writing when it does, curl's write fails and it says so on stderr. The tag had already been captured, which is why everything downstream worked. Whether it appeared at all depended on whether the response outran the pipe buffer, which is why it was intermittent rather than constant — and why it is the kind of thing that reaches a stranger before it reaches the people who built it.
**The installer also puts chief on your PATH itself**, instead of printing a line to copy. It writes to `~/.bashrc` and `~/.zshrc` when they exist, creates the one matching your shell when neither does, and **names every file it touched** — a script that edits somebody's dotfiles and does not say which is asking to be distrusted. Running it again changes nothing: a profile that already exports that directory is recognised and left alone, matched on the PATH rather than on an exact line, so a hand-edited variant counts as done.
Expand Down
26 changes: 26 additions & 0 deletions DECISIONS.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,29 @@
- 2026-08-28 — **An exit code is a claim about STATE, not about effort, so a
missing prerequisite may never exit zero.** install.sh already refused to call
a declined Pi upgrade success; it then reported a FAILED install as success
via `|| true`, leaving the agent runtime ABSENT — the worse state — while
telling every calling script that chief was ready. The defect class is the one
this branch keeps producing in new clothes: **the rationale proved more than
the code implemented**, and the carve-out's own words were the argument
against the line three branches away. So the rule is stated positively: every
path that ends without a usable prerequisite exits nonzero and names both what
did not happen and the command that repairs it, and no comment counts those
paths, because a count in a comment goes stale silently. Pinned by
`installer-pi-exit-codes.test.mjs`, which executes the installer's own
extracted bytes against stub `pi`/`npm` rather than asserting over its text,
REFUSES in words when its marker no longer locates the block, and carries a
control that reinstates `|| true` in a copy and requires the block to go green
— a pin nobody has flipped is a claim, not evidence.

- 2026-08-28 — **A shell guard tests USABILITY, never EXISTENCE.** Three separate defects this week were one defect: the check asked whether a thing was THERE when the only question that matters is whether the next line can USE it. A profile file was selected by `[ -f ]` and then written to a path that was not writable; a link was accepted because it resolved to a name, and the name pointed at nothing; and `[ -r /dev/tty ]` answered true inside a container whose `/dev/tty` cannot actually be opened, so the prompt that followed killed the installer with status 2 and no message. The general form is that `-e`, `-f`, `-r` and `-w` report a directory entry and its permission bits, which are a claim about the FILESYSTEM, while the operation that follows is a claim about the KERNEL — and the two disagree for containers, dangling symlinks, read-only mounts, full disks and revoked terminals. So the guard performs the real operation and reads its status: open the file, follow the link, redirect the terminal. **And it performs it where a failure is survivable**, which is the second half of the rule and the half `/dev/tty` taught — `{ : < /dev/tty; }` is a redirect on a special built-in, and under `set -eu` a redirect error there is fatal to the whole shell, so the probe must run in a subshell, `( : < /dev/tty )`, where the status can be read instead of ending the script. A probe that cannot fail safely is not a probe, it is the bug it was written to prevent.

- 2026-08-28 — **Install what is missing; ask before replacing what somebody already has.** The installer now handles Pi, and the asymmetry is the decision: an ABSENT prerequisite is installed with no prompt, because chief cannot run a person without it and there is nothing for the user to weigh; an EXISTING one that is merely below the floor belongs to them, and replacing a working tool without asking is a different act, so it prompts with a default of yes. Declining exits NONZERO — a zero status would tell a calling script the box is ready when the runtime every person needs is too old. Recorded with the constraint that shaped the implementation: **a number with one definition must be READ at every other site, never restated** — the floor is stamped into the release manifest by the release process, so a version bump touches no script, and the single-definition guard is satisfied rather than worked around. And two shell facts learned by running rather than reading, both of which end the installer silently if got wrong: a prompt in a `curl … | sh` script must read `/dev/tty` because stdin is the script itself; and `/dev/tty` must be tested by OPENING it inside a SUBSHELL, since the node exists in containers that have no terminal and a redirection failure on a special built-in is fatal to a non-interactive shell.

- 2026-08-28 — **A message from a teammate reaches a busy person on the same terms as a message from the operator.** Human typing mid-turn rides Pi's steering queue and is consumed at the next step boundary; ordinary mail rode the follow-up queue and waited for the turn to end, which on an hour-long turn is an hour. Interrupt mail already rode the steering lane, so this widens a production-proven path rather than building one, and the digest stays exactly as it was — batching answers volume, and only the moment of consumption moved. Two boundaries recorded with it. **The wake lease is untouched by construction**: this is client-side delivery-mode selection, and the lease's readers key on `person_activity.operator_wake_at`, which this neither reads nor writes — the idle and boot-window rows are byte-identical, so nothing about waking a stopped person changed. And **a decision written as a literal at a call site cannot be asserted**: the delivery table was pinned while the mode passed into it was not, so the timing of every delivery could change with every test still green. Naming the decision was what made the change reviewable, and that is the general lesson — a constant repeated at two call sites is two decisions, and neither of them is testable.

- 2026-08-28 — **A parameter description is a promise about the schema, and the schema is what the model obeys.** A documented default that the schema does not implement does not produce a caller who ignores the prose — it produces one who believes the prose, discovers the field is required anyway, and IMPROVISES a value. That is what happened: "DEFAULT: the department YOU head" over a required field, and an agent that reasoned its way to omitting the field, could not, and filled it with the most salient name in context. The instrument is the schema; the description is a claim about it; they must agree, and where they disagree the claim is what gets believed and the schema is what gets obeyed. The general question worth asking of any tool surface — **does the schema permit what the prose promises?** — is answerable mechanically, which is why the sweep was the substance of this change rather than an appendix to it. Recorded with the sharper reason the company-name alias stays rejected: with a real default in place the alias would exist ONLY on the override path, where a mid-level head naming the company means their own team — so it would resolve to the impressive-sounding wrong target rather than the semantically right one, which is worse than the refusal it would replace.
- 2026-08-28 — **The word a failure card uses is a claim about whose fault it was, so it follows the classification and never a list.** "Refused" invites a corrected call and "failed" invites a retry; using either for the other tells a reader to take the wrong action, and calling a crash a refusal is the worse direction because it sends somebody to fix a call that was right. The rule lives in ONE predicate because three renderers build failure titles, and a rule copied per renderer is a rule that will diverge at the first bespoke card. Recorded with the discovery that made it non-trivial: **a status carried for CONTEXT is not a classification.** The partial-hire card carries one so it can name the people already hired — that is data a retry needs, not an assertion about fault, and the error it wraps may be either kind. Where the two diverge, the error's own type decides and the producer marks itself; the renderer never grows a list of exceptions, because a list is where the next case is missed.

- 2026-08-28 — **Read your own pushed bytes: a claim that reads as verified is not verified, and the checks do not cover claims.** Three defects in one day were of this shape and none was catchable by any test in the repository: a comment naming THREE adapters when there were eight (a wrong number); a comment citing a guard called `CatchPathsFunnelThroughRefusalResult` that nothing answers to (a wrong referent); and a report that a group of conversions was complete on the strength of three sampled greps (a wrong scope). Each read as though somebody had checked, each was green, and each was found by looking at the pushed artifact rather than by running anything. The rule that follows is cheap and mechanical: after pushing, read what you pushed — the bytes at the SHA, not the diff you intended — and read the prose in it as sceptically as the code, because a comment is the one part of a change that no gate can falsify. Its companion, learned the same day: when a claim is disputed, settle it with the bytes at a named SHA rather than with a commit id, since a commit id proves when you pushed and only the bytes prove what is there.

- 2026-08-28 — **A refusal must not lie about whose fault it is, because the label chooses the recovery.** A system fault invites the same call again; a caller error invites a corrected one. So a failure a tool DECIDED must be distinguishable from an exception it SUFFERED, all the way to the surface — and where the distinction is carried by the presence of a field, every adapter that flattens an error has to preserve it or the classification is lost in transit. It was: validation refusals were thrown as plain errors and seven catch adapters dropped them into status-less results, so a whole class of deliberate refusals rendered as crashes. The marker travels ON the error rather than being re-derived, because re-deriving it means matching message text, which is a second parser by another name. Two corollaries recorded with it. The plain error keeps its meaning — an invariant no input should reach IS a system fault and retrying it is right — and the fix is pinned from both sides, since a test asserting only "this is not a system fault" can be satisfied by labelling everything a refusal, which deletes the distinction instead of repairing it. And on the same incident: an ambiguous name is PREVENTED at the parameter that accepts it, never accepted as an alias — an alias binds everywhere the parameter appears, so a convenience at hire becomes a destructive action at remove, which is the wrong-target-from-name-confusion class arriving by a friendlier road.
Expand Down
Loading