@event4u/agent-config 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +109 -0
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/roadmap/next.md +38 -0
  5. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +22 -1
  6. package/dist/agent-src/contexts/contracts/frugality-charter.md +1 -1
  7. package/dist/agent-src/rules/context-hygiene.md +39 -0
  8. package/dist/agent-src/rules/token-budget-discipline.md +3 -2
  9. package/dist/agent-src/rules/token-efficiency.md +22 -0
  10. package/dist/agent-src/skills/ai-council/SKILL.md +28 -6
  11. package/dist/agent-src/skills/condense-memory/SKILL.md +1 -1
  12. package/dist/agent-src/skills/roadmap-writing/SKILL.md +1 -1
  13. package/dist/agent-src/skills/token-optimizer/SKILL.md +5 -1
  14. package/dist/agent-src/templates/agent-settings.md +12 -28
  15. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +2 -2
  16. package/dist/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  17. package/dist/cli/registry.js +1 -1
  18. package/dist/cli/registry.js.map +1 -1
  19. package/dist/cli-delegate/{chunk-3JUTT26I.js → chunk-6ENSXSBW.js} +1 -1
  20. package/dist/cli-delegate/{chunk-TVGXH7VA.js → chunk-6LOPHJLD.js} +1 -1
  21. package/dist/cli-delegate/{chunk-XT2YTJCZ.js → chunk-AYKQGIBG.js} +4 -0
  22. package/dist/cli-delegate/{chunk-HJSKTMIF.js → chunk-BV3WR67D.js} +3 -19
  23. package/dist/cli-delegate/{chunk-HTIU3W56.js → chunk-BWMXLD4B.js} +6 -2
  24. package/dist/cli-delegate/{chunk-2H5SE7DU.js → chunk-E4RUZKXB.js} +165 -38
  25. package/dist/cli-delegate/{chunk-XTC5ARB6.js → chunk-IR3IVRVC.js} +7 -2
  26. package/dist/cli-delegate/{chunk-LRXJK6GN.js → chunk-JMQZ7MM5.js} +1 -1
  27. package/dist/cli-delegate/{chunk-NX5AXSLD.js → chunk-X3VOARZD.js} +3 -3
  28. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  29. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  30. package/dist/cli-delegate/cmd_conformance.js +881 -204
  31. package/dist/cli-delegate/cmd_doctor.js +3 -3
  32. package/dist/cli-delegate/cmd_explain.js +2 -2
  33. package/dist/cli-delegate/cmd_export.js +4 -4
  34. package/dist/cli-delegate/cmd_handoff.js +6 -5
  35. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  36. package/dist/cli-delegate/cmd_migrate.js +2 -2
  37. package/dist/cli-delegate/cmd_packs_active.js +2 -2
  38. package/dist/cli-delegate/cmd_prune.js +4 -4
  39. package/dist/cli-delegate/cmd_route_audit.js +2 -2
  40. package/dist/cli-delegate/cmd_route_explain.js +1 -1
  41. package/dist/cli-delegate/cmd_session_recycle.js +44 -9
  42. package/dist/cli-delegate/cmd_settings_get.js +2 -2
  43. package/dist/cli-delegate/cmd_settings_migrate.js +4 -4
  44. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  45. package/dist/cli-delegate/cmd_sync.js +4 -4
  46. package/dist/cli-delegate/cmd_uninstall.js +4 -4
  47. package/dist/cli-delegate/cmd_update.js +1 -1
  48. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  49. package/dist/cli-delegate/cmd_validate.js +4 -4
  50. package/dist/cli-delegate/cmd_versions.js +1 -1
  51. package/dist/discovery/deprecation-report.md +1 -1
  52. package/dist/discovery/discovery-manifest.json +11 -11
  53. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  54. package/dist/discovery/discovery-manifest.summary.md +1 -1
  55. package/dist/discovery/dormancy-report.md +1 -1
  56. package/dist/discovery/orphan-report.md +1 -1
  57. package/dist/discovery/packs.json +2 -2
  58. package/dist/discovery/trust-report.md +1 -1
  59. package/dist/discovery/workspaces.json +2 -2
  60. package/dist/hooks/dispatch.js +88 -20
  61. package/dist/install/install.mjs +9 -21
  62. package/dist/mcp/registry-manifest.json +1 -1
  63. package/dist/mcp/server.json +2 -2
  64. package/dist/mcp/server.mjs +1336 -489
  65. package/dist/scripts/_lib/agent_settings.js +25 -2
  66. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  67. package/dist/scripts/_lib/compile_time_toggles.js +4 -3
  68. package/dist/scripts/_lib/compile_time_toggles.js.map +1 -1
  69. package/dist/server/io/yamlIO.js +0 -2
  70. package/dist/server/io/yamlIO.js.map +1 -1
  71. package/dist/server/schemas/settings.js +3 -9
  72. package/dist/server/schemas/settings.js.map +1 -1
  73. package/dist/shared/settingsClasses.js +64 -2
  74. package/dist/shared/settingsClasses.js.map +1 -1
  75. package/dist/ui/assets/{index-B5qGYOJo.js → index-5fXOHoVh.js} +2 -2
  76. package/dist/ui/assets/index-5fXOHoVh.js.map +1 -0
  77. package/dist/ui/index.html +1 -1
  78. package/docs/contracts/settings-classes.md +307 -145
  79. package/docs/guidelines/agent-infra/layered-settings.md +1 -2
  80. package/docs/guidelines/agent-infra/symptom-driven-harvest-loop.md +104 -0
  81. package/package.json +1 -1
  82. package/src/agent-src/templates/scripts/work_engine/_lib/agent_settings.ts +0 -1
  83. package/src/config/agent-settings.template.yml +12 -30
  84. package/src/config/gate-violation-baselines.json +9 -3
  85. package/src/config/profiles/balanced.ini +0 -2
  86. package/src/config/profiles/full.ini +1 -3
  87. package/src/config/profiles/minimal.ini +0 -2
  88. package/src/scripts/_cli/cmd_session_recycle.ts +66 -9
  89. package/src/scripts/_cli/handoff_sessions.ts +3 -2
  90. package/src/scripts/_lib/agent_settings.ts +25 -2
  91. package/src/scripts/_lib/bench_telegraph.ts +1 -1
  92. package/src/scripts/_lib/cc_transcript.ts +25 -0
  93. package/src/scripts/_lib/compile_time_toggles.ts +4 -3
  94. package/src/scripts/ai_council/clients.ts +228 -33
  95. package/src/scripts/cache_realization_report.ts +3 -2
  96. package/src/scripts/check_rule_projection_integrity.ts +3 -1
  97. package/src/scripts/conformance_scan.ts +282 -9
  98. package/src/scripts/cost/track.mjs +10 -4
  99. package/src/scripts/council_cli.ts +33 -3
  100. package/src/scripts/external_sources_denylist.json +3 -1
  101. package/src/scripts/hook_manifest.yaml +17 -6
  102. package/src/scripts/hooks/block_unauthorized_git.ts +54 -8
  103. package/src/scripts/hooks/delegation_nudge_hook.ts +33 -8
  104. package/src/scripts/hooks/evidence_independence.ts +61 -9
  105. package/src/scripts/hooks/session_eol_hook.ts +123 -3
  106. package/src/scripts/hooks/turn_end_gate_hook.ts +31 -0
  107. package/src/scripts/lint_settings_classes.ts +273 -11
  108. package/src/scripts/lint_symptom_intake.ts +241 -0
  109. package/src/scripts/report_consultation_rate.ts +2 -1
  110. package/src/scripts/report_skill_activation.ts +6 -4
  111. package/src/scripts/report_skill_obligation_violations.ts +2 -1
  112. package/src/server/io/yamlIO.ts +0 -2
  113. package/src/server/schemas/settings.ts +3 -19
  114. package/src/shared/settingsClasses.ts +93 -2
  115. package/dist/ui/assets/index-B5qGYOJo.js.map +0 -1
@@ -0,0 +1,104 @@
1
+ # Symptom-driven harvest loop
2
+
3
+ > The procedure that turns an **operator's production symptom** into a roadmap
4
+ > this repository can execute. It is the inverse of the source-driven harvest:
5
+ > that one starts from external material pushed onto the repo and asks what is
6
+ > worth adopting; this one starts from a failure someone hit in production and
7
+ > pulls external material in **per confirmed defect**.
8
+ >
9
+ > Body of `road-to-symptom-driven-harvest-loop` Phase 2. It lives here rather
10
+ > than inline in [`roadmap-writing`](../../../src/skills/roadmap-writing/SKILL.md)
11
+ > because that skill is already ~4.8k tokens — well past the 3,500-token band —
12
+ > and a procedure needed a few times a quarter should not be paid for on every
13
+ > roadmap-authoring turn. The skill carries the pointer.
14
+
15
+ ## When this applies
16
+
17
+ An operator reports a failure in their own words — "runs never end", "the agent
18
+ screenshots instead of reading the code" — rather than a defect with a file and
19
+ a line. File it first under [`agents/tickets/symptoms/`](../../../agents/tickets/symptoms/README.md);
20
+ that entry is what this loop resolves.
21
+
22
+ Not this loop: a defect already traced to a file (that is a roadmap item), a
23
+ feature request (roadmap or ticket), or external material arriving unprompted
24
+ (the source-driven harvest family).
25
+
26
+ ## The four steps
27
+
28
+ **1. Confirm against the live tree, at a pinned commit, before any external
29
+ search.** One `file:line` per claim, and the pin recorded next to it. The
30
+ ordering is the load-bearing part: searching externally first produces a plan
31
+ about the ecosystem's problems rather than this tree's, and every subsequent
32
+ round inherits that frame. A symptom that does not reproduce as a confirmed
33
+ defect gets a `null:` block and stops here — that is a result.
34
+
35
+ **2. Triangulate externally, per confirmed defect, never additively.** For each
36
+ defect, and only for each defect: the host tool's own issue tracker, its official
37
+ docs **fetched fresh**, and practitioner reports. Inverted form throughout — the
38
+ defect is the anchor, the source is drawn in to explain or bound it. A source
39
+ that matches no confirmed defect is not evidence for this roadmap; note it and
40
+ drop it.
41
+
42
+ **3. Iterate at least two further rounds, each stating its delta.** A round that
43
+ changes nothing ends the loop early and says so. This is the step that is
44
+ skipped under time pressure and the step that pays: on the first run of this
45
+ loop, round 2 refuted a standing repo blocker from the official payload spec,
46
+ imported a constraint that reshaped a whole phase from "block on invalid" to
47
+ "command-hook, disk-fallback, block-once", and round 3 found a reopen-clause
48
+ deadlock that was the *only* argument for reopening cancelled work at all. A
49
+ one-pass version would have shipped a roadmap re-proposing cancelled items with
50
+ no reopening argument and a mechanism the host would have broken.
51
+
52
+ **4. Emit with a symptom→defect map and per-phase falsifiers.** The map is the
53
+ artifact the operator can read: their words in one column, confirmed defects with
54
+ anchors in the next, owning phase in the third. Every phase carries the
55
+ measurement that would delete it.
56
+
57
+ ## Step 5, added by the first three runs: re-verify at adoption
58
+
59
+ A roadmap drafted at a pin and adopted later is a **snapshot of an opinion about
60
+ a tree that has since moved**. Before adopting, re-verify every repo claim at the
61
+ current tip and record the deltas.
62
+
63
+ This is not ceremony. Adopting the first three runs of this loop, 81 commits
64
+ past their shared pin, re-verification found 32 of 36 claims still true — and:
65
+
66
+ - one defect had **fixed itself** (a function measured as having zero production
67
+ callers had acquired one), which deleted a roadmap step before anyone built it;
68
+ - one claim was **wrong at the pin too** (a count stated as six where the tree
69
+ binds seven), which no amount of re-reading the draft would have surfaced;
70
+ - three anchors had **drifted** to new line numbers while their content held;
71
+ - one cross-cutting conflict was invisible until two drafts were read together
72
+ (a proposed "adopt the artifact's code" duty against an existing
73
+ `NEVER ADOPT EXTERNAL CODE VERBATIM` Iron Law).
74
+
75
+ The cheapest of those to find was the already-fixed one, and it was worth the
76
+ most: an item prevented costs nothing to maintain.
77
+
78
+ ## Worked example — why round 2 is mandatory
79
+
80
+ `hook_manifest.yaml` carried a comment stating that in-process subagents "inherit
81
+ the host process env and cannot be marked per-spawn", and that comment had stood
82
+ for months as a recorded blocker on a whole design axis. It was true about the
83
+ mechanism it named — the env var — and false about the conclusion.
84
+
85
+ Round 2 fetched the host's current payload specification, which states that tool
86
+ events inside a subagent carry `agent_id` and `agent_type` precisely so
87
+ subagent calls can be distinguished from main-thread ones. The env cannot mark a
88
+ spawn; the payload already did. One fetch refuted a months-old blocker, and the
89
+ axis it had closed reopened.
90
+
91
+ The general shape: **a repo comment records what was tried, not what is
92
+ possible.** Round 1 finds the comment and believes it, because it is this tree's
93
+ own evidence. Only an external round can date it.
94
+
95
+ ## See also
96
+
97
+ - [`roadmap-writing`](../../../src/skills/roadmap-writing/SKILL.md) — the authoring
98
+ skill this procedure extends; it owns phases, falsifiers, and the output format.
99
+ - [`agents/tickets/symptoms/README.md`](../../../agents/tickets/symptoms/README.md) —
100
+ the intake convention and the two resolution blocks.
101
+ - [`source-discovery-gate`](../../../src/rules/source-discovery-gate.md) — the
102
+ no-structural-claim-without-evidence rule step 1 is an application of.
103
+ - [`external-reference-deep-dive`](../../../src/rules/external-reference-deep-dive.md) —
104
+ how to read a named external source properly in step 2.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@event4u/agent-config",
3
3
  "mcpName": "io.github.event4u-app/agent-config",
4
- "version": "10.1.0",
4
+ "version": "10.2.0",
5
5
  "description": "Governed skills, rules and commands for AI coding tools (Claude Code, Cursor, Windsurf, Copilot) — every claim machine-checked, including \"zero runtime daemon\".",
6
6
  "license": "MIT",
7
7
  "private": false,
@@ -210,7 +210,6 @@ export const MERGEABLE_KEYS: readonly string[] = [
210
210
  'memory.cadence',
211
211
  'personal.bot_icon',
212
212
  'personal.autonomy',
213
- 'telegraph.speak_scope',
214
213
  // Knowledge-card global cross-project sharing is a USER-GLOBAL setting
215
214
  // (ADR-100 / road-to-structure-grounding-v2). Whitelisted so the
216
215
  // ~/.event4u/agent-config/agent-settings.yml values are honoured.
@@ -166,33 +166,25 @@ lean_projection:
166
166
 
167
167
  # --- Telegraph condensation (output-side) ---
168
168
  #
169
- # speak_scope: off | prose_only | aggressive (QUOTE the value — bare `off` is a
170
- # YAML 1.1 boolean, not the string)
171
- #
172
- # `off` is the shipped default per docs/adrs/telegraph/0001-default-off-until-bench.md
173
- # (Status: accepted) and docs/contracts/condensation-default-kill-criterion.md.
169
+ # `speak: false` is the shipped default per
170
+ # docs/adrs/telegraph/0001-default-off-until-bench.md (Status: accepted) and
171
+ # docs/contracts/condensation-default-kill-criterion.md.
174
172
  # Reason it is off: the only output-side bench (telegraph-v1, n=10, 2026-05-16)
175
173
  # measured median vs_terse -9.27% (API counts) / -5.47% (exact cl100k_base
176
174
  # re-analysis) — telegraph emits MORE tokens than a plain "be terse" instruction.
177
175
  # The kill-criterion table therefore lands on "criterion not met — defer" and the
178
176
  # telemetry multiplier stays suspended (0.9155 < 1.0).
179
177
  #
180
- # NOTE: `speak_scope: off` disables the BEHAVIOUR but not the COST — the projector
181
- # gates this rule on `telegraph.speak`, never on speak_scope, so under
182
- # `lean_projection.mode: eager-all` the ~982-token body still ships every session.
183
- # Set `speak: false` for zero-cost dormancy.
184
- #
185
178
  # Stated explicitly here because until 2026-07-29 this default existed only in
186
- # prose — and two prose sources disagreed (the rule said prose_only, the accepted
187
- # ADR said off). Do not flip without a passing output-side bench.
179
+ # prose — and two prose sources disagreed. Do not flip without a passing
180
+ # output-side bench.
188
181
  telegraph:
189
182
  # speak: false = DORMANT (the shipped default per docs/adrs/telegraph/0002).
190
183
  # The rule is omitted from dist/router.json entirely, so its ~982-token body
191
- # never ships. This — not speak_scope — is the lever that stops the cost:
192
- # compile_router gates on `speak` and never reads speak_scope.
184
+ # never ships. This is the only lever that stops the cost: compile_router
185
+ # gates on `speak` and on nothing else.
193
186
  # Set true only after an output-side bench clears the kill-criterion bar.
194
187
  speak: false
195
- speak_scope: "off"
196
188
 
197
189
  # --- Token-rich skills governance ---
198
190
  #
@@ -420,14 +412,6 @@ chat_history:
420
412
  # per_tool = one entry per tool call (most verbose)
421
413
  frequency: __CHAT_HISTORY_FREQUENCY__
422
414
 
423
- # Max file size in KB before overflow handling kicks in (integer)
424
- max_size_kb: __CHAT_HISTORY_MAX_SIZE_KB__
425
-
426
- # Overflow behavior
427
- # rotate = drop oldest entries to stay under max_size_kb
428
- # condense = mark for summarization on the next agent turn
429
- on_overflow: __CHAT_HISTORY_ON_OVERFLOW__
430
-
431
415
  # Per-entry-type text-length caps (chars). 0 = verbatim, no slice.
432
416
  # N > 0 = collapse whitespace, slice to N chars, append " … [+K chars]"
433
417
  # so the log self-reports truncation. Defaults match the hardcoded
@@ -656,12 +640,6 @@ quality:
656
640
  # gates delegated to remote CI", never claim an unrun pipeline passed.
657
641
  local_auto_run: false
658
642
 
659
- # Wait for remote CI to finish on the PR / pipeline (true, false)
660
- # true = after push, poll GitHub check-runs / pipeline status and
661
- # report green / red before handing back
662
- # false = push and hand back immediately; user inspects CI
663
- # themselves (default — saves agent runtime and tokens)
664
- wait_for_remote_ci: false
665
643
 
666
644
  # --- Design fidelity ---
667
645
  #
@@ -744,6 +722,11 @@ screenshots:
744
722
  # ship-and-hope. Illustrative / no-data screenshots (diagrams, logos,
745
723
  # placeholder-only UI) may embed with a stated justification.
746
724
  # off = no data-bearing gate (legacy; the anonymization taxonomy still applies).
725
+ #
726
+ # NOTE: nothing reads this key yet — the gate it names is unconditional prose in
727
+ # `doc-screenshot-hygiene`. Kept rather than deleted so the missing reader is a
728
+ # visible gap instead of a silently unguarded action; see settings-classes.md
729
+ # § The six unread keys for the disagreement about which repair is right.
747
730
  data_bearing_gate: "on"
748
731
 
749
732
  code_style:
@@ -1365,7 +1348,6 @@ explain:
1365
1348
  # rule refuses every legal-review-prep skill until `acknowledged: true`.
1366
1349
  legal_review_prep:
1367
1350
  acknowledged: false
1368
- consented_at: ""
1369
1351
  # Gate legal work-product behind a multi-model council / deep-research pass
1370
1352
  # (defense-in-depth + audit trail; fail-closed). Off lets single-model legal
1371
1353
  # output through — not recommended for this high-risk pack.
@@ -22,9 +22,9 @@
22
22
  "note": "Markdown links between source artefacts that cross a pack boundary the source pack's `requires` graph does not reach — revealed when the scan root moved off the deleted packages/ tree to the live corpus (0 -> 753 artefacts scanned). 82 distinct pack pairs across 131 source files; largest single class is engineering-base -> meta (73), skills in a consumer-installable pack citing this package's own maintenance skills. Each is a link that dangles for a consumer who installs the source pack without the target's. Judged real drift rather than an over-strict gate because it is long-tailed — top bucket 22%, no single systematic cause — unlike the lint_artefact_frontmatter case, where one required shape had 0-of-288 adoption. Fix per finding: add the edge to packs.yml requires, retarget the link, or move the artefact."
23
23
  },
24
24
  "check_gate_completeness": {
25
- "count": 217,
26
- "landed": "2026-08-05",
27
- "note": "Ledger adoption ratchet. 217 of 221 CI-registered gates do not yet carry per-target completeness accounting (`_lib/gate_ledger.ts`) or a `// ledger-exempt:` marker. This is deliberately NOT a sweep: road-to-skill-ecosystem-gate-integrity Phase 1 adopts exactly three gates (lint_framework_leakage, check_references, lint_roadmap_complexity) plus the checker itself to prove the interface, and the remainder migrates as gates are touched. Shrink-only from here — the number may not grow, so a NEW gate must adopt or exempt.",
25
+ "count": 216,
26
+ "landed": "2026-08-12",
27
+ "note": "Ledger adoption ratchet. 216 of 244 registered gates do not yet carry per-target completeness accounting (`_lib/gate_ledger.ts`) or a `// ledger-exempt:` marker. This is deliberately NOT a sweep: road-to-skill-ecosystem-gate-integrity Phase 1 adopts exactly three gates (lint_framework_leakage, check_references, lint_roadmap_complexity) plus the checker itself to prove the interface, and the remainder migrates as gates are touched. Shrink-only from here — the number may not grow, so a NEW gate must adopt or exempt.",
28
28
  "falsifier": "./scripts-run src/scripts/check_gate_completeness"
29
29
  },
30
30
  "check_depth_budget": {
@@ -37,6 +37,12 @@
37
37
  "landed": "2026-08-06",
38
38
  "note": "Self-test adoption ratchet, expressed as a shrink-only NON-adopter count. 24 of the 32 gates registered enforced in gate-coverage.yml carry neither a `--self-test` (`_lib/gate_self_test.ts`) nor a `// self-test-exempt: <reason>` marker, so nothing proves they DISCRIMINATE — an enforced `scanned:` floor proves a gate read something, only a self-test proves the reading changes the verdict. Deliberately NOT an adoption column ratcheted upward: check_gate_coverage.ts's own report_hardening_ratchet rejects that shape in this same file (\"grades the solution instead of the problem\"), and the sibling adoption ratchet (check_gate_completeness) is already a non-adopter count. Population is the manifest, not every script under src/scripts: padding the manifest to move this number would mean adding a floor per row. Shrink-only from here — a NEW registered gate must adopt or exempt. Migration mechanism, same as the ledger ratchet above: gates adopt as they are touched, not in a sweep — the eight current adopters were each added alongside a real change to that gate. The 56-day non-stagnation clause applies unchanged, so a count parked here fails on its own.",
39
39
  "falsifier": "./scripts-run src/scripts/check_gate_coverage"
40
+ },
41
+ "lint_settings_classes:derivable-surface": {
42
+ "count": 83,
43
+ "landed": "2026-08-12",
44
+ "note": "Anti-regrowth ratchet for the settings disposition axis. 83 of 134 template leaves are classified `derivable` — the mechanism can decide from the situation, so the key should not exist — and each names the replacement that would decide instead (`lint_settings_classes` rejects the bare label, which is what keeps the class from becoming a synonym for \"inconvenient to keep\"). This is a DELETION QUEUE, not 83 deletions: a row stays until the mechanism it names actually exists, and deleting a key ahead of its mechanism is a silently-changed default rather than a simplification. Shrink-only from here, which is the anti-regrowth half — a NEW key classified `derivable` raises the number and reds, so a key whose answer the situation already carries cannot be added at all. The 56-day non-stagnation clause is the other half and is wanted here rather than tolerated: a queue that never drains is a stated direction nobody is walking. The queue opened at 88 of 140 on 2026-08-12 and drained to 83 of 134 the same day: six keys had no reader whatsoever (telegraph.speak_scope, chat_history.max_size_kb, chat_history.on_overflow, quality.wait_for_remote_ci, screenshots.data_bearing_gate, legal_review_prep.consented_at), five of them `derivable` and one a `consent` row reclassified because a published-egress confirmation is a Hard Floor no setting may lift. An unread key is the only deletion that cannot silently change a default, which is why that batch went first. The classification itself is docs/contracts/settings-classes.md.",
45
+ "falsifier": "./scripts-run src/scripts/lint_settings_classes"
40
46
  }
41
47
  }
42
48
  }
@@ -17,5 +17,3 @@ discipline_profile=auto
17
17
 
18
18
  ; Chat history (crash recovery) — balanced = phase-level traceability
19
19
  chat_history_frequency=per_phase
20
- chat_history_max_size_kb=256
21
- chat_history_on_overflow=rotate
@@ -13,7 +13,5 @@ rule_loading_tier=full
13
13
  ; via the legacy mapping (minimal->off, balanced->essential, full->full).
14
14
  discipline_profile=full
15
15
 
16
- ; Chat history (crash recovery) — full = tool-level traceability + summarize on overflow
16
+ ; Chat history (crash recovery) — full = tool-level traceability
17
17
  chat_history_frequency=per_tool
18
- chat_history_max_size_kb=512
19
- chat_history_on_overflow=condense
@@ -15,5 +15,3 @@ discipline_profile=off
15
15
 
16
16
  ; Chat history (crash recovery) — minimal = lightest footprint
17
17
  chat_history_frequency=per_turn
18
- chat_history_max_size_kb=128
19
- chat_history_on_overflow=rotate
@@ -20,6 +20,7 @@
20
20
  * agent-config session:recycle --file <envelope.json> # read from file
21
21
  * agent-config session:recycle < envelope.json # read from stdin
22
22
  * agent-config session:recycle --template # print a skeleton
23
+ * agent-config session:recycle --project <path> # name the repo explicitly
23
24
  *
24
25
  * Exit codes: 0 written (or template printed) · 1 invalid / refused · 2 usage.
25
26
  */
@@ -29,7 +30,7 @@ import * as path from 'node:path';
29
30
  import process from 'node:process';
30
31
  import { fileURLToPath, pathToFileURL } from 'node:url';
31
32
 
32
- import { resolve_project_root } from '../_lib/agent_settings.js';
33
+ import { ORIGIN_CWD_FALLBACK, resolve_project_root } from '../_lib/agent_settings.js';
33
34
  import {
34
35
  RECYCLE_ENVELOPE_MAX_BYTES,
35
36
  RECYCLE_ENVELOPE_REL,
@@ -77,6 +78,7 @@ interface ParsedArgv {
77
78
  ok: boolean;
78
79
  message?: string;
79
80
  file?: string;
81
+ project?: string;
80
82
  template?: boolean;
81
83
  verify?: boolean;
82
84
  }
@@ -94,6 +96,11 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
94
96
  if (!value) return { ok: false, message: '--file requires a path' };
95
97
  parsed.file = value;
96
98
  i += 1;
99
+ } else if (a === '--project') {
100
+ const value = argv[i + 1];
101
+ if (!value) return { ok: false, message: '--project requires a path' };
102
+ parsed.project = value;
103
+ i += 1;
97
104
  } else if (a === '-h' || a === '--help') {
98
105
  return { ok: false, message: 'usage' };
99
106
  } else {
@@ -110,11 +117,50 @@ export function parseArgv(argv: readonly string[]): ParsedArgv {
110
117
  */
111
118
  export function runSessionRecycle(
112
119
  input: string,
113
- opts: { cwd: string; now?: Date; verify?: boolean },
120
+ opts: { cwd: string; now?: Date; verify?: boolean; project?: string },
114
121
  ): RecycleResult {
115
122
  const out: string[] = [];
116
123
  const err: string[] = [];
117
- const [projectRoot] = resolve_project_root(null, { cwd: opts.cwd });
124
+
125
+ // `--project` is validated by the resolver (existence + directory), which
126
+ // signals by THROWING. Uncaught, that would surface as a stack trace from
127
+ // a command whose entire subject is legible failure.
128
+ let projectRoot: string;
129
+ let origin: string;
130
+ try {
131
+ [projectRoot, origin] = resolve_project_root(opts.project ?? null, { cwd: opts.cwd });
132
+ } catch (exc) {
133
+ return { code: 1, out, err: [`recycle envelope refused — ${String(exc)}`] };
134
+ }
135
+
136
+ // An unanchored cwd is the ONE case where a successful write is worse than
137
+ // a refusal. `resolve_project_root` falls back to the cwd itself when it
138
+ // reaches the filesystem root without finding an anchor, so a call from
139
+ // outside any repo — the shape a consumer with a toolchain pin is forced
140
+ // into — writes a valid envelope into a directory the successor session
141
+ // never looks at, and then prints a resume instruction whose next step is
142
+ // `/clear`. The exit code says it worked; the session is gone anyway.
143
+ //
144
+ // Refused BEFORE the parse: everything downstream reads this root, so a
145
+ // wrong one also produces a wrong `workspace` field and grounding read
146
+ // from the wrong git tree. `--verify` is refused too — validating against
147
+ // a root the write would not use answers a question nobody asked.
148
+ if (origin === ORIGIN_CWD_FALLBACK) {
149
+ return {
150
+ code: 1,
151
+ out,
152
+ err: [
153
+ `recycle envelope refused — no project anchor at or above ${projectRoot}`,
154
+ 'Writing here would put the envelope where the successor session never reads it,',
155
+ 'and the resume instruction would then advise /clear on a session that cannot resume.',
156
+ 'Name the repository explicitly — it must be the WORKSPACE ROOT OF THE SESSION',
157
+ 'that will resume, because that is the directory the successor reads the envelope',
158
+ 'from; a path that merely happens to be a project is not enough:',
159
+ ` agent-config session:recycle --project <session-workspace-root> …`,
160
+ ` AGENT_CONFIG_PROJECT_ROOT=<session-workspace-root> agent-config session:recycle …`,
161
+ ],
162
+ };
163
+ }
118
164
 
119
165
  let parsed: unknown;
120
166
  try {
@@ -179,23 +225,28 @@ export function runSessionRecycle(
179
225
  };
180
226
  }
181
227
 
228
+ // Absolute, not `RECYCLE_ENVELOPE_REL`: the relative form is identical for
229
+ // every root, so it cannot tell the reader WHICH tree was written — the
230
+ // one thing in doubt at the moment this line is read.
231
+ const target = path.join(projectRoot, RECYCLE_ENVELOPE_REL);
232
+
182
233
  // `--verify` stops HERE: every rejection above has already run, and the
183
234
  // only thing skipped is the write. Validating through a different path
184
235
  // than the one that writes would make the check a second implementation
185
236
  // to keep in sync — the failure this suite refuses elsewhere.
186
237
  if (opts.verify === true) {
187
238
  out.push(`recycle envelope VALID — ${bytes} bytes, not written (--verify).`);
239
+ out.push(`Target would be: ${target}`);
188
240
  return { code: 0, out, err };
189
241
  }
190
242
 
191
- const target = path.join(projectRoot, RECYCLE_ENVELOPE_REL);
192
243
  try {
193
244
  atomic_write_json(target, envelope);
194
245
  } catch (exc) {
195
246
  return { code: 1, out, err: [`could not write ${target}: ${String(exc)}`] };
196
247
  }
197
248
 
198
- out.push(`recycle envelope written — ${RECYCLE_ENVELOPE_REL} (${bytes} bytes)`);
249
+ out.push(`recycle envelope written — ${target} (${bytes} bytes)`);
199
250
  out.push('');
200
251
  out.push('Resume instruction:');
201
252
  out.push(' 1. End this session now: run /clear (or start a fresh session in this workspace).');
@@ -210,10 +261,15 @@ export function main(argv: string[] = process.argv.slice(2)): number {
210
261
  const parsed = parseArgv(argv);
211
262
  if (!parsed.ok) {
212
263
  const usage = [
213
- 'usage: agent-config session:recycle [--file <envelope.json>] [--template] [--verify]',
214
- ' --file <path> read the envelope JSON from a file (default: stdin)',
215
- ' --template print a skeleton envelope and exit',
216
- ' --verify validate only — same rejections, no write',
264
+ 'usage: agent-config session:recycle [--file <envelope.json>] [--project <path>] [--template] [--verify]',
265
+ ' --file <path> read the envelope JSON from a file (default: stdin)',
266
+ ' --project <path> the workspace root of the session that will resume — required',
267
+ ' when the working directory is outside any project (the command',
268
+ ' refuses rather than writing an envelope the successor cannot',
269
+ ' find). The successor reads the envelope from ITS OWN workspace',
270
+ ' root, so a path that is merely a valid project is not enough.',
271
+ ' --template print a skeleton envelope and exit',
272
+ ' --verify validate only — same rejections, no write',
217
273
  ].join('\n');
218
274
  if (parsed.message === 'usage') {
219
275
  process.stdout.write(`${usage}\n`);
@@ -246,6 +302,7 @@ export function main(argv: string[] = process.argv.slice(2)): number {
246
302
  const result = runSessionRecycle(input, {
247
303
  cwd: process.cwd(),
248
304
  ...(parsed.verify === true ? { verify: true } : {}),
305
+ ...(parsed.project !== undefined ? { project: parsed.project } : {}),
249
306
  });
250
307
  for (const line of result.out) process.stdout.write(`${line}\n`);
251
308
  for (const line of result.err) process.stderr.write(`${line}\n`);
@@ -30,6 +30,7 @@ import * as fs from 'node:fs';
30
30
  import * as os from 'node:os';
31
31
  import * as path from 'node:path';
32
32
 
33
+ import { projectStoreSlug } from '../_lib/cc_transcript.js';
33
34
  import { derive_session_tag, list_sessions, read_entries } from '../chat_history.js';
34
35
  import { eolSessionKey, readEolCounters, type StoredEolCounters } from '../_lib/session_eol.js';
35
36
 
@@ -102,9 +103,9 @@ export interface ListOptions {
102
103
  workspaceRoot?: string;
103
104
  }
104
105
 
105
- /** Mirrors the Claude Code project-dir naming: `/` and `.` → `-`. */
106
+ /** Mirrors the Claude Code project-dir naming — see `_lib/cc_transcript.projectStoreSlug`. */
106
107
  export function encode_project_path(cwd: string): string {
107
- return cwd.replace(/[/.]/g, '-');
108
+ return projectStoreSlug(cwd);
108
109
  }
109
110
 
110
111
  function _snippet(text: string, max: number = SUMMARY_SNIPPET_CHARS): string {
@@ -283,7 +283,6 @@ export const MERGEABLE_KEYS: readonly string[] = [
283
283
  'personal.bot_icon',
284
284
  'personal.pr_comment_bot_icon',
285
285
  'personal.autonomy',
286
- 'telegraph.speak_scope',
287
286
  // Knowledge-card global cross-project sharing is a USER-GLOBAL setting
288
287
  // (ADR-100 / road-to-structure-grounding-v2). Whitelisted so the
289
288
  // ~/.event4u/agent-config/agent-settings.yml values are honoured.
@@ -908,8 +907,27 @@ export function load_agent_settings(
908
907
  * made the soak impossible; whether the gate fires is now decided by each
909
908
  * detector's own trigger conditions. See `turn_end_gate_hook.ts` § "Always
910
909
  * armed".
910
+ *
911
+ * The five `derivable` leaves below (2026-08-12, road-to-zero-settings Phase
912
+ * 2.1) are a different case from every entry above them, and the difference is
913
+ * the whole safety argument: no code path ever *consulted* them. Each appeared
914
+ * in the template, the schema and the reference page, and several in the setup
915
+ * wizard, while the decision they looked like they governed was taken
916
+ * elsewhere. A key nothing reads cannot change a default by leaving — so this
917
+ * is the one deletion batch that needs no replacement mechanism, only the
918
+ * per-key statement of what already decides.
919
+ *
920
+ * A sixth unread key, `screenshots.data_bearing_gate`, was found in the same
921
+ * pass and deliberately NOT deleted — it is `consent`, and the open question is
922
+ * whether the repair is to build its missing reader or to delete a promise a
923
+ * Hard Floor can never honour. See `settings-classes.md` § The six unread keys.
924
+ *
925
+ * Every reason string must name what decides INSTEAD, never just "removed".
926
+ * Exported so `lint_settings_classes` can check the deletion side of the
927
+ * surface: a reason that names no replacement, or an entry whose key is live in
928
+ * the template again, is a contradiction the loader itself cannot notice.
911
929
  */
912
- const REMOVED_KEYS: ReadonlyMap<string, string> = new Map([
930
+ export const REMOVED_KEYS: ReadonlyMap<string, string> = new Map([
913
931
  ['subagents.enabled', 'always-on orchestration'],
914
932
  ['subagents.auto', 'always-on orchestration'],
915
933
  ['subagents.host_capabilities', 'always-on orchestration'],
@@ -919,6 +937,11 @@ const REMOVED_KEYS: ReadonlyMap<string, string> = new Map([
919
937
  ['hooks.turn_end_gate.promissory', 'the turn-end gate is always armed'],
920
938
  ['hooks.turn_end_gate.language', 'the turn-end gate is always armed'],
921
939
  ['hooks.turn_end_gate.verification', 'the turn-end gate is always armed'],
940
+ ['telegraph.speak_scope', 'the rule body states its own scope; compile_router gates on telegraph.speak alone'],
941
+ ['chat_history.max_size_kb', 'the rotate command takes --max-kb from argv; session-count pruning bounds the file'],
942
+ ['chat_history.on_overflow', 'the overflow mode comes from the rotate command --mode argv'],
943
+ ['quality.wait_for_remote_ci', 'whether to poll follows from the push plus a detectable remote pipeline'],
944
+ ['legal_review_prep.consented_at', 'the provenance sidecar settings:set writes and consentVerdict reads'],
922
945
  ]);
923
946
 
924
947
  /** Keys already warned about in THIS process — the "once per run" dedupe. */
@@ -23,7 +23,7 @@ import YAML from 'yaml';
23
23
 
24
24
  // ── system prompts per arm ──────────────────────────────────────────────
25
25
 
26
- export const SYSTEM_PROMPT_CONDENSED = `You are speaking in TELEGRAPH-SPEAK mode (speak_scope=aggressive).
26
+ export const SYSTEM_PROMPT_CONDENSED = `You are speaking in TELEGRAPH-SPEAK mode (condense aggressively).
27
27
 
28
28
  Condense all body prose to telegraph grammar:
29
29
  - Drop articles (the, a, an).
@@ -35,6 +35,31 @@ import { homedir } from 'node:os';
35
35
  /** Default base directory holding one subdirectory per Claude Code project. */
36
36
  export const DEFAULT_PROJECTS_ROOT = path.join(homedir(), '.claude', 'projects');
37
37
 
38
+ /**
39
+ * The store directory name Claude Code derives from a working directory.
40
+ *
41
+ * Every character outside `[A-Za-z0-9-]` becomes `-`: `/Users/x/.claude` →
42
+ * `-Users-x--claude`, and a worktree `feat+turn-end-gate-always-on` →
43
+ * `feat-turn-end-gate-always-on`.
44
+ *
45
+ * This lives here because it was wrong in six places at once. The sweep on
46
+ * 2026-08-12 found `replace(/[/.]/g, '-')` copied across every transcript-reading
47
+ * report; each one computed a directory that does not exist for any path
48
+ * carrying a `+` and returned an empty corpus — indistinguishable from a clean
49
+ * result. One of those copies already carried a comment about the round BEFORE
50
+ * that, when only `/` was replaced and dotted segments missed. Widening the
51
+ * class by one character per incident is how a defect survives two fixes, so
52
+ * the class is closed instead: anything not `[A-Za-z0-9-]` is slugged.
53
+ */
54
+ export function projectStoreSlug(cwd: string): string {
55
+ return cwd.replace(/[^A-Za-z0-9-]/g, '-');
56
+ }
57
+
58
+ /** Absolute path to a working directory's transcript store. */
59
+ export function projectStoreDir(cwd: string, root: string = DEFAULT_PROJECTS_ROOT): string {
60
+ return path.join(root, projectStoreSlug(cwd));
61
+ }
62
+
38
63
  export type TranscriptBucket = 'main' | 'subagent';
39
64
 
40
65
  /** The six token counters every downstream metric (cost, weight, share) is built from. */
@@ -60,9 +60,10 @@ export const COMPILE_TIME_TOGGLES: Record<string, (s: ToggleSettings) => boolean
60
60
  // family master switch, and flipping it would silence sibling telegraph settings
61
61
  // that were never measured negative.
62
62
  //
63
- // This predicate deliberately does NOT read `speak_scope`. That key disables the
64
- // rule's *behaviour* and never removed its token cost — the trap the measurement
65
- // exposed.
63
+ // The predicate gates on `telegraph.speak` because that is the only lever that
64
+ // removes the rule's ~982-token body from the router and the projection. A knob
65
+ // that merely narrowed the grammar at runtime would leave the token cost in place
66
+ // — the trap the measurement exposed.
66
67
  'telegraph-speak': (s: ToggleSettings): boolean => {
67
68
  const tg = (s['telegraph'] as ToggleSettings | undefined) ?? {};
68
69
  const enabled = tg['enabled'] === undefined ? true : tg['enabled'];