|
|
|
@@ -1202,3 +1202,25 @@ Start here, not with Rust. Measured state: pyproject.toml already exists and mak
|
|
|
|
|
The real deliverable is ''one bare command reliably on PATH'', not ''a CLI framework''. A [project.scripts] entrypoint lands in .venv/bin/, which is on PATH only when activated — and agents and git hooks never activate it. That is the same split VENV_PY papers over in the Makefile, and the same failure recorded for tea (absolute path breaks the Bash(tea *) rule and prompts every time). Install via uv tool/pipx into ~/.local/bin, or symlink. A Typer app behind an absolute venv path solves nothing.
|
|
|
|
|
|
|
|
|
|
Watch startup cost: a single entrypoint importing every subcommand eagerly pays all 123 modules on every invocation, four times per push. Lazy subcommand registration is mandatory, not an optimisation — measure before and after.', 'backlog', 'low', NULL, 'tooling', 'Q-124', '2026-08-19 23:54:55.776', '2026-08-20 00:00:11.592', NULL, 'b48a1523bdfbddb038c083b365414e9f', 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 ('06G1S37Y1ARMV68RJT802Z3VPG', 'initiative', NULL, 'reach — one CLI for all repo tooling (D-263)', '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/<name>/ 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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:17.258', '2026-08-20 00:23:17.258', NULL, 'bac7a14e2e36595a5348ea47f6c5dea8', 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 ('06G1S392DEG41MRGEFSYWJWP2W', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E1 — The door: reach on PATH, routing-only main.py, lazy domains', 'The walking skeleton, and the epic every other one depends on. Deliverable: typing reach in any shell — interactive, agent, or git hook — prints the domain list, and one domain is ported end to end through router/service as the reference every later port copies. Pieces that become tickets: (1) package skeleton — tooling/__init__.py, main.py, core/, domains/, setuptools discovery configured to exclude the econ-sim Rust crate; (2) pyproject [project.scripts] reach = tooling.main:app, plus typer and pydantic added to dependencies; (3) the PATH guarantee — uv tool install --editable, verified from a NON-INTERACTIVE context (git hook, agent Bash) and not just a login shell, since that split is the whole point; (4) lazy router registration — main.py must list domains in --help WITHOUT importing their services, which is the mechanism the 104 ms gate budget depends on; (5) help at every level: reach --help enumerates domains, reach <domain> --help enumerates verbs; (6) one domain ported end to end as the reference implementation; (7) Bash(reach *) added to .claude/settings.json. Done when: reach runs from a git hook with no venv activation, --help answers what tooling exists without an ls, and the reference domain has zero logic in its router.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-08-20 00:23:26.571', '2026-08-20 00:23:26.571', NULL, '1d9add2650ae88a9b96bd96df97577ab', 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 ('06G1S39T92PW9JNBPWD0F4JFR0', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E2 — Decorators: shared logging, robust error handling, failures that teach', 'The requirement with the real teeth: every non-zero exit prints the command that would fix it, and still exits non-zero. Cross-cutting concerns are decorators, never call-site discipline. Pieces that become tickets: (1) core/errors.py — ReachError(message, fix=...) plus the @handle_errors decorator that renders message and remedy to stderr and PRESERVES the exit code; unrecognised exceptions still exit non-zero with the traceback behind --verbose; (2) core/logging.py — the shared logger plus @logged, one structured line per invocation (command, args, duration, outcome) to STDERR never stdout, quiet by default so hooks are not spammed; (3) the composed @command decorator so no command can carry one and not the other; (4) core/console.py as the single output path; (5) closed-set enumeration — whenever the accepted set is known, print it, which is the specific gap measured in pql (invalid status without naming the six valid ones); (6) --no-input plus TTY gating, hooks pass it unconditionally, because tea prompts crash in Claude Code with no TTY; (7) the conformance test: every registered command has help at its own level, every command carries @command, no typer/click outside main.py and router.py, no bare print outside core/console.py, no pydantic reachable from main.py. Done when the contract is a test, not a style guide.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-08-20 00:23:32.680', '2026-08-20 00:23:32.680', NULL, '4e7715d95d89a3030c6e82c5dce8b5cc', 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 ('06G1S3D0M1TQW0GMFBBPQZG3ZM', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E3 — domains/: move the 123-file tree into the layered package', 'The mechanical bulk. Move every Python file into tooling/domains/<name>/ split router/service/schemas/helpers, normalising names on the way in. The split is DISCOVERED not invented — the domains are already encoded as filename prefixes (blender x14, atlas x8, generate x7, check x7, visual/validate/test x3, godot/garment/pql/install x2), which is the main evidence this is safe to do mechanically. Pieces that become tickets, roughly one per domain: atlas, check, validate, generate, visual, godot, wiki, db, dev. Cross-cutting pieces: (1) naming normalisation — modules snake_case, CLI verbs kebab-case, replacing the four conventions currently coexisting (assign-astro-ids.py, canvas_sources.py, check-canvas-version, blender_ prefix-as-namespace); (2) the four hyphenated directories planet-gen, economy-db, garment-fit, pql-migrate are not importable and must be renamed; (3) tooling/econ-sim is a Rust crate — excluded from package discovery, not moved; (4) the Blender carve-out — tooling/blender stays a bash wrapper and the 35 blender_*.py payload files move to tooling/scripts/blender/, executed under Blender bundled Python and never imported, with reach fronting them; (5) ruff clean under the existing config, and the E402/E702/F841 ignores revisited since the script-style sys.path.insert that forced E402 disappears with the package. Watch for: a domain that only has a router and a service does not need empty schemas.py and dependencies.py — the layering is a vocabulary, not a quota.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-20 00:23:58.880', NULL, '60b3389274f65fb923e5444ffaf631e1', 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 ('06G1S3DTRHSBYJS55NDT9YR4R0', 'epic', '06G1S37Y1ARMV68RJT802Z3VPG', 'E4 — The gate family and the push hook, inside the 104 ms budget', 'The highest-frequency, highest-friction surface, and the one with a hard number attached. Baseline measured 2026-08-20: check-client-version 20 ms, check-dataflow-graph 38 ms, check-canvas-version 46 ms — about 104 ms for the three unconditional checks; check-systems-db-stamp runs only when systems.db is in the push. The ported gates must not exceed that. Pieces that become tickets: (1) port the four checks into domains/check/ — client-version, canvas-version, dataflow-graph, systems-db-stamp; (2) port validate-content, validate-checklist, validate-ron, check-fact-ids into domains/validate/; (3) rewire .config/hooks/pre-push to call reach, keeping the fail_check by-name reporting that hook deliberately has; (4) prove each gate still FAILS — a check that explains itself and exits 0 silently disables its own gate, which is exactly the clide failure recorded in D-263; test each one against a deliberately broken tree, not just a clean one; (5) the wall-clock assertion that turns the budget into a test; (6) confirm domains/check/ imports no pydantic — 87 ms against a 20 ms check is a 3.5x regression bought for nothing, four times per push. Depends on E1 (lazy registration) and E2 (the error contract) being real first.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-08-20 00:24:05.572', '2026-08-20 00:24:05.572', NULL, 'ff8b3ac61202a3e1facc4d723bd3d6e0', 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 ('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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:16.844', '2026-08-20 00:24:16.844', NULL, '796fd7edeb556309549bbff444046d1c', 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/.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:24:23.657', '2026-08-20 00:24:23.657', NULL, '46e71123daa96f64882085b936674e92', 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', '06G1S3G1D2828RX2F3ZFNWRJGC', '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 <domain> --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/<d>/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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:25:32.817', '2026-08-20 00:25:32.817', NULL, 'f65a35978c2117e381ce77fa4afc1394', 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', '06G1S3G1D2828RX2F3ZFNWRJGC', '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/<script>.py entrypoints. A new tool is a verb on an existing domain, or a new domain under tooling/domains/. Rationale to state in one line: the tree fragmented into 123 files and four naming conventions precisely because there was never a rule against adding one more, and a consolidation with no standing rule re-fragments. Name the two legitimate exceptions so they are not litigated later: (1) the Blender payload scripts under tooling/scripts/blender/, which run under Blender bundled interpreter and physically cannot import the package; (2) tooling/econ-sim, a Rust crate. Also update .claude/rules/project-structure.md tooling/ entry to describe the package layout. NOTE ON WORDING — Jeroen phrased this 2026-08-20 as all tooling should not be developed in that new cli; read as no tooling developed OUTSIDE it, since the opposite reading contradicts D-263 entirely. Confirm before writing the rule.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:25:41.820', '2026-08-20 00:25:41.820', NULL, '32d39b03d15628da8f810ec710b28f37', 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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:26:26.644', '2026-08-20 00:26:26.644', NULL, '26d362ab165b2fc7868a977a214875c7', 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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:26:34.952', '2026-08-20 00:26:34.952', NULL, '46c63386f34fbb52f8abf946afa854bd', 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 <domain> --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/<d>/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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:25:32.817', '2026-08-20 00:26:44.635', NULL, '9435f993de52370214847529ddb30bdf', 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/<script>.py entrypoints. A new tool is a verb on an existing domain, or a new domain under tooling/domains/. Rationale to state in one line: the tree fragmented into 123 files and four naming conventions precisely because there was never a rule against adding one more, and a consolidation with no standing rule re-fragments. Name the two legitimate exceptions so they are not litigated later: (1) the Blender payload scripts under tooling/scripts/blender/, which run under Blender bundled interpreter and physically cannot import the package; (2) tooling/econ-sim, a Rust crate. Also update .claude/rules/project-structure.md tooling/ entry to describe the package layout. NOTE ON WORDING — Jeroen phrased this 2026-08-20 as all tooling should not be developed in that new cli; read as no tooling developed OUTSIDE it, since the opposite reading contradicts D-263 entirely. Confirm before writing the rule.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:25:41.820', '2026-08-20 00:26:44.768', NULL, '784a21074f9cb5f4a89094cd715455c9', 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 ('06G1S37Y1ARMV68RJT802Z3VPG', 'initiative', NULL, 'reach — one CLI for all repo tooling (D-263)', '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/<name>/ 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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:17.258', '2026-08-20 00:26:54.553', NULL, 'd8a1d79223020984e9a9d65d4bf0ed04', 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/<script>.py entrypoints. A new tool is a verb on an existing domain, or a new domain under tooling/domains/. Rationale to state in one line: the tree fragmented into 123 files and four naming conventions precisely because there was never a rule against adding one more, and a consolidation with no standing rule re-fragments. Name the two legitimate exceptions so they are not litigated later: (1) the Blender payload scripts under tooling/scripts/blender/, which run under Blender bundled interpreter and physically cannot import the package; (2) tooling/econ-sim, a Rust crate. Also update .claude/rules/project-structure.md tooling/ entry to describe the package layout. NOTE ON WORDING — Jeroen phrased this 2026-08-20 as all tooling should not be developed in that new cli; read as no tooling developed OUTSIDE it, since the opposite reading contradicts D-263 entirely. Confirm before writing the rule.
|
|
|
|
|
|
|
|
|
|
CONFIRMED 2026-08-20 by Jeroen: your read is correct for the tooling. The rule is NO TOOLING DEVELOPED OUTSIDE THE CLI. No further confirmation needed before writing it.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:25:41.820', '2026-08-20 00:28:43.014', NULL, '11bef4568939ff71fdf9f4e9f5b6e6fb', 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.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:26:26.644', '2026-08-20 00:28:49.216', NULL, '06d68cc8318313dd746a0f55e5834dc0', 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;
|
|
|
|
|