docs(governance): D-263 — the primary user is an agent, and that changes things

Stated plainly because the record was quietly assuming otherwise: Jeroen runs
make and plays the game; the caller typing reach all day is Claude.

It resolves several arguments in the opposite direction from human-CLI
instinct. --help is a discovery mechanism rather than documentation, since it
is how the tool gets relearned from nothing every session — which makes the
domain list and closed-set enumeration load-bearing rather than polish. Output
volume is a context cost, so quiet-by-default is right for a better reason than
not spamming a hook. Latency matters less than legibility: nobody drums their
fingers at 300 ms, but a multi-minute silence is expensive because a wedge is
indistinguishable from work. And errors that name the next command are the
highest-value requirement here, because the reader is usually deciding what to
run next — "no" costs a whole exploratory turn.

One correction follows directly. D-263 had scoped streaming to "callers with no
escape — a human terminal, a Makefile, a git hook", reasoning that Claude
Code's background mode already solved the timeout for agents. That got the
audience backwards. Background mode solves the timeout and nothing else: it
returns when the process exits, so a nine-minute wedge still looks exactly like
nine minutes of work. Streaming is what makes a long run legible while it runs,
and reattach is worth most to the caller whose attention is not continuous.
Both are primary-user features, not fallbacks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-31 15:41:29 +02:00
co-authored by Claude Opus 5
parent 6b31111cd2
commit 91e25a3e7a
3 changed files with 60 additions and 1 deletions
+33
View File
@@ -1836,3 +1836,36 @@ CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per dom
REFINED 2026-08-31 four cross-cutting tasks filed; the per-domain port tickets are DELIBERATELY NOT filed yet. T-1271 the domain map (blocking design every file assigned before any file moves), T-1272 rename the five hyphenated Python directories, T-1273 the Blender carve-out, T-1274 retire the ruff ignores the package makes unnecessary. T-1272 and T-1273 are blocked on T-1271; T-1274 runs LAST, since re-enabling E402 mid-move means fixing violations in files that are about to move again. The per-domain tickets get filed from the map rather than from the epic''s proposed list, because that list is missing at least one domain (PR/workflow tea-comment, pr-watchlist-diff) and leaves 28 singleton-prefix files unassigned. Filing nine tickets now and renegotiating their boundaries mid-move is the failure this ordering avoids.
SEQUENCING SET 2026-08-31 by decision. (1) T-1264 (streaming, as decorators) lands BEFORE the per-domain ports, so every ported command arrives already streaming. (2) Old scripts retire PER DOMAIN, the moment that domain''s port passes a parity test not in one sweep at T-1253. So each per-domain port ticket owns its own retirement, and the tree shrinks continuously rather than existing twice for months while edits risk landing in the dead copy. (3) Domain names as mapped are confirmed no renames. (4) Per D-263''s amended make/reach split, a per-domain port also RETIRES that domain''s tooling make targets rather than leaving wrappers; build/test orchestration targets stay with make.', NULL, '2026-08-31 13:38:09', '2026-08-31 13:38:09.189', '2026-08-31 13:38:09.189', NULL, '3ed37cd81f1112ac323a7797c594de84', 2) ON CONFLICT(hash) DO NOTHING;
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06G1S7EZ0HZSZT5HYHJ5HPBQ2G', 'description', 'Add Bash(reach *) to .claude/settings.json. This single line is the friction Q-124 was actually filed about — the permission gate prefix-matches whole command strings, a blanket Bash(python3 *) grant is explicitly forbidden as an unbounded write grant, so today ten separate hand-written Bash(tooling/...) entries each cover one script and every unlisted tool prompts. One bare command with subcommands is one entry covering the whole surface, which is exactly why pql is frictionless today. Scope note: ADD the reach entry here, do not remove the ten tooling entries — the old paths stay live through the deprecation window and removing their permissions early would make them prompt during E3 to E5 when they are still the working tools. The removal is E6. Verify the rule actually matches by making a real reach call through the permission gate rather than by reading the JSON, since a rule that looks right and does not match is the failure mode this whole initiative exists to fix.
DONE 2026-08-31, with an honest limit on the verification read that part before trusting this.
DELIVERED TWO entries, not one.
Added both Bash(reach) and Bash(reach *) to .claude/settings.json, placed beside the pql pair. The ticket asked only for Bash(reach *), but a rule ending in " *" does NOT match the bare word, and bare `reach` is a real invocation now that it prints the domain list. The established convention here confirms it: pql, make, cargo test and ruff check each carry a bare-form entry alongside the wildcard one, for exactly this reason. Adding only the wildcard would have left `reach` prompting while `reach check ...` did not a confusing half-fix.
The ten tooling/ entries were left in place per the scope note; removing them is T-1253, and taking their permissions away while they are still the working tools would make them prompt through T-1250 to T-1252.
VERIFICATION WHAT WAS AND WAS NOT PROVEN. This matters given the ticket''s own warning.
Ran real calls through the gate: `reach check client-version` (exit 0) and `reach --help` (domain list). Both succeeded with no prompt.
BUT THAT IS NOT EVIDENCE THE RULE WORKED. Those same calls succeeded throughout this session BEFORE the rule existed I checked, and there was no Bash(reach ...) entry in either .claude/settings.json or .claude/settings.local.json, and no blanket Bash(*) grant in either. So something else (session permission mode) was already allowing them, and the observation is confounded. A passing call here cannot distinguish "the rule matched" from "the rule was never consulted".
WHAT WOULD ACTUALLY PROVE IT: a session started AFTER this commit, in a mode that prompts, where `reach check client-version` runs without a prompt. settings.json is read at session start, so this cannot be self-verified from inside the session that wrote it. Flagging rather than claiming a green.
What IS established: the JSON parses, both entries are present in permissions.allow, and the rule shape is byte-identical in form to the pql pair, which is empirically frictionless in this repo. That is inference from a working precedent, not proof.
FINDING env-prefixed calls will still prompt, and that is accepted.
Permission rules prefix-match the WHOLE command string (root CLAUDE.md: `PATH=/opt/bin make build` does not match `Bash(make *)`). So `SR_REPO_ROOT=... reach ...` and `SR_OUTPUT_FORMAT=text reach ...` do not match Bash(reach *) and will prompt. Deliberately not worked around: an environment override IS a genuine departure from normal invocation, and it is the ordinary form that needs to be frictionless. Recorded in docs/DEVOPS.md with the mitigation for tests pass overrides through the subprocess environment rather than the command string, as tooling/test_check_parity.py does.', 'Add Bash(reach *) to .claude/settings.json. This single line is the friction Q-124 was actually filed about the permission gate prefix-matches whole command strings, a blanket Bash(python3 *) grant is explicitly forbidden as an unbounded write grant, so today ten separate hand-written Bash(tooling/...) entries each cover one script and every unlisted tool prompts. One bare command with subcommands is one entry covering the whole surface, which is exactly why pql is frictionless today. Scope note: ADD the reach entry here, do not remove the ten tooling entries the old paths stay live through the deprecation window and removing their permissions early would make them prompt during E3 to E5 when they are still the working tools. The removal is E6. Verify the rule actually matches by making a real reach call through the permission gate rather than by reading the JSON, since a rule that looks right and does not match is the failure mode this whole initiative exists to fix.
DONE 2026-08-31, with an honest limit on the verification read that part before trusting this.
DELIVERED TWO entries, not one.
Added both Bash(reach) and Bash(reach *) to .claude/settings.json, placed beside the pql pair. The ticket asked only for Bash(reach *), but a rule ending in " *" does NOT match the bare word, and bare `reach` is a real invocation now that it prints the domain list. The established convention here confirms it: pql, make, cargo test and ruff check each carry a bare-form entry alongside the wildcard one, for exactly this reason. Adding only the wildcard would have left `reach` prompting while `reach check ...` did not a confusing half-fix.
The ten tooling/ entries were left in place per the scope note; removing them is T-1253, and taking their permissions away while they are still the working tools would make them prompt through T-1250 to T-1252.
VERIFICATION WHAT WAS AND WAS NOT PROVEN. This matters given the ticket''s own warning.
Ran real calls through the gate: `reach check client-version` (exit 0) and `reach --help` (domain list). Both succeeded with no prompt.
BUT THAT IS NOT EVIDENCE THE RULE WORKED. Those same calls succeeded throughout this session BEFORE the rule existed I checked, and there was no Bash(reach ...) entry in either .claude/settings.json or .claude/settings.local.json, and no blanket Bash(*) grant in either. So something else (session permission mode) was already allowing them, and the observation is confounded. A passing call here cannot distinguish "the rule matched" from "the rule was never consulted".
WHAT WOULD ACTUALLY PROVE IT: a session started AFTER this commit, in a mode that prompts, where `reach check client-version` runs without a prompt. settings.json is read at session start, so this cannot be self-verified from inside the session that wrote it. Flagging rather than claiming a green.
What IS established: the JSON parses, both entries are present in permissions.allow, and the rule shape is byte-identical in form to the pql pair, which is empirically frictionless in this repo. That is inference from a working precedent, not proof.
FINDING env-prefixed calls will still prompt, and that is accepted.
Permission rules prefix-match the WHOLE command string (root CLAUDE.md: `PATH=/opt/bin make build` does not match `Bash(make *)`). So `SR_REPO_ROOT=... reach ...` and `SR_OUTPUT_FORMAT=text reach ...` do not match Bash(reach *) and will prompt. Deliberately not worked around: an environment override IS a genuine departure from normal invocation, and it is the ordinary form that needs to be frictionless. Recorded in docs/DEVOPS.md with the mitigation for tests pass overrides through the subprocess environment rather than the command string, as tooling/test_check_parity.py does.
REVISITED 2026-08-31: the env-prefix limitation this ticket recorded as ''accepted'' deserves a second look, because the primary user of reach is an AGENT (D-263, stated 2026-08-31), not a human at a terminal. Permission rules prefix-match the whole command string, so SR_REPO_ROOT=... reach ... and SR_OUTPUT_FORMAT=text reach ... do not match Bash(reach *) and will prompt. I called that acceptable on the grounds that an env override is a departure from normal invocation which is true for a human and less true for me, since the override forms are exactly what a test harness and a debugging session reach for. NOT changing it here: the ordinary form is frictionless, the override forms are rare in normal use, and a broad Bash(env *) grant would be a much larger permission surface for a small convenience. Recorded so the tradeoff is visible if it starts costing prompts in practice if it does, the fix is a narrow rule per variable, not a blanket one.', NULL, '2026-08-31 13:41:02', '2026-08-31 13:41:02.169', '2026-08-31 13:41:02.169', NULL, '00216fed794c544c5b7e8264f83e835f', 2) ON CONFLICT(hash) DO NOTHING;
+18
View File
@@ -2120,3 +2120,21 @@ CONSEQUENCE FOR REFINEMENT: the description proposes "roughly one ticket per dom
REFINED 2026-08-31 four cross-cutting tasks filed; the per-domain port tickets are DELIBERATELY NOT filed yet. T-1271 the domain map (blocking design every file assigned before any file moves), T-1272 rename the five hyphenated Python directories, T-1273 the Blender carve-out, T-1274 retire the ruff ignores the package makes unnecessary. T-1272 and T-1273 are blocked on T-1271; T-1274 runs LAST, since re-enabling E402 mid-move means fixing violations in files that are about to move again. The per-domain tickets get filed from the map rather than from the epic''s proposed list, because that list is missing at least one domain (PR/workflow tea-comment, pr-watchlist-diff) and leaves 28 singleton-prefix files unassigned. Filing nine tickets now and renegotiating their boundaries mid-move is the failure this ordering avoids.
SEQUENCING SET 2026-08-31 by decision. (1) T-1264 (streaming, as decorators) lands BEFORE the per-domain ports, so every ported command arrives already streaming. (2) Old scripts retire PER DOMAIN, the moment that domain''s port passes a parity test not in one sweep at T-1253. So each per-domain port ticket owns its own retirement, and the tree shrinks continuously rather than existing twice for months while edits risk landing in the dead copy. (3) Domain names as mapped are confirmed no renames. (4) Per D-263''s amended make/reach split, a per-domain port also RETIRES that domain''s tooling make targets rather than leaving wrappers; build/test orchestration targets stay with make.', 'in_progress', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:23:58.880', '2026-08-31 13:38:09.189', NULL, 'f4baef3d1ff514ca3ee4823cf2c12043', 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 ('06G1S7EZ0HZSZT5HYHJ5HPBQ2G', 'task', '06G1S392DEG41MRGEFSYWJWP2W', 'Permission entry: Bash(reach *) replaces per-script prompting', 'Add Bash(reach *) to .claude/settings.json. This single line is the friction Q-124 was actually filed about — the permission gate prefix-matches whole command strings, a blanket Bash(python3 *) grant is explicitly forbidden as an unbounded write grant, so today ten separate hand-written Bash(tooling/...) entries each cover one script and every unlisted tool prompts. One bare command with subcommands is one entry covering the whole surface, which is exactly why pql is frictionless today. Scope note: ADD the reach entry here, do not remove the ten tooling entries — the old paths stay live through the deprecation window and removing their permissions early would make them prompt during E3 to E5 when they are still the working tools. The removal is E6. Verify the rule actually matches by making a real reach call through the permission gate rather than by reading the JSON, since a rule that looks right and does not match is the failure mode this whole initiative exists to fix.
DONE 2026-08-31, with an honest limit on the verification read that part before trusting this.
DELIVERED TWO entries, not one.
Added both Bash(reach) and Bash(reach *) to .claude/settings.json, placed beside the pql pair. The ticket asked only for Bash(reach *), but a rule ending in " *" does NOT match the bare word, and bare `reach` is a real invocation now that it prints the domain list. The established convention here confirms it: pql, make, cargo test and ruff check each carry a bare-form entry alongside the wildcard one, for exactly this reason. Adding only the wildcard would have left `reach` prompting while `reach check ...` did not a confusing half-fix.
The ten tooling/ entries were left in place per the scope note; removing them is T-1253, and taking their permissions away while they are still the working tools would make them prompt through T-1250 to T-1252.
VERIFICATION WHAT WAS AND WAS NOT PROVEN. This matters given the ticket''s own warning.
Ran real calls through the gate: `reach check client-version` (exit 0) and `reach --help` (domain list). Both succeeded with no prompt.
BUT THAT IS NOT EVIDENCE THE RULE WORKED. Those same calls succeeded throughout this session BEFORE the rule existed I checked, and there was no Bash(reach ...) entry in either .claude/settings.json or .claude/settings.local.json, and no blanket Bash(*) grant in either. So something else (session permission mode) was already allowing them, and the observation is confounded. A passing call here cannot distinguish "the rule matched" from "the rule was never consulted".
WHAT WOULD ACTUALLY PROVE IT: a session started AFTER this commit, in a mode that prompts, where `reach check client-version` runs without a prompt. settings.json is read at session start, so this cannot be self-verified from inside the session that wrote it. Flagging rather than claiming a green.
What IS established: the JSON parses, both entries are present in permissions.allow, and the rule shape is byte-identical in form to the pql pair, which is empirically frictionless in this repo. That is inference from a working precedent, not proof.
FINDING env-prefixed calls will still prompt, and that is accepted.
Permission rules prefix-match the WHOLE command string (root CLAUDE.md: `PATH=/opt/bin make build` does not match `Bash(make *)`). So `SR_REPO_ROOT=... reach ...` and `SR_OUTPUT_FORMAT=text reach ...` do not match Bash(reach *) and will prompt. Deliberately not worked around: an environment override IS a genuine departure from normal invocation, and it is the ordinary form that needs to be frictionless. Recorded in docs/DEVOPS.md with the mitigation for tests pass overrides through the subprocess environment rather than the command string, as tooling/test_check_parity.py does.
REVISITED 2026-08-31: the env-prefix limitation this ticket recorded as ''accepted'' deserves a second look, because the primary user of reach is an AGENT (D-263, stated 2026-08-31), not a human at a terminal. Permission rules prefix-match the whole command string, so SR_REPO_ROOT=... reach ... and SR_OUTPUT_FORMAT=text reach ... do not match Bash(reach *) and will prompt. I called that acceptable on the grounds that an env override is a departure from normal invocation which is true for a human and less true for me, since the override forms are exactly what a test harness and a debugging session reach for. NOT changing it here: the ordinary form is frictionless, the override forms are rare in normal use, and a broad Bash(env *) grant would be a much larger permission surface for a small convenience. Recorded so the tradeoff is visible if it starts costing prompts in practice if it does, the fix is a narrow rule per variable, not a blanket one.', 'done', 'medium', NULL, NULL, 'D-263', '2026-08-20 00:41:43.428', '2026-08-31 13:41:02.168', NULL, '0d424100c4529a4ad0ac8ca2111edfbd', 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;