Skip to content

Latest commit

 

History

History
808 lines (675 loc) · 47.5 KB

File metadata and controls

808 lines (675 loc) · 47.5 KB

Setup — Evolution Log

History of how this repo's setup was built and why. Not an inventory (that's script/help for commands, the filesystem for everything else) — just the decisions and the reasoning behind them, so they don't have to be re-derived or re-litigated later.


2026-07-10 — Non-bare repo with sibling worktrees (not bare + worktrees)

Started with a bare repo (.bare/ + a .git pointer file) so no worktree would be "special." Switched to a plain repo initialized directly in main/, with other worktrees added as siblings via git worktree add ../<branch>.

Why: the bare setup achieves nothing extra a plain repo doesn't also give here (worktrees still land as siblings under redlich/), but adds a non-standard .git-as-file layout and loses the ability to git clone straight into a working state. The one risk of the plain approach — deleting main/ orphans other worktrees since it holds the real .git — was judged low-probability for a solo project and easy to just not do.

2026-07-10 — script/ header convention: Description + Side-effects

Every script/* file carries # Description: and # Side-effects: header comments; script/help greps them to generate the command list, no manually maintained inventory anywhere.

Why: copied from emsig/script/* — the header is the documentation, so it can't drift out of sync with a separate doc the way prose easily does.

2026-07-10 — wt renamed to worktree

Why: script names should be clear, not short — a name only saves typing once, but costs clarity every time someone else (human or agent) reads it cold.

2026-07-10 — check-script-integrity added, then flattened out of script/meta/

Added a script to mechanically verify every script/* file has the required headers. Initially placed it under a new script/meta/ subfolder, then moved it back to flat script/.

Why: emsig's actual convention has no script/meta/ — general-purpose, human-runnable commands live flat in script/; only logic with zero standalone value and a single skill-specific consumer lives inside that skill's own folder. check-script-integrity has standalone value (run it any time after adding a script), so it belongs with the other general commands.

2026-07-10 — kaizen and housekeeping skills added, bare minimum

Modeled on emsig's agent/skills/kaizen/ and agent/skills/housekeeping/, but trimmed hard: no audit.py/check.py orchestrator, no kaizen-log file, no meta-audit subagent, no STATUS/PROJECT-LOG integration. kaizen calls script/check-script-integrity directly; housekeeping calls a new script/check-repo-hygiene (git status, unpushed commits, worktree list).

Why: emsig's versions solved problems (toolchain drift across a large PHP codebase, multi-session slice tracking) this project doesn't have yet. The "promotion rule" carries over: judgment checks that become mechanical get written into a script and dropped from the skill's prose.

2026-07-10 — COMMANDS.md added, then sunset same day

Added a COMMANDS.md (procedure reference, modeled on emsig's) covering command discovery and a few conventions. Removed it in favor of CLAUDE.md (always loaded by Claude Code, current rules only) plus this file (history and rationale, loaded on demand).

Why: COMMANDS.md only existed as a home for those rules because no CLAUDE.md existed yet — in emsig, COMMANDS.md is a deliberate offload from CLAUDE.md once it got too large (164 lines), not a foundational file. At this project's size, a standalone procedure file was solving a problem that doesn't exist yet, while adding a discovery step (an agent has to remember to open it) that a project's own docs shouldn't rely on for the things that matter every session.

2026-07-10 — # Docs: optional third header field, with dangling-path check

Scripts wrapping a tool with real docs to know (a framework CLI, a linter) can carry # Docs: pointing to more info; script/help prints it when present. check-script-integrity verifies path-like targets (a repo file, or a script/* command) actually exist — URLs and inline prose are left unverified since neither is mechanically checkable without a network call or judgment.

Why: centralizing "which script maps to which doc" in a separate table (like emsig's STACK.md) is itself a thing that can drift if a script is renamed and the table isn't updated. Putting the pointer in the script's own header removes that failure mode. Preference order for the field's value: a command whose own output is the truth > a repo file > a URL > inline prose — roughly in order of how self-updating each option is.

2026-07-10 — setup-log skill added; CLAUDE.md and COMMANDS.md-successor content moved into script/help's footer

Added a setup-log skill (append an entry here, no discussion) so decisions get logged as they happen, not backfilled later — called from kaizen's Update discipline step, but meant to fire on its own too. Then trimmed CLAUDE.md down to the project one-liner plus "run script/help first"; the WRITES-confirm reminder and the setup-log/SETUP-LOG.md pointers moved into help's printed footer instead.

Why: CLAUDE.md is always loaded, so every line in it is a recurring token cost whether or not it's relevant this session — worth paying for the mandate to run script/help, not worth paying to duplicate what that command's own output can say at the exact moment it's relevant. Same logic as the # Docs: field: don't let a static file be the source of truth for something a live command can tell you instead.

2026-07-10 — skill-check skill + check-skill-integrity added; step numbers dropped; housekeeping findings promoted into its script

A pre-commit review of housekeeping (before this repo's first commit) surfaced three things: hardcoded ## Step N — headers are a renumber-on-edit smell across every skill; half of housekeeping's "Judgment" step was really a fixed fact→severity mapping, not judgment; and kaizen's checklist didn't cover either issue, so it wouldn't have caught them on its own.

Fixed all three: added script/check-skill-integrity (mechanical: missing frontmatter, Step N headers) and a skill-check skill that runs it plus judgment (promotable work, description clarity) — triggered right after any SKILL.md is written or edited, not gated behind a kaizen run. Dropped Step N — from every skill's headers (kaizen, housekeeping, setup-log, skill-check). Moved housekeeping's fixed severity rules (dirty tree, unpushed, behind, untracked) into check-repo-hygiene as a pre-graded FINDINGS: block; housekeeping's Judgment step now only covers what actually needs context (per-file untracked disposition, worktree abandonment calls, drafting the commit message). kaizen's Judgment pass now delegates skill-quality checks to skill-check instead of re-deriving them, and its mechanical step runs both integrity scripts.

Why: this is the "promotion rule" applied to a category of finding, not just an instance — hardcoded numbering and fact→severity prose are both things that look like judgment but are actually mechanical once named. Encoding that recognition as a dedicated, auto-triggering skill (rather than just fixing this one instance) means the next skill written gets the same gate for free, instead of relying on someone noticing again by hand.

2026-07-10 — kaizen generalization rule added, after finding it wouldn't have self-caught the previous entry

Asked directly why kaizen didn't catch the Step N smell or the housekeeping promotion opportunity on its own. Two causes: it was never actually run this session despite ~10 setup changes (a checklist that doesn't execute can't catch anything); and even if it had run, its "Structural drift" bullet and its frontmatter description: trigger list were both worded scripts-only, left that way when skill coverage was added a few turns earlier. Widened both, and added an explicit generalization rule: when a finding was missed because a bullet was scoped narrower than kaizen's actual coverage, widen the bullet, don't just fix the instance.

Why: kaizen's own checklist is subject to the exact staleness/narrow-scope drift it exists to catch elsewhere, and nothing was checking that. The fix isn't a heavier mechanism (e.g. emsig's meta-audit subagent, which this project deliberately skipped as too much for its size) — it's naming the pattern so the next time a bullet turns out to be scoped too narrowly, widening it is the reflex, not just patching the one instance.

2026-07-10 — kaizen Self-reflection step added

The generalization rule (above) only fires after a human points at a finding kaizen missed — it doesn't make kaizen look for that on its own. Added an explicit Self-reflection step, run every time (not just when something feels off), positioned right before the report so its findings share the same output: checks the description: still matches what the mechanical step + Judgment bullets actually cover, audits each bullet's wording against kaizen's real scope, and treats "this run was human-prompted by something kaizen should have caught" as a 🔴 — the strongest signal of a coverage gap available. Also noted emsig's heavier subagent meta-audit as a named escalation path, not built now.

Why: a rule that only applies reactively (after being told) isn't self-reflection, it's just a cleanup checklist. The two prior "kaizen" entries in this log were both human-prompted, not self-caught — Self-reflection is meant to be the thing that changes that going forward, without requiring the heavier subagent machinery this project's size doesn't justify yet.

2026-07-10 — kaizen named the keystone skill explicitly

Declared it in the skill file itself: everything else in this setup — scripts, other skills, conventions — can drift or break and kaizen is what finds and fixes it; nothing plays that role for kaizen itself except Self-reflection. Also made the report format enforce visibility of that: Self-reflection findings are tagged [self] within the normal severity buckets (so they're actionable through the same numbered accept/fix flow as everything else), and a Self-reflection: ran, ... line prints every report regardless of outcome — never folded into "all clear," never silently omitted.

Why: everything built this session — check-script-integrity, check-skill-integrity, skill-check, the promotion and generalization rules — only has teeth if kaizen actually keeps running it and keeps noticing when its own coverage lags. A sharp kaizen makes the rest of this setup recoverable no matter how much it degrades; a dull one means nothing is being checked at all, even while every individual file still looks fine. Stating that plainly in the skill itself is meant to keep it the highest-priority target for care and review going forward — everything else in this repo is in service of keeping this one file sharp.

2026-07-10 — KAIZEN-LOG.md added; meta-audit subagent deliberately deferred, not built

Prompted by patterns from a prior project: a per-run audit log + backlog for kaizen itself, and a meta-check subagent auditing kaizen runs. Built the former (KAIZEN-LOG.md: ## Audit Runs — one line per run, YYYY-MM-DD · 🔴N 🟡N 🟢N · labels — plus ## Backlog (Try Next), resolved items struck through with a dated reason rather than deleted, matching the real vault kaizen-log's format). Wired it into Self-reflection as cross-run memory: a finding label recurring in 5+ consecutive runs is a chronic-flag candidate; a rule whose condition never fires in 14 runs is a dead-rule candidate — neither is detectable from a single run alone. Added Backlog review and Log the run steps to kaizen.

Declined to build the meta-audit subagent (a fresh readonly reviewer auditing a kaizen run + the skill itself, per emsig's pattern) despite it being asked for directly — logged it in KAIZEN-LOG.md's Backlog instead.

Why: the log fills a gap Self-reflection structurally cannot fill without persisted history — that's not "more machinery," it's a prerequisite for a check that doesn't exist at all yet, so it was safe to build immediately. The subagent is different: kaizen's own "Growing this skill" section (written minutes earlier) explicitly says not to build that until Self-reflection has run enough times to demonstrably keep missing the same class of gap on its own — and Self-reflection has never run once. Building it now would have contradicted a rule just written, on the same day it was written, for no reason better than "it's available." Held to the rule instead.

2026-07-10 — KAIZEN-LOG.md moved into .claude/skills/kaizen/, renamed LOG.md

Moved from repo root (visible alongside README.md/CLAUDE.md/SETUP-LOG.md) into the kaizen skill's own folder, and dropped the KAIZEN- prefix since the folder name already says that.

Why: it's an internal artifact of one skill, not something anyone browsing the repo root needs to see — unlike SETUP-LOG.md, which is genuinely repo-wide history worth that visibility. Matches the vault's own precedent too: its equivalent lives at .claude/kaizen-log.md, not at vault root.

2026-07-10 — First kaizen run: README fixed, check-* glob convention, Docs guidance moved to script/help

First real kaizen run (findings logged in the skill's LOG.md). Fixed the 🔴: README.md still described the abandoned bare-repo layout with commands that no longer worked — rewritten around the non-bare layout and script/worktree. Two convention changes from the [self] findings: kaizen's structural step now says "run every script/check-*" instead of naming scripts, so new checks join automatically; and its self-reflection bullet lost the "both entries so far" snapshot count that had gone stale within hours. Also: script/worktree's side-effects wording now matches actual rm behavior (branch survives), and the # Docs: preference order (live command > repo file > URL > prose) moved into script/help's footer — human's call: the always-seen live output is its home, not this log's history.

Why: the two [self] findings shared one root — counts and enumerations baked into prose go stale silently; conventions and globs don't. Same reasoning that produced the header-based script inventory on day one. The README miss also showed the reference-grep habit (applied when moving KAIZEN-LOG.md) wasn't applied at the bare→non-bare migration — layout changes need a doc re-read, not just a plumbing fix.

2026-07-10 — kaizen-overdue finding added to check-repo-hygiene

Kaizen now has a mechanical trigger: check-repo-hygiene compares the newest entry date in SETUP-LOG.md against the newest Audit Runs date in the kaizen skill's LOG.md and emits a pre-graded yellow kaizen-overdue finding when setup changed after the last run (or when kaizen never ran). Housekeeping adopts FINDINGS: as-is, so the nudge fires at end-of-session with zero skill edits. Date granularity only: same-day events can't be ordered, so same-day stays silent — wrong in the quiet direction, never a false nag.

Two alternatives considered: a prose nudge in housekeeping's judgment section (rejected — prose relying on an LLM noticing is the failure mode this whole session kept finding), and a pre-commit hook running script/check-* (deferred to kaizen's Backlog — no commit rhythm exists yet to trigger it, and it only covers the mechanical half).

Why: every kaizen-shaped run so far was human-prompted; a checker without a trigger moment depends on memory, and memory demonstrably didn't fire. This wires kaizen's cadence into housekeeping's genuinely natural trigger (the end-of-session feeling) via the one channel that's deterministic — the script's pre-graded findings. It's also the promotion rule applied to the nudge itself: the "should kaizen run?" question turned out to be mechanically decidable, so it became script logic, not skill prose.

2026-07-13 — INBOX/ at repo root, gitignored — the human's drop zone

Workflow/stack decision records arrived in an ad-hoc agent/INBOX/. Decided: INBOX/ lives at repo root and is gitignored — a purely local drop zone for the human's unfinished thoughts and context that agents work from when pointed at it, never a committed artifact. agent/ was removed again (it had existed only to hold the inbox; adopting emsig's agent/ layout wholesale is a separate, undecided question). One README line documents the convention, since a gitignored folder is invisible in a fresh clone.

Why: inbox content is by definition unfinished — committing it would publish half-thoughts to GitHub and create pressure to polish input that only exists to be consumed and deleted. The gitignore also makes housekeeping's exemption mechanical: check-repo-hygiene counts untracked files via --exclude-standard, so ignored files never appear — no "don't check INBOX" prose needed anywhere (emsig carried exactly that prose rule; this setup gets it as checked state instead).

2026-07-13 — Layout: Claude-native, no agent/ indirection layer

First question settled in marrying the INBOX decision records into this setup: adopt emsig's tool-neutral agent/ home (canonical files behind per-harness wrappers, kept in sync by gen-docs) or stay Claude-native? Decided: full Claude for this experiment — Claude models, Claude Code, Claude Agent SDK as runner substrate, Claude API for the chatbot. Harness-coupled artifacts live in their native homes: skills in .claude/skills/, role definitions in .claude/agents/, hooks in .claude/settings.json. No wrapper layer, no mirrors, no sync tooling. Gates (script/*), specs, logs, and metrics stay plain files — per stack-decision §5, portability falls out of that layering instead of being designed in. The next project will likely be agent-agnostic again, pulling learnings from here.

Why: emsig's agnostic layer bought portability at the price of a standing maintenance surface (wrappers, INDEX/.mdc regeneration, drift checks between three homes). This experiment's variable under test is the workflow, not harness portability — paying that tax here would blur the measurement. Committing to one harness lets the runner use SDK-native machinery (sessions, per-role subagents, scoped hooks) without an abstraction layer on top. Exit cost is bounded and recorded: the harness-coupled surface is the runner adapter plus skill/agent files; everything load-bearing stays harness-neutral by construction. No file moves needed — the setup was Claude-native from day one, so this forecloses an option rather than changing anything.

2026-07-13 — check-* scripts adopt the three-valued gate exit contract

All check scripts now distinguish red from broken per stack-decision §0: exit 0 green / 1 red (offenders listed) / 2 broken (the check itself failed — nothing was verified). Implemented as set -Eeuo pipefail + trap 'exit 2' ERR: any unexpected command failure exits 2, while the deliberate offender-found paths keep their explicit exit 1 (the ERR trap doesn't fire on the exit builtin — verified). check-repo-hygiene, a reporter rather than a gate, exits 0 or 2 and never 1 — findings live in its output, not its exit code. Kaizen's structural step gained one rule: broken outranks every red, fix the check itself first. All three states tested, including a genuine broken run (script transplanted outside a git repo).

Decided now rather than with the bootstrap's meta-qa slice: the ambiguity already existed (under plain set -e, a grep hiccup and a genuine offender both exited 1 — indistinguishable to any consumer), and every check-* written before bootstrap would have copied the 0/1 pattern, growing the retrofit. Adopting the contract while the family is three scripts small makes it the convention new siblings inherit.

Accepted caveat, eyes open: errors swallowed by intentional grep … || true guards (e.g. an unreadable file) still surface as red (missing header) rather than broken — misclassified, but in the loud direction: a red fails the run and gets human eyes, unlike broken→green, which would pass silently. Distinguishing grep's exit 1 (no match) from ≥2 (error) per call site isn't worth the complexity until it bites once — revisit trigger: a red finding whose root cause turns out to be a check error.

2026-07-13 — Kaizen demoted to bootstrap scaffold; retirement at first harvest pre-registered

The sanity check that decided it, run in reverse: if harvest + meta-qa already existed, would kaizen be introduced? No — every function decomposes into something the workflow design already owns: mechanical checks → script/meta-qa; judgment sweep → harvest input (budgeted); cross-run memory and backlog → harvest's own history and agenda; "something feels off" → running the checks directly plus ordinary dialogue. Kaizen is an emsig pattern, and harvest is the deliberate counter-design to exactly that pattern — an unbudgeted judgment patrol surviving alongside harvest would be a side door around the budget system.

Trimmed now to a minimal transitional skill (~⅓ its size): checks + judgment sweep + report + run log. Removed: keystone framing, Self-reflection section, cross-run memory, backlog-review step, promotion/generalization rules, meta-audit escalation (backlog item struck as retired). Added the repair-don't-grow rule effective immediately: kaizen restores existing conventions inline, but new capacity is proposed and parked, built only on explicit human ask. What survives kaizen's retirement untouched: the check-* scripts (become meta-qa components), the overdue-nudge mechanism (comparand changes to harvest), SETUP-LOG.md (never kaizen-specific), housekeeping (different job).

Why: the week's evidence says the value was never kaizen-the-loop — it was mechanical checks plus a deterministic trigger moment; every kaizen-shaped run was human-prompted. Pre-registering the retirement (rather than deciding it later) prevents the scaffold from becoming permanent by inertia, mirroring the workflow doc's own degraded-mode bootstrap logic. Accepted trade-off: between harvests, judgment-class drift (a stale README) has up to a week of latency — fine for a solo prototype, and the setup's own direction (live commands as truth, generated inventories) shrinks that class over time.

2026-07-13 — skill-check retired

Deleted the skill-check skill (same inversion test as kaizen: with meta-qa running on workflow-layer diffs, nobody would introduce a skill whose job is "remember to run the linter"). Its mechanical half lives on as script/check-skill-integrity — which now carries the step-numbering rationale in its own failure message, so the # Docs: pointer was dropped rather than repointed. Its judgment half (promotable-work detection, description clarity) is harvest-agenda material when harvest exists; until then, kaizen's judgment sweep covers skills like any other setup artifact.

Why: its trigger ("fires whenever a SKILL.md is edited") was stochastic hope — the docs' central criticism of skill-borne discipline — and with kaizen under a no-growth rule the skill population it was built to police is converging, not expanding. One-day lifespan; cheap lesson recorded: check the inversion before building the enforcement layer, not after.

2026-07-13 — Repo boundary: synthetic-only data policy, check-repo-boundary, FRAME.md started

Gap analysis of the combined system (docs + setup) found the biggest hole at the repo/GitHub boundary: the PII invariant guards the LLM payload, but nothing guarded the tracked tree — no .env discipline, no fixture-data rule, for a product whose domain is banking data. Four decisions, all taken via guided questions:

  1. Synthetic-only: committed fixtures/seeds/spec examples are invented or generated, never real member/banking data (not even anonymized — anonymization is judgment work, one mistake publishes PII). Real data lives only in gitignored runtime locations (data/, *.db, .env).
  2. Enforced now, mechanically: .gitignore entries plus script/check-repo-boundary — red if any .env* (except .env.example), database file, or data/ path is ever tracked, catching git add -f and renames that dodge the ignore rules. Its git ls-files call is deliberately unguarded: git failing = broken (2), never silently green. Content-level scanning (IBAN patterns) deferred to the Semgrep/qa layer.
  3. FRAME.md started — the docs' designated hand-written-first artifact, opened with the prototype question, the riskiest assumption, both invariants (LLM boundary + repo boundary), and the deployment regime assumption. CLAUDE.md points to it. Remaining stack-decision §8 entries adopt as bootstrap reaches them.
  4. Private GitHub remote — created after these rules land, so the boundary check exists before the first push, in the right order.

Why: the repo boundary is exactly the block-gate category — invisible, expensive to reverse (a push publishes; history rewrites don't un-leak), and the kind of rule that must exist before the first violation, not after. Path-based synthetic-only keeps the check mechanical: no per-file judgment about what counts as sensitive.

2026-07-13 — Spec template v0 (specs/TEMPLATE.md) + script/check-spec

The last hand-written-ever artifact (workflow doc §15) and its lint, via guided dialogue. Decisions beyond what the docs already fixed:

  1. Naming YYYY-MM-DD-kebab-name.md, filename-is-the-id, never renamed. Sequence numbers rejected: a pull system reorders execution on purpose, so a number claims an order the design doesn't promise — and minting one is a coordination point (parallel dialogues collide). The date is a fact about the spec itself; within-day order is deliberately meaningless (the queue state owns real order), so Emsig's tiebreaker problem dissolves. Rejected YY-MM-DD (DD-MM-YY ambiguity) and epoch stamps (needs a tool to decode).
  2. All sections always present; non-applicable ones carry an explicit none. Absence-by-decision must differ from absence-by-forgetting: Pre-approved: none is also the overnight-queue admission claim. A reflexive none fails safe — gates stay the diff-time authority.
  3. Surface: section added — the docs' lint rules demand a glob-free, file-capped diff surface but their §3 skeleton has no home for it. Caps: 50 lines (one screen), 8 surface files; both v0 numbers, harvest-tunable.
  4. Shipped specs are exempt from shape-lint by design — they're frozen and hash-guarded (meta-qa), so template evolution never flags old specs; the exemption mechanism arrives with the runner's queue state file. Until then everything in specs/ is pre-ship and check-spec lints it all.

Why: the template is the meta-gate that must precede slice 1 — it lints the one artifact existing from day one. The cap's failure message says "cut the slice, don't compress" because compression is the real clarity risk of a line cap; the tripwire is spec-attributed rework plus specs sitting at cap.

2026-07-13 — README is static: describes now, never its own history

Convention set while trimming the layout section: README states what is ("Stack: Python 3.12 + FastAPI"), never decision dates, rejected alternatives, or pending adoptions — that residue lives here in SETUP-LOG.

Why: a README that narrates its own evolution is a second history file that drifts (the stack line and layout diagram both went stale within a day of being written); one place per fact, and the place for "was" is this log.

2026-07-13 — Path conventions + emptiness-is-broken (review finding)

Sebastian's review of the runner found a silent-green hole: gate discovery used cwd-relative paths, so a runner invoked outside main/ found zero gates and completed unverified. Two conventions set with the fix:

  1. Paths anchor at the composition edge, never at cwd: library code (state, phases) takes Path parameters and constructs no anchors; the edge (cli) resolves everything from git rev-parse --show-toplevel. Relative-path literals outside the edge are a defect — Semgrep rule candidate when the qa layer lands (parked in kaizen backlog).
  2. Emptiness is broken: a discovery step that finds nothing to verify (zero gates) is exit-2-class — halted, never green. Generalizes the "existence, not strength" doctrine to collection steps; future gates should treat empty discovery the same way.

Why: the backstop (2) makes the failure loud regardless of cause, the anchor rule (1) removes the cause; convention alone would be discipline, so (1) is pre-registered for mechanical enforcement.

2026-07-13 — uv scaffold: stack adoption lands, hand-written before slice 1

Python 3.12 project via pyproject.toml: claude-agent-sdk as the sole runtime dep, dev group pytest/pyright/ruff, pyright strict, hatchling building the flat runner/ package, runner script entry point wired to a stub that refuses with a pointer to the spec.

Why: the runner-core slice's Surface is runner/*.py + one test file only — so everything the slice presumes but must not touch (pyproject with the entry point, lockfile, tool config) has to pre-exist, hand-written, like the repo skeleton. Hatchling over uv_build because the package is flat runner/ (workflow tooling, not the product) while uv_build defaults to src-layout; the product package will make its own layout call in slice 2. Stub-refusing entry point over no entry point: uv run runner failing with "not yet built + spec pointer" beats an import error, and the wiring is scaffold (pyproject), only the behavior is slice.

2026-07-13 — uv workspace + stabilisation slice type (workspace-split)

Repo is now a uv workspace: virtual root (dev group, tool config) with a src-layout runner/ member owning its own deps; the product joins as an app/ member (package redlich) in slice 2. src-layout is the workspace-wide rule. Executed as the first stabilisation micro-spec (Emsig import): behavior-preserving, proof inverted to green-at-baseline → green-at-handoff with tests moved-never-edited; hand-driven because the runner rightly enforces red-first.

Why: per-member dependency declaration is what makes the future pip-audit/bandit gates audit the product and not the tooling, and it makes the "workflow layer is the portable asset" framing physical. src-layout over flat: tests can only import the installed package, closing the green-against-uninstalled-code hole (rejected: flat product package; rejected: hand-written scaffold commit — the loop should run its own restructures). The runner extension for the stabilisation type is parked in the kaizen backlog until the pattern recurs.

2026-07-13 — diffity for human review: raw CLI, upstream skills rejected

Taste review now runs through diffity (npm global, v0.9.5): diffity HEAD~1 serves a GitHub-style diff; the agent posts findings inline via diffity agent comment and reads replies back via agent list/reply. The nilbuild/diffity skills were deliberately NOT installed.

Why: the skills are thin wrappers over the same diffity agent CLI plus imported review methodology — which collides with the recorded review design (criteria live in explicit criteria files, and bootstrap step 5 ports the diffity fork with criteria files emptied). Mechanism over doctrine: use the CLI directly; a project-owned script/skill graduates via harvest if the pattern recurs.

2026-07-14 — script/meta-qa built; first harvest's precondition met

The workflow-layer qa twin (agent-workflow-concept §5). v0 lints two families over artifacts that already exist: budget caps (CLAUDE.md ≤30, FRAME.md ≤20, FRAME invariants ≤3, skill descriptions ≤500 chars) and shipped-spec immutability (each status:done slice in data/runner/state.json has its spec file hash-compared against its committed blob at the recorded ship commit; git is the source of truth, no hand-maintained manifest). Three-valued (0/1/2), proven red-first by hand across all branches incl. broken=2. A test_scaffold smoke test asserts it is a well-formed three-valued gate. Docs: points here, not to the gitignored INBOX design doc.

Why: harvest is agent-led and drifts like product code; meta-qa is its deterministic floor, and green meta-qa is the pre-registered precondition for the first harvest (§15 step 6–7). Scope kept minimal — reflection-quarantine has no artifact to lint yet, and gate-sharpness already lives in check-spec; building either now would be speculation or duplication (review-layer P2, no wolf-crying). Deliberately not a check-* and not run by script/qa: meta-qa triggers at harvest and on workflow-layer diffs, a separate cadence from product qa — the smoke test accepts a red (1) verdict precisely so product qa never gates on a budget breach. FRAME cap set to its current reality (~20, now 19), not the design's ~10 target: a day-one red would block the very harvest it gates; trimming FRAME toward 10 is deferred to that harvest's agenda (Sebastian's call). Hand-built, not run through the runner: every check-* gate was hand-built, the runner is pytest-shaped for product slices, and this is the bootstrap the design itself calls "manual with the agent."

2026-07-14 — First harvest: harvest-overdue nudge (time OR volume); kaizen kept one cycle

Ran the first harvest. Added a dual-trigger harvest-overdue finding to check-repo-hygiene: fires on EITHER >7 days since the last harvest OR >=6 status:done slices shipped since then (HARVEST_VOLUME_TRIGGER, tunable). Last-harvest state lives as a YYYY-MM-DD · slices=N line under a new "Harvest Runs" section in the kaizen LOG. Graduated two rules: the spec template's Decisions guidance now says "pin the ambiguities the builder hits in THIS slice, not future-state hedges" (the success-contrast finding), and the implementer/test-writer role prompts gained strictly-spec-bound + comment-discipline + baseline-red-by-assertion.

Why: a time-only cadence lets a fast build streak outrun the harvest — exactly the momentum-beats-discipline risk the workflow design pre-registered; a work-volume trigger closes that (Sebastian's call, mid-harvest). Kaizen was not retired despite its precondition (green meta-qa) now being met — kept one more cycle so harvest cadence proves out before the scaffold is removed (Sebastian's call), so kaizen-overdue and harvest-overdue coexist until then. Harvest Runs co-located in the kaizen LOG rather than a new file: minimal now, repoints when kaizen retires. The success contrast (member-fees roughest: Decisions budget spent on a dead Postgres hedge, real ambiguities unpinned → rework) unified four separate backlog findings into one template fix.

2026-07-14 — No-cd convention: PreToolUse hook + help footer

Agents kept prefixing cd <repo> && …, which is waste (the shell cwd persists at the repo root and every script/* self-anchors via cd "$(dirname "$0")/..") and triggered a permission prompt each time. Enforced with a PreToolUse(Bash) hook (.claude/hooks/no-cd.sh, wired in the committed .claude/settings.json) that denies any command using cd as a command — at the start or after &&/;/|, not cd as a substring or trailing target — with a message pointing to git -C/absolute paths. Discoverability lives in the script/help footer.

Why: two channels by design — the help footer is proactive pickup, the hook is the forcing function (FRAME §5: "prevent at call time where a deny is cheap and precise"). A doc line alone is discipline; the hook makes it structural, and its deny message teaches on contact. Danger of a blanket cd ban is low: script internals are unaffected (the hook sees only the Bash-tool command string), and the only real loss — changing the shell cwd, e.g. into a sibling worktree — has escape hatches (git -C, absolute paths). Hook lives in .claude/hooks/ not script/: it is harness plumbing tied to settings.json, not a project command.

2026-07-15 — Spec line-cap measures prose only (check-spec)

check-spec's 50-line cap now counts prose lines only — Surface: and Pre-approved: list entries are subtracted (the 8-file Surface cap already bounds them). Behaviour: prose = wc -l − surface_entries − preapproved_entries. No spec changes: all 15 stay green; the negative probe (63 prose lines) still fails.

Why: the first harvest (2026-07-14) deferred this candidate — "do NOT tune before spec-attributed rework exists." Both tripwire halves are now confirmed: 11 of 15 specs sit at the 50-line total cap while their prose is ≤47 (the cap was firing on data, not prose), and the second half — spec-attributed rework — materialised in the member-fees/member-list fix-forwards (current-fee tie-break 2cab62d, email retrofit). Compressing prose to keep data under the cap is the exact clarity risk the cap exists to prevent, so measuring prose is the fix.

2026-07-15 — script/worktree: robust main-root, branch from main/'s HEAD

script/worktree new now resolves the main working tree via git rev-parse --git-common-dir (always main/.git) instead of --show-toplevel (which resolves to the invoking worktree and nested new worktrees wrongly), and runs the add from that root with git -C so the new branch takes main/'s HEAD regardless of where the tool is invoked. rm uses the same -C anchor.

Why: this session opened in a Claude-managed worktree based on the stale origin/main (13 commits behind local main, which was unpushed) and spent a whole harvest re-discovering already-shipped work before checking the base — the trigger for hardening the tool. First cut asserted main explicitly as the base; reconciled against the same-day runner-worktree slice, whose _ensure_worktree deliberately branches from main/'s HEAD and whose reviewer explicitly rejected "assert it is specifically main" (main/ never leaves main is a repo invariant). Aligned to that: same --git-common-dir root the runner uses, base on HEAD via git -C "$main_root" rather than re-asserting main. (The real culprit was the Claude EnterWorktree origin/main default, which no script controls — parked in the kaizen backlog.) Self-tested from inside a worktree: sibling placement, based on main's HEAD, clean removal.

2026-07-15 — check-spec exempts shipped specs from shape-linting

check-spec now skips specs the runner records as shipped (data/runner/state.json, status=done) — the same source of truth meta-qa uses for immutability. State or jq absent => nothing known-shipped, so all specs are linted (safe fallback). The clean message now reports "N pre-ship spec(s) conform (M shipped, exempt)".

Why: the scope comment always promised this ("shipped specs … not shape-linted; the exemption mechanism lands with the runner's state file"), deferred until that file existed. It does now, and meta-qa already keys off it. Without the exemption, check-spec and meta-qa were on a collision course: the next template tightening would red a frozen shipped spec that meta-qa forbids editing — an unfixable red. Proven by fault-injection: a done spec with a cap violation stays green, a building spec with the same violation reds.

Reconciled same day against the finished runner-worktree run: that slice moved builds into sibling slice worktrees (qa, hence check-spec, run there), which hold no local data/ — so a cwd-local state lookup would leave the exemption inert in exactly the flow it was built for. State is now resolved via --git-common-dir (always main/.git), the way the runner locates it, so the exemption fires from any worktree; only a genuinely stateless tree or a missing jq falls back to lint-all.

2026-07-19 — README trimmed for the public repo; retired the pinned test tying it to runner-land's operational detail

README.md (02b4f24) dropped ## Repo layout and ## Runner flow — worktree structure, uv run runner run/land commands, script/help/ SETUP-LOG.md pointers, INBOX/ — down to a short framing paragraph. Deliberate: the repo went public on GitHub and that operational detail isn't judged helpful there yet; trimmed to make room for something more useful later, not lost by accident.

Two pinned tests from the runner-land spec's Context ("README.md — land replaces the hand-merge in the documented flow") asserted README's old content verbatim and went red on the trim: test_readme_documents_runner_land, test_readme_no_longer_mentions_mark_reviewed. Removed both (runner/tests/test_runner_land.py) rather than restoring the content — the spec itself is shipped and immutable, but the test enforcing a Context note isn't, and the human call that superseded it is the more current source of truth. script/worktree's Docs: README.md § Repo layout pointer was dangling the same way (silently — check-script-integrity only verifies the target file exists, not the section); replaced with the sibling-worktree rationale inlined as a plain comment instead of an external pointer.

Why: a pinned regression test is only as good as the premise it pins; once a human deliberately overrides that premise, keeping the test red forever (or hand-waving it back to green with restored filler content) would be worse than removing it. The operational detail itself isn't lost — script/help, CLAUDE.md, and HARVEST-LOG.md already carry it, or will.

2026-07-20 — /spec skill: guided front end for the spec runner

Added .claude/skills/spec/SKILL.md — the guided idea→committed-spec entry. It runs a live dialog to shape a rough/unfinished idea into a template-conformant draft, then drives runner spec run (precheck → sharpen → apply → lint → awaiting-approval, or awaiting-decision halt) and runner spec approve (commit to main with a Spec: mark).

Why: the spec runner (core/halt/approve-commit, built 2026-07-19/20) is the autonomous tail; the human's most-ambiguous input (a raw idea) needed a first-class guided entry rather than ad-hoc freehand drafting. Skill, not runner code — the dialog is inherently interactive, and skills are how this repo packages interactive workflows.

2026-07-21 — devcontainer: physical folders over named volumes for state that must survive docker rm; zsh as the real default shell; script/devcontainer-shell

~/.claude and /commandhistory switched from named Docker volumes to bind-mounted physical folders (.claude-home/, .commandhistory-home/, siblings of main/). vscode's passwd shell switched from bash to zsh (already preinstalled with oh-my-zsh, just unused), with HISTFILE redirected into the same persisted mount. Added script/devcontainer-shellup (idempotent) then exec a shell into the container, zsh by default, prefers a global devcontainer on PATH but falls back to npx @devcontainers/cli.

Why: found by hand that named volumes are keyed off ${devcontainerId}, which is only stable while a container matching the workspace folder still exists — a plain docker rm (not just a rebuild) then up mints a fresh id and an empty volume, silently orphaning the old one (lost a real Claude Code session this way mid-session; recovered it by hand from the orphaned volume before switching). A physical folder has no lookup step to fail. ~/.cache/uv stayed a volume — re-warming a package cache on the rare hard-removal is low-stakes, unlike losing a login session or (now that zsh is the daily driver) shell history.

2026-08-20 — devcontainer: npm systemwide-prefix fix (for real this time) + unsnooze

.devcontainer/Dockerfile's npm block now writes prefix=/usr/local directly to /usr/etc/npmrc (npm's well-known systemwide config path) instead of npm config set prefix /usr/local. Also added unsnooze (auto-resumes Claude Code/Codex/etc. sessions when usage limits reset) to the same npm install -g line as claude-code and ast-grep/cli.

Why: an earlier fix (commit 5caf75c, 2026-07-22) chown'd the already-installed global packages to vscode but never fixed which config file vscode's own npm reads — npm config set prefix without --global writes to the invoking user's own npmrc (root's, at build time), so the self-update permission failure (EACCES/no_permissions) silently came back the next time anyone ran a global npm install as vscode. Reproduced live via npm install -g unsnooze (failed) → sudo npm install -g unsnooze (landed root-owned again). --location=global was tried next and also rejected: npm resolves the systemwide config file's path relative to whatever prefix is already in effect for the invoking user, so once root's own prefix had drifted, root's and vscode's npm disagreed on which file "systemwide" even meant. Writing the file directly sidesteps that. Tracked as harvest idea devcontainer-npm-root-owned-blocks-selfupdate.

2026-08-20 — unsnooze: activation moved to postCreate.sh, tmux added

The prior entry above only got npm install -g unsnooze onto the image; the binary was never actually wired up. unsnooze doctor on a live container showed the StopFailure hook and shell wrappers both missing and zero tracked sessions. Root cause: activation (unsnooze install --yes) writes to ~/.claude/settings.json and ~/.bashrc/~/.zshrc, none of which are reachable from the Dockerfile -- ~/.claude is a devcontainer.json bind mount only attached once the container runs, and .bashrc/.zshrc get regenerated from scratch by the Dockerfile on every rebuild (unlike ~/.claude, they aren't mounted, so any wrapper lines written into them at runtime don't survive a rebuild either). Moved the call into postCreate.sh, right after the existing ~/.claude chown -- it has to re-run on every postCreate, not just once, and is safe to because it's marker-guarded/idempotent (confirmed by running it twice).

Also added tmux to the Dockerfile's apt-get list. Without any multiplexer, unsnooze's multiplexer: auto was silently resolving to headless, which still auto-resumes a session at reset but can't answer Claude's own interactive limit-menu prompt or wait out a busy pane (both need pane-scraping). ~/.unsnooze/config.json itself needed no changes -- every value was already at its (correct-for-this-environment) default, and that file isn't mounted either, but there was nothing customized in it to lose. Deliberately skipped unsnooze install --daemon (GUI-surface watch for the VS Code extension panel, as opposed to terminal use): it installs a systemd user unit, and this container has no systemd running at all (cron needs the same manual service start every boot, for the same reason) -- the unit would never activate, and this repo's workflow is terminal/CLI-driven, not the extension panel, so there is no session for it to watch anyway.

2026-08-30 — script/devcontainer-shell: route command-form invocation through interactive zsh so unsnooze's wrapper applies

script/devcontainer-shell claude (one-liner form) now runs devcontainer exec ... zsh -ic "claude" instead of devcontainer exec ... claude directly. The no-arg form (drop into an interactive shell) is unchanged.

Why: devcontainer exec <cmd> execs the binary directly without sourcing .zshrc -- the file where unsnooze's claude/codex wrapper functions live. The one-liner form silently bypassed the wrapper and ran untracked, unresumable sessions; only script/devcontainer-shell (no args) into an interactive shell, then claude typed by hand, ever picked up the wrapper. Confirmed live: 2 of 3 running claude processes in the container weren't among unsnooze-resumed's tmux panes. Doesn't retroactively fix already-running unwrapped sessions. Harvest idea: unsnooze-unwrapped-sessions-not-tracked (seen → built).