Files
settled-reach/.pql/changelog/tickets/2026-09.sql
T
jpmschweitzerandClaude Opus 5 59d644cc49 Revert "chore(assets): regen character manifest — was stale since the wardrobe landed"
This reverts commit 04241989b. The premise was wrong: the manifest is not
stale, it is AHEAD of its generator.

Both versions list the same 24 garments. What the regen dropped is the
`style` and `logo_capable` fields that T-1089 added to the manifest schema
but never taught `generate-character-manifest` to emit. So the generator
faithfully reproduces a manifest that is missing half its schema, and
`test_garment_engine_t1089.gd` catches it — which is how this was found, on
the push gate rather than in review.

`make manifest` is therefore a trap for anyone who runs it: it silently
reverts the T-1089 extension. Filed as T-1287.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-02 17:55:37 +02:00

268 lines
70 KiB
SQL

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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-01 15:22:13.360', NULL, 'd8cc608ee1be87634289397ebae7fd61', 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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-01 15:22:27.856', NULL, '89c48e98147081cb5465e7373234ec9f', 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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.
DONE 2026-09-02. reach validate content / checklist / ron / name-collisions. All three old scripts retired, make targets retired, permission entry dropped.
PRINTS GO THROUGH THE LOGGING SINK, per Jeroen mid-ticket: "print statements go through the logging decorator" and "it already is a collector, the logging sink". My first pass added a per-module _lines list; that was redundant, because console IS the collector. The 48 sites in content.py and 24 in checklist.py now emit console events, message strings and order unchanged. Consequence beyond tidiness: a long content validation STREAMS as it runs rather than going quiet and dumping at the end, and every line carries the invocation''s job id.
validate-ron WAS THREE LANGUAGES DEEP — bash dispatching on a flag, a Python heredoc doing collision detection, cargo run for schema validation. Logic embedded in a shell string cannot be imported, tested, or found by anything that indexes Python; that is the clearest case yet for the rewrite decision. The heredoc became Python, the cargo call became a guarded exec through core/process.run with missing_fix naming `make setup-rust`.
It also SPLIT INTO TWO VERBS. --check-name-collisions answered a different question from the default path — whether the SET of cultures is coherent versus whether ONE file is well-formed — and the old script had to branch on the flag before doing anything. Two verbs, no branch.
THE MOVE BROKE SOMETHING, QUIETLY, which is the argument for doing these one domain at a time. validate-checklist computed ROOT as Path(__file__).resolve().parent.parent — the repo root while the file lived at tooling/validate-checklist, and tooling/domains once moved. Its schema and gauntlet paths silently repointed at nothing, the gauntlet directory "did not exist", find_checklists returned [], and it reported "skipped (no content yet)" with exit 0. Caught only by running it beside the original: old exit 1 (schema not found), new exit 0. Fixed to config.repo_root(). Also converted load_schema''s sys.exit(1) to a ReachError — a service must not end the process, and the caller now gets a remedy instead of a bare 1.
PARITY on the live tree: content reproduces the original byte for byte including its counts (0 validated, 7 skipped, 13 errors); name-collisions likewise (OK across 3 cultures). tooling/test_validate.py pins what those runs cannot reach — collision DETECTION (the repo currently has none), the comment-stripping rule, the empty and missing directory cases, and the two argument errors. Proven to fail by removing comment stripping, which tripped both the decoy assertion and the no-collision case.
TWO FINDINGS LEFT ALONE, deliberately:
- validate-content FAILS on the live tree: 13 MISSING SCHEMA errors under server/content/_schema/. Pre-existing, unrelated to this port, and it means `make pre-pr-validate` has been failing. Not fixed here because writing 13 JSON schemas is content work, not a port. Worth its own ticket.
- THIS TICKET''S DESCRIPTION IS WRONG about validate-content being in the pre-commit hook. That hook runs only check-fact-ids (ported in T-1281) and pql decisions validate. There was no shared hook edit to coordinate.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-02 10:50:19.803', NULL, '137939508f1a89fb45ebfa2fe7580a8e', 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 ('06G5GWDPQ95RSCKK51K8DTYRCM', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the validate domain — content, checklist, ron', 'validate-content, validate-checklist and validate-ron into domains/validate/. Standard port acceptance as defined on T-1281: transport-agnostic service, logic-free router, every command decorated, a parity test per verb against the old script, old scripts retired once parity passes, and that domain''s tooling make targets retired rather than wrapped. Note validate-content is wired into the PRE-COMMIT hook (via make validate-content) as well as being a make target, so the hook is part of this port''s blast radius, not just the Makefile — and a pre-commit failure is felt on every commit rather than every push, so getting the exit codes right matters more here than for a push-only gate. check-fact-ids is ALSO in the pre-commit hook but belongs to the check domain (T-1281); coordinate so the hook is edited once rather than twice.
DONE 2026-09-02. reach validate content / checklist / ron / name-collisions. All three old scripts retired, make targets retired, permission entry dropped.
PRINTS GO THROUGH THE LOGGING SINK, per Jeroen mid-ticket: "print statements go through the logging decorator" and "it already is a collector, the logging sink". My first pass added a per-module _lines list; that was redundant, because console IS the collector. The 48 sites in content.py and 24 in checklist.py now emit console events, message strings and order unchanged. Consequence beyond tidiness: a long content validation STREAMS as it runs rather than going quiet and dumping at the end, and every line carries the invocation''s job id.
validate-ron WAS THREE LANGUAGES DEEP — bash dispatching on a flag, a Python heredoc doing collision detection, cargo run for schema validation. Logic embedded in a shell string cannot be imported, tested, or found by anything that indexes Python; that is the clearest case yet for the rewrite decision. The heredoc became Python, the cargo call became a guarded exec through core/process.run with missing_fix naming `make setup-rust`.
It also SPLIT INTO TWO VERBS. --check-name-collisions answered a different question from the default path — whether the SET of cultures is coherent versus whether ONE file is well-formed — and the old script had to branch on the flag before doing anything. Two verbs, no branch.
THE MOVE BROKE SOMETHING, QUIETLY, which is the argument for doing these one domain at a time. validate-checklist computed ROOT as Path(__file__).resolve().parent.parent — the repo root while the file lived at tooling/validate-checklist, and tooling/domains once moved. Its schema and gauntlet paths silently repointed at nothing, the gauntlet directory "did not exist", find_checklists returned [], and it reported "skipped (no content yet)" with exit 0. Caught only by running it beside the original: old exit 1 (schema not found), new exit 0. Fixed to config.repo_root(). Also converted load_schema''s sys.exit(1) to a ReachError — a service must not end the process, and the caller now gets a remedy instead of a bare 1.
PARITY on the live tree: content reproduces the original byte for byte including its counts (0 validated, 7 skipped, 13 errors); name-collisions likewise (OK across 3 cultures). tooling/test_validate.py pins what those runs cannot reach — collision DETECTION (the repo currently has none), the comment-stripping rule, the empty and missing directory cases, and the two argument errors. Proven to fail by removing comment stripping, which tripped both the decoy assertion and the no-collision case.
TWO FINDINGS LEFT ALONE, deliberately:
- validate-content FAILS on the live tree: 13 MISSING SCHEMA errors under server/content/_schema/. Pre-existing, unrelated to this port, and it means `make pre-pr-validate` has been failing. Not fixed here because writing 13 JSON schemas is content work, not a port. Worth its own ticket.
- THIS TICKET''S DESCRIPTION IS WRONG about validate-content being in the pre-commit hook. That hook runs only check-fact-ids (ported in T-1281) and pql decisions validate. There was no shared hook edit to coordinate.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:42.490', '2026-09-02 10:50:19.824', NULL, 'e6eed1166d37970c10376ba6ad9b2270', 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 ('06G63FEA51J4JKE3A7473D3QZ0', 'task', '06FB0TNSRZXCHGS16BFHSSGSV4', 'Write the 13 missing content JSON schemas', 'reach validate content currently FAILS on the live tree with 13 MISSING SCHEMA errors: the campaigns tree references schemas that do not exist under server/content/_schema/ — system.schema.json among them. Found 2026-09-02 while porting the validator (T-1282); PRE-EXISTING and unrelated to that port, which reproduces the failure byte for byte because it reproduces the original''s behaviour. Consequence: make pre-pr-validate has been failing, so that chain has not been a working gate. Writing the schemas is content work, not tooling work, which is why it was not folded into the port. Run reach validate content for the current list.', 'backlog', 'medium', NULL, NULL, NULL, '2026-09-02 10:50:23.144', '2026-09-02 10:50:23.144', NULL, '64e70334d534198b9b07160eced5d909', 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 ('06G5GWEC9P1HNABW6S0BSNTZ2W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the godot and visual domains', 'Two small domains, filed together because each is two or three files and neither has surprises. godot: godot-parse-sweep and godot-cold-parse into domains/godot/. visual: visual-diff, visual-thumbnail and visual-blank-check into domains/visual/. Standard port acceptance as defined on T-1281. Worth knowing before starting: both domains shell out to external binaries — godot4 and image tooling respectively — so their services need a launcher, and core/process.py already has the spawn primitive from T-1277. Do NOT let each service grow its own subprocess handling; if what is needed is broader than what core/process.py offers, extend it there rather than twice locally. That is the difference between a shared substrate and two copies that drift. Also: these are the first ports whose commands could genuinely be slow (a parse sweep over the whole client), so they are the first real candidates for progress events via console.event — the streaming machinery exists and this is where it starts earning.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:48.013', '2026-09-02 10:53:39.240', NULL, '9a437c8d09c93b7edbf080000399952a', 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 ('06G5GWEC9P1HNABW6S0BSNTZ2W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the godot and visual domains', 'Two small domains, filed together because each is two or three files and neither has surprises. godot: godot-parse-sweep and godot-cold-parse into domains/godot/. visual: visual-diff, visual-thumbnail and visual-blank-check into domains/visual/. Standard port acceptance as defined on T-1281. Worth knowing before starting: both domains shell out to external binaries — godot4 and image tooling respectively — so their services need a launcher, and core/process.py already has the spawn primitive from T-1277. Do NOT let each service grow its own subprocess handling; if what is needed is broader than what core/process.py offers, extend it there rather than twice locally. That is the difference between a shared substrate and two copies that drift. Also: these are the first ports whose commands could genuinely be slow (a parse sweep over the whole client), so they are the first real candidates for progress events via console.event — the streaming machinery exists and this is where it starts earning.
DONE 2026-09-02. reach godot parse-sweep / cold-parse and reach visual diff / blank-check / thumbnail. Five old scripts retired, callers rewired.
GODOT — two bash scripts whose logic was grep pipelines encoding five separate hard-won lessons, each a comment nobody could test. Now Python filters with the reasons attached, and the engine invocation as a guarded exec. Verified on the real client: "godot-parse-sweep: clean — opened 229 scripts (0 unreadable dirs)".
Three not-ok states kept DISTINCT, because only one of them is a verdict about the code: engine_failed (godot crashed or is missing — reporting that as a parse failure blames the tree for a broken toolchain), did_not_run (the sweep emitted no completion marker, so it checked nothing — zero errors from a check that never ran reads as clean, the false-green this tool exists to close), and genuine parse errors.
The asymmetry between the two checks is preserved and documented: cold-parse filters "Cannot infer the type", the sweep does NOT. That suppression is why cold-parse stayed silent about a test helper that genuinely does not parse.
VISUAL — three Python scripts with argparse mains. argparse removed: a parser inside a service is a second transport layer, and the router declares the options now.
THE SAME ROOT BUG AS T-1282, in all three files. Every one computed Path(__file__).resolve().parent.parent — the repo root at tooling/, and tooling/domains/visual once moved, two levels too deep. Fixed to config.repo_root() during the move rather than after, having learned from validate-checklist that this fails SILENTLY: paths resolve to nothing, the work appears to have nothing to do, and the tool reports success. That is now three domains where a __file__-relative root would have shipped a false pass.
TWO BUGS MY OWN TRANSFORMATION INTRODUCED, both caught by running rather than reading:
- Multi-line print(..., file=sys.stderr) became console.event(..., file=sys.stderr). console.event puts unknown kwargs into the event payload, so a file OBJECT would have gone to json.dumps — a crash at the moment something was already being reported as an error. Removed, level="error" instead.
- The escaping in my replacement script wrote level=\"error\" into three files, which ruff caught as a syntax error.
Neither was visible by inspection of the diff; both surfaced on the first real run. Mechanical transformations need mechanical verification.
sys.exit REMOVED from diff.py and thumbnail.py — four sites. A service must not end the process; they raise ReachError with a remedy now.
CALLERS REWIRED, and this is the wider blast radius than the make targets: tests/run-visual invoked visual-blank-check, visual-thumbnail and visual-diff by path at four sites, and the pre-push hook invoked godot-parse-sweep. All now call reach --no-input. .claude/settings.json loses the godot-cold-parse entry; Bash(reach *) covers it.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:48.013', '2026-09-02 11:59:14.517', NULL, '9789ab592ea31d9474bca2166d1fb79e', 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 ('06G5GWEC9P1HNABW6S0BSNTZ2W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the godot and visual domains', 'Two small domains, filed together because each is two or three files and neither has surprises. godot: godot-parse-sweep and godot-cold-parse into domains/godot/. visual: visual-diff, visual-thumbnail and visual-blank-check into domains/visual/. Standard port acceptance as defined on T-1281. Worth knowing before starting: both domains shell out to external binaries — godot4 and image tooling respectively — so their services need a launcher, and core/process.py already has the spawn primitive from T-1277. Do NOT let each service grow its own subprocess handling; if what is needed is broader than what core/process.py offers, extend it there rather than twice locally. That is the difference between a shared substrate and two copies that drift. Also: these are the first ports whose commands could genuinely be slow (a parse sweep over the whole client), so they are the first real candidates for progress events via console.event — the streaming machinery exists and this is where it starts earning.
DONE 2026-09-02. reach godot parse-sweep / cold-parse and reach visual diff / blank-check / thumbnail. Five old scripts retired, callers rewired.
GODOT — two bash scripts whose logic was grep pipelines encoding five separate hard-won lessons, each a comment nobody could test. Now Python filters with the reasons attached, and the engine invocation as a guarded exec. Verified on the real client: "godot-parse-sweep: clean — opened 229 scripts (0 unreadable dirs)".
Three not-ok states kept DISTINCT, because only one of them is a verdict about the code: engine_failed (godot crashed or is missing — reporting that as a parse failure blames the tree for a broken toolchain), did_not_run (the sweep emitted no completion marker, so it checked nothing — zero errors from a check that never ran reads as clean, the false-green this tool exists to close), and genuine parse errors.
The asymmetry between the two checks is preserved and documented: cold-parse filters "Cannot infer the type", the sweep does NOT. That suppression is why cold-parse stayed silent about a test helper that genuinely does not parse.
VISUAL — three Python scripts with argparse mains. argparse removed: a parser inside a service is a second transport layer, and the router declares the options now.
THE SAME ROOT BUG AS T-1282, in all three files. Every one computed Path(__file__).resolve().parent.parent — the repo root at tooling/, and tooling/domains/visual once moved, two levels too deep. Fixed to config.repo_root() during the move rather than after, having learned from validate-checklist that this fails SILENTLY: paths resolve to nothing, the work appears to have nothing to do, and the tool reports success. That is now three domains where a __file__-relative root would have shipped a false pass.
TWO BUGS MY OWN TRANSFORMATION INTRODUCED, both caught by running rather than reading:
- Multi-line print(..., file=sys.stderr) became console.event(..., file=sys.stderr). console.event puts unknown kwargs into the event payload, so a file OBJECT would have gone to json.dumps — a crash at the moment something was already being reported as an error. Removed, level="error" instead.
- The escaping in my replacement script wrote level=\"error\" into three files, which ruff caught as a syntax error.
Neither was visible by inspection of the diff; both surfaced on the first real run. Mechanical transformations need mechanical verification.
sys.exit REMOVED from diff.py and thumbnail.py — four sites. A service must not end the process; they raise ReachError with a remedy now.
CALLERS REWIRED, and this is the wider blast radius than the make targets: tests/run-visual invoked visual-blank-check, visual-thumbnail and visual-diff by path at four sites, and the pre-push hook invoked godot-parse-sweep. All now call reach --no-input. .claude/settings.json loses the godot-cold-parse entry; Bash(reach *) covers it.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-31 15:30:48.013', '2026-09-02 11:59:14.536', NULL, 'a3a0b27a427faca6f2c47ae6a9a6f9a5', 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 ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:14:48.290', NULL, 'cf4a2315117474f165f1e689d6271300', 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 ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.
RESOLVED BEFORE PORTING, item (3): atlas-update-field is NOT the asset-pipeline violation it resembles. The rule in .claude/rules/asset-pipeline.md forbids raw SQL against systems.db because ''any changes you make are silently reverted by the next regen-db run''. That reasoning does not apply here. Checked: import_economics touches only currency_zone and gate_energy_connected ON star_systems — it does not recreate the table. The fields this script edits (star_type, spectral_class, proper_name, geographic_sector, habitable_planet_count, plus system_gates fields) originate from the ONE-TIME BAKED imports the same rule describes separately: ''the surviving planet-gen importers are one-time build imports baked into the committed DB — not part of make regen-db''. So these edits persist; there is no regen path to route them through, and update-field is the sanctioned way to amend baked atlas data rather than a bypass of a generator. PORT IT, and carry that distinction in the verb''s help so the next reader does not see raw SQL against systems.db and delete it on sight. The genuine residual concern is different and worth stating: a value amended this way lives only in a committed binary, so it cannot be regenerated and does not show in a diff. That is a property of the baked-import design, not of this verb, and belongs to whoever revisits that design.', 'backlog', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:15:55.644', NULL, '4bdad90f7acb1be68b6db444b921c7c3', 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 ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.
RESOLVED BEFORE PORTING, item (3): atlas-update-field is NOT the asset-pipeline violation it resembles. The rule in .claude/rules/asset-pipeline.md forbids raw SQL against systems.db because ''any changes you make are silently reverted by the next regen-db run''. That reasoning does not apply here. Checked: import_economics touches only currency_zone and gate_energy_connected ON star_systems — it does not recreate the table. The fields this script edits (star_type, spectral_class, proper_name, geographic_sector, habitable_planet_count, plus system_gates fields) originate from the ONE-TIME BAKED imports the same rule describes separately: ''the surviving planet-gen importers are one-time build imports baked into the committed DB — not part of make regen-db''. So these edits persist; there is no regen path to route them through, and update-field is the sanctioned way to amend baked atlas data rather than a bypass of a generator. PORT IT, and carry that distinction in the verb''s help so the next reader does not see raw SQL against systems.db and delete it on sight. The genuine residual concern is different and worth stating: a value amended this way lives only in a committed binary, so it cannot be regenerated and does not show in a diff. That is a property of the baked-import design, not of this verb, and belongs to whoever revisits that design.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:16:58.892', NULL, 'b1921817707b37ef0299b31b50309fd2', 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 ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.
RESOLVED BEFORE PORTING, item (3): atlas-update-field is NOT the asset-pipeline violation it resembles. The rule in .claude/rules/asset-pipeline.md forbids raw SQL against systems.db because ''any changes you make are silently reverted by the next regen-db run''. That reasoning does not apply here. Checked: import_economics touches only currency_zone and gate_energy_connected ON star_systems — it does not recreate the table. The fields this script edits (star_type, spectral_class, proper_name, geographic_sector, habitable_planet_count, plus system_gates fields) originate from the ONE-TIME BAKED imports the same rule describes separately: ''the surviving planet-gen importers are one-time build imports baked into the committed DB — not part of make regen-db''. So these edits persist; there is no regen path to route them through, and update-field is the sanctioned way to amend baked atlas data rather than a bypass of a generator. PORT IT, and carry that distinction in the verb''s help so the next reader does not see raw SQL against systems.db and delete it on sight. The genuine residual concern is different and worth stating: a value amended this way lives only in a committed binary, so it cannot be regenerated and does not show in a diff. That is a property of the baked-import design, not of this verb, and belongs to whoever revisits that design.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:17:04.589', NULL, 'bfaf47422a711699c9adcac79abb8f6b', 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 ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.
RESOLVED BEFORE PORTING, item (3): atlas-update-field is NOT the asset-pipeline violation it resembles. The rule in .claude/rules/asset-pipeline.md forbids raw SQL against systems.db because ''any changes you make are silently reverted by the next regen-db run''. That reasoning does not apply here. Checked: import_economics touches only currency_zone and gate_energy_connected ON star_systems — it does not recreate the table. The fields this script edits (star_type, spectral_class, proper_name, geographic_sector, habitable_planet_count, plus system_gates fields) originate from the ONE-TIME BAKED imports the same rule describes separately: ''the surviving planet-gen importers are one-time build imports baked into the committed DB — not part of make regen-db''. So these edits persist; there is no regen path to route them through, and update-field is the sanctioned way to amend baked atlas data rather than a bypass of a generator. PORT IT, and carry that distinction in the verb''s help so the next reader does not see raw SQL against systems.db and delete it on sight. The genuine residual concern is different and worth stating: a value amended this way lives only in a committed binary, so it cannot be regenerated and does not show in a diff. That is a property of the baked-import design, not of this verb, and belongs to whoever revisits that design.
DONE 2026-09-02. reach atlas db / names / systems-done / check / verify / commit-and-sync / update-field / flatness. Five bash scripts and three Python ones retired; make atlas-verify and the tooling/atlas permission entry gone.
PARITY: reach atlas verify reproduces the original exactly — 2 errors across 301 proposals, exit 1. names and systems-done both exercised against the live DB through the Rust binary.
THE BINARY HAS MORE VERBS THAN ITS WRAPPER DOCUMENTED. The bash usage text listed stats, show-system, list-bodies, populate. atlas-commit-and-sync calls four more the wrapper never mentioned: list-stations, wipe-system, commit-system, sync-wiki. All eight are declared in binary.KNOWN_VERBS so reach atlas --help is a complete index, AND unknown verbs are still forwarded — a hand-maintained list falls behind the binary it describes, so rejecting on it would break the day someone adds a Rust subcommand.
COMMIT-AND-SYNC NOW STAGES BY DEFAULT, committing only with --commit, per the ticket. The original always committed. Nothing else in reach writes to git history, and a tool that commits as a side effect of "sync" is a different risk class from one that writes a file. The default prints the message it WOULD use, so the decision stays where it was rather than being made silently.
Also fixed while there: the original hardcoded "Co-Authored-By: Claude Opus 4.6 (1M context)" into every atlas commit — a pinned model version, which .claude/skills/git-commit names as the root cause of attribution drift. The trailer is dropped; the harness appends its own.
And a real ordering improvement: the original verified the proposal but did not stop on failure — verify''s exit code was never checked, so a proposal that failed verification was still wiped, committed and synced. It now refuses, because bad data in systems.db is far harder to undo than a failed command.
UPDATE-FIELD ported with the asset-pipeline distinction in its help, per the pre-port resolution. The FIELD_TABLE map that lived inside a bash heredoc string — where nothing could check it against the schema — is a module-level dict now, and an unknown field raises unknown_choice, so it names the nine accepted fields instead of silently updating nothing. The original would have run an UPDATE against a table of None.
ALSO CAUGHT: update_field now checks rowcount and fails when no row matched. The original''s SQL would succeed silently against a system_id that does not exist — a no-op reported as success, which is the exact shape of failure this initiative keeps finding.
THE THREE PYTHON SCRIPTS moved with the usual treatment: prints to console events, argparse mains replaced by typed functions the router calls, __file__-relative roots to config.repo_root(). No root bug this time — checked before moving rather than after, which is the third domain running.', 'in_progress', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:36:36.194', NULL, 'a8b8b19662d8b3f6c458732b921a0746', 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 ('06G642RKW8PNW7AP9BY9203SPG', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port the atlas domain — the authoring and inspection verbs', 'The eight top-level atlas-* scripts become FLAT verbs on reach atlas: the Rust binary wrapper (stats, show-system, list-bodies, populate), atlas-check, atlas-verify, atlas-names, atlas-systems-done, atlas-update-field, atlas-commit-and-sync, atlas-flatness. Flat rather than nested because these act on the atlas as a whole; the nested groups are for GENERATION per rung (reach atlas map, reach atlas planet), which are separate tickets. Five of the eight are BASH, so those are rewrites: atlas 24, atlas-names 17, atlas-systems-done 9, atlas-update-field 59, atlas-commit-and-sync 60. Standard port acceptance as on T-1281. THREE THINGS NEEDING A DECISION OR CARE, each recorded rather than assumed: (1) the Rust binary — tooling/atlas builds server/target/debug/atlas if missing then execs it with all args, so its verbs live in Rust not in the shell; declare the four known verbs in the router so reach --help stays a complete index, and keep a passthrough for anything the binary gains later. (2) atlas-commit-and-sync MAKES GIT COMMITS, which is a risk class nothing else in reach has; port it to stage and report by default with committing behind an explicit flag, and say so in the verb help rather than changing behaviour silently. (3) atlas-update-field MUTATES systems.db directly, which .claude/rules/asset-pipeline.md forbids outside a migration — establish whether it predates that rule or is a sanctioned exception BEFORE porting, because the port must not launder a violation into a first-class verb.
RESOLVED BEFORE PORTING, item (3): atlas-update-field is NOT the asset-pipeline violation it resembles. The rule in .claude/rules/asset-pipeline.md forbids raw SQL against systems.db because ''any changes you make are silently reverted by the next regen-db run''. That reasoning does not apply here. Checked: import_economics touches only currency_zone and gate_energy_connected ON star_systems — it does not recreate the table. The fields this script edits (star_type, spectral_class, proper_name, geographic_sector, habitable_planet_count, plus system_gates fields) originate from the ONE-TIME BAKED imports the same rule describes separately: ''the surviving planet-gen importers are one-time build imports baked into the committed DB — not part of make regen-db''. So these edits persist; there is no regen path to route them through, and update-field is the sanctioned way to amend baked atlas data rather than a bypass of a generator. PORT IT, and carry that distinction in the verb''s help so the next reader does not see raw SQL against systems.db and delete it on sight. The genuine residual concern is different and worth stating: a value amended this way lives only in a committed binary, so it cannot be regenerated and does not show in a diff. That is a property of the baked-import design, not of this verb, and belongs to whoever revisits that design.
DONE 2026-09-02. reach atlas db / names / systems-done / check / verify / commit-and-sync / update-field / flatness. Five bash scripts and three Python ones retired; make atlas-verify and the tooling/atlas permission entry gone.
PARITY: reach atlas verify reproduces the original exactly — 2 errors across 301 proposals, exit 1. names and systems-done both exercised against the live DB through the Rust binary.
THE BINARY HAS MORE VERBS THAN ITS WRAPPER DOCUMENTED. The bash usage text listed stats, show-system, list-bodies, populate. atlas-commit-and-sync calls four more the wrapper never mentioned: list-stations, wipe-system, commit-system, sync-wiki. All eight are declared in binary.KNOWN_VERBS so reach atlas --help is a complete index, AND unknown verbs are still forwarded — a hand-maintained list falls behind the binary it describes, so rejecting on it would break the day someone adds a Rust subcommand.
COMMIT-AND-SYNC NOW STAGES BY DEFAULT, committing only with --commit, per the ticket. The original always committed. Nothing else in reach writes to git history, and a tool that commits as a side effect of "sync" is a different risk class from one that writes a file. The default prints the message it WOULD use, so the decision stays where it was rather than being made silently.
Also fixed while there: the original hardcoded "Co-Authored-By: Claude Opus 4.6 (1M context)" into every atlas commit — a pinned model version, which .claude/skills/git-commit names as the root cause of attribution drift. The trailer is dropped; the harness appends its own.
And a real ordering improvement: the original verified the proposal but did not stop on failure — verify''s exit code was never checked, so a proposal that failed verification was still wiped, committed and synced. It now refuses, because bad data in systems.db is far harder to undo than a failed command.
UPDATE-FIELD ported with the asset-pipeline distinction in its help, per the pre-port resolution. The FIELD_TABLE map that lived inside a bash heredoc string — where nothing could check it against the schema — is a module-level dict now, and an unknown field raises unknown_choice, so it names the nine accepted fields instead of silently updating nothing. The original would have run an UPDATE against a table of None.
ALSO CAUGHT: update_field now checks rowcount and fails when no row matched. The original''s SQL would succeed silently against a system_id that does not exist — a no-op reported as success, which is the exact shape of failure this initiative keeps finding.
THE THREE PYTHON SCRIPTS moved with the usual treatment: prints to console events, argparse mains replaced by typed functions the router calls, __file__-relative roots to config.repo_root(). No root bug this time — checked before moving rather than after, which is the third domain running.', 'done', 'high', NULL, NULL, 'D-263', '2026-09-02 12:14:48.290', '2026-09-02 12:36:36.208', NULL, '6eff7972f7304c334f347ae8598bdf6b', 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 ('06G64ASTXBBHMWMBQSVPM9MK1W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port generate, pr, and the rest of dev', 'Twelve small files, three domains, no ladder entanglement — deliberately batched to clear the tail before the two large ports (atlas planet, ledger). GENERATE: generate-brands (bash 23, wraps the Rust generate_brands binary), generate-corporations (bash 23), generate-character-manifest (py 111), generate_corp_stubs.py (py 413). PR: tea-comment (bash 41), pr-watchlist-diff (bash 37), pql-board-html (py). DEV, joining the existing selftest verb: install-rust (bash 33), install-godot (bash 108), worktree-setup (bash 49), perf-baseline (py), clerk-review (py). Standard port acceptance as on T-1281. SEVEN OF THE TWELVE ARE BASH so those are rewrites. THE THREE ENVIRONMENT SCRIPTS need the D-263 guarded-exec treatment specifically: install-godot downloads and unzips a pinned Godot build, install-rust drives rustup, worktree-setup manipulates git worktrees — port the DECISION logic (which version is wanted, is it already present, what does the tree look like) into a testable service and keep the irreducible external calls behind core/process.run. The test of a correct port is whether the decisions can be exercised WITHOUT performing them; a rewrite that cannot be tested has to be trusted instead, and trusting an installer is how a working environment becomes an unreproducible one. WATCH tea-comment: .claude/rules/tea-cli.md says tea prompts crash in Claude Code with no TTY and that tooling/tea-comment exists specifically to avoid subshells breaking permission matching — preserve both properties, and pass --no-input through.', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-09-02 12:49:55.434', '2026-09-02 12:49:55.434', NULL, '5676581f3cae08432c174d73b39a8cd5', 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 ('06G64ASTXBBHMWMBQSVPM9MK1W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port generate, pr, and the rest of dev', 'Twelve small files, three domains, no ladder entanglement — deliberately batched to clear the tail before the two large ports (atlas planet, ledger). GENERATE: generate-brands (bash 23, wraps the Rust generate_brands binary), generate-corporations (bash 23), generate-character-manifest (py 111), generate_corp_stubs.py (py 413). PR: tea-comment (bash 41), pr-watchlist-diff (bash 37), pql-board-html (py). DEV, joining the existing selftest verb: install-rust (bash 33), install-godot (bash 108), worktree-setup (bash 49), perf-baseline (py), clerk-review (py). Standard port acceptance as on T-1281. SEVEN OF THE TWELVE ARE BASH so those are rewrites. THE THREE ENVIRONMENT SCRIPTS need the D-263 guarded-exec treatment specifically: install-godot downloads and unzips a pinned Godot build, install-rust drives rustup, worktree-setup manipulates git worktrees — port the DECISION logic (which version is wanted, is it already present, what does the tree look like) into a testable service and keep the irreducible external calls behind core/process.run. The test of a correct port is whether the decisions can be exercised WITHOUT performing them; a rewrite that cannot be tested has to be trusted instead, and trusting an installer is how a working environment becomes an unreproducible one. WATCH tea-comment: .claude/rules/tea-cli.md says tea prompts crash in Claude Code with no TTY and that tooling/tea-comment exists specifically to avoid subshells breaking permission matching — preserve both properties, and pass --no-input through.
Ported. `generate`, `pr` and `dev` are registered; 12 scripts retired.
**generate** — `generate-brands` and `generate-corporations` were the *third*
and second copies of the same 24-line build-if-missing-then-exec bash that
`tooling/atlas` carried, so both collapsed into `core.process.cargo_binary`
rather than being ported. `generate-character-manifest` and
`generate_corp_stubs.py` moved as modules; both had `__file__`-relative roots
that break on move (the fourth and fifth instances of that trap this epic) and
now use `config.repo_root()`.
**pr** — `watchlist-diff` now derives its watched set from
`generator_sources.GENERATOR_SOURCES` instead of restating it, so it cannot
drift from the stamp check (185 registered paths + 5 non-stamped extras).
Verified with a positive control: silent on a commit touching only a test file,
reports `economy_import/atlas.py` + `import_economics.py` on 8da9670e0.
**dev** — the risky third. Split per the ticket''s own test: `godot_plan()` and
`worktree_plan()` are pure and decide *what would happen*; `install_godot()`,
`install_rust()` and `setup_worktree()` perform it. `tooling/test_environment.py`
pins the version pin, the `--version` and `GODOT_VERSION` precedence, the
already-current skip decision, the unsupported-platform refusal, and both
worktree refusals — none of them performed. Every assertion was mutation-checked.
## Found while porting
1. **`make regen-db` was about to break.** `import_economics` shells out to
`tooling/generate-brands`, which this ticket deleted. Rerouted through
`cargo_binary`; `generated_brands.toml` came back byte-identical, and the
stamp registry swapped the retired wrapper for `core/process.py`. Regen run,
stamp fresh.
2. **The clerk has been grepping a directory that does not exist.** `load_index()`
read `decisions/README.md`; the DQR tree moved to `governance/` long ago, so
every clerk agent got "(not found)" as its domain index. The prompt told it to
grep `decisions/*.md` too. Both fixed. (Clerk is disabled by default per #965,
so this cost nothing yet — but it would have been invisible on re-enable.)
3. **The conformance exec-check fired on the wrong receiver.** It matched any
`x.system()`, so `platform.system()` read as `os.system()`. Narrowed to an
`os` receiver and re-proved against a real `os.system` mutant.
4. **The committed character manifest was stale** — it predates the 24-garment
wardrobe. Confirmed the port is not the cause: the original script, run from
its own directory, produces the identical new output. Regenerated.
5. **`make setup` had a bootstrap-order problem** once `setup-rust`/`setup-godot`
became reach verbs — they ran before `install-reach`. `install-reach` needs
only `uv`, so it now leads the chain.
6. `process.run` gained `input=` and `timeout=` (the clerk needs both) plus a
`ProcessTimeout` subclass, so a killed run is distinguishable from a verdict.
The pre-push hook''s clerk capture no longer merges stderr into stdout — under
streaming the last merged line is a JSONL event, which would have read as an
unrecognised verdict and blocked the push.
Gate: `ruff check tooling/` clean, `make test-tooling` PASS (12 checks).', 'backlog', 'medium', NULL, NULL, 'D-263', '2026-09-02 12:49:55.434', '2026-09-02 14:54:35.577', NULL, 'c010e2f8bdc2cfe4843926351d87c6bb', 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 ('06G64ASTXBBHMWMBQSVPM9MK1W', 'task', '06G1S3D0M1TQW0GMFBBPQZG3ZM', 'Port generate, pr, and the rest of dev', 'Twelve small files, three domains, no ladder entanglement — deliberately batched to clear the tail before the two large ports (atlas planet, ledger). GENERATE: generate-brands (bash 23, wraps the Rust generate_brands binary), generate-corporations (bash 23), generate-character-manifest (py 111), generate_corp_stubs.py (py 413). PR: tea-comment (bash 41), pr-watchlist-diff (bash 37), pql-board-html (py). DEV, joining the existing selftest verb: install-rust (bash 33), install-godot (bash 108), worktree-setup (bash 49), perf-baseline (py), clerk-review (py). Standard port acceptance as on T-1281. SEVEN OF THE TWELVE ARE BASH so those are rewrites. THE THREE ENVIRONMENT SCRIPTS need the D-263 guarded-exec treatment specifically: install-godot downloads and unzips a pinned Godot build, install-rust drives rustup, worktree-setup manipulates git worktrees — port the DECISION logic (which version is wanted, is it already present, what does the tree look like) into a testable service and keep the irreducible external calls behind core/process.run. The test of a correct port is whether the decisions can be exercised WITHOUT performing them; a rewrite that cannot be tested has to be trusted instead, and trusting an installer is how a working environment becomes an unreproducible one. WATCH tea-comment: .claude/rules/tea-cli.md says tea prompts crash in Claude Code with no TTY and that tooling/tea-comment exists specifically to avoid subshells breaking permission matching — preserve both properties, and pass --no-input through.
Ported. `generate`, `pr` and `dev` are registered; 12 scripts retired.
**generate** — `generate-brands` and `generate-corporations` were the *third*
and second copies of the same 24-line build-if-missing-then-exec bash that
`tooling/atlas` carried, so both collapsed into `core.process.cargo_binary`
rather than being ported. `generate-character-manifest` and
`generate_corp_stubs.py` moved as modules; both had `__file__`-relative roots
that break on move (the fourth and fifth instances of that trap this epic) and
now use `config.repo_root()`.
**pr** — `watchlist-diff` now derives its watched set from
`generator_sources.GENERATOR_SOURCES` instead of restating it, so it cannot
drift from the stamp check (185 registered paths + 5 non-stamped extras).
Verified with a positive control: silent on a commit touching only a test file,
reports `economy_import/atlas.py` + `import_economics.py` on 8da9670e0.
**dev** — the risky third. Split per the ticket''s own test: `godot_plan()` and
`worktree_plan()` are pure and decide *what would happen*; `install_godot()`,
`install_rust()` and `setup_worktree()` perform it. `tooling/test_environment.py`
pins the version pin, the `--version` and `GODOT_VERSION` precedence, the
already-current skip decision, the unsupported-platform refusal, and both
worktree refusals — none of them performed. Every assertion was mutation-checked.
## Found while porting
1. **`make regen-db` was about to break.** `import_economics` shells out to
`tooling/generate-brands`, which this ticket deleted. Rerouted through
`cargo_binary`; `generated_brands.toml` came back byte-identical, and the
stamp registry swapped the retired wrapper for `core/process.py`. Regen run,
stamp fresh.
2. **The clerk has been grepping a directory that does not exist.** `load_index()`
read `decisions/README.md`; the DQR tree moved to `governance/` long ago, so
every clerk agent got "(not found)" as its domain index. The prompt told it to
grep `decisions/*.md` too. Both fixed. (Clerk is disabled by default per #965,
so this cost nothing yet — but it would have been invisible on re-enable.)
3. **The conformance exec-check fired on the wrong receiver.** It matched any
`x.system()`, so `platform.system()` read as `os.system()`. Narrowed to an
`os` receiver and re-proved against a real `os.system` mutant.
4. **The committed character manifest was stale** — it predates the 24-garment
wardrobe. Confirmed the port is not the cause: the original script, run from
its own directory, produces the identical new output. Regenerated.
5. **`make setup` had a bootstrap-order problem** once `setup-rust`/`setup-godot`
became reach verbs — they ran before `install-reach`. `install-reach` needs
only `uv`, so it now leads the chain.
6. `process.run` gained `input=` and `timeout=` (the clerk needs both) plus a
`ProcessTimeout` subclass, so a killed run is distinguishable from a verdict.
The pre-push hook''s clerk capture no longer merges stderr into stdout — under
streaming the last merged line is a JSONL event, which would have read as an
unrecognised verdict and blocked the push.
Gate: `ruff check tooling/` clean, `make test-tooling` PASS (12 checks).', 'done', 'medium', NULL, NULL, 'D-263', '2026-09-02 12:49:55.434', '2026-09-02 14:54:37.762', NULL, '678c0d0578c2255438d2e51829d87a64', 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 ('06G65N2W393K7A4B8JA8FGP22R', 'bug', '06FBPPMZNNEV052DBYYY3A897C', 'make manifest silently drops the T-1089 garment schema fields', NULL, 'backlog', 'medium', NULL, 'client', NULL, '2026-09-02 15:54:39.514', '2026-09-02 15:54:39.514', NULL, 'ddb9c65aa4c4537924de591c4c017051', 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 ('06G65N2W393K7A4B8JA8FGP22R', 'bug', '06FBPPMZNNEV052DBYYY3A897C', 'make manifest silently drops the T-1089 garment schema fields', '`generate-character-manifest` (now `reach generate character-manifest`) emits
only `{"slot": ...}` per clothing entry. The committed
`client/assets/characters/manifest.json` also carries `style` and
`logo_capable`, added to the schema by T-1089 — the generator was never taught
to produce them.
So the artifact is AHEAD of its generator, and `make manifest` silently reverts
the T-1089 extension. Confirmed on 2026-09-02: a regen produced the same 24
garments with those two fields dropped, and `test_garment_engine_t1089.gd >
test_manifest_has_tshirt_with_style_and_logo_flag` failed on the push gate.
Reverted in 6024045fc.
This is the exact failure mode `.claude/rules/asset-pipeline.md` exists to
prevent, one directory over: an artifact that cannot be reproduced from its
source. The manifest is not covered by that rule (no meta stamp, not
systems.db), which is why it drifted unnoticed.
WHAT TO DECIDE FIRST — where do `style` and `logo_capable` come from? They are
not derivable from a filename. Either:
(a) a sidecar the generator reads (e.g. the clothing `coverage.json` already
beside the assets, or per-garment metadata), or
(b) the manifest is genuinely hand-maintained, in which case the generator
should MERGE rather than overwrite, and say so.
(a) is the asset-pipeline-consistent answer. (b) needs the generator to stop
claiming to be the source of truth.
ALSO: `test_garment_engine_t1089.gd:108` reports `Expecting ''modern'' but was
''modern''` — the strings print identically, so the assertion is comparing
something other than what it displays (String vs StringName, or a trailing
character). Worth a look while in there; a failure message that shows two
identical values costs whoever hits it real time.
Acceptance: `reach generate character-manifest` on a clean tree leaves
manifest.json byte-identical, and the T-1089 test still passes after it runs.', 'backlog', 'medium', NULL, 'client', NULL, '2026-09-02 15:54:39.514', '2026-09-02 15:54:41.911', NULL, '2f6393f44525977bfeba1200e8f79561', 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;