@event4u/agent-config 9.27.0 → 9.29.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 (220) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/AGENTS.md +13 -12
  3. package/CHANGELOG.md +165 -0
  4. package/README.md +1 -1
  5. package/agents/templates/.ai-council.yml.example +55 -24
  6. package/dist/agent-src/commands/analyze/conformance.md +24 -0
  7. package/dist/agent-src/commands/cost/report.md +2 -2
  8. package/dist/agent-src/commands/team/adversarial.md +8 -7
  9. package/dist/agent-src/commands/team/delegate.md +13 -12
  10. package/dist/agent-src/commands/team/review.md +9 -8
  11. package/dist/agent-src/commands/team/status.md +9 -8
  12. package/dist/agent-src/commands/team.md +29 -19
  13. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +88 -2
  14. package/dist/agent-src/contexts/execution/auto-orchestration-activation.md +29 -21
  15. package/dist/agent-src/contexts/execution/host-capability-manifest.md +26 -15
  16. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +28 -3
  17. package/dist/agent-src/contexts/execution/subagent-response-contract.md +54 -0
  18. package/dist/agent-src/contexts/execution/subagent-routing.md +13 -11
  19. package/dist/agent-src/contexts/execution/subagent-spawn-contract.md +25 -0
  20. package/dist/agent-src/contexts/execution/subagent-steering.md +6 -3
  21. package/dist/agent-src/rules/artifact-engagement-recording.md +4 -1
  22. package/dist/agent-src/rules/autonomous-execution.md +1 -1
  23. package/dist/agent-src/rules/brand-source-of-truth.md +33 -6
  24. package/dist/agent-src/rules/context-hygiene.md +3 -1
  25. package/dist/agent-src/rules/council-availability.md +93 -0
  26. package/dist/agent-src/rules/cross-source-consistency.md +3 -0
  27. package/dist/agent-src/rules/delegation-policy.md +38 -10
  28. package/dist/agent-src/rules/design-fidelity.md +3 -0
  29. package/dist/agent-src/rules/evaluator-independence.md +2 -1
  30. package/dist/agent-src/rules/external-code-graph-interop.md +2 -1
  31. package/dist/agent-src/rules/finance-safety-floor.md +1 -1
  32. package/dist/agent-src/rules/git-history-discipline.md +1 -1
  33. package/dist/agent-src/rules/history-discipline.md +3 -1
  34. package/dist/agent-src/rules/legal-safety-floor.md +1 -1
  35. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -1
  36. package/dist/agent-src/rules/onboarding-gate.md +3 -1
  37. package/dist/agent-src/rules/roadmap-ci-steps-policy.md +4 -1
  38. package/dist/agent-src/rules/role-mode-adherence.md +1 -1
  39. package/dist/agent-src/rules/scale-discipline.md +3 -1
  40. package/dist/agent-src/rules/self-repair-loop.md +103 -0
  41. package/dist/agent-src/rules/session-canary.md +2 -1
  42. package/dist/agent-src/rules/settings-ask-protocol.md +8 -7
  43. package/dist/agent-src/rules/strategy-safety-floor.md +1 -1
  44. package/dist/agent-src/rules/token-budget-discipline.md +4 -2
  45. package/dist/agent-src/rules/tool-safety.md +5 -1
  46. package/dist/agent-src/skills/adversarial-review/SKILL.md +1 -1
  47. package/dist/agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  48. package/dist/agent-src/skills/forensics-report/SKILL.md +114 -0
  49. package/dist/agent-src/skills/forensics-report/evals/triggers.json +17 -0
  50. package/dist/agent-src/skills/performance-analysis/SKILL.md +1 -1
  51. package/dist/agent-src/skills/persona-improvement/SKILL.md +1 -1
  52. package/dist/agent-src/skills/premortem/SKILL.md +30 -1
  53. package/dist/agent-src/skills/project-analyzer/SKILL.md +1 -1
  54. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +2 -1
  55. package/dist/agent-src/skills/security-audit/SKILL.md +1 -1
  56. package/dist/agent-src/skills/sequential-thinking/SKILL.md +1 -1
  57. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  58. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +7 -5
  59. package/dist/agent-src/skills/subagent-orchestration/schemas/subagent-status.json +61 -3
  60. package/dist/agent-src/skills/test-case-discovery/SKILL.md +1 -1
  61. package/dist/agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  62. package/dist/agent-src/templates/AGENTS.md +8 -12
  63. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  64. package/dist/agent-src/templates/roadmaps.md +23 -0
  65. package/dist/cli/registry.js +7 -0
  66. package/dist/cli/registry.js.map +1 -1
  67. package/dist/cli-delegate/chunk-66OVWGB5.js +270 -0
  68. package/dist/cli-delegate/{chunk-HJ6DAJHR.js → chunk-LCFJIQ4J.js} +89 -7
  69. package/dist/cli-delegate/{chunk-VSYT7OHO.js → chunk-LVRYBKTI.js} +31 -2
  70. package/dist/cli-delegate/{chunk-DVRS2KVR.js → chunk-MMXA5GY7.js} +10 -17
  71. package/dist/cli-delegate/{chunk-WR6GTRHJ.js → chunk-MP3JT3IU.js} +3 -3
  72. package/dist/cli-delegate/{chunk-3KJIMFTB.js → chunk-NZ5N2XQL.js} +467 -305
  73. package/dist/cli-delegate/{chunk-AIIUU73N.js → chunk-OJM3MBJM.js} +1 -1
  74. package/dist/cli-delegate/{chunk-BCPIOAGV.js → chunk-RCIA67XN.js} +117 -96
  75. package/dist/cli-delegate/chunk-REKCMYOU.js +2991 -0
  76. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  77. package/dist/cli-delegate/cmd_brand_status.js +134 -0
  78. package/dist/cli-delegate/cmd_conformance.js +10 -47
  79. package/dist/cli-delegate/cmd_converge.js +3 -3
  80. package/dist/cli-delegate/cmd_doctor.js +10 -6
  81. package/dist/cli-delegate/cmd_explain.js +80 -330
  82. package/dist/cli-delegate/cmd_export.js +6 -6
  83. package/dist/cli-delegate/cmd_fleet.js +3 -3
  84. package/dist/cli-delegate/cmd_handoff.js +3 -3
  85. package/dist/cli-delegate/cmd_mcp_available.js +433 -0
  86. package/dist/cli-delegate/cmd_migrate.js +3 -3
  87. package/dist/cli-delegate/cmd_packs_active.js +153 -0
  88. package/dist/cli-delegate/cmd_preflight.js +1 -1
  89. package/dist/cli-delegate/cmd_prune.js +6 -6
  90. package/dist/cli-delegate/cmd_route_audit.js +8 -8
  91. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  92. package/dist/cli-delegate/cmd_settings_get.js +266 -0
  93. package/dist/cli-delegate/cmd_settings_migrate.js +6 -6
  94. package/dist/cli-delegate/cmd_settings_set.js +8 -2968
  95. package/dist/cli-delegate/cmd_sync.js +6 -6
  96. package/dist/cli-delegate/cmd_uninstall.js +6 -6
  97. package/dist/cli-delegate/cmd_update.js +4 -4
  98. package/dist/cli-delegate/cmd_upgrade.js +4 -4
  99. package/dist/cli-delegate/cmd_validate.js +6 -6
  100. package/dist/cli-delegate/cmd_versions.js +4 -4
  101. package/dist/discovery/deprecation-report.md +1 -1
  102. package/dist/discovery/discovery-manifest.json +164 -55
  103. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  104. package/dist/discovery/discovery-manifest.summary.md +4 -3
  105. package/dist/discovery/orphan-report.md +1 -1
  106. package/dist/discovery/packs.json +44 -5
  107. package/dist/discovery/trust-report.md +11 -11
  108. package/dist/discovery/workspaces.json +16 -6
  109. package/dist/hooks/dispatch.js +9341 -6131
  110. package/dist/install/install.mjs +739 -524
  111. package/dist/mcp/registry-manifest.json +3 -3
  112. package/dist/mcp/server.json +2 -2
  113. package/dist/mcp/server.mjs +777 -281
  114. package/dist/router.json +1 -1
  115. package/dist/scripts/_lib/agent_settings.js +134 -15
  116. package/dist/scripts/_lib/agent_settings.js.map +1 -1
  117. package/dist/scripts/condense.js +147 -2
  118. package/dist/scripts/condense.js.map +1 -1
  119. package/dist/server/schemas/settings.js +6 -7
  120. package/dist/server/schemas/settings.js.map +1 -1
  121. package/dist/shared/settingsCarveOut.js +0 -9
  122. package/dist/shared/settingsCarveOut.js.map +1 -1
  123. package/dist/shared/settingsClasses.js +11 -9
  124. package/dist/shared/settingsClasses.js.map +1 -1
  125. package/dist/ui/assets/{index-DmR0mbk4.js → index-JDAF00bC.js} +2 -2
  126. package/dist/ui/assets/{index-DmR0mbk4.js.map → index-JDAF00bC.js.map} +1 -1
  127. package/dist/ui/index.html +1 -1
  128. package/docs/contracts/settings-classes.md +68 -17
  129. package/docs/guidelines/agent-infra/memory-access.md +14 -3
  130. package/package.json +1 -1
  131. package/src/agent-src/templates/AGENTS.md +8 -12
  132. package/src/config/agent-settings.template.yml +52 -66
  133. package/src/config/budgets.yml +29 -0
  134. package/src/config/discovery/packs.yml +11 -0
  135. package/src/config/discovery/workspaces.yml +1 -1
  136. package/src/config/evaluator-budgets.json +4 -4
  137. package/src/config/gate-coverage.yml +26 -0
  138. package/src/config/gate-violation-baselines.json +3 -3
  139. package/src/scripts/_cli/cmd_brand_status.ts +174 -0
  140. package/src/scripts/_cli/cmd_conformance.ts +14 -42
  141. package/src/scripts/_cli/cmd_doctor.ts +65 -48
  142. package/src/scripts/_cli/cmd_mcp_available.ts +266 -0
  143. package/src/scripts/_cli/cmd_packs_active.ts +225 -0
  144. package/src/scripts/_cli/cmd_settings_get.ts +324 -0
  145. package/src/scripts/_dispatch.bash +57 -0
  146. package/src/scripts/_lib/agent_settings.ts +140 -16
  147. package/src/scripts/_lib/auto_dispatch.corpus.test.ts +1 -1
  148. package/src/scripts/_lib/auto_dispatch.ts +23 -18
  149. package/src/scripts/_lib/capsule_trigger.ts +124 -0
  150. package/src/scripts/_lib/carrier_divergence.ts +135 -0
  151. package/src/scripts/_lib/host_capability.ts +144 -4
  152. package/src/scripts/_lib/judgment_ladder.ts +382 -0
  153. package/src/scripts/_lib/orchestration_gate.ts +8 -0
  154. package/src/scripts/_lib/orchestration_record.ts +37 -0
  155. package/src/scripts/_lib/orchestration_savings.ts +1 -1
  156. package/src/scripts/_lib/prompt_shape.ts +88 -0
  157. package/src/scripts/_lib/review_skipped_record.ts +102 -0
  158. package/src/scripts/_lib/rule_layer_overlap.ts +254 -0
  159. package/src/scripts/_lib/self_repair.ts +489 -0
  160. package/src/scripts/_lib/self_repair_store.ts +128 -0
  161. package/src/scripts/_lib/subagent_capsule.ts +182 -0
  162. package/src/scripts/_lib/subagent_response.ts +19 -0
  163. package/src/scripts/_lib/subagent_spawn.ts +13 -0
  164. package/src/scripts/_lib/subagent_steering.ts +9 -4
  165. package/src/scripts/_lib/tier_budget_routing.ts +6 -1
  166. package/src/scripts/_lib/transcript_entry.ts +80 -0
  167. package/src/scripts/_lib/worker_budget.ts +37 -2
  168. package/src/scripts/ai_council/clients.ts +76 -54
  169. package/src/scripts/ai_council/config.ts +94 -6
  170. package/src/scripts/ai_council/handoff.ts +113 -0
  171. package/src/scripts/ai_council/modes.ts +19 -4
  172. package/src/scripts/ai_council/orchestrator.ts +105 -0
  173. package/src/scripts/ai_council/quorum.ts +84 -0
  174. package/src/scripts/ai_council/session.ts +44 -0
  175. package/src/scripts/ai_council/transport_resolver.ts +127 -1
  176. package/src/scripts/ai_team/availability.ts +110 -0
  177. package/src/scripts/ai_team/config.ts +34 -14
  178. package/src/scripts/ai_team/team_dispatch.ts +71 -29
  179. package/src/scripts/bench_critic_protocol.ts +396 -0
  180. package/src/scripts/check_branch_freshness.ts +255 -0
  181. package/src/scripts/check_council_config_location.ts +125 -1
  182. package/src/scripts/check_detector_corpus.ts +202 -0
  183. package/src/scripts/check_hook_bundle_freshness.ts +4 -1
  184. package/src/scripts/check_standing_rule_delivery.ts +314 -0
  185. package/src/scripts/condense.ts +162 -2
  186. package/src/scripts/config/profiles.ts +13 -3
  187. package/src/scripts/conformance_scan.ts +272 -24
  188. package/src/scripts/council_availability_hook.ts +188 -0
  189. package/src/scripts/council_cli.ts +527 -32
  190. package/src/scripts/forensics_report.ts +333 -0
  191. package/src/scripts/hook_manifest.yaml +107 -26
  192. package/src/scripts/hooks/block_no_verify.ts +39 -2
  193. package/src/scripts/hooks/block_unauthorized_git.ts +156 -7
  194. package/src/scripts/hooks/concern_registry.ts +10 -0
  195. package/src/scripts/hooks/delegation_nudge_hook.ts +484 -0
  196. package/src/scripts/hooks/end_review_nudge_hook.ts +726 -0
  197. package/src/scripts/hooks/orchestration_record_hook.ts +342 -0
  198. package/src/scripts/install.ts +151 -0
  199. package/src/scripts/language_mirror_hook.ts +30 -37
  200. package/src/scripts/lint_discovery_vocabulary.ts +2 -0
  201. package/src/scripts/lint_never_silent.ts +276 -0
  202. package/src/scripts/lint_no_activation_gates.ts +271 -0
  203. package/src/scripts/measure_scope_dedup.ts +19 -13
  204. package/src/scripts/onboarding_gate_hook.ts +40 -1
  205. package/src/scripts/orchestration_record.ts +12 -1
  206. package/src/scripts/preamble_byte_census.ts +9 -20
  207. package/src/scripts/rebuild_hook_bundle.ts +177 -0
  208. package/src/scripts/release.ts +74 -2
  209. package/src/scripts/report_carrier_divergence.ts +364 -0
  210. package/src/scripts/report_skill_activation.ts +257 -0
  211. package/src/scripts/report_skill_obligation_violations.ts +579 -0
  212. package/src/scripts/routing_doctor.ts +95 -39
  213. package/src/scripts/score_skill_selection.ts +19 -5
  214. package/src/scripts/self_repair_cli.ts +470 -0
  215. package/src/scripts/self_repair_hook.ts +241 -0
  216. package/src/scripts/team_events_spike.ts +271 -0
  217. package/src/scripts/team_review_gate_hook.ts +31 -9
  218. package/src/server/schemas/settings.ts +10 -17
  219. package/src/shared/settingsCarveOut.ts +0 -10
  220. package/src/shared/settingsClasses.ts +11 -9
@@ -0,0 +1,254 @@
1
+ /**
2
+ * Rule-layer overlap detection — is the corpus about to be delivered twice?
3
+ *
4
+ * Claude Code loads `~/.claude/rules/` and `<project>/.claude/rules/` **both**,
5
+ * user layer first, with no dedup between them: a rule present in both is in
6
+ * standing context twice. Measured on a maintainer machine 2026-08-08 — 112
7
+ * global entries + 92 project entries, 91 shared basenames, 176,354 exact-BPE
8
+ * tokens of which 74,137 (42%) redundant. Evidence:
9
+ * `agents/evidence/analysis/standing-rule-delivery-topologies.md`; the host's
10
+ * loading contract: `agents/evidence/analysis/claude-code-rules-dir-contract.md`.
11
+ *
12
+ * This module answers three questions and nothing else:
13
+ *
14
+ * 1. **Do two layers overlap, and by how much?** ({@link compareLayers})
15
+ * 2. **Is the overlap a pure duplicate, or a content skew?** — the difference
16
+ * decides whether suppression is enough or a refresh is also needed.
17
+ * 3. **How is a layer suppressed without deleting it?** — the host's own
18
+ * `claudeMdExcludes` settings key ({@link claudeMdExcludesGlob},
19
+ * {@link mergeClaudeMdExcludes}).
20
+ *
21
+ * The comparison half is pure so it is testable from fixtures; only
22
+ * {@link readRuleLayer} touches the filesystem, and nothing here ever writes or
23
+ * unlinks. Deleting a user's `~/.claude/rules/` would be a Hard-Floor action
24
+ * (`non-destructive-by-default`), so no function in this module can reach one.
25
+ */
26
+
27
+ import * as fs from 'node:fs';
28
+ import * as path from 'node:path';
29
+
30
+ /**
31
+ * Frontmatter keys the installer stamps onto its OWN copies, and which therefore
32
+ * differ between a global (installed) copy and a project (projected) copy of the
33
+ * same rule without either being stale.
34
+ *
35
+ * Kept in lockstep with `install.ts`'s `_set_key(fm_lines, 'package', …)` /
36
+ * `_set_key(fm_lines, 'source_path', …)` pair. Adding a third stamped key there
37
+ * without adding it here makes every rule read as divergent.
38
+ */
39
+ export const INSTALLER_PROVENANCE_KEYS: readonly string[] = ['package', 'source_path'];
40
+
41
+ /** One rule layer: its directory and `basename → text` for every `.md` in it. */
42
+ export interface RuleLayer {
43
+ readonly dir: string;
44
+ readonly files: ReadonlyMap<string, string>;
45
+ }
46
+
47
+ /** What {@link compareLayers} found. Counts are chars; token conversion is the caller's. */
48
+ export interface OverlapReport {
49
+ readonly schema_version: 1;
50
+ /** Basenames present in both layers. */
51
+ readonly overlap: readonly string[];
52
+ /** Of {@link overlap}: bodies equal once the provenance keys are stripped — pure duplicates. */
53
+ readonly duplicate: readonly string[];
54
+ /** Of {@link overlap}: bodies genuinely differ — a content skew. Suppression alone is NOT enough. */
55
+ readonly divergent: readonly string[];
56
+ /** Basenames only in the first (global) layer. */
57
+ readonly global_only: readonly string[];
58
+ /** Basenames only in the second (project) layer. */
59
+ readonly project_only: readonly string[];
60
+ /** Chars the SECOND layer contributes redundantly — what suppressing it recovers. */
61
+ readonly redundant_chars: number;
62
+ }
63
+
64
+ /** The three answers a user may give when an overlap is detected. */
65
+ export type LayerChoice = 'global' | 'project' | 'both-acknowledged';
66
+
67
+ /**
68
+ * Strip the installer's provenance keys from a rule's frontmatter block.
69
+ *
70
+ * Only lines inside the leading `---` … `---` fence are considered, and only
71
+ * those whose key is in {@link INSTALLER_PROVENANCE_KEYS}. A body line that
72
+ * happens to start with `package:` survives — stripping it would make two
73
+ * genuinely different rules compare equal, which is the failure direction that
74
+ * matters here.
75
+ *
76
+ * A file with no frontmatter fence is returned unchanged.
77
+ */
78
+ export function stripProvenance(text: string): string {
79
+ const lines = text.split('\n');
80
+ if (lines[0]?.trim() !== '---') return text;
81
+ let close = -1;
82
+ for (let i = 1; i < lines.length; i++) {
83
+ if (lines[i]?.trim() === '---') {
84
+ close = i;
85
+ break;
86
+ }
87
+ }
88
+ if (close === -1) return text;
89
+ const kept: string[] = [];
90
+ for (let i = 0; i < lines.length; i++) {
91
+ if (i > 0 && i < close) {
92
+ const key = /^([A-Za-z_][A-Za-z0-9_-]*):/.exec(lines[i] ?? '')?.[1];
93
+ if (key !== undefined && INSTALLER_PROVENANCE_KEYS.includes(key)) continue;
94
+ }
95
+ kept.push(lines[i] ?? '');
96
+ }
97
+ return kept.join('\n');
98
+ }
99
+
100
+ /** Sorted basenames of `a` that are absent from `b`. */
101
+ function only_in(a: ReadonlyMap<string, string>, b: ReadonlyMap<string, string>): string[] {
102
+ return [...a.keys()].filter((k) => !b.has(k)).sort();
103
+ }
104
+
105
+ /**
106
+ * Compare two rule layers. Pure — no filesystem, no clock, no environment.
107
+ *
108
+ * `global_layer` is the user-level layer and `project_layer` the project one,
109
+ * because {@link OverlapReport.redundant_chars} is measured on the SECOND
110
+ * argument: it answers "what does suppressing the project layer recover", which
111
+ * is the direction the installer offers first (the project layer is the one it
112
+ * is usually about to write).
113
+ */
114
+ export function compareLayers(
115
+ global_layer: ReadonlyMap<string, string>,
116
+ project_layer: ReadonlyMap<string, string>,
117
+ ): OverlapReport {
118
+ const overlap = [...global_layer.keys()].filter((k) => project_layer.has(k)).sort();
119
+ const duplicate: string[] = [];
120
+ const divergent: string[] = [];
121
+ let redundant_chars = 0;
122
+ for (const name of overlap) {
123
+ const g = stripProvenance(global_layer.get(name) ?? '');
124
+ const p = stripProvenance(project_layer.get(name) ?? '');
125
+ if (g === p) {
126
+ duplicate.push(name);
127
+ redundant_chars += (project_layer.get(name) ?? '').length;
128
+ } else {
129
+ divergent.push(name);
130
+ }
131
+ }
132
+ return {
133
+ schema_version: 1,
134
+ overlap,
135
+ duplicate,
136
+ divergent,
137
+ global_only: only_in(global_layer, project_layer),
138
+ project_only: only_in(project_layer, global_layer),
139
+ redundant_chars,
140
+ };
141
+ }
142
+
143
+ /**
144
+ * Read every `.md` in `dir` into a {@link RuleLayer}, or `null` when the
145
+ * directory does not exist.
146
+ *
147
+ * Symlinks are followed, because the project layer is a symlink projection into
148
+ * `dist/agent-src/rules/` and its CONTENT is what lands in context. An entry
149
+ * whose target is unreadable (a dangling symlink) is skipped rather than
150
+ * throwing — a broken link contributes no bytes to the session either.
151
+ */
152
+ export function readRuleLayer(dir: string): RuleLayer | null {
153
+ let names: string[];
154
+ try {
155
+ names = fs.readdirSync(dir);
156
+ } catch {
157
+ return null;
158
+ }
159
+ const files = new Map<string, string>();
160
+ for (const name of names.filter((n) => n.endsWith('.md')).sort()) {
161
+ try {
162
+ files.set(name, fs.readFileSync(path.join(dir, name), 'utf-8'));
163
+ } catch {
164
+ continue;
165
+ }
166
+ }
167
+ return { dir, files };
168
+ }
169
+
170
+ /**
171
+ * The `claudeMdExcludes` glob that suppresses one rules directory.
172
+ *
173
+ * The host matches this key against ABSOLUTE paths, so the directory is
174
+ * resolved before the `/**` suffix is appended. Documented example from the
175
+ * host's own docs: `"/home/user/monorepo/other-team/.claude/rules/**"`.
176
+ */
177
+ export function claudeMdExcludesGlob(dir: string): string {
178
+ return `${path.resolve(dir).replace(/\/+$/, '')}/**`;
179
+ }
180
+
181
+ /**
182
+ * Append `entry` to an existing `claudeMdExcludes` value, preserving everything
183
+ * already there.
184
+ *
185
+ * `claudeMdExcludes` arrays MERGE across settings layers, so an install that
186
+ * replaced the array would silently drop another layer's exclusions — a
187
+ * different defect from the one being fixed. Non-string members of a
188
+ * hand-edited array are preserved in order; an absent or non-array value starts
189
+ * a fresh single-entry array. Already present → returned unchanged, so the
190
+ * operation is idempotent across re-installs.
191
+ */
192
+ export function mergeClaudeMdExcludes(existing: unknown, entry: string): unknown[] {
193
+ if (!Array.isArray(existing)) return [entry];
194
+ if (existing.includes(entry)) return [...existing];
195
+ return [...existing, entry];
196
+ }
197
+
198
+ /** What the installer should do, given an overlap report and the user's choice. */
199
+ export interface LayerAction {
200
+ /** Which layer this install writes. */
201
+ readonly write: 'global' | 'project' | 'both';
202
+ /** Directory to suppress via `claudeMdExcludes`, or `null` when nothing is suppressed. */
203
+ readonly suppress_dir: string | null;
204
+ /** True when the overlap contains divergent bodies — suppression alone loses obligations. */
205
+ readonly refresh_required: boolean;
206
+ /** One-line human summary; the installer prints it verbatim. */
207
+ readonly note: string;
208
+ }
209
+
210
+ /**
211
+ * Turn a report plus a {@link LayerChoice} into the action to take. Pure.
212
+ *
213
+ * `both-acknowledged` writes both and suppresses nothing — a deliberate
214
+ * two-layer setup is legitimate and the flag exists so the installer does not
215
+ * have to guess. The other two choices name the layer to KEEP and suppress the
216
+ * other; suppression never deletes.
217
+ *
218
+ * `refresh_required` fires on any divergent body: suppressing a layer whose
219
+ * copies differ would drop whichever obligations only the suppressed copy
220
+ * carried, so the caller must refresh before suppressing. This is the guard
221
+ * against the version-skew case, which the 2026-08-08 measurement found absent
222
+ * but nothing prevents later.
223
+ */
224
+ export function decideLayerAction(
225
+ report: OverlapReport,
226
+ choice: LayerChoice,
227
+ global_dir: string,
228
+ project_dir: string,
229
+ ): LayerAction {
230
+ const refresh_required = report.divergent.length > 0;
231
+ const skew = refresh_required
232
+ ? ` ${report.divergent.length} shared rule(s) differ in body — refresh before suppressing, or obligations only the suppressed copy carries are lost.`
233
+ : '';
234
+ if (choice === 'both-acknowledged') {
235
+ return {
236
+ write: 'both',
237
+ suppress_dir: null,
238
+ refresh_required,
239
+ note:
240
+ `Keeping both rule layers by request: ${report.overlap.length} shared rule(s), `
241
+ + `${report.redundant_chars} chars delivered twice per session. Nothing suppressed, nothing deleted.${skew}`,
242
+ };
243
+ }
244
+ const keep = choice === 'global' ? global_dir : project_dir;
245
+ const drop = choice === 'global' ? project_dir : global_dir;
246
+ return {
247
+ write: choice,
248
+ suppress_dir: drop,
249
+ refresh_required,
250
+ note:
251
+ `Keeping ${choice} rule layer (${keep}); suppressing ${drop} via claudeMdExcludes — `
252
+ + `recovers ${report.redundant_chars} chars per session. No file is deleted or rewritten.${skew}`,
253
+ };
254
+ }