@event4u/agent-config 14.5.0 → 14.7.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 (203) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +384 -3
  3. package/README.md +1 -1
  4. package/dist/agent-src/commands/analyze/decision.md +7 -0
  5. package/dist/agent-src/commands/analyze/inbox.md +264 -10
  6. package/dist/agent-src/commands/analyze.md +1 -1
  7. package/dist/agent-src/commands/optimize/project.md +10 -0
  8. package/dist/agent-src/commands/roadmap/process-full.md +132 -4
  9. package/dist/agent-src/contexts/execution/roadmap-execution-contract.md +31 -0
  10. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +161 -1
  11. package/dist/agent-src/rules/context-hygiene.md +2 -0
  12. package/dist/agent-src/rules/decision-revisit-gate.md +111 -7
  13. package/dist/agent-src/rules/fix-what-you-see.md +108 -0
  14. package/dist/agent-src/rules/no-roadmap-references.md +0 -5
  15. package/dist/agent-src/rules/recurring-criticism.md +110 -0
  16. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  17. package/dist/agent-src/rules/rule-type-governance.md +0 -2
  18. package/dist/agent-src/rules/self-repair-loop.md +17 -0
  19. package/dist/agent-src/rules/skill-quality.md +0 -2
  20. package/dist/agent-src/rules/source-confidentiality.md +37 -7
  21. package/dist/agent-src/scripts/archival_sweep.ts +117 -0
  22. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  23. package/dist/agent-src/scripts/gate_budget.ts +282 -0
  24. package/dist/agent-src/scripts/gate_execute.ts +72 -31
  25. package/dist/agent-src/scripts/roadmap_gates.ts +74 -3
  26. package/dist/agent-src/scripts/update_roadmap_progress.ts +251 -7
  27. package/dist/agent-src/skills/adr-create/SKILL.md +25 -3
  28. package/dist/agent-src/skills/ai-council/SKILL.md +48 -807
  29. package/dist/agent-src/skills/ai-council/references/advanced-modes.md +320 -0
  30. package/dist/agent-src/skills/ai-council/references/cost-and-redaction.md +99 -0
  31. package/dist/agent-src/skills/ai-council/references/execution-modes.md +162 -0
  32. package/dist/agent-src/skills/ai-council/references/output-and-synthesis.md +125 -0
  33. package/dist/agent-src/skills/ai-council/references/procedure.md +143 -0
  34. package/dist/agent-src/skills/decision-review/SKILL.md +18 -0
  35. package/dist/agent-src/skills/quality-tools/SKILL.md +34 -326
  36. package/dist/agent-src/skills/quality-tools/references/js-ts-tools.md +113 -0
  37. package/dist/agent-src/skills/quality-tools/references/php-tools.md +222 -0
  38. package/dist/agent-src/skills/roadmap-management/SKILL.md +15 -409
  39. package/dist/agent-src/skills/roadmap-management/references/archival.md +226 -0
  40. package/dist/agent-src/skills/roadmap-management/references/authoring.md +107 -0
  41. package/dist/agent-src/skills/roadmap-management/references/dashboard.md +47 -0
  42. package/dist/agent-src/skills/roadmap-management/references/lifecycle.md +58 -0
  43. package/dist/agent-src/skills/skill-writing/SKILL.md +34 -570
  44. package/dist/agent-src/skills/skill-writing/references/procedure.md +275 -0
  45. package/dist/agent-src/skills/skill-writing/references/read-only-scripts.md +18 -0
  46. package/dist/agent-src/skills/skill-writing/references/section-patterns.md +298 -0
  47. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  48. package/dist/agent-src/templates/roadmaps.md +45 -2
  49. package/dist/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  50. package/dist/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  51. package/dist/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  52. package/dist/cli/registry.js +1 -1
  53. package/dist/cli/registry.js.map +1 -1
  54. package/dist/cli-delegate/chunk-2GNWGMAL.js +172 -0
  55. package/dist/cli-delegate/{chunk-RLN73PR5.js → chunk-4VR4ORQ6.js} +10 -7
  56. package/dist/cli-delegate/chunk-6EOCNQAM.js +402 -0
  57. package/dist/cli-delegate/{chunk-XURJ4AQC.js → chunk-74E3TKWV.js} +381 -280
  58. package/dist/cli-delegate/{chunk-3UCJWMMP.js → chunk-7F3VYSM2.js} +4 -4
  59. package/dist/cli-delegate/{chunk-BKO2AZ7Z.js → chunk-B2ERU2VA.js} +1 -1
  60. package/dist/cli-delegate/{chunk-NXFWFAYV.js → chunk-G23AT6GD.js} +1 -0
  61. package/dist/cli-delegate/{chunk-63WSP6YZ.js → chunk-GJ5BVHXM.js} +72 -165
  62. package/dist/cli-delegate/{chunk-ZPSXJTLI.js → chunk-JZ4NR2TW.js} +353 -1
  63. package/dist/cli-delegate/chunk-MWRD5YOH.js +100 -0
  64. package/dist/cli-delegate/{chunk-GRRPEPRN.js → chunk-T7L2QK3K.js} +24 -5
  65. package/dist/cli-delegate/{chunk-EVTY7ISR.js → chunk-YM4GZDJG.js} +9 -1
  66. package/dist/cli-delegate/cmd_conformance.js +459 -709
  67. package/dist/cli-delegate/cmd_doctor.js +6 -6
  68. package/dist/cli-delegate/cmd_explain.js +1 -1
  69. package/dist/cli-delegate/cmd_export.js +7 -6
  70. package/dist/cli-delegate/cmd_handoff.js +6 -6
  71. package/dist/cli-delegate/cmd_packs_active.js +1 -1
  72. package/dist/cli-delegate/cmd_preflight.js +2 -2
  73. package/dist/cli-delegate/cmd_prune.js +7 -6
  74. package/dist/cli-delegate/cmd_session_recycle.js +840 -30
  75. package/dist/cli-delegate/cmd_settings_migrate.js +7 -6
  76. package/dist/cli-delegate/cmd_settings_set.js +1 -1
  77. package/dist/cli-delegate/cmd_sync.js +7 -6
  78. package/dist/cli-delegate/cmd_uninstall.js +9 -7
  79. package/dist/cli-delegate/cmd_update.js +1 -1
  80. package/dist/cli-delegate/cmd_upgrade.js +1 -1
  81. package/dist/cli-delegate/cmd_validate.js +7 -6
  82. package/dist/discovery/deprecation-report.md +1 -1
  83. package/dist/discovery/discovery-manifest.json +72 -29
  84. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  85. package/dist/discovery/discovery-manifest.summary.md +4 -4
  86. package/dist/discovery/dormancy-report.md +1 -1
  87. package/dist/discovery/orphan-report.md +1 -1
  88. package/dist/discovery/packs.json +10 -8
  89. package/dist/discovery/trust-report.md +3 -3
  90. package/dist/discovery/workspaces.json +8 -6
  91. package/dist/hooks/dispatch.js +3127 -2022
  92. package/dist/install/claudePathsPlan.js +187 -0
  93. package/dist/install/claudePathsPlan.js.map +1 -0
  94. package/dist/install/hostLayerFingerprint.js +136 -0
  95. package/dist/install/hostLayerFingerprint.js.map +1 -0
  96. package/dist/install/install.mjs +553 -425
  97. package/dist/install/partitionEligibility.js +276 -0
  98. package/dist/install/partitionEligibility.js.map +1 -0
  99. package/dist/mcp/registry-manifest.json +3 -3
  100. package/dist/mcp/server.json +2 -2
  101. package/dist/mcp/server.mjs +1744 -1191
  102. package/dist/router.json +1 -1
  103. package/dist/scripts/_lib/agent_src.js +41 -0
  104. package/dist/scripts/_lib/agent_src.js.map +1 -1
  105. package/dist/scripts/_lib/install_layout.js +83 -0
  106. package/dist/scripts/_lib/install_layout.js.map +1 -0
  107. package/dist/scripts/_lib/installed_lock.js +444 -0
  108. package/dist/scripts/_lib/installed_lock.js.map +1 -0
  109. package/dist/scripts/condense.js +13 -164
  110. package/dist/scripts/condense.js.map +1 -1
  111. package/dist/server/schemas/settings.js +4 -0
  112. package/dist/server/schemas/settings.js.map +1 -1
  113. package/docs/contracts/settings-classes.md +12 -6
  114. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +8 -3
  115. package/docs/guidelines/agent-infra/context-hygiene-mechanics.md +26 -0
  116. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +49 -0
  117. package/docs/guidelines/agent-infra/memory-access.md +17 -0
  118. package/docs/guidelines/agent-infra/recurring-criticism-mechanics.md +96 -0
  119. package/package.json +1 -1
  120. package/src/agent-src/scripts/archival_sweep.ts +117 -0
  121. package/src/agent-src/scripts/archive_completed_roadmaps.ts +19 -3
  122. package/src/agent-src/scripts/gate_budget.ts +282 -0
  123. package/src/agent-src/scripts/gate_execute.ts +72 -31
  124. package/src/agent-src/scripts/roadmap_gates.ts +74 -3
  125. package/src/agent-src/scripts/update_roadmap_progress.ts +251 -7
  126. package/src/agent-src/templates/scripts/telemetry/flush_sender.mjs +163 -0
  127. package/src/agent-src/templates/scripts/telemetry/remote.ts +160 -7
  128. package/src/agent-src/templates/scripts/telemetry/transport.ts +169 -0
  129. package/src/config/agent-settings.template.yml +27 -2
  130. package/src/config/ci-local-parity.yml +18 -0
  131. package/src/config/estate-count-budget.json +367 -3
  132. package/src/config/gate-violation-baselines.json +41 -41
  133. package/src/config/pack-size-budget.json +3 -2
  134. package/src/config/rule-activation-census.json +14 -6
  135. package/src/config/rule-stub-ceilings.json +19 -6
  136. package/src/install/claudePathsPlan.ts +196 -0
  137. package/src/install/hostLayerFingerprint.ts +145 -0
  138. package/src/install/partitionEligibility.ts +325 -0
  139. package/src/scripts/_cli/cmd_session_recycle.ts +9 -1
  140. package/src/scripts/_dispatch.bash +6 -1
  141. package/src/scripts/_lib/ac_heading.ts +30 -0
  142. package/src/scripts/_lib/agent_src.ts +42 -0
  143. package/src/scripts/_lib/bench_ab_pinned_repo.ts +162 -0
  144. package/src/scripts/_lib/bench_ab_workspace.ts +10 -1
  145. package/src/scripts/_lib/envelope_grounding.ts +38 -8
  146. package/src/scripts/_lib/installed_lock.ts +50 -5
  147. package/src/scripts/_lib/layer_overlap_notice.ts +85 -0
  148. package/src/scripts/_lib/md_section.ts +129 -0
  149. package/src/scripts/_lib/scope_guard.sh +86 -4
  150. package/src/scripts/_lib/self_repair.ts +19 -0
  151. package/src/scripts/_lib/self_repair_class_b.ts +171 -0
  152. package/src/scripts/_lib/subagent_capsule.ts +81 -1
  153. package/src/scripts/adr/regenerate_index.ts +13 -2
  154. package/src/scripts/adr_cite_check.ts +424 -0
  155. package/src/scripts/ai_council/cli_help.ts +55 -0
  156. package/src/scripts/before_complete_hook.ts +153 -28
  157. package/src/scripts/bench_ab_v2_complexity.ts +27 -7
  158. package/src/scripts/check_adr_frontmatter.ts +187 -1
  159. package/src/scripts/check_references.ts +60 -0
  160. package/src/scripts/check_release_published.ts +32 -11
  161. package/src/scripts/check_single_delivery.ts +517 -0
  162. package/src/scripts/ci_settle.ts +170 -0
  163. package/src/scripts/condense.ts +20 -170
  164. package/src/scripts/council_cli.ts +3 -3
  165. package/src/scripts/dispatch_r2_reviewer.ts +6 -14
  166. package/src/scripts/hook_manifest.json +1 -1
  167. package/src/scripts/hook_manifest.yaml +77 -14
  168. package/src/scripts/hooks/block_no_verify.ts +61 -10
  169. package/src/scripts/hooks/block_unauthorized_git.ts +1 -1
  170. package/src/scripts/hooks/concern_registry.ts +4 -0
  171. package/src/scripts/hooks/rtk_wrap_hook.ts +8 -3
  172. package/src/scripts/hooks/run_continuation_hook.ts +887 -68
  173. package/src/scripts/hooks/ship_diff_volume_hook.ts +81 -15
  174. package/src/scripts/hooks/source_first_gate_hook.ts +464 -0
  175. package/src/scripts/hooks/state_io.ts +782 -20
  176. package/src/scripts/hooks/subagent_ledger_hook.ts +44 -15
  177. package/src/scripts/hooks/telemetry_flush_hook.ts +139 -0
  178. package/src/scripts/hooks/telemetry_self_repair.ts +128 -0
  179. package/src/scripts/hooks/telemetry_usage_hook.ts +22 -4
  180. package/src/scripts/hooks/turn_end_gate_hook.ts +81 -15
  181. package/src/scripts/install.sh +12 -2
  182. package/src/scripts/install.ts +16 -11
  183. package/src/scripts/language_mirror_hook.ts +561 -25
  184. package/src/scripts/lint_framework_leakage_allowlist.json +2 -2
  185. package/src/scripts/lint_plan_risk_register.ts +4 -3
  186. package/src/scripts/lint_skill_router_head.ts +4 -4
  187. package/src/scripts/mcp_server/tools.ts +2 -2
  188. package/src/scripts/memory_consolidation.ts +189 -0
  189. package/src/scripts/memory_eviction.ts +384 -0
  190. package/src/scripts/new_roadmap.ts +136 -0
  191. package/src/scripts/pack_mcp_content.ts +20 -5
  192. package/src/scripts/release.ts +37 -38
  193. package/src/scripts/report_layer_overlap.ts +78 -0
  194. package/src/scripts/report_memory_pointers.ts +622 -0
  195. package/src/scripts/self_repair_hook.ts +37 -5
  196. package/src/scripts/session_register_hook.ts +115 -15
  197. package/src/scripts/skill_usage_collect.ts +11 -1
  198. package/src/scripts/skill_usage_report.ts +278 -8
  199. package/src/scripts/sync_pr_branch.ts +7 -0
  200. package/src/server/schemas/settings.ts +8 -0
  201. package/dist/cli-delegate/chunk-EJITJBJX.js +0 -172
  202. package/dist/cli-delegate/chunk-PZWRPRWP.js +0 -38
  203. /package/dist/cli-delegate/{chunk-RYNADUFG.js → chunk-LWTYTX6F.js} +0 -0
@@ -12,13 +12,29 @@
12
12
  # Verdicts (per the roadmap contract):
13
13
  # OK — no install at the other scope; the install can proceed.
14
14
  # WARN — install at the other scope, SAME version as the one being installed.
15
- # Same content; duplicate registration but no drift.
15
+ # No drift — and NOT free. The host loads both registrations with no
16
+ # dedup, so every shared artefact is delivered twice. This verdict used
17
+ # to read "Same content; duplicate registration but no drift", which
18
+ # classified duplication by DRIFT RISK and never by CONTEXT COST, and
19
+ # so taught every reader that identical copies cost nothing. Measured
20
+ # 2026-08-19 on a freshly regenerated maintainer projection: 110 rules
21
+ # and 290 skills delivered twice, 203,873 tok of standing rule prose
22
+ # against a 110,000 cap. See ADR-236 and
23
+ # agents/evidence/analysis/single-delivery-partition-census.md.
16
24
  # DRIFT — install at the other scope, DIFFERENT version (or unreadable).
17
25
  # Drift will produce the 2026-05-25 failure mode (stale frontmatter
18
26
  # registered alongside fresh frontmatter).
19
27
  #
20
28
  # Output shape (line-oriented, parseable by install.sh):
21
- # <verdict>\t<tool-id>\t<other-scope-path>\t<other-version>\t<this-version>
29
+ # <verdict>\t<tool-id>\t<other-scope-path>\t<other-version>\t<this-version>[\t<overlap>]
30
+ #
31
+ # Field 6 (<overlap>) is emitted on WARN lines only: the number of entry names
32
+ # present in BOTH scope directories — what a same-version duplicate registration
33
+ # actually delivers twice. It is APPENDED rather than inserted, so every existing
34
+ # field-indexed consumer keeps working unchanged. `-1` means the count could not
35
+ # be taken — the scope being installed to does not exist yet, or a directory is
36
+ # unreadable — which is reported rather than silently rendered as 0. See
37
+ # `count_overlap` for why the two are different facts.
22
38
  #
23
39
  # A final summary line is emitted:
24
40
  # SUMMARY\t<verdict>\t<count-OK>\t<count-WARN>\t<count-DRIFT>
@@ -67,6 +83,44 @@ dir_nonempty() {
67
83
  [[ -d "$1" ]] && [[ -n "$(ls -A "$1" 2>/dev/null | head -1)" ]]
68
84
  }
69
85
 
86
+ # Count entry names present in BOTH scope directories — what a same-version
87
+ # duplicate registration actually delivers twice, which is the cost the WARN
88
+ # verdict used to omit.
89
+ #
90
+ # Returns -1 rather than 0 when the count cannot be taken, because the two are
91
+ # different facts and reporting "0 overlap" for "could not look" is how a gate
92
+ # starts reading as coverage.
93
+ #
94
+ # R2 review corrected the docstring twice over. The copilot probe was named here
95
+ # as the concrete -1 case; it is UNREACHABLE, because `dir_nonempty`'s `[[ -d ]]`
96
+ # test short-circuits a single-file path to OK before probe_tool ever reaches a
97
+ # version comparison. The reachable cases are: the scope being installed TO does
98
+ # not exist yet (a first install at this scope — genuinely no overlap to count,
99
+ # not a failure), and a directory that exists but cannot be read.
100
+ #
101
+ # The unreadable case used to return 0 — the precise failure this function's own
102
+ # docstring claimed to prevent, because `cd` failed inside the process
103
+ # substitution and `comm` compared two empty streams. It is now detected before
104
+ # the comparison.
105
+ count_overlap() {
106
+ local a="$1" b="$2" n
107
+ if [[ ! -d "$a" ]] || [[ ! -d "$b" ]]; then
108
+ printf '%s' '-1'
109
+ return 0
110
+ fi
111
+ # Readability is checked explicitly: `ls` on an unreadable directory fails,
112
+ # and inside `<(...)` that failure is invisible to the caller.
113
+ if ! ls -A "$a" >/dev/null 2>&1 || ! ls -A "$b" >/dev/null 2>&1; then
114
+ printf '%s' '-1'
115
+ return 0
116
+ fi
117
+ n="$(comm -12 \
118
+ <(ls -A "$a" 2>/dev/null | sort) \
119
+ <(ls -A "$b" 2>/dev/null | sort) \
120
+ 2>/dev/null | wc -l | tr -d ' ')" || n=''
121
+ printf '%s' "${n:--1}"
122
+ }
123
+
70
124
  # Per-tool probe definitions: tool-id, user-scope path, project-scope path.
71
125
  # Project-scope path is relative to "$TARGET_DIR" (or "$HOME/<proj>" for user).
72
126
  probe_tool() {
@@ -111,10 +165,38 @@ probe_tool() {
111
165
  local this_ver
112
166
  this_ver="$(this_version)"
113
167
 
168
+ # Field 6 is the duplicate-registration count, and it is printed on BOTH
169
+ # verdicts. It used to appear only on WARN, which required the two scopes to
170
+ # report the SAME version — and `installed_version_at` returns `unknown` for
171
+ # `~/.claude/…` because a host directory has no `package.json` beside it. So
172
+ # every real cross-scope install classified as DRIFT and the count never
173
+ # printed at all: the instrumentation was live and unreachable, which reads as
174
+ # coverage in a diff and is strictly worse than absent.
175
+ #
176
+ # Resolved 2026-08-20, road-to-single-delivery Phase 5, blocker
177
+ # `warn-path-unreachable-without-version-marker`, option (b). The council had
178
+ # already put the reasoning on record in the round that decided the partition:
179
+ # *"overlap reporting should not depend on version equality in the first
180
+ # place. Duplicate files can and should be reported whether versions match,
181
+ # drift, or are unknown."* A duplicate is a duplicate at any version pair.
182
+ #
183
+ # (a) — having the installer stamp a version beside every host layer — was the
184
+ # larger change and is NOT what shipped: it alters a consumer-facing surface to
185
+ # repair a reporting line. (c) — treating `unknown` as "same version" for the
186
+ # overlap half — was rejected because it weakens the DRIFT block itself.
187
+ #
188
+ # Wire compatibility: the DRIFT line gains a SIXTH field. The only structured
189
+ # consumer (`src/server/routes/wizard.ts`) destructures the first five and
190
+ # ignores the rest, so this is additive.
191
+ local overlap
192
+ overlap="$(count_overlap "$other_path" "$this_scope_path")"
114
193
  if [[ "$other_ver" == "unknown" ]] || [[ "$this_ver" == "unknown" ]] || [[ "$other_ver" != "$this_ver" ]]; then
115
- printf 'DRIFT\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver"
194
+ printf 'DRIFT\t%s\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver" "$overlap"
116
195
  else
117
- printf 'WARN\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver"
196
+ # Same version, so no drift — but the duplicate registration is delivered
197
+ # twice, and field 6 is what makes that cost visible at the moment of the
198
+ # decision instead of only in a token census nobody runs.
199
+ printf 'WARN\t%s\t%s\t%s\t%s\t%s\n' "$tool" "$other_path" "$other_ver" "$this_ver" "$overlap"
118
200
  fi
119
201
  }
120
202
 
@@ -338,6 +338,25 @@ const COMPLAINT_PATTERNS: readonly RegExp[] = [
338
338
  /\byou (didn'?t|did not|failed to|ignored|forgot)\b/i,
339
339
  /\bthat'?s (wrong|not right|incorrect)\b/i,
340
340
  /\byou (worked|did (it|that)) (wrong|incorrectly)\b/i,
341
+ // Recurrence phrasings. A restated complaint often carries none of the fault
342
+ // words above — "das habe ich dir schon dreimal gesagt" names no fault at
343
+ // all — so without these the SECOND and THIRD report of a defect were
344
+ // silently not records, which is exactly when the occurrences counter
345
+ // carries the most signal.
346
+ //
347
+ // Every branch requires an explicit COUNT word next to a saying-verb, and
348
+ // that is the precision knob: "nochmal" / "wieder" / "again" are deliberately
349
+ // NOT counts, because a bare repetition word fires on a large share of
350
+ // ordinary prompts ("mach das nochmal") and a spurious record becomes a
351
+ // spurious PR. Ordinals and numerals only.
352
+ /\b(schon|bereits)[^.!?]{0,30}(zum (zweiten|dritten|vierten|fünften) mal|(zwei|drei|vier|fünf)mal|mehrmals|mehrfach|\d+ ?mal)[^.!?]{0,20}(gesagt|erwähnt|geschrieben|angesprochen)\b/i,
353
+ /\b(sage|sag|sagte)[^.!?]{0,30}(zum (zweiten|dritten|vierten|fünften) mal|(zwei|drei|vier|fünf)mal|mehrmals|mehrfach|\d+ ?mal)\b/i,
354
+ /\bwie oft (soll|muss|müssen)\b/i,
355
+ // Typographic AND ASCII apostrophe: the curly form is what a real prompt
356
+ // carries, and matching only the ASCII one is a dead branch on live input.
357
+ /\bi ?(have|['’]ve)? ?already (told|said)\b/i,
358
+ /\b(told|said) (you )?(this|that|it)? ?(\w+|\d+) times?\b/i,
359
+ /\bhow many times (do|must|should) i\b/i,
341
360
  ];
342
361
 
343
362
  // Phrasings that MATCH a complaint pattern but are not complaints — a curious
@@ -0,0 +1,171 @@
1
+ /**
2
+ * The Class-B path (road-to-org-telemetry Phase 5, step 5.2).
3
+ *
4
+ * Class A is the automatic half: structural fields, no content, shipped under
5
+ * recorded org consent. Class B is an abstracted CASE — what was expected
6
+ * versus what happened, and which artefacts were involved — and it ships only
7
+ * on explicit per-case approval of the exact text that would leave.
8
+ *
9
+ * THE GATE IS THE HUMAN READING THE OUTBOUND TEXT, NOT A FILTER. Rank 1 of the
10
+ * roadmap risk register is "an abstraction miss ships a path or an identifier
11
+ * out of a consumer project", and its stated mitigation is per-case review of
12
+ * the exact outbound text rather than a filter heuristic. So this module's job
13
+ * is to make the exact bytes visible and then refuse to release them unless the
14
+ * approval names those same bytes. `approve` takes a digest of the rendered
15
+ * text; a text that changed after the human read it no longer matches, and the
16
+ * release refuses rather than shipping something nobody approved.
17
+ *
18
+ * SILENT SHIPPING IS PERMANENTLY OUT OF SCOPE, and the step says so in those
19
+ * words. There is deliberately no "auto-approve", no allow-list of
20
+ * self-evidently safe classes, and no timeout that approves by default.
21
+ *
22
+ * TWO EXISTING PRIMITIVES ARE REUSED RATHER THAN RE-INVENTED:
23
+ * - `renderReport` is "the existing symptom format" the step names.
24
+ * - `egressBlockedReason` is the audited privacy floor — the same one the
25
+ * local corpus write and the upstream release already pass through. It
26
+ * REFUSES rather than rewriting, which is the property that makes it a gate;
27
+ * a module that silently scrubbed and shipped would be a soft gate wearing
28
+ * a hard gate's name.
29
+ *
30
+ * WHAT THIS MODULE DOES NOT DO: send anything. Transport and sink-side storage
31
+ * are Phase 5 step 5.3, transferred with `sink-choice` — no sink exists for an
32
+ * approved case to travel to. `ClassBRecord` is therefore built, gated and
33
+ * held; the field that carries the text is `case_text` and it is a typed
34
+ * member of a typed record, never a string interpolated into a prompt.
35
+ */
36
+ import * as crypto from 'node:crypto';
37
+
38
+ import {
39
+ type DefectRecord,
40
+ egressBlockedReason,
41
+ renderReport,
42
+ } from './self_repair.js';
43
+
44
+ /** Schema version for the Class-B envelope, independent of Class A. */
45
+ export const CLASS_B_SCHEMA_VERSION = 1;
46
+
47
+ /**
48
+ * One approved case.
49
+ *
50
+ * `case_text` is the ONLY free-form member and it exists on purpose: Class B
51
+ * is defined as an abstracted case, so a Class-B record without text would be
52
+ * a Class-A record. The controls are that it is (a) a named, typed field
53
+ * rather than an open `payload`, (b) present only after `approve` matched a
54
+ * digest a human read, and (c) documented as data end to end — see
55
+ * `assertNeverInterpolated`.
56
+ */
57
+ export interface ClassBRecord {
58
+ schema_version: number;
59
+ record_class: 'case';
60
+ /** Detector / intake class, from the pinned vocabulary. */
61
+ defect_class: string;
62
+ /** Fingerprint of the local record this case abstracts. */
63
+ fingerprint: string;
64
+ /** SHA-256 of `case_text`, so a consumer can verify what was approved. */
65
+ case_digest: string;
66
+ /** The abstracted case, exactly as the human approved it. */
67
+ case_text: string;
68
+ }
69
+
70
+ export type ClassBRefusal =
71
+ | { ok: false; reason: string; kind: 'privacy-floor' | 'digest-mismatch' | 'empty' };
72
+
73
+ export type ClassBResult = { ok: true; record: ClassBRecord } | ClassBRefusal;
74
+
75
+ /** SHA-256 hex of the exact bytes a human is asked to approve. */
76
+ export function caseDigest(text: string): string {
77
+ return crypto.createHash('sha256').update(text, 'utf-8').digest('hex');
78
+ }
79
+
80
+ /**
81
+ * Render the exact outbound text for one record, or refuse.
82
+ *
83
+ * Refusal comes from the audited privacy floor and is returned as a reason
84
+ * string for the caller to show. It is NOT downgraded to a scrub-and-continue:
85
+ * a record whose evidence trips the floor stays local, which is the same
86
+ * verdict the upstream release path already reaches.
87
+ */
88
+ export function renderCase(
89
+ record: DefectRecord,
90
+ repoRoot?: string | null,
91
+ ): { ok: true; text: string; digest: string } | ClassBRefusal {
92
+ const blocked = egressBlockedReason(record, repoRoot ?? null);
93
+ if (blocked !== null) {
94
+ return { ok: false, reason: blocked, kind: 'privacy-floor' };
95
+ }
96
+ // `issue` is the route whose rendering carries no patch and no repository
97
+ // path — the narrowest of the existing shapes, and the right one for a
98
+ // case that travels to a telemetry sink rather than to a code host.
99
+ const text = renderReport(record, 'issue');
100
+ if (text.trim() === '') {
101
+ return { ok: false, reason: 'nothing to send', kind: 'empty' };
102
+ }
103
+ return { ok: true, text, digest: caseDigest(text) };
104
+ }
105
+
106
+ /**
107
+ * Approve one rendered case for release.
108
+ *
109
+ * `approved_digest` is what the human saw. If the text has changed since —
110
+ * a new occurrence folded in, a re-render, anything — the digests differ and
111
+ * this refuses. That is the difference between approving a case and approving
112
+ * a category.
113
+ */
114
+ export function approve(
115
+ record: DefectRecord,
116
+ text: string,
117
+ approved_digest: string,
118
+ ): ClassBResult {
119
+ const actual = caseDigest(text);
120
+ if (actual !== approved_digest) {
121
+ return {
122
+ ok: false,
123
+ kind: 'digest-mismatch',
124
+ reason:
125
+ 'the approved text is not the text that would be sent — '
126
+ + 're-render, read it again, and approve the new digest',
127
+ };
128
+ }
129
+ return {
130
+ ok: true,
131
+ record: {
132
+ schema_version: CLASS_B_SCHEMA_VERSION,
133
+ record_class: 'case',
134
+ defect_class: record.defect_class,
135
+ fingerprint: record.fingerprint,
136
+ case_digest: actual,
137
+ case_text: text,
138
+ },
139
+ };
140
+ }
141
+
142
+ /**
143
+ * Serialise an approved case as ONE JSON object.
144
+ *
145
+ * This is the whole of "quoted, typed data" (step 5.3's repository-side half):
146
+ * the text is a JSON string value, so a downstream reader parses it into a
147
+ * field. Nothing here concatenates it into a sentence, a prompt, or a shell
148
+ * argument, and rank 2 of the risk register — a crafted complaint steering a
149
+ * generated change — is what that discipline is for.
150
+ */
151
+ export function serialiseCase(record: ClassBRecord): string {
152
+ return JSON.stringify(record);
153
+ }
154
+
155
+ /**
156
+ * Guard for a downstream consumer: refuse a prompt-shaped use of case text.
157
+ *
158
+ * Deliberately a narrow, honest tool. It cannot stop a caller that never asks;
159
+ * what it does is give the generation step in Phase 6 something to call, so
160
+ * "reads taxonomy fields only, never Class-B free text" is a check rather than
161
+ * a convention. The intended use is at the boundary where a record becomes
162
+ * model input.
163
+ */
164
+ export function assertNeverInterpolated(candidate: string, record: ClassBRecord): void {
165
+ if (record.case_text !== '' && candidate.includes(record.case_text)) {
166
+ throw new Error(
167
+ 'Class-B case text must reach a model as quoted data, never as part '
168
+ + 'of an instruction string — pass the record, not a concatenation',
169
+ );
170
+ }
171
+ }
@@ -120,6 +120,76 @@ function isShortLine(value: unknown, max: number): value is string {
120
120
  return typeof value === 'string' && value.length > 0 && value.length <= max && !value.includes('\n');
121
121
  }
122
122
 
123
+ /**
124
+ * Is this a path ref rather than prose?
125
+ *
126
+ * The discriminator is **whitespace**, and it is deliberately the only one.
127
+ * A path ref is one token; a prose sentence is several words. Anything
128
+ * narrower — a character class, a required extension, a mandatory `/` —
129
+ * rejects entries this tree really uses: a bare directory (`docs`), a sibling
130
+ * worktree (`../other-worktree/`), a dotfile. Anything wider stops telling the
131
+ * two shapes apart at all.
132
+ *
133
+ * This is a SHAPE check, never an existence or semantics check. A single-token
134
+ * value that is not a real path passes and then simply never matches anything,
135
+ * which is correct: the failure this closes is a field whose own documentation
136
+ * says "a list of path refs" silently accepting full sentences.
137
+ *
138
+ * **It is not a matcher, and a consumer must not mistake it for one.** It
139
+ * answers "is this one token", never "does this entry cover that write target".
140
+ * It admits a bare directory, a trailing-slash relative ref, a glob and a
141
+ * `file:line` ref; normalisation, directory-prefix matching and glob matching
142
+ * are deliberately unspecified here, because they are a separate decision that
143
+ * belongs to whatever ships the comparison. Reuse this for what VALIDATES;
144
+ * decide separately what MATCHES.
145
+ *
146
+ * **Stated default, not a measured optimum:** zero tracked paths in this repo
147
+ * contain whitespace, so the rule costs nothing here. *Revisit-if* a consumer
148
+ * legitimately needs a path containing a space — that needs a quoting
149
+ * convention, not a wider character class.
150
+ */
151
+ export function isPathRef(value: unknown): value is string {
152
+ return typeof value === 'string' && value.length > 0 && !/\s/.test(value);
153
+ }
154
+
155
+ /**
156
+ * `checkList` plus the path-ref shape, for a field whose own documentation
157
+ * calls its entries path refs. Names the offending entries: "one of your
158
+ * twelve entries is wrong" is not an actionable error.
159
+ *
160
+ * The shape report is suppressed for exactly one reason — the same entries
161
+ * already failed the per-entry budget, where reporting both says the same thing
162
+ * twice. It is deliberately NOT suppressed by the entry-COUNT error, which is
163
+ * orthogonal: a 41-entry list of prose sentences must report both problems in
164
+ * one pass, or the author trims to 40, re-validates, and only then learns the
165
+ * entries were prose — an extra refuse-and-repair round-trip on the write path
166
+ * this contract points at as the repair mechanism.
167
+ */
168
+ function checkPathRefList(
169
+ errors: string[],
170
+ field: string,
171
+ value: unknown,
172
+ max: number,
173
+ required: boolean,
174
+ ): void {
175
+ checkList(errors, field, value, max, required);
176
+ if (!Array.isArray(value)) return;
177
+ // Same-entry duplicate only: an entry that failed the budget is already
178
+ // reported, and its shape adds nothing a reader can act on separately.
179
+ if (value.some((x) => !isShortLine(x, max))) return;
180
+
181
+ const bad = value.filter((x) => !isPathRef(x));
182
+ if (bad.length === 0) return;
183
+
184
+ const shown = bad.slice(0, 3).map((x) => JSON.stringify(String(x))).join(', ');
185
+ errors.push(
186
+ `${field} carries ${bad.length} entr${bad.length === 1 ? 'y' : 'ies'} that are prose, not path refs: ` +
187
+ `${shown}${bad.length > 3 ? ', …' : ''} — write the path (src/generated/api.ts, ../other-worktree/) ` +
188
+ 'or drop the entry; a sentence naming a file cannot be compared against a write target, and being ' +
189
+ 'comparable is the whole reason this is a field rather than a line of prose',
190
+ );
191
+ }
192
+
123
193
  function checkList(
124
194
  errors: string[],
125
195
  field: string,
@@ -257,6 +327,14 @@ export interface MainSessionRecycleEnvelope {
257
327
  * adjudication set (a field whose presence is checkable survives where a
258
328
  * doctrine whose effect is unmeasured did not).
259
329
  *
330
+ * The path-ref shape is **enforced** (`isPathRef`), not merely documented.
331
+ * It was documented-only until 2026-08-19, and the gap was not academic:
332
+ * the validator applied the ref CHARACTER BUDGET and no shape rule, so the
333
+ * one real non-empty producer — a composing model — wrote prose sentences
334
+ * naming files, and every one of them validated. A field that accepts
335
+ * whatever prose the writer felt like is `constraints` with extra steps,
336
+ * and nothing downstream can compare it to a write target.
337
+ *
260
338
  * Optional: an envelope with nothing off limits states nothing here, and an
261
339
  * empty list is not a claim that everything is writable.
262
340
  */
@@ -596,7 +674,9 @@ export function validateRecycleEnvelope(input: unknown): string[] {
596
674
  errors.push(...decisionTagErrors(e['decisions']));
597
675
  checkList(errors, 'constraints', e['constraints'], MAX_LINE_CHARS, false);
598
676
  // Path refs, so the REF budget applies — not the prose one `constraints` uses.
599
- checkList(errors, 'do_not_touch', e['do_not_touch'], MAX_REF_CHARS, false);
677
+ // And the SHAPE is checked, not only the budget: the budget alone accepted a
678
+ // prose sentence naming a file, which is what the only real producer wrote.
679
+ checkPathRefList(errors, 'do_not_touch', e['do_not_touch'], MAX_REF_CHARS, false);
600
680
  checkList(errors, 'open_worker_envelopes', e['open_worker_envelopes'], MAX_REF_CHARS, false);
601
681
  checkList(errors, 'artifact_paths', e['artifact_paths'], MAX_REF_CHARS, false);
602
682
  checkList(errors, 'suggested_skills', e['suggested_skills'], MAX_REF_CHARS, false);
@@ -23,7 +23,17 @@ const NAMED = /^ADR-(\d{3})-([a-z0-9-]+)\.md$/;
23
23
  const FM = /^---\n([\s\S]*?)\n---/;
24
24
  // ^([a-z_]+):\s*(.+?)\s*$ (MULTILINE)
25
25
  const FIELD = /^([a-z_]+):[ \t]*(.+?)[ \t]*$/gm;
26
- const HEAD = '| # | Title | Status | Date | Supersedes |\n|---|---|---|---|---|';
26
+ /**
27
+ * The index used to render `| # | Title | Status | Date | Supersedes |`, which
28
+ * is forward-only: a dead ADR showed `superseded | —` and the reader learned it
29
+ * was dead but not by what. `superseded_by` was read nowhere despite being a
30
+ * documented field, and `amended_by` had no representation at all — 18 ADRs
31
+ * carry an amendment block and the index could not distinguish one with five
32
+ * amendments from one never touched.
33
+ */
34
+ const HEAD =
35
+ '| # | Title | Status | Date | Supersedes | Superseded by | Amended by |\n' +
36
+ '|---|---|---|---|---|---|---|';
27
37
 
28
38
  type Meta = Record<string, string>;
29
39
  interface Row extends Meta {
@@ -182,7 +192,8 @@ export function row(r: Row): string {
182
192
  const label = 'num' in r ? `ADR-${r.num}` : (r.path as string).slice(0, -3);
183
193
  return (
184
194
  `| [${label}](${r.path}) | ${title} | ${r.status ?? '—'} ` +
185
- `| ${r.date ?? '—'} | ${r.supersedes ?? '—'} |`
195
+ `| ${r.date ?? '—'} | ${r.supersedes ?? '—'} | ${r.superseded_by ?? '—'} ` +
196
+ `| ${r.amended_by ?? '—'} |`
186
197
  );
187
198
  }
188
199