From 4baaec7e9019ae82a0321ce1fb9f1a099e118e04 Mon Sep 17 00:00:00 2001 From: Jeroen Schweitzer Date: Wed, 23 Sep 2026 20:22:00 +0200 Subject: [PATCH] =?UTF-8?q?chore(meta):=20T-1247=20=E2=80=94=20the=20reach?= =?UTF-8?q?=20initiative=20is=20done;=20E4-E8=20closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .pql/changelog/ticket_history/2026-09.sql | 46 +++++++++++++++++ .pql/changelog/tickets/2026-09.sql | 60 +++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/.pql/changelog/ticket_history/2026-09.sql b/.pql/changelog/ticket_history/2026-09.sql index e358bd230..c07cf307b 100644 --- a/.pql/changelog/ticket_history/2026-09.sql +++ b/.pql/changelog/ticket_history/2026-09.sql @@ -638,3 +638,49 @@ INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, chang DONE 2026-09-23 — delivered by the E3 ports, item by item. (1) import_economics + economy_import moved to domains/ledger/ (the map renamed db to ledger), generator_sources.py updated in the same commit, regen run, stamp verified: T-1289. (2) generate_brands kept as a subroutine of the importer, and the relationship is now tighter: cargo_binary rebuilds a stale binary, where before a Rust edit could be stamped over output from the old binary (fix commit e3daf561d). (3) schema_version.py moved with the ledger; canvas_sources.py stays a top-level registry module per the domain map, and check-canvas-version still resolves it. (4) planet-gen became domains/atlas/planet (T-1288); the one-time importers stayed unstamped. (5) garment-fit: 22 payloads to scripts/blender (T-1273) + make_logo to domains/character (T-1290). (6) wiki_sync + connectors to domains/wiki and domains/assets; ensure_venv is gone (T-1290). (7) test_sim_determinism and test_oasis_ring_scaling moved to tooling/test_planet_*.py and pass; globe renders were pixel-identical across the T-1274 cleanup. Nothing numeric changed: every byte-comparable generator output was checked identical before/after.', NULL, '2026-09-23 18:16:36', '2026-09-23 18:16:36.730', '2026-09-23 18:16:36.730', NULL, '94372e7d3f4ec83ccfda9cf5844e0b93', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3DTRHSBYJS55NDT9YR4R0', 'status', 'backlog', 'done', NULL, '2026-09-23 18:16:37', '2026-09-23 18:16:37.323', '2026-09-23 18:16:37.323', NULL, '3f5b676d54ad0b58a6f0879f9a5c7642', 2) ON CONFLICT(hash) DO NOTHING; INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3F6SKZ6EQQ7TY0RKT2970', 'status', 'backlog', 'done', NULL, '2026-09-23 18:16:37', '2026-09-23 18:16:37.331', '2026-09-23 18:16:37.331', NULL, '28db5755c043d2ee316b03594cbc8de1', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3G1D2828RX2F3ZFNWRJGC', 'description', 'The migration is not done when reach works; it is done when nothing calls the old paths. Surface: 84 Makefile targets, 160 markdown files under .claude/ and docs/ that name tooling/ paths, the pre-push hook, and the skills. Pieces that become tickets: (1) make targets become THIN WRAPPERS over reach — make stays the door for zero-argument repo verbs, reach is the door for anything taking arguments; one implementation, and the door holding it is reach; note Bash(make *) is already blanket-allowed so this epic does not claim to make make cheaper; (2) the 10 hand-written Bash(tooling/...) entries in .claude/settings.json are deleted, replaced by the single Bash(reach *); (3) deprecation shims — each retired path leaves a stub that prints the new command and exits NON-ZERO, the failure contract applied to the migration itself, removed after a window rather than deleted under the callers; (4) sweep the 160 markdown callers, including .claude/rules/*.md and every skill that shells out to a tooling path; (5) the reach skill documenting usage AND how to add a domain or command; (6) the CLAUDE.md rule that no new tooling is written outside the CLI, so the tree cannot re-fragment; (7) delete the shims and confirm no tooling/ script path survives outside scripts/blender/. + +RESHAPED 2026-08-31 by decision. The description''s plan — 84 make targets become reach wrappers — is REPLACED. make and reach now split BY KIND, not by preference (D-263 amended): make keeps genuine build and test orchestration (cargo, Godot, the test gate, anything sequencing a build), and targets that are really tooling wrappers are RETIRED in favour of reach verbs rather than wrapped. Reason: a wrapper leaves two ways to invoke every tool, and then reach --help is no longer the answer to what tooling exists because the Makefile is still a competing index — two doors is the condition this initiative exists to end, so preserving both would defeat it while looking like caution. Where a target must survive for muscle memory it delegates to reach in one line and says so; the test is whether a reader can tell from the target alone which of the two owns the behaviour. ALSO CHANGED — retirement timing. Old scripts are now retired PER DOMAIN, the moment that domain''s port passes a parity test, rather than all at the end in one sweep here. So this epic shrinks: most retirement happens inside the per-domain port tickets under T-1250, and what remains here is the final sweep for anything not owned by a domain plus the Makefile split above. Rationale: a continuous shrink avoids months where every tool exists twice and edits can land in the dead copy.', 'The migration is not done when reach works; it is done when nothing calls the old paths. Surface: 84 Makefile targets, 160 markdown files under .claude/ and docs/ that name tooling/ paths, the pre-push hook, and the skills. Pieces that become tickets: (1) make targets become THIN WRAPPERS over reach — make stays the door for zero-argument repo verbs, reach is the door for anything taking arguments; one implementation, and the door holding it is reach; note Bash(make *) is already blanket-allowed so this epic does not claim to make make cheaper; (2) the 10 hand-written Bash(tooling/...) entries in .claude/settings.json are deleted, replaced by the single Bash(reach *); (3) deprecation shims — each retired path leaves a stub that prints the new command and exits NON-ZERO, the failure contract applied to the migration itself, removed after a window rather than deleted under the callers; (4) sweep the 160 markdown callers, including .claude/rules/*.md and every skill that shells out to a tooling path; (5) the reach skill documenting usage AND how to add a domain or command; (6) the CLAUDE.md rule that no new tooling is written outside the CLI, so the tree cannot re-fragment; (7) delete the shims and confirm no tooling/ script path survives outside scripts/blender/. + +RESHAPED 2026-08-31 by decision. The description''s plan — 84 make targets become reach wrappers — is REPLACED. make and reach now split BY KIND, not by preference (D-263 amended): make keeps genuine build and test orchestration (cargo, Godot, the test gate, anything sequencing a build), and targets that are really tooling wrappers are RETIRED in favour of reach verbs rather than wrapped. Reason: a wrapper leaves two ways to invoke every tool, and then reach --help is no longer the answer to what tooling exists because the Makefile is still a competing index — two doors is the condition this initiative exists to end, so preserving both would defeat it while looking like caution. Where a target must survive for muscle memory it delegates to reach in one line and says so; the test is whether a reader can tell from the target alone which of the two owns the behaviour. ALSO CHANGED — retirement timing. Old scripts are now retired PER DOMAIN, the moment that domain''s port passes a parity test, rather than all at the end in one sweep here. So this epic shrinks: most retirement happens inside the per-domain port tickets under T-1250, and what remains here is the final sweep for anything not owned by a domain plus the Makefile split above. Rationale: a continuous shrink avoids months where every tool exists twice and edits can land in the dead copy. + +DONE 2026-09-23. Per the 2026-08-31 reshape, per-domain retirement happened inside each E3 port; this epic owned the final sweep, done by measurement rather than grep-and-hope. A script scanned every tracked doc, rule, skill, agent, hook and source file for tooling/ paths that no longer exist (skipping historical records): 62 found. Every instructional one now names its reach verb — the atlas skill''s ~40 retired commands (with commit-and-sync''s new stage-by-default behaviour stated), the clerk agent, the Si and clerk briefings (also still on pre-governance decisions/ paths and the retired sqlite CLIs), the culture authoring guide, the 41 Blender payloads'' usage lines, and comments in server/client/wiki sources. The ticket-cli rule''s nonexistent `pql decisions read` was fixed along the way. What remains is deliberate provenance and dated docs. Makefile: every tooling target is either retired (per port) or a one-line delegate that says so (regen-db, check-star-map, star-map-data); what still runs tooling/ paths directly is the gate-test scripts, which the domain map keeps as scripts. .claude/settings.json holds no per-script tooling entries. Deprecation shims (item 3) were not needed: callers were updated in the same commits that retired each path, so there was no window to bridge. + +Two findings from the sweep: cargo_binary only built a MISSING binary, so Rust edits were stamped over stale generator output (fixed, commit e3daf561d); and a comment-only edit to a canvas-registered file still needs a version bump (0.4.14), which the gate enforced on a real push. + +Left for Jeroen: .claude/settings.local.json (untracked, machine-local) still has 11 allow rules for deleted tooling/atlas* and tooling/validate-* paths. Harmless, but dead.', NULL, '2026-09-23 18:21:46', '2026-09-23 18:21:46.968', '2026-09-23 18:21:46.968', NULL, 'aaa96526b6fdb871ced4d90fc431d447', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S40293BXWK39D9HAXHM8Q4', 'description', 'make test-tooling is the tooling half of the push gate and it runs five suites that ALL move during this initiative: planet-gen test_sim_determinism.py and test_oasis_ring_scaling.py (via VENV_PY), test_canvas_version_check.py, economy-db test_traits.py and test_atlas_idempotency.py, plus an import_economics --dry-run against the committed DB. Nobody owns rewiring them, so this epic does. Pieces that become tickets: (1) the five suites move with their domains and keep passing — they are the guard that the numerics did not shift under the relocation, so a green run before and after the move is the actual acceptance evidence; (2) VENV_PY disappears from the Makefile — the whole point of uv tool install is that there is no interpreter fork between a human shell and a hook, and every surviving VENV_PY is a place where that promise is not yet kept; (3) a home for service-level tests — the layering exists so services can be called directly without a CLI round trip, and if no test ever does that, the layering is unverified decoration; (4) the conformance test from E2 joins this gate rather than living alone; (5) the wall-clock budget assertion from E4 joins it too; (6) confirm the gate still FAILS on a broken tree — a test gate that only passes has never been tested. Watch: ruff config currently ignores E402, E702 and F841; the E402 debt is script-style sys.path.insert before imports and should largely vanish with a real package, so re-enable it here rather than carrying the ignore forever. + +AMENDED 2026-08-20 — item (5), the wall-clock budget assertion from E4, is WITHDRAWN along with the 104 ms timing-parity budget it enforced (D-263 amendment, T-1251). It is replaced by the import-graph assertion: after reach --help, no domain service module and no heavy dependency (numpy, scipy, PIL, pydantic) may appear in sys.modules. That test is not flaky on a loaded machine and it names the offending import rather than reporting that something got slower. + +FROM T-1261 (2026-08-30) — the negative-invocation check is yours, and it is currently unguarded. T-1261 verified by hand that nothing invokes the CLI through an interpreter or a path, but wrote no automated check, deliberately, so as not to take this ticket''s work. The exact command that passed: grep -rn for the patterns ''python -m tooling'', ''python3 -m tooling'' and ''.venv/bin/reach'' across Makefile, .config/hooks, .claude and docs — currently returns nothing. Automate that in the conformance suite. It matters most at T-1253, where 84 make targets become reach wrappers and one interpreter-prefixed invocation among them silently undoes the PATH guarantee. Also add PYTHON_VERSION drift detection while you are there: T-1261 found uv had installed the reach tool on CPython 3.11 while .venv and system python were 3.14, because uv picks the lowest interpreter satisfying requires-python. The Makefile now pins PYTHON_VERSION for both, but nothing detects the two environments diverging again — and that divergence is invisible until a version-specific bug appears in one and not the other.', 'make test-tooling is the tooling half of the push gate and it runs five suites that ALL move during this initiative: planet-gen test_sim_determinism.py and test_oasis_ring_scaling.py (via VENV_PY), test_canvas_version_check.py, economy-db test_traits.py and test_atlas_idempotency.py, plus an import_economics --dry-run against the committed DB. Nobody owns rewiring them, so this epic does. Pieces that become tickets: (1) the five suites move with their domains and keep passing — they are the guard that the numerics did not shift under the relocation, so a green run before and after the move is the actual acceptance evidence; (2) VENV_PY disappears from the Makefile — the whole point of uv tool install is that there is no interpreter fork between a human shell and a hook, and every surviving VENV_PY is a place where that promise is not yet kept; (3) a home for service-level tests — the layering exists so services can be called directly without a CLI round trip, and if no test ever does that, the layering is unverified decoration; (4) the conformance test from E2 joins this gate rather than living alone; (5) the wall-clock budget assertion from E4 joins it too; (6) confirm the gate still FAILS on a broken tree — a test gate that only passes has never been tested. Watch: ruff config currently ignores E402, E702 and F841; the E402 debt is script-style sys.path.insert before imports and should largely vanish with a real package, so re-enable it here rather than carrying the ignore forever. + +AMENDED 2026-08-20 — item (5), the wall-clock budget assertion from E4, is WITHDRAWN along with the 104 ms timing-parity budget it enforced (D-263 amendment, T-1251). It is replaced by the import-graph assertion: after reach --help, no domain service module and no heavy dependency (numpy, scipy, PIL, pydantic) may appear in sys.modules. That test is not flaky on a loaded machine and it names the offending import rather than reporting that something got slower. + +FROM T-1261 (2026-08-30) — the negative-invocation check is yours, and it is currently unguarded. T-1261 verified by hand that nothing invokes the CLI through an interpreter or a path, but wrote no automated check, deliberately, so as not to take this ticket''s work. The exact command that passed: grep -rn for the patterns ''python -m tooling'', ''python3 -m tooling'' and ''.venv/bin/reach'' across Makefile, .config/hooks, .claude and docs — currently returns nothing. Automate that in the conformance suite. It matters most at T-1253, where 84 make targets become reach wrappers and one interpreter-prefixed invocation among them silently undoes the PATH guarantee. Also add PYTHON_VERSION drift detection while you are there: T-1261 found uv had installed the reach tool on CPython 3.11 while .venv and system python were 3.14, because uv picks the lowest interpreter satisfying requires-python. The Makefile now pins PYTHON_VERSION for both, but nothing detects the two environments diverging again — and that divergence is invisible until a version-specific bug appears in one and not the other. + +DONE 2026-09-23. (1) All suites moved with their domains and pass: test_planet_* (from planet-gen) and test_ledger_* (from economy-db), plus new ones added by the ports — test_planet_router, test_assets (fake Gradio), test_character. (2) VENV_PY is KEPT, deliberately. The risk it stood for, an interpreter fork between hooks and tests, is now DETECTED instead: conformance invariant 9 fails when the installed reach and the .venv differ in Python minor version, or differ from the Makefile pin. Replacing VENV_PY would mean running tests through uv''s private tool-environment path, which couples the gate to uv internals for no added safety. (3) Service-level tests exist and call services without a CLI (test_environment, test_assets, test_character, test_ledger_traits, test_check). (4) Conformance is in the gate. (5) Replaced by the import-graph assertion in test_lazy_domains (see E4). (6) Every check added this session was mutation-proven. Negative-invocation automation, handed over from T-1261: conformance invariant 8. PYTHON_VERSION drift detection: invariant 9. Both mutation-proven. Ruff ignores retired in T-1274.', NULL, '2026-09-23 18:21:47', '2026-09-23 18:21:47.507', '2026-09-23 18:21:47.507', NULL, '6dc5b0a4ea4f0a799ecbcca3116f40f7', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3Z1TJHNK1A3YS4HGMBPE4', 'description', 'A consolidation with no standing rule re-fragments. The tree reached 123 files and four naming conventions precisely because there was never a rule against adding one more, so the documentation half of this initiative is not decoration — it is the part that makes the other six stick. Distinct from E6, which retires OLD paths; this establishes the NEW way as the only way. Pieces that become tickets: (1) the reach skill — usage and authoring (T-1254); (2) the CLAUDE.md standing rule that no tooling is developed outside the CLI (T-1255); (3) .claude/rules/project-structure.md — the tooling/ entry rewritten to describe the package layout rather than a file list; (4) the D-263 diagram at docs/diagrams/architecture/reach-cli.d2 — the door, the layers, the domains and the Blender carve-out; D-263 already cross-references this path, so until it exists the record names a file that is not there; (5) a short authoring-contract section wherever a reviewer will look — the invariants that make the layering real (no logic in routers, no Typer in services, every command decorated, pydantic off the gate path) are easy to state and easy to skip. Sequence AFTER E1 and E2 so the docs describe a CLI that exists. + +Amended 2026-08-20: the forward reference to docs/diagrams/architecture/reach-cli.d2 was REMOVED from D-263 rather than left pointing at a file that does not exist. So the diagram ticket now carries a second half — when the diagram lands, ADD the cross-reference back into D-263. Per .claude/rules/diagrams.md a diagram not referenced from its D-record is unreachable from the governance tree, which is the failure this avoids in the other direction.', 'A consolidation with no standing rule re-fragments. The tree reached 123 files and four naming conventions precisely because there was never a rule against adding one more, so the documentation half of this initiative is not decoration — it is the part that makes the other six stick. Distinct from E6, which retires OLD paths; this establishes the NEW way as the only way. Pieces that become tickets: (1) the reach skill — usage and authoring (T-1254); (2) the CLAUDE.md standing rule that no tooling is developed outside the CLI (T-1255); (3) .claude/rules/project-structure.md — the tooling/ entry rewritten to describe the package layout rather than a file list; (4) the D-263 diagram at docs/diagrams/architecture/reach-cli.d2 — the door, the layers, the domains and the Blender carve-out; D-263 already cross-references this path, so until it exists the record names a file that is not there; (5) a short authoring-contract section wherever a reviewer will look — the invariants that make the layering real (no logic in routers, no Typer in services, every command decorated, pydantic off the gate path) are easy to state and easy to skip. Sequence AFTER E1 and E2 so the docs describe a CLI that exists. + +Amended 2026-08-20: the forward reference to docs/diagrams/architecture/reach-cli.d2 was REMOVED from D-263 rather than left pointing at a file that does not exist. So the diagram ticket now carries a second half — when the diagram lands, ADD the cross-reference back into D-263. Per .claude/rules/diagrams.md a diagram not referenced from its D-record is unreachable from the governance tree, which is the failure this avoids in the other direction. + +DONE 2026-09-23. (1) Skill(reach) - T-1254. (2) CLAUDE.md standing rule - T-1255. (3) project-structure.md tooling entry rewritten to the package layout (T-1290 commit). (4) docs/diagrams/architecture/reach-cli.d2 + svg, and D-263 now links it. (5) The authoring contract lives in Skill(reach) and is enforced by test_conformance.py; the diagram legend repeats the enforced list.', NULL, '2026-09-23 18:21:49', '2026-09-23 18:21:49.344', '2026-09-23 18:21:49.344', NULL, '679aa232fe44533b50c71a5cbd048e62', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3G1D2828RX2F3ZFNWRJGC', 'status', 'backlog', 'done', NULL, '2026-09-23 18:21:50', '2026-09-23 18:21:50.147', '2026-09-23 18:21:50.147', NULL, 'a826eb0e2a4d9242ff9fe4879949e1b8', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3SJQKECYH3CKZ3Z215CMW', 'status', 'backlog', 'done', NULL, '2026-09-23 18:21:50', '2026-09-23 18:21:50.150', '2026-09-23 18:21:50.150', NULL, '36cf4bdd48cd20998076b9d92e79aced', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3RFJ739EWN4JDG8ZEX89W', 'status', 'backlog', 'done', NULL, '2026-09-23 18:21:50', '2026-09-23 18:21:50.150', '2026-09-23 18:21:50.150', NULL, '678c79d1de034dceb3b274cd73c8dd4d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3Z1TJHNK1A3YS4HGMBPE4', 'status', 'backlog', 'done', NULL, '2026-09-23 18:21:50', '2026-09-23 18:21:50.150', '2026-09-23 18:21:50.150', NULL, '85a01da1700927eda436b973a694674d', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S40293BXWK39D9HAXHM8Q4', 'status', 'backlog', 'done', NULL, '2026-09-23 18:21:50', '2026-09-23 18:21:50.151', '2026-09-23 18:21:50.151', NULL, 'b78dc7e74f61da7121076f20e217f76b', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S37Y1ARMV68RJT802Z3VPG', 'description', 'Consolidate the 123-file tooling/ tree into one installable Python package behind a single console command, reach. Resolves Q-124; Rust rejected as R-014 because the friction is packaging, not language. Shape per D-263: a routing-only main.py, every domain under tooling/domains// split router/service/schemas/helpers, a bounded core/ holding only what has no domain, shared logging and error handling attached as decorators, pydantic for data shapes but kept off the push-gate import path, installed via uv tool install so reach is a bare name on PATH. Deliberately parentless — this is phase-independent tooling, not cascade work. Six epics; each unfolds into tickets when refined. + +Amended 2026-08-20 during the planning session: EIGHT epics, not six. E7 (Adoption) was split out of E6 because retiring old paths and establishing the new way as the only way are different jobs with different failure modes. E8 (Test surface) was added because make test-tooling runs five suites that all move during E3/E5 and nothing else owned rewiring them. + +Amended again 2026-08-20: NINE epics. T-1264 was added after a session finding that the gates are milliseconds but the generators are minutes, and an agent Bash call gives up at two minutes sending nothing — so reach streams JSONL progress events and detached runs append to a tailable per-job log, with no daemon. Epic ids, since the E-prefixes in the titles are a reading aid and not handles: T-1248 the door, T-1249 decorators, T-1250 the domain move, T-1251 the gate family, T-1252 generators and numerics, T-1253 retiring old paths, T-1256 adoption, T-1257 the test surface, T-1264 streaming and jobs. Also amended this session: the 104 ms timing-parity budget was withdrawn in favour of output parity plus a 250 ms feels-instant ceiling, which in turn dropped the no-pydantic-in-check carve-out.', 'Consolidate the 123-file tooling/ tree into one installable Python package behind a single console command, reach. Resolves Q-124; Rust rejected as R-014 because the friction is packaging, not language. Shape per D-263: a routing-only main.py, every domain under tooling/domains// split router/service/schemas/helpers, a bounded core/ holding only what has no domain, shared logging and error handling attached as decorators, pydantic for data shapes but kept off the push-gate import path, installed via uv tool install so reach is a bare name on PATH. Deliberately parentless — this is phase-independent tooling, not cascade work. Six epics; each unfolds into tickets when refined. + +Amended 2026-08-20 during the planning session: EIGHT epics, not six. E7 (Adoption) was split out of E6 because retiring old paths and establishing the new way as the only way are different jobs with different failure modes. E8 (Test surface) was added because make test-tooling runs five suites that all move during E3/E5 and nothing else owned rewiring them. + +Amended again 2026-08-20: NINE epics. T-1264 was added after a session finding that the gates are milliseconds but the generators are minutes, and an agent Bash call gives up at two minutes sending nothing — so reach streams JSONL progress events and detached runs append to a tailable per-job log, with no daemon. Epic ids, since the E-prefixes in the titles are a reading aid and not handles: T-1248 the door, T-1249 decorators, T-1250 the domain move, T-1251 the gate family, T-1252 generators and numerics, T-1253 retiring old paths, T-1256 adoption, T-1257 the test surface, T-1264 streaming and jobs. Also amended this session: the 104 ms timing-parity budget was withdrawn in favour of output parity plus a 250 ms feels-instant ceiling, which in turn dropped the no-pydantic-in-check carve-out. + +DONE 2026-09-23. All eight epics closed (E1-E8). tooling/ is one package behind reach: 14 domains, 41 Blender payloads in the carve-out, provenance in tooling/archive/, nine conformance invariants in the push gate. Adoption: Skill(reach), the CLAUDE.md standing rule, and docs/diagrams/architecture/reach-cli.d2 linked from D-263. Follow-ups filed from what the ports found: T-1291 (reliefmap render drift), T-1292 (the destructive wiki renderer), T-1294 (frozen S-keyed star-map diagrams).', NULL, '2026-09-23 18:21:58', '2026-09-23 18:21:58.287', '2026-09-23 18:21:58.287', NULL, '1764f4e010e67cb55b49398a9ffb42af', 2) ON CONFLICT(hash) DO NOTHING; +INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S37Y1ARMV68RJT802Z3VPG', 'status', 'in_progress', 'done', NULL, '2026-09-23 18:21:58', '2026-09-23 18:21:58.799', '2026-09-23 18:21:58.799', NULL, '7f4b87f4397a8ea322f299199f144b59', 2) ON CONFLICT(hash) DO NOTHING; diff --git a/.pql/changelog/tickets/2026-09.sql b/.pql/changelog/tickets/2026-09.sql index 8db535a57..c6fdc26e4 100644 --- a/.pql/changelog/tickets/2026-09.sql +++ b/.pql/changelog/tickets/2026-09.sql @@ -790,3 +790,63 @@ DONE 2026-09-23, audited against the items rather than assumed. (1)+(2) the gate INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3F6SKZ6EQQ7TY0RKT2970', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E5 — Generators and numerics, without breaking the systems.db stamp', 'The riskiest epic, and the one with a hard sequencing constraint. tooling/generator_sources.py SHAs the concatenated bytes of the generator sources SORTED BY PATH, so renaming a file changes the stamp even when its content is byte-identical. The relocation must therefore land as ONE commit — registry paths updated, make regen-db run, stamp verified — never split across pushes, or the pre-push gate rejects an intermediate state that is in fact correct. Pieces that become tickets: (1) move import_economics.py and the economy_import package into domains/db/, update generator_sources.py in the same change, regen and verify the stamp; (2) the generate_brands Rust binary is a SUBROUTINE of the Python importer, not an independent generator — keep that relationship intact; (3) canvas_sources.py and schema_version.py move with their consumers, and check-canvas-version must keep resolving the registry; (4) planet-gen into domains/planet/ — import_heightmaps, import_province_boundaries, planet_simulation, scaffold_bodies, render_heightmap; these are one-time build imports baked into the committed DB, intentionally unstamped, and they lean on numpy/scipy/PIL; (5) garment-fit into domains/garment/ with its 23 blender_author_* files going to scripts/blender/ per the carve-out; (6) db/wiki_sync.py and the audio/image/trellis connectors, which resolve a venv explicitly today in tooling/db/common.py and should stop needing to; (7) test_sim_determinism and test_oasis_ring_scaling keep passing — they are the guard that the numerics did not move under us. NOTHING here is rewritten. Code is relocated and re-fronted; no numerical behaviour changes, which is precisely why Rust was rejected. DONE 2026-09-23 — delivered by the E3 ports, item by item. (1) import_economics + economy_import moved to domains/ledger/ (the map renamed db to ledger), generator_sources.py updated in the same commit, regen run, stamp verified: T-1289. (2) generate_brands kept as a subroutine of the importer, and the relationship is now tighter: cargo_binary rebuilds a stale binary, where before a Rust edit could be stamped over output from the old binary (fix commit e3daf561d). (3) schema_version.py moved with the ledger; canvas_sources.py stays a top-level registry module per the domain map, and check-canvas-version still resolves it. (4) planet-gen became domains/atlas/planet (T-1288); the one-time importers stayed unstamped. (5) garment-fit: 22 payloads to scripts/blender (T-1273) + make_logo to domains/character (T-1290). (6) wiki_sync + connectors to domains/wiki and domains/assets; ensure_venv is gone (T-1290). (7) test_sim_determinism and test_oasis_ring_scaling moved to tooling/test_planet_*.py and pass; globe renders were pixel-identical across the T-1274 cleanup. Nothing numeric changed: every byte-comparable generator output was checked identical before/after.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:16.844', '2026-09-23 18:16:37.328', NULL, 'c67cf493bc8411754fdd7d1e7871440f', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3G1D2828RX2F3ZFNWRJGC', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E6 — Retire the old paths: make targets, docs, skills, permissions', 'The migration is not done when reach works; it is done when nothing calls the old paths. Surface: 84 Makefile targets, 160 markdown files under .claude/ and docs/ that name tooling/ paths, the pre-push hook, and the skills. Pieces that become tickets: (1) make targets become THIN WRAPPERS over reach — make stays the door for zero-argument repo verbs, reach is the door for anything taking arguments; one implementation, and the door holding it is reach; note Bash(make *) is already blanket-allowed so this epic does not claim to make make cheaper; (2) the 10 hand-written Bash(tooling/...) entries in .claude/settings.json are deleted, replaced by the single Bash(reach *); (3) deprecation shims — each retired path leaves a stub that prints the new command and exits NON-ZERO, the failure contract applied to the migration itself, removed after a window rather than deleted under the callers; (4) sweep the 160 markdown callers, including .claude/rules/*.md and every skill that shells out to a tooling path; (5) the reach skill documenting usage AND how to add a domain or command; (6) the CLAUDE.md rule that no new tooling is written outside the CLI, so the tree cannot re-fragment; (7) delete the shims and confirm no tooling/ script path survives outside scripts/blender/. + +RESHAPED 2026-08-31 by decision. The description''s plan — 84 make targets become reach wrappers — is REPLACED. make and reach now split BY KIND, not by preference (D-263 amended): make keeps genuine build and test orchestration (cargo, Godot, the test gate, anything sequencing a build), and targets that are really tooling wrappers are RETIRED in favour of reach verbs rather than wrapped. Reason: a wrapper leaves two ways to invoke every tool, and then reach --help is no longer the answer to what tooling exists because the Makefile is still a competing index — two doors is the condition this initiative exists to end, so preserving both would defeat it while looking like caution. Where a target must survive for muscle memory it delegates to reach in one line and says so; the test is whether a reader can tell from the target alone which of the two owns the behaviour. ALSO CHANGED — retirement timing. Old scripts are now retired PER DOMAIN, the moment that domain''s port passes a parity test, rather than all at the end in one sweep here. So this epic shrinks: most retirement happens inside the per-domain port tickets under T-1250, and what remains here is the final sweep for anything not owned by a domain plus the Makefile split above. Rationale: a continuous shrink avoids months where every tool exists twice and edits can land in the dead copy. + +DONE 2026-09-23. Per the 2026-08-31 reshape, per-domain retirement happened inside each E3 port; this epic owned the final sweep, done by measurement rather than grep-and-hope. A script scanned every tracked doc, rule, skill, agent, hook and source file for tooling/ paths that no longer exist (skipping historical records): 62 found. Every instructional one now names its reach verb — the atlas skill''s ~40 retired commands (with commit-and-sync''s new stage-by-default behaviour stated), the clerk agent, the Si and clerk briefings (also still on pre-governance decisions/ paths and the retired sqlite CLIs), the culture authoring guide, the 41 Blender payloads'' usage lines, and comments in server/client/wiki sources. The ticket-cli rule''s nonexistent `pql decisions read` was fixed along the way. What remains is deliberate provenance and dated docs. Makefile: every tooling target is either retired (per port) or a one-line delegate that says so (regen-db, check-star-map, star-map-data); what still runs tooling/ paths directly is the gate-test scripts, which the domain map keeps as scripts. .claude/settings.json holds no per-script tooling entries. Deprecation shims (item 3) were not needed: callers were updated in the same commits that retired each path, so there was no window to bridge. + +Two findings from the sweep: cargo_binary only built a MISSING binary, so Rust edits were stamped over stale generator output (fixed, commit e3daf561d); and a comment-only edit to a canvas-registered file still needs a version bump (0.4.14), which the gate enforced on a real push. + +Left for Jeroen: .claude/settings.local.json (untracked, machine-local) still has 11 allow rules for deleted tooling/atlas* and tooling/validate-* paths. Harmless, but dead.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:23.657', '2026-09-23 18:21:46.968', NULL, '15bf8712b1302e2b96ddd3f4fac56636', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S40293BXWK39D9HAXHM8Q4', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E8 — The tooling test surface, rewired', 'make test-tooling is the tooling half of the push gate and it runs five suites that ALL move during this initiative: planet-gen test_sim_determinism.py and test_oasis_ring_scaling.py (via VENV_PY), test_canvas_version_check.py, economy-db test_traits.py and test_atlas_idempotency.py, plus an import_economics --dry-run against the committed DB. Nobody owns rewiring them, so this epic does. Pieces that become tickets: (1) the five suites move with their domains and keep passing — they are the guard that the numerics did not shift under the relocation, so a green run before and after the move is the actual acceptance evidence; (2) VENV_PY disappears from the Makefile — the whole point of uv tool install is that there is no interpreter fork between a human shell and a hook, and every surviving VENV_PY is a place where that promise is not yet kept; (3) a home for service-level tests — the layering exists so services can be called directly without a CLI round trip, and if no test ever does that, the layering is unverified decoration; (4) the conformance test from E2 joins this gate rather than living alone; (5) the wall-clock budget assertion from E4 joins it too; (6) confirm the gate still FAILS on a broken tree — a test gate that only passes has never been tested. Watch: ruff config currently ignores E402, E702 and F841; the E402 debt is script-style sys.path.insert before imports and should largely vanish with a real package, so re-enable it here rather than carrying the ignore forever. + +AMENDED 2026-08-20 — item (5), the wall-clock budget assertion from E4, is WITHDRAWN along with the 104 ms timing-parity budget it enforced (D-263 amendment, T-1251). It is replaced by the import-graph assertion: after reach --help, no domain service module and no heavy dependency (numpy, scipy, PIL, pydantic) may appear in sys.modules. That test is not flaky on a loaded machine and it names the offending import rather than reporting that something got slower. + +FROM T-1261 (2026-08-30) — the negative-invocation check is yours, and it is currently unguarded. T-1261 verified by hand that nothing invokes the CLI through an interpreter or a path, but wrote no automated check, deliberately, so as not to take this ticket''s work. The exact command that passed: grep -rn for the patterns ''python -m tooling'', ''python3 -m tooling'' and ''.venv/bin/reach'' across Makefile, .config/hooks, .claude and docs — currently returns nothing. Automate that in the conformance suite. It matters most at T-1253, where 84 make targets become reach wrappers and one interpreter-prefixed invocation among them silently undoes the PATH guarantee. Also add PYTHON_VERSION drift detection while you are there: T-1261 found uv had installed the reach tool on CPython 3.11 while .venv and system python were 3.14, because uv picks the lowest interpreter satisfying requires-python. The Makefile now pins PYTHON_VERSION for both, but nothing detects the two environments diverging again — and that divergence is invisible until a version-specific bug appears in one and not the other. + +DONE 2026-09-23. (1) All suites moved with their domains and pass: test_planet_* (from planet-gen) and test_ledger_* (from economy-db), plus new ones added by the ports — test_planet_router, test_assets (fake Gradio), test_character. (2) VENV_PY is KEPT, deliberately. The risk it stood for, an interpreter fork between hooks and tests, is now DETECTED instead: conformance invariant 9 fails when the installed reach and the .venv differ in Python minor version, or differ from the Makefile pin. Replacing VENV_PY would mean running tests through uv''s private tool-environment path, which couples the gate to uv internals for no added safety. (3) Service-level tests exist and call services without a CLI (test_environment, test_assets, test_character, test_ledger_traits, test_check). (4) Conformance is in the gate. (5) Replaced by the import-graph assertion in test_lazy_domains (see E4). (6) Every check added this session was mutation-proven. Negative-invocation automation, handed over from T-1261: conformance invariant 8. PYTHON_VERSION drift detection: invariant 9. Both mutation-proven. Ruff ignores retired in T-1274.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:26:34.952', '2026-09-23 18:21:47.507', NULL, '13fc5db9e1416780240d1acd61a7a3fa', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3Z1TJHNK1A3YS4HGMBPE4', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E7 — Adoption: the skill, the rule, and the docs that stop re-fragmentation', 'A consolidation with no standing rule re-fragments. The tree reached 123 files and four naming conventions precisely because there was never a rule against adding one more, so the documentation half of this initiative is not decoration — it is the part that makes the other six stick. Distinct from E6, which retires OLD paths; this establishes the NEW way as the only way. Pieces that become tickets: (1) the reach skill — usage and authoring (T-1254); (2) the CLAUDE.md standing rule that no tooling is developed outside the CLI (T-1255); (3) .claude/rules/project-structure.md — the tooling/ entry rewritten to describe the package layout rather than a file list; (4) the D-263 diagram at docs/diagrams/architecture/reach-cli.d2 — the door, the layers, the domains and the Blender carve-out; D-263 already cross-references this path, so until it exists the record names a file that is not there; (5) a short authoring-contract section wherever a reviewer will look — the invariants that make the layering real (no logic in routers, no Typer in services, every command decorated, pydantic off the gate path) are easy to state and easy to skip. Sequence AFTER E1 and E2 so the docs describe a CLI that exists. + +Amended 2026-08-20: the forward reference to docs/diagrams/architecture/reach-cli.d2 was REMOVED from D-263 rather than left pointing at a file that does not exist. So the diagram ticket now carries a second half — when the diagram lands, ADD the cross-reference back into D-263. Per .claude/rules/diagrams.md a diagram not referenced from its D-record is unreachable from the governance tree, which is the failure this avoids in the other direction. + +DONE 2026-09-23. (1) Skill(reach) - T-1254. (2) CLAUDE.md standing rule - T-1255. (3) project-structure.md tooling entry rewritten to the package layout (T-1290 commit). (4) docs/diagrams/architecture/reach-cli.d2 + svg, and D-263 now links it. (5) The authoring contract lives in Skill(reach) and is enforced by test_conformance.py; the diagram legend repeats the enforced list.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:26:26.644', '2026-09-23 18:21:49.344', NULL, '8b2ce4d3061609575bfc6bba99b14b44', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3G1D2828RX2F3ZFNWRJGC', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E6 — Retire the old paths: make targets, docs, skills, permissions', 'The migration is not done when reach works; it is done when nothing calls the old paths. Surface: 84 Makefile targets, 160 markdown files under .claude/ and docs/ that name tooling/ paths, the pre-push hook, and the skills. Pieces that become tickets: (1) make targets become THIN WRAPPERS over reach — make stays the door for zero-argument repo verbs, reach is the door for anything taking arguments; one implementation, and the door holding it is reach; note Bash(make *) is already blanket-allowed so this epic does not claim to make make cheaper; (2) the 10 hand-written Bash(tooling/...) entries in .claude/settings.json are deleted, replaced by the single Bash(reach *); (3) deprecation shims — each retired path leaves a stub that prints the new command and exits NON-ZERO, the failure contract applied to the migration itself, removed after a window rather than deleted under the callers; (4) sweep the 160 markdown callers, including .claude/rules/*.md and every skill that shells out to a tooling path; (5) the reach skill documenting usage AND how to add a domain or command; (6) the CLAUDE.md rule that no new tooling is written outside the CLI, so the tree cannot re-fragment; (7) delete the shims and confirm no tooling/ script path survives outside scripts/blender/. + +RESHAPED 2026-08-31 by decision. The description''s plan — 84 make targets become reach wrappers — is REPLACED. make and reach now split BY KIND, not by preference (D-263 amended): make keeps genuine build and test orchestration (cargo, Godot, the test gate, anything sequencing a build), and targets that are really tooling wrappers are RETIRED in favour of reach verbs rather than wrapped. Reason: a wrapper leaves two ways to invoke every tool, and then reach --help is no longer the answer to what tooling exists because the Makefile is still a competing index — two doors is the condition this initiative exists to end, so preserving both would defeat it while looking like caution. Where a target must survive for muscle memory it delegates to reach in one line and says so; the test is whether a reader can tell from the target alone which of the two owns the behaviour. ALSO CHANGED — retirement timing. Old scripts are now retired PER DOMAIN, the moment that domain''s port passes a parity test, rather than all at the end in one sweep here. So this epic shrinks: most retirement happens inside the per-domain port tickets under T-1250, and what remains here is the final sweep for anything not owned by a domain plus the Makefile split above. Rationale: a continuous shrink avoids months where every tool exists twice and edits can land in the dead copy. + +DONE 2026-09-23. Per the 2026-08-31 reshape, per-domain retirement happened inside each E3 port; this epic owned the final sweep, done by measurement rather than grep-and-hope. A script scanned every tracked doc, rule, skill, agent, hook and source file for tooling/ paths that no longer exist (skipping historical records): 62 found. Every instructional one now names its reach verb — the atlas skill''s ~40 retired commands (with commit-and-sync''s new stage-by-default behaviour stated), the clerk agent, the Si and clerk briefings (also still on pre-governance decisions/ paths and the retired sqlite CLIs), the culture authoring guide, the 41 Blender payloads'' usage lines, and comments in server/client/wiki sources. The ticket-cli rule''s nonexistent `pql decisions read` was fixed along the way. What remains is deliberate provenance and dated docs. Makefile: every tooling target is either retired (per port) or a one-line delegate that says so (regen-db, check-star-map, star-map-data); what still runs tooling/ paths directly is the gate-test scripts, which the domain map keeps as scripts. .claude/settings.json holds no per-script tooling entries. Deprecation shims (item 3) were not needed: callers were updated in the same commits that retired each path, so there was no window to bridge. + +Two findings from the sweep: cargo_binary only built a MISSING binary, so Rust edits were stamped over stale generator output (fixed, commit e3daf561d); and a comment-only edit to a canvas-registered file still needs a version bump (0.4.14), which the gate enforced on a real push. + +Left for Jeroen: .claude/settings.local.json (untracked, machine-local) still has 11 allow rules for deleted tooling/atlas* and tooling/validate-* paths. Harmless, but dead.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:23.657', '2026-09-23 18:21:50.142', NULL, 'b3a1b3a7871c343034a1bd637089c9f3', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3RFJ739EWN4JDG8ZEX89W', 'story', '06G1S3Z1TJHNK1A3YS4HGMBPE4', 'reach skill — usage, and how to add a domain or command', 'Project-scope skill at .claude/skills/reach/SKILL.md, user-invocable, covering two audiences. USAGE: the domain list, how to discover verbs (reach --help then reach --help), the flags that matter to agents and hooks (--no-input, --verbose), and the rule that a non-zero exit always carries the command that fixes it. AUTHORING — the part that keeps the tree from re-fragmenting: how to add a verb to an existing domain, how to add a whole domain, and the invariants from D-263 that a reviewer will check — router.py holds no logic, service.py holds no Typer, every command carries the composed @command decorator (logging plus error handling), all output goes through core/console.py, pydantic models live in domains//schemas.py and must not be reachable from main.py, and core/ holds only what has no domain. Include the failure-message contract with a worked example (raise ReachError with a fix= remedy) because that is the requirement most likely to be skipped under time pressure. Write this AFTER E1 and E2 land so the skill documents a CLI that exists rather than one that is planned.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:25:32.817', '2026-09-23 18:21:50.150', NULL, '1f6cf50b525b20b65fe69af59de6ba03', 2) ON CONFLICT(record_id) DO UPDATE SET type=excluded.type, parent_record_id=excluded.parent_record_id, title=excluded.title, description=excluded.description, status=excluded.status, priority=excluded.priority, assigned_to=excluded.assigned_to, team=excluded.team, decision_ref=excluded.decision_ref, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at >= tickets.updated_at; +INSERT INTO tickets (record_id, type, parent_record_id, title, description, status, priority, assigned_to, team, decision_ref, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S3SJQKECYH3CKZ3Z215CMW', 'story', '06G1S3Z1TJHNK1A3YS4HGMBPE4', 'CLAUDE.md rule — no new tooling outside the reach CLI', 'Add the standing rule to settled-reach/CLAUDE.md that all tooling is developed IN the reach CLI and nowhere else — no new loose scripts under tooling/, no new extensionless executables, no new python3 tooling/