avorelo 0.1.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 (258) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +56 -0
  3. package/bin/avorelo +9 -0
  4. package/package.json +135 -0
  5. package/scripts/README.md +40 -0
  6. package/scripts/cco-dashboard.js +252 -0
  7. package/scripts/cco-status.js +430 -0
  8. package/scripts/lib/activation/account-state.js +37 -0
  9. package/scripts/lib/activation/activation-runner.js +546 -0
  10. package/scripts/lib/activation/activation-self-healing.js +480 -0
  11. package/scripts/lib/activation/activation-state.js +83 -0
  12. package/scripts/lib/activation/activation-summary.js +191 -0
  13. package/scripts/lib/activation/adapters/claude-code.js +77 -0
  14. package/scripts/lib/activation/adapters/codex-cli.js +52 -0
  15. package/scripts/lib/activation/adapters/cursor.js +37 -0
  16. package/scripts/lib/activation/adapters/github-agent.js +39 -0
  17. package/scripts/lib/activation/adapters/terminal.js +42 -0
  18. package/scripts/lib/activation/adapters/vscode.js +39 -0
  19. package/scripts/lib/activation/adapters/windsurf.js +37 -0
  20. package/scripts/lib/activation/ai-surface-detector.js +151 -0
  21. package/scripts/lib/activation/connect-account.js +145 -0
  22. package/scripts/lib/activation/detect-environment.js +75 -0
  23. package/scripts/lib/activation/detect-hosts.js +62 -0
  24. package/scripts/lib/activation/format-activation-output.js +109 -0
  25. package/scripts/lib/activation/next-action.js +43 -0
  26. package/scripts/lib/activation/repair-engine.js +219 -0
  27. package/scripts/lib/activation-distribution-readiness.js +507 -0
  28. package/scripts/lib/adapter-conformance.js +176 -0
  29. package/scripts/lib/adapter-readiness.js +417 -0
  30. package/scripts/lib/adapter-safety-boundaries.js +335 -0
  31. package/scripts/lib/adapter-technical-readiness-gate.js +205 -0
  32. package/scripts/lib/agent-access-governance.js +455 -0
  33. package/scripts/lib/agent-enforcement.js +765 -0
  34. package/scripts/lib/agent-policy-profile.js +210 -0
  35. package/scripts/lib/agent-security/action-evaluator.js +507 -0
  36. package/scripts/lib/agent-security/adapter-registry.js +98 -0
  37. package/scripts/lib/agent-security/auto-policy.js +139 -0
  38. package/scripts/lib/agent-security/bounded-scan.js +93 -0
  39. package/scripts/lib/agent-security/enforcement-adapter.js +174 -0
  40. package/scripts/lib/agent-security/enforcement-engine.js +1129 -0
  41. package/scripts/lib/agent-security/file-write-adapter.js +183 -0
  42. package/scripts/lib/agent-security/file-write-rules.js +178 -0
  43. package/scripts/lib/agent-security/index.js +3342 -0
  44. package/scripts/lib/agent-security/instruction-risk.js +181 -0
  45. package/scripts/lib/agent-security/mcp-action-adapter.js +185 -0
  46. package/scripts/lib/agent-security/mcp-action-rules.js +184 -0
  47. package/scripts/lib/agent-security/package-action-adapter.js +175 -0
  48. package/scripts/lib/agent-security/package-action-rules.js +233 -0
  49. package/scripts/lib/agent-security/performance.js +148 -0
  50. package/scripts/lib/agent-security/permission-minimizer.js +403 -0
  51. package/scripts/lib/agent-security/scan-cache.js +74 -0
  52. package/scripts/lib/agent-security/source-trust.js +146 -0
  53. package/scripts/lib/ai-install-prompt.js +288 -0
  54. package/scripts/lib/ai-workspace-hygiene.js +1499 -0
  55. package/scripts/lib/alpha-activation.js +520 -0
  56. package/scripts/lib/alpha-feedback.js +263 -0
  57. package/scripts/lib/alpha-readiness-gate.js +332 -0
  58. package/scripts/lib/anti-gaming.js +169 -0
  59. package/scripts/lib/artifact-health.js +431 -0
  60. package/scripts/lib/attribution.js +180 -0
  61. package/scripts/lib/audit.js +289 -0
  62. package/scripts/lib/avorelo-skill-registry.js +810 -0
  63. package/scripts/lib/batch-jobs.js +71 -0
  64. package/scripts/lib/brain-pack.js +578 -0
  65. package/scripts/lib/brand-boundary.js +424 -0
  66. package/scripts/lib/brand.js +74 -0
  67. package/scripts/lib/browser-capability.js +1048 -0
  68. package/scripts/lib/browser-proof-preflight.js +321 -0
  69. package/scripts/lib/cache-readiness.js +187 -0
  70. package/scripts/lib/canonical-reentry.js +162 -0
  71. package/scripts/lib/capability-packs.js +314 -0
  72. package/scripts/lib/capability-recommender.js +512 -0
  73. package/scripts/lib/capability-registry.js +1059 -0
  74. package/scripts/lib/carry-forward-surfacing.js +194 -0
  75. package/scripts/lib/ccusage-adapter.js +188 -0
  76. package/scripts/lib/company-loop.js +1149 -0
  77. package/scripts/lib/config.js +637 -0
  78. package/scripts/lib/context-acquisition-plan.js +287 -0
  79. package/scripts/lib/context-budget-guard.js +170 -0
  80. package/scripts/lib/context-budget-scanner.js +257 -0
  81. package/scripts/lib/context-optimizer.js +715 -0
  82. package/scripts/lib/context-reduction-plan.js +178 -0
  83. package/scripts/lib/context-safety.js +88 -0
  84. package/scripts/lib/context-savings-engine.js +158 -0
  85. package/scripts/lib/cost-evidence.js +254 -0
  86. package/scripts/lib/cross-host-install-plan.js +308 -0
  87. package/scripts/lib/cross-host-install-readiness.js +237 -0
  88. package/scripts/lib/cross-host-value-flow.js +268 -0
  89. package/scripts/lib/dashboard.js +900 -0
  90. package/scripts/lib/design-partner-feedback.js +346 -0
  91. package/scripts/lib/entitlements.js +100 -0
  92. package/scripts/lib/execution-packet.js +559 -0
  93. package/scripts/lib/experimentation-events.js +547 -0
  94. package/scripts/lib/external-capability-compliance.js +107 -0
  95. package/scripts/lib/external-user-simulation.js +166 -0
  96. package/scripts/lib/failure-recovery-readiness.js +81 -0
  97. package/scripts/lib/failure-recovery.js +419 -0
  98. package/scripts/lib/feedback-intelligence.js +537 -0
  99. package/scripts/lib/feedback-signals.js +205 -0
  100. package/scripts/lib/file-integrity.js +68 -0
  101. package/scripts/lib/fsx.js +127 -0
  102. package/scripts/lib/full-readiness-gate.js +451 -0
  103. package/scripts/lib/guidance-builder.js +174 -0
  104. package/scripts/lib/hook-apply.js +1019 -0
  105. package/scripts/lib/hook-baseline.js +310 -0
  106. package/scripts/lib/hook-config-preview.js +275 -0
  107. package/scripts/lib/hook-contracts.js +290 -0
  108. package/scripts/lib/hook-safety-boundary-readiness.js +80 -0
  109. package/scripts/lib/host-capability-matrix.js +351 -0
  110. package/scripts/lib/host-support-context.js +254 -0
  111. package/scripts/lib/http-hook-action.js +538 -0
  112. package/scripts/lib/install-ai-readiness.js +84 -0
  113. package/scripts/lib/install-intake-risk.js +1037 -0
  114. package/scripts/lib/install-journey-intelligence.js +329 -0
  115. package/scripts/lib/intervention-guidance.js +57 -0
  116. package/scripts/lib/known-limitations.js +115 -0
  117. package/scripts/lib/l8-path-truth.js +146 -0
  118. package/scripts/lib/launch-hardening-gate.js +436 -0
  119. package/scripts/lib/launch-readiness.js +628 -0
  120. package/scripts/lib/learning-memory.js +686 -0
  121. package/scripts/lib/lifecycle-hooks.js +802 -0
  122. package/scripts/lib/local-package-smoke.js +423 -0
  123. package/scripts/lib/local-pricing.js +299 -0
  124. package/scripts/lib/mcp-enforcement.js +311 -0
  125. package/scripts/lib/mcp-least-privilege-policy.js +303 -0
  126. package/scripts/lib/mcp-tool-inventory.js +388 -0
  127. package/scripts/lib/mcp-tool-risk.js +0 -0
  128. package/scripts/lib/memory.js +335 -0
  129. package/scripts/lib/metrics.js +699 -0
  130. package/scripts/lib/micro-proof.js +133 -0
  131. package/scripts/lib/next-run-context.js +436 -0
  132. package/scripts/lib/operating-value.js +1648 -0
  133. package/scripts/lib/optimization-v3.js +122 -0
  134. package/scripts/lib/orchestration/adapters/_shared.js +49 -0
  135. package/scripts/lib/orchestration/adapters/aider.js +18 -0
  136. package/scripts/lib/orchestration/adapters/claude-code.js +35 -0
  137. package/scripts/lib/orchestration/adapters/codex.js +35 -0
  138. package/scripts/lib/orchestration/adapters/gemini-cli.js +18 -0
  139. package/scripts/lib/orchestration/adapters/git.js +25 -0
  140. package/scripts/lib/orchestration/adapters/index.js +31 -0
  141. package/scripts/lib/orchestration/adapters/lm-studio.js +18 -0
  142. package/scripts/lib/orchestration/adapters/ollama.js +18 -0
  143. package/scripts/lib/orchestration/adapters/opencode.js +18 -0
  144. package/scripts/lib/orchestration/adapters/openrouter.js +18 -0
  145. package/scripts/lib/orchestration/adapters/test-runner.js +25 -0
  146. package/scripts/lib/orchestration/cli.js +438 -0
  147. package/scripts/lib/orchestration/execution-manager.js +279 -0
  148. package/scripts/lib/orchestration/handoff.js +314 -0
  149. package/scripts/lib/orchestration/index.js +456 -0
  150. package/scripts/lib/orchestration/inventory.js +47 -0
  151. package/scripts/lib/orchestration/model-discovery.js +498 -0
  152. package/scripts/lib/orchestration/model-profiler.js +170 -0
  153. package/scripts/lib/orchestration/model-profiles.js +252 -0
  154. package/scripts/lib/orchestration/model-refresh-policy.js +72 -0
  155. package/scripts/lib/orchestration/proof-writer.js +349 -0
  156. package/scripts/lib/orchestration/provider-discovery/aider.js +49 -0
  157. package/scripts/lib/orchestration/provider-discovery/claude-code.js +56 -0
  158. package/scripts/lib/orchestration/provider-discovery/codex.js +49 -0
  159. package/scripts/lib/orchestration/provider-discovery/common.js +186 -0
  160. package/scripts/lib/orchestration/provider-discovery/gemini.js +106 -0
  161. package/scripts/lib/orchestration/provider-discovery/lm-studio.js +118 -0
  162. package/scripts/lib/orchestration/provider-discovery/models-dev.js +12 -0
  163. package/scripts/lib/orchestration/provider-discovery/ollama.js +100 -0
  164. package/scripts/lib/orchestration/provider-discovery/opencode.js +47 -0
  165. package/scripts/lib/orchestration/provider-discovery/openrouter.js +44 -0
  166. package/scripts/lib/orchestration/risk-classifier.js +130 -0
  167. package/scripts/lib/orchestration/routing-policy.js +486 -0
  168. package/scripts/lib/orchestration/settings.js +112 -0
  169. package/scripts/lib/orchestration/state.js +165 -0
  170. package/scripts/lib/orchestration/verification-manager.js +138 -0
  171. package/scripts/lib/output-profiles.js +146 -0
  172. package/scripts/lib/package-content-audit.js +368 -0
  173. package/scripts/lib/package-runtime.js +278 -0
  174. package/scripts/lib/plan-surface.js +53 -0
  175. package/scripts/lib/plans.js +2318 -0
  176. package/scripts/lib/policy-provider.js +27 -0
  177. package/scripts/lib/prelaunch-activation-readiness.js +409 -0
  178. package/scripts/lib/prelaunch-evidence-store.js +816 -0
  179. package/scripts/lib/prelaunch-intelligence.js +869 -0
  180. package/scripts/lib/pricing-experiment.js +118 -0
  181. package/scripts/lib/pro-moment-events.js +77 -0
  182. package/scripts/lib/pro-moment-state.js +227 -0
  183. package/scripts/lib/pro-moments.js +1216 -0
  184. package/scripts/lib/product-learning-events.js +629 -0
  185. package/scripts/lib/project-profile.js +555 -0
  186. package/scripts/lib/prompt-compiler.js +280 -0
  187. package/scripts/lib/prompt-lint.js +32 -0
  188. package/scripts/lib/prompt-suggestions.js +52 -0
  189. package/scripts/lib/proof-canonical.js +398 -0
  190. package/scripts/lib/proof-drilldown.js +383 -0
  191. package/scripts/lib/proof-events.js +342 -0
  192. package/scripts/lib/proof-history.js +243 -0
  193. package/scripts/lib/proof-metrics.js +296 -0
  194. package/scripts/lib/proof-outcome-evidence.js +134 -0
  195. package/scripts/lib/proof-receipt.js +335 -0
  196. package/scripts/lib/proof-record.js +461 -0
  197. package/scripts/lib/public-activation-distribution-gate.js +258 -0
  198. package/scripts/lib/public-cli.js +3891 -0
  199. package/scripts/lib/public-distribution-truth.js +211 -0
  200. package/scripts/lib/public-install-claim-checker.js +294 -0
  201. package/scripts/lib/publish-provenance-readiness.js +283 -0
  202. package/scripts/lib/readiness-delta.js +218 -0
  203. package/scripts/lib/readiness-evidence-closure.js +196 -0
  204. package/scripts/lib/reentry-memory-capture.js +241 -0
  205. package/scripts/lib/reentry-memory-retrieval.js +302 -0
  206. package/scripts/lib/reentry-memory-status.js +146 -0
  207. package/scripts/lib/reentry-memory-store.js +178 -0
  208. package/scripts/lib/reentry-state.js +66 -0
  209. package/scripts/lib/release-candidate-bundle.js +166 -0
  210. package/scripts/lib/remediation.js +81 -0
  211. package/scripts/lib/repo-map.js +391 -0
  212. package/scripts/lib/run-improvements-lifecycle.js +330 -0
  213. package/scripts/lib/run-improvements.js +789 -0
  214. package/scripts/lib/runtime-decision-policy.js +387 -0
  215. package/scripts/lib/safe-path-engine.js +705 -0
  216. package/scripts/lib/safe-run-controller.js +887 -0
  217. package/scripts/lib/score.js +262 -0
  218. package/scripts/lib/seamless-enforcement.js +329 -0
  219. package/scripts/lib/seamless-outcome.js +689 -0
  220. package/scripts/lib/seamless-reality-gate.js +5043 -0
  221. package/scripts/lib/security-risk-classifier.js +511 -0
  222. package/scripts/lib/security-scan.js +384 -0
  223. package/scripts/lib/session-context-optimizer.js +1211 -0
  224. package/scripts/lib/session-timing.js +315 -0
  225. package/scripts/lib/skill-hygiene.js +805 -0
  226. package/scripts/lib/skill-packs.js +161 -0
  227. package/scripts/lib/skills-operating-layer.js +580 -0
  228. package/scripts/lib/smart-work-routing.js +768 -0
  229. package/scripts/lib/source-catalog.js +700 -0
  230. package/scripts/lib/status-value-summary.js +32 -0
  231. package/scripts/lib/support-bundle.js +578 -0
  232. package/scripts/lib/task-continuation.js +440 -0
  233. package/scripts/lib/test-helpers.js +15 -0
  234. package/scripts/lib/tier.js +38 -0
  235. package/scripts/lib/token-context-quality-gate.js +370 -0
  236. package/scripts/lib/token-cost-capture.js +187 -0
  237. package/scripts/lib/token-cost-intelligence.js +358 -0
  238. package/scripts/lib/token-efficiency-evidence.js +213 -0
  239. package/scripts/lib/token-evidence.js +699 -0
  240. package/scripts/lib/tokenish.js +17 -0
  241. package/scripts/lib/tool-output-sandbox.js +304 -0
  242. package/scripts/lib/trust-audit.js +136 -0
  243. package/scripts/lib/unified-events.js +396 -0
  244. package/scripts/lib/upgrade-interruption-recovery.js +407 -0
  245. package/scripts/lib/usage-ledger.js +201 -0
  246. package/scripts/lib/value-ledger.js +130 -0
  247. package/scripts/lib/value-proof-calibration.js +531 -0
  248. package/scripts/lib/visual-qa.js +231 -0
  249. package/scripts/lib/voice-alpha.js +29 -0
  250. package/scripts/lib/work-aware-orchestration.js +976 -0
  251. package/scripts/lib/work-control-receipts.js +577 -0
  252. package/scripts/lib/work-ledger.js +1123 -0
  253. package/scripts/lib/work-panel-preview.js +352 -0
  254. package/scripts/lib/workflow-discipline.js +280 -0
  255. package/scripts/lib/workflow-signals.js +419 -0
  256. package/scripts/lib/workspace-map.js +281 -0
  257. package/scripts/lib/workspace-registry.js +1367 -0
  258. package/scripts/lib/workspace-resolver.js +480 -0
@@ -0,0 +1,805 @@
1
+ "use strict";
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { parseSkillFile } = require("./avorelo-skill-registry");
6
+
7
+ const HYGIENE_CONTRACT = "avorelo.skillHygiene.v1";
8
+ const HYGIENE_SCHEMA_VERSION = 1;
9
+ const MAX_ACCEPTED_CONTRIBUTIONS = 3;
10
+ const MAX_CONTRIBUTION_TOKENS = 30;
11
+ const DESCRIPTION_OVERLAP_THRESHOLD = 0.45;
12
+
13
+ const ALLOWED_CONTRIBUTION_TYPES = new Set([
14
+ "verification_check",
15
+ "edge_case",
16
+ "required_output_field",
17
+ "security_caveat",
18
+ "compact_example",
19
+ ]);
20
+
21
+ const BLOCKED_CONTRIBUTION_TYPES = new Set([
22
+ "duplicate_workflow",
23
+ "new_main_workflow_authority",
24
+ "new_risky_tool_mcp_authority",
25
+ "policy_conflicting_instruction",
26
+ "broad_execution_instruction",
27
+ "long_context_dump",
28
+ "workflow_step",
29
+ ]);
30
+
31
+ const RISKY_CONTRIBUTION_REASON_CODES = new Set([
32
+ "conflicting_invocation_policy",
33
+ "conflicting_tool_policy",
34
+ "conflicting_verification_policy",
35
+ "conflicting_context_policy",
36
+ "conflicting_source_priority",
37
+ "policy_bypass_phrase",
38
+ "unsafe_auto_execution_phrase",
39
+ "mcp_server_reference",
40
+ "browser_control_reference",
41
+ "broad_file_write_language",
42
+ "secret_access_language",
43
+ "shell_execution_language",
44
+ "network_or_remote_fetch",
45
+ ]);
46
+
47
+ const GENERIC_CAPABILITY_IDS = new Set([
48
+ "capability_binding_basic",
49
+ "skill_md_parser_basic",
50
+ "skill_pattern_extraction_basic",
51
+ "skill_trust_scan_basic",
52
+ "source_backed_skill_router_basic",
53
+ "reference_source_intake_basic",
54
+ "thin_adapter_guidance_sync_basic",
55
+ ]);
56
+
57
+ function tokenize(text) {
58
+ return String(text || "")
59
+ .toLowerCase()
60
+ .replace(/[^a-z0-9/_-]+/g, " ")
61
+ .split(/\s+/)
62
+ .map((t) => t.trim())
63
+ .filter((t) => t.length >= 3);
64
+ }
65
+
66
+ function jaccardSimilarity(setA, setB) {
67
+ if (setA.size === 0 && setB.size === 0) return 0;
68
+ const intersection = new Set([...setA].filter((x) => setB.has(x)));
69
+ const union = new Set([...setA, ...setB]);
70
+ return intersection.size / union.size;
71
+ }
72
+
73
+ function descriptionOverlap(a, b) {
74
+ const tokA = new Set(tokenize(a));
75
+ const tokB = new Set(tokenize(b));
76
+ return jaccardSimilarity(tokA, tokB);
77
+ }
78
+
79
+ function hasCapabilityOverlap(a, b) {
80
+ const capsA = new Set((a.capabilityBindingSuggestions || []).filter((c) => !GENERIC_CAPABILITY_IDS.has(c)));
81
+ const capsB = new Set((b.capabilityBindingSuggestions || []).filter((c) => !GENERIC_CAPABILITY_IDS.has(c)));
82
+ const intersection = [...capsA].filter((x) => capsB.has(x));
83
+ return intersection.length > 0 ? intersection : null;
84
+ }
85
+
86
+ function hasTaskSignalOverlap(a, b) {
87
+ const sigA = new Set(tokenize((a.whenToUse || "") + " " + (a.description || "")));
88
+ const sigB = new Set(tokenize((b.whenToUse || "") + " " + (b.description || "")));
89
+ return jaccardSimilarity(sigA, sigB);
90
+ }
91
+
92
+ function detectDuplicateReasonCodes(skillA, skillB) {
93
+ const codes = [];
94
+
95
+ if (skillA.name && skillB.name && skillA.name === skillB.name) {
96
+ codes.push("duplicate_name");
97
+ }
98
+
99
+ const overlap = descriptionOverlap(skillA.description, skillB.description);
100
+ if (overlap >= DESCRIPTION_OVERLAP_THRESHOLD) {
101
+ codes.push("high_description_overlap");
102
+ }
103
+
104
+ const capOverlap = hasCapabilityOverlap(skillA, skillB);
105
+ if (capOverlap && capOverlap.length >= 1) {
106
+ codes.push("same_capability_binding");
107
+ }
108
+
109
+ const taskSig = hasTaskSignalOverlap(skillA, skillB);
110
+ if (taskSig >= 0.4) {
111
+ codes.push("same_task_signal");
112
+ }
113
+
114
+ const refsA = new Set(skillA.references || []);
115
+ const refsB = new Set(skillB.references || []);
116
+ const refOverlap = [...refsA].filter((r) => refsB.has(r));
117
+ if (refOverlap.length >= 2) {
118
+ codes.push("same_supporting_reference");
119
+ }
120
+
121
+ const reqA = tokenize(skillA.requiredOutput || skillA.requiredChecks || "");
122
+ const reqB = tokenize(skillB.requiredOutput || skillB.requiredChecks || "");
123
+ const reqOverlap = jaccardSimilarity(new Set(reqA), new Set(reqB));
124
+ if (reqOverlap >= 0.5 && reqA.length > 2 && reqB.length > 2) {
125
+ codes.push("same_required_output");
126
+ }
127
+
128
+ return codes;
129
+ }
130
+
131
+ function detectConflictReasonCodes(skillA, skillB) {
132
+ const codes = [];
133
+ const rawA = [skillA.workflow, skillA.antiPatterns, skillA.stopConditions, skillA.whenToUse, skillA.requiredChecks].join(" ");
134
+ const rawB = [skillB.workflow, skillB.antiPatterns, skillB.stopConditions, skillB.whenToUse, skillB.requiredChecks].join(" ");
135
+
136
+ const bypassPattern = /\b(ignore|bypass|disable|remove)\s+(?:the\s+)?(?:policy|guardrails|audit|proof|verification|review)\b/i;
137
+ if (bypassPattern.test(rawA) || bypassPattern.test(rawB)) {
138
+ codes.push("policy_bypass_phrase");
139
+ }
140
+
141
+ const autoExecPattern = /\b(auto-?run|execute automatically|run immediately|always execute)\b/i;
142
+ if (autoExecPattern.test(rawA) || autoExecPattern.test(rawB)) {
143
+ codes.push("unsafe_auto_execution_phrase");
144
+ }
145
+
146
+ const hasNeverA = /\bnever\s+(run|execute|call|invoke)\b/i.test(rawA);
147
+ const hasAlwaysB = /\balways\s+(run|execute|call|invoke)\b/i.test(rawB);
148
+ const hasNeverB = /\bnever\s+(run|execute|call|invoke)\b/i.test(rawB);
149
+ const hasAlwaysA = /\balways\s+(run|execute|call|invoke)\b/i.test(rawA);
150
+ if ((hasNeverA && hasAlwaysB) || (hasNeverB && hasAlwaysA)) {
151
+ codes.push("conflicting_invocation_policy");
152
+ }
153
+
154
+ return codes;
155
+ }
156
+
157
+ function detectContextHeavyReasonCodes(skill) {
158
+ const codes = [];
159
+ if (skill.contextWeight?.label === "high") {
160
+ codes.push("context_heavy_skill");
161
+ }
162
+ if ((skill.examples || "").length > 2000) {
163
+ codes.push("long_inline_examples");
164
+ }
165
+ if (skill.contextWeight?.lazyLoadRecommended) {
166
+ codes.push("lazy_load_recommended");
167
+ }
168
+ return codes;
169
+ }
170
+
171
+ function detectSecurityHygieneReasonCodes(skill) {
172
+ const codes = [];
173
+ const warningCodes = new Set((skill.warnings || []).map((w) => w.code));
174
+ if (warningCodes.has("hidden_unicode")) codes.push("hidden_unicode");
175
+ if (warningCodes.has("external_script_metadata_only")) codes.push("external_script_metadata_only");
176
+ if (warningCodes.has("unreviewed_source") || warningCodes.has("blocked_source")) {
177
+ codes.push("blocked_or_unreviewed_source");
178
+ }
179
+
180
+ const rawText = [skill.workflow, skill.whenToUse, skill.description, skill.antiPatterns, skill.stopConditions].join(" ");
181
+
182
+ const bypassPat = /\b(ignore|bypass|disable|remove)\s+(?:the\s+)?(?:policy|guardrails|audit|proof|verification|review)\b/i;
183
+ if (warningCodes.has("policy_bypass_phrase") || bypassPat.test(rawText)) codes.push("policy_bypass_phrase");
184
+
185
+ const autoExecPat = /\b(auto-?run|execute automatically|run immediately|always execute)\b/i;
186
+ if (warningCodes.has("unsafe_auto_execution_phrase") || autoExecPat.test(rawText)) codes.push("unsafe_auto_execution_phrase");
187
+
188
+ if (/\b(curl|wget|fetch)\b.*\b(http|https):\/\//i.test(rawText)) codes.push("network_or_remote_fetch");
189
+ if (/\b(bash|sh|shell|exec|spawn|child_process)\b/i.test(rawText)) codes.push("shell_execution_language");
190
+ if (/\bmcp[_\s]server\b/i.test(rawText)) codes.push("mcp_server_reference");
191
+ if (/\b(browser[_\s]control|headless[_\s]browser|pw[_\s]browser|selenium[_\s]driver)\b/i.test(rawText)) codes.push("browser_control_reference");
192
+ if (/\b(write[_\s]file|fs\.write|writeFileSync|create[_\s]file)\b/i.test(rawText)) codes.push("broad_file_write_language");
193
+ if (/\b(secret|api[_\s]key|password|credential|token)\b/i.test(rawText)) codes.push("secret_access_language");
194
+
195
+ return [...new Set(codes)];
196
+ }
197
+
198
+ function detectStalenessReasonCodes(skill) {
199
+ const codes = [];
200
+ if (skill.warnings?.some((w) => w.code === "missing_supporting_file")) {
201
+ codes.push("missing_supporting_file");
202
+ }
203
+ if (skill.sourceReviewStatus === "deferred" || skill.deferredReason?.includes("deferred")) {
204
+ codes.push("deferred_or_blocked_source");
205
+ }
206
+ return codes;
207
+ }
208
+
209
+ function isSkillUnsafe(skill) {
210
+ const secCodes = detectSecurityHygieneReasonCodes(skill);
211
+ const criticalCodes = new Set([
212
+ "hidden_unicode",
213
+ "policy_bypass_phrase",
214
+ "unsafe_auto_execution_phrase",
215
+ "blocked_or_unreviewed_source",
216
+ ]);
217
+ return secCodes.some((c) => criticalCodes.has(c));
218
+ }
219
+
220
+ function selectPrimaryFromGroup(skills) {
221
+ const sorted = [...skills].sort((a, b) => {
222
+ const aScore = primaryScore(a);
223
+ const bScore = primaryScore(b);
224
+ if (bScore !== aScore) return bScore - aScore;
225
+ return a.id.localeCompare(b.id);
226
+ });
227
+ return sorted[0];
228
+ }
229
+
230
+ function primaryScore(skill) {
231
+ let score = 0;
232
+ if (skill.packId === "avorelo") score += 10;
233
+ else if (skill.sourceReviewStatus === "trusted") score += 8;
234
+ else if (skill.sourceReviewStatus === "reviewed") score += 6;
235
+ if (skill.routeEligible) score += 4;
236
+ const secCodes = detectSecurityHygieneReasonCodes(skill);
237
+ score -= secCodes.length * 2;
238
+ if (skill.contextWeight?.label === "low") score += 2;
239
+ else if (skill.contextWeight?.label === "medium") score += 1;
240
+ else if (skill.contextWeight?.label === "high") score -= 1;
241
+ return score;
242
+ }
243
+
244
+ function extractBoundedContributions(primarySkill, shadowSkill) {
245
+ const contributions = [];
246
+
247
+ const primaryChecks = tokenize(primarySkill.requiredChecks || primarySkill.definitionOfDone || "");
248
+ const shadowChecks = tokenize(shadowSkill.requiredChecks || shadowSkill.definitionOfDone || "");
249
+ if (shadowChecks.length > 0) {
250
+ const overlap = jaccardSimilarity(new Set(primaryChecks), new Set(shadowChecks));
251
+ if (overlap < 0.6 && shadowChecks.length <= 20) {
252
+ const uniqueTokens = shadowChecks.filter((t) => !primaryChecks.includes(t));
253
+ if (uniqueTokens.length >= 2) {
254
+ const secCodes = detectSecurityHygieneReasonCodes(shadowSkill);
255
+ const hasRisky = secCodes.some((c) => RISKY_CONTRIBUTION_REASON_CODES.has(c));
256
+ if (!hasRisky) {
257
+ contributions.push({
258
+ fromSkill: shadowSkill.id,
259
+ type: "verification_check",
260
+ text: `Verify: ${uniqueTokens.slice(0, 8).join(", ")}`,
261
+ reasonCodes: ["unique_verification_check", "non_conflicting"],
262
+ contextTokensEstimated: Math.min(uniqueTokens.length * 2, MAX_CONTRIBUTION_TOKENS),
263
+ });
264
+ }
265
+ }
266
+ }
267
+ }
268
+
269
+ const primaryWhenNot = tokenize(primarySkill.whenNotToUse || "");
270
+ const shadowWhenNot = tokenize(shadowSkill.whenNotToUse || "");
271
+ if (shadowWhenNot.length > 2) {
272
+ const overlap = jaccardSimilarity(new Set(primaryWhenNot), new Set(shadowWhenNot));
273
+ if (overlap < 0.5 && shadowWhenNot.length <= 15) {
274
+ const uniqueTokens = shadowWhenNot.filter((t) => !primaryWhenNot.includes(t));
275
+ if (uniqueTokens.length >= 2) {
276
+ contributions.push({
277
+ fromSkill: shadowSkill.id,
278
+ type: "edge_case",
279
+ text: `Edge: ${uniqueTokens.slice(0, 6).join(", ")}`,
280
+ reasonCodes: ["unique_edge_case", "non_conflicting"],
281
+ contextTokensEstimated: Math.min(uniqueTokens.length * 2, MAX_CONTRIBUTION_TOKENS),
282
+ });
283
+ }
284
+ }
285
+ }
286
+
287
+ return contributions.slice(0, MAX_ACCEPTED_CONTRIBUTIONS);
288
+ }
289
+
290
+ function classifyContribution(contribution, primarySkill, shadowSkill) {
291
+ if (!ALLOWED_CONTRIBUTION_TYPES.has(contribution.type)) {
292
+ return {
293
+ ...contribution,
294
+ accepted: false,
295
+ reasonCodes: [...(contribution.reasonCodes || []), "disallowed_contribution_type"],
296
+ };
297
+ }
298
+
299
+ if (BLOCKED_CONTRIBUTION_TYPES.has(contribution.type)) {
300
+ return {
301
+ ...contribution,
302
+ accepted: false,
303
+ reasonCodes: [...(contribution.reasonCodes || []), "blocked_type"],
304
+ };
305
+ }
306
+
307
+ const secCodes = detectSecurityHygieneReasonCodes(shadowSkill);
308
+ const hasRisky = secCodes.some((c) => RISKY_CONTRIBUTION_REASON_CODES.has(c));
309
+ if (hasRisky) {
310
+ return {
311
+ ...contribution,
312
+ accepted: false,
313
+ reasonCodes: [...(contribution.reasonCodes || []), ...secCodes.filter((c) => RISKY_CONTRIBUTION_REASON_CODES.has(c))],
314
+ };
315
+ }
316
+
317
+ return { ...contribution, accepted: true };
318
+ }
319
+
320
+ function detectDuplicateGroups(skills) {
321
+ const groups = [];
322
+ const grouped = new Set();
323
+
324
+ for (let i = 0; i < skills.length; i++) {
325
+ if (grouped.has(skills[i].id)) continue;
326
+ const group = [skills[i]];
327
+ const groupReasons = {};
328
+
329
+ for (let j = i + 1; j < skills.length; j++) {
330
+ if (grouped.has(skills[j].id)) continue;
331
+ const codes = detectDuplicateReasonCodes(skills[i], skills[j]);
332
+ if (codes.length > 0) {
333
+ group.push(skills[j]);
334
+ groupReasons[skills[j].id] = codes;
335
+ grouped.add(skills[j].id);
336
+ }
337
+ }
338
+
339
+ if (group.length > 1) {
340
+ grouped.add(skills[i].id);
341
+ groups.push({
342
+ skillIds: group.map((s) => s.id),
343
+ reasonCodes: groupReasons,
344
+ });
345
+ }
346
+ }
347
+
348
+ return groups;
349
+ }
350
+
351
+ function detectConflictGroups(skills) {
352
+ const groups = [];
353
+ const seen = new Set();
354
+
355
+ for (let i = 0; i < skills.length; i++) {
356
+ for (let j = i + 1; j < skills.length; j++) {
357
+ const key = `${skills[i].id}::${skills[j].id}`;
358
+ if (seen.has(key)) continue;
359
+ const codes = detectConflictReasonCodes(skills[i], skills[j]);
360
+ if (codes.length > 0) {
361
+ seen.add(key);
362
+ groups.push({
363
+ skillIds: [skills[i].id, skills[j].id],
364
+ reasonCodes: codes,
365
+ });
366
+ }
367
+ }
368
+ }
369
+
370
+ return groups;
371
+ }
372
+
373
+ function buildSkillHygiene(registry, options = {}) {
374
+ const skills = (registry.skills || []).filter((s) => s.routeEligible);
375
+ const allSkills = registry.skills || [];
376
+
377
+ const duplicateGroups = detectDuplicateGroups(skills);
378
+ const conflictGroups = detectConflictGroups(skills);
379
+
380
+ const contextHeavySkills = allSkills
381
+ .filter((s) => s.contextWeight?.label === "high")
382
+ .map((s) => ({ skillId: s.id, reasonCodes: detectContextHeavyReasonCodes(s) }));
383
+
384
+ const unsafeSkills = allSkills
385
+ .filter((s) => isSkillUnsafe(s))
386
+ .map((s) => ({ skillId: s.id, reasonCodes: detectSecurityHygieneReasonCodes(s) }));
387
+
388
+ const shadowCandidates = [];
389
+ const routingGuards = [];
390
+
391
+ duplicateGroups.forEach((group) => {
392
+ const groupSkills = group.skillIds
393
+ .map((id) => skills.find((s) => s.id === id))
394
+ .filter(Boolean);
395
+ const primary = selectPrimaryFromGroup(groupSkills);
396
+ const shadows = groupSkills.filter((s) => s.id !== primary.id);
397
+
398
+ shadows.forEach((shadow) => {
399
+ const rawContribs = extractBoundedContributions(primary, shadow);
400
+ const classified = rawContribs.map((c) => classifyContribution(c, primary, shadow));
401
+ const accepted = classified.filter((c) => c.accepted);
402
+ const rejected = classified.filter((c) => !c.accepted);
403
+
404
+ shadowCandidates.push({
405
+ skillId: shadow.id,
406
+ primarySkillId: primary.id,
407
+ duplicateReasonCodes: group.reasonCodes[shadow.id] || [],
408
+ acceptedContributions: accepted,
409
+ rejectedContributions: rejected,
410
+ });
411
+ });
412
+
413
+ const conflictsForGroup = conflictGroups.filter((cg) =>
414
+ cg.skillIds.some((id) => group.skillIds.includes(id))
415
+ );
416
+
417
+ const acceptedFromShadows = shadowCandidates
418
+ .filter((sc) => sc.primarySkillId === primary.id)
419
+ .flatMap((sc) => sc.acceptedContributions);
420
+
421
+ const hygieneDecision = conflictsForGroup.length > 0
422
+ ? "selected_primary_only"
423
+ : acceptedFromShadows.length > 0
424
+ ? "primary_plus_bounded_contributions"
425
+ : "deduped";
426
+
427
+ routingGuards.push({
428
+ taskCapabilityScope: primary.capabilityBindingSuggestions?.slice(0, 3) || [],
429
+ primarySkillId: primary.id,
430
+ suppressedSkillIds: shadows.map((s) => s.id),
431
+ hygieneDecision,
432
+ reasonCodes: group.reasonCodes,
433
+ conflictAvoided: conflictsForGroup.flatMap((cg) => cg.reasonCodes),
434
+ });
435
+ });
436
+
437
+ const allReasonCodes = [
438
+ ...duplicateGroups.flatMap((g) => Object.values(g.reasonCodes).flat()),
439
+ ...conflictGroups.flatMap((g) => g.reasonCodes),
440
+ ...contextHeavySkills.flatMap((s) => s.reasonCodes),
441
+ ];
442
+
443
+ const reasonCodeCounts = {};
444
+ allReasonCodes.forEach((code) => {
445
+ reasonCodeCounts[code] = (reasonCodeCounts[code] || 0) + 1;
446
+ });
447
+ const topReasonCodes = Object.entries(reasonCodeCounts)
448
+ .sort((a, b) => b[1] - a[1])
449
+ .slice(0, 8)
450
+ .map(([code]) => code);
451
+
452
+ const totalAccepted = shadowCandidates.reduce((sum, sc) => sum + sc.acceptedContributions.length, 0);
453
+ const totalRejected = shadowCandidates.reduce((sum, sc) => sum + sc.rejectedContributions.length, 0);
454
+ const routeEligibleAfterHygiene = skills.length - shadowCandidates.length;
455
+
456
+ const hygieneStatus =
457
+ unsafeSkills.length > 0 || conflictGroups.length > 0 ? "action_needed"
458
+ : duplicateGroups.length > 0 || contextHeavySkills.length > 0 ? "watch"
459
+ : "clean";
460
+
461
+ const nextAction =
462
+ hygieneStatus === "action_needed"
463
+ ? "Review unsafe or conflicting skills. Run `avorelo skills hygiene --json` for details."
464
+ : hygieneStatus === "watch"
465
+ ? "Duplicate groups detected. Check shadow candidates for context savings."
466
+ : "Skills hygiene is clean. No duplicate full activation detected.";
467
+
468
+ return {
469
+ schemaVersion: HYGIENE_SCHEMA_VERSION,
470
+ contract: HYGIENE_CONTRACT,
471
+ generatedAt: new Date().toISOString(),
472
+ summary: {
473
+ totalSkills: allSkills.length,
474
+ routeEligibleSkills: skills.length,
475
+ duplicateGroups: duplicateGroups.length,
476
+ conflictGroups: conflictGroups.length,
477
+ shadowCandidates: shadowCandidates.length,
478
+ acceptedContributions: totalAccepted,
479
+ rejectedContributions: totalRejected,
480
+ contextHeavySkills: contextHeavySkills.length,
481
+ unsafeSkills: unsafeSkills.length,
482
+ routeEligibleAfterHygiene,
483
+ },
484
+ duplicateGroups,
485
+ conflictGroups,
486
+ shadowCandidates,
487
+ routingGuards,
488
+ contextHeavySkills,
489
+ unsafeSkills,
490
+ topReasonCodes,
491
+ hygieneStatus,
492
+ nextAction,
493
+ redacted: true,
494
+ };
495
+ }
496
+
497
+ function selectPrimarySkillForTask(skills, task, options = {}) {
498
+ const taskText = String(task || "").trim();
499
+ const eligible = skills.filter((s) => s.routeEligible && !isSkillUnsafe(s));
500
+ if (eligible.length === 0) return null;
501
+
502
+ const scored = eligible.map((s) => ({ skill: s, score: primaryScore(s) }));
503
+ scored.sort((a, b) => {
504
+ if (b.score !== a.score) return b.score - a.score;
505
+ return a.skill.id.localeCompare(b.skill.id);
506
+ });
507
+
508
+ return scored[0]?.skill || null;
509
+ }
510
+
511
+ function buildBoundedContributions(primarySkill, shadowSkills, options = {}) {
512
+ const allContributions = [];
513
+
514
+ for (const shadow of shadowSkills) {
515
+ const raw = extractBoundedContributions(primarySkill, shadow);
516
+ const classified = raw.map((c) => classifyContribution(c, primarySkill, shadow));
517
+ allContributions.push(...classified);
518
+ if (allContributions.filter((c) => c.accepted).length >= MAX_ACCEPTED_CONTRIBUTIONS) break;
519
+ }
520
+
521
+ return {
522
+ accepted: allContributions.filter((c) => c.accepted).slice(0, MAX_ACCEPTED_CONTRIBUTIONS),
523
+ rejected: allContributions.filter((c) => !c.accepted),
524
+ };
525
+ }
526
+
527
+ function buildRouteHygiene(registry, routeResult, options = {}) {
528
+ const hygiene = buildSkillHygiene(registry, options);
529
+ const primaryId = routeResult.primarySkill?.id || null;
530
+ if (!primaryId) {
531
+ return {
532
+ hygieneDecision: "clean",
533
+ primarySkill: null,
534
+ suppressedDuplicateSkills: [],
535
+ shadowSkills: [],
536
+ acceptedContributions: [],
537
+ rejectedContributions: [],
538
+ conflictAvoided: [],
539
+ contextBudgetApplied: false,
540
+ reasonCodes: [],
541
+ hygieneWarnings: [],
542
+ };
543
+ }
544
+
545
+ const isUnsafe = hygiene.unsafeSkills.some((u) => u.skillId === primaryId);
546
+ if (isUnsafe) {
547
+ return {
548
+ hygieneDecision: "blocked_from_auto_route",
549
+ primarySkill: primaryId,
550
+ suppressedDuplicateSkills: [],
551
+ shadowSkills: [],
552
+ acceptedContributions: [],
553
+ rejectedContributions: [],
554
+ conflictAvoided: [],
555
+ contextBudgetApplied: false,
556
+ reasonCodes: hygiene.unsafeSkills.find((u) => u.skillId === primaryId)?.reasonCodes || [],
557
+ hygieneWarnings: ["primary_skill_flagged_unsafe"],
558
+ };
559
+ }
560
+
561
+ const guardForPrimary = hygiene.routingGuards.find((g) => g.primarySkillId === primaryId);
562
+ if (guardForPrimary) {
563
+ const shadowCandidatesForPrimary = hygiene.shadowCandidates.filter((sc) => sc.primarySkillId === primaryId);
564
+ return {
565
+ hygieneDecision: guardForPrimary.hygieneDecision,
566
+ primarySkill: primaryId,
567
+ suppressedDuplicateSkills: guardForPrimary.suppressedSkillIds,
568
+ shadowSkills: shadowCandidatesForPrimary.map((sc) => sc.skillId),
569
+ acceptedContributions: shadowCandidatesForPrimary.flatMap((sc) => sc.acceptedContributions),
570
+ rejectedContributions: shadowCandidatesForPrimary.flatMap((sc) => sc.rejectedContributions),
571
+ conflictAvoided: guardForPrimary.conflictAvoided,
572
+ contextBudgetApplied: guardForPrimary.suppressedSkillIds.length > 0,
573
+ reasonCodes: Object.values(guardForPrimary.reasonCodes || {}).flat(),
574
+ hygieneWarnings: [],
575
+ };
576
+ }
577
+
578
+ const shadowEntry = hygiene.shadowCandidates.find((sc) => sc.skillId === primaryId);
579
+ if (shadowEntry) {
580
+ const hygienePrimary = shadowEntry.primarySkillId;
581
+ const guardForHygienePrimary = hygiene.routingGuards.find((g) => g.primarySkillId === hygienePrimary);
582
+ return {
583
+ hygieneDecision: "deduped",
584
+ primarySkill: primaryId,
585
+ suppressedDuplicateSkills: [],
586
+ shadowSkills: [],
587
+ acceptedContributions: shadowEntry.acceptedContributions || [],
588
+ rejectedContributions: shadowEntry.rejectedContributions || [],
589
+ conflictAvoided: guardForHygienePrimary?.conflictAvoided || [],
590
+ contextBudgetApplied: false,
591
+ reasonCodes: shadowEntry.duplicateReasonCodes || [],
592
+ hygieneWarnings: [`route_primary_is_shadow_under_${hygienePrimary}`],
593
+ };
594
+ }
595
+
596
+ const isContextHeavy = hygiene.contextHeavySkills.some((c) => c.skillId === primaryId);
597
+ return {
598
+ hygieneDecision: "clean",
599
+ primarySkill: primaryId,
600
+ suppressedDuplicateSkills: [],
601
+ shadowSkills: [],
602
+ acceptedContributions: [],
603
+ rejectedContributions: [],
604
+ conflictAvoided: [],
605
+ contextBudgetApplied: false,
606
+ reasonCodes: [],
607
+ hygieneWarnings: isContextHeavy ? ["primary_skill_context_heavy_lazy_load_recommended"] : [],
608
+ };
609
+ }
610
+
611
+ function buildSkillIntakeCheck(cwd, candidateSkillPath, options = {}) {
612
+ const registry = options.registry || null;
613
+
614
+ let candidateSkill;
615
+ try {
616
+ const absPath = path.resolve(cwd, candidateSkillPath);
617
+ candidateSkill = parseSkillFile(cwd, absPath);
618
+ } catch (err) {
619
+ return {
620
+ schemaVersion: HYGIENE_SCHEMA_VERSION,
621
+ contract: "avorelo.skillIntakeCheck.v1",
622
+ generatedAt: new Date().toISOString(),
623
+ candidatePath: candidateSkillPath,
624
+ decision: "block",
625
+ reasonCodes: ["parse_error"],
626
+ message: `Could not parse candidate skill: ${err.message}`,
627
+ duplicateMatches: [],
628
+ conflictMatches: [],
629
+ securityFlags: [],
630
+ redacted: true,
631
+ };
632
+ }
633
+
634
+ const secCodes = detectSecurityHygieneReasonCodes(candidateSkill);
635
+ const criticalSec = ["hidden_unicode", "policy_bypass_phrase", "unsafe_auto_execution_phrase", "blocked_or_unreviewed_source"];
636
+ if (secCodes.some((c) => criticalCodes.has(c))) {
637
+ return {
638
+ schemaVersion: HYGIENE_SCHEMA_VERSION,
639
+ contract: "avorelo.skillIntakeCheck.v1",
640
+ generatedAt: new Date().toISOString(),
641
+ candidatePath: candidateSkillPath,
642
+ decision: "block",
643
+ reasonCodes: secCodes,
644
+ message: "Candidate skill has critical security flags and cannot be admitted.",
645
+ duplicateMatches: [],
646
+ conflictMatches: [],
647
+ securityFlags: secCodes,
648
+ redacted: true,
649
+ };
650
+ }
651
+
652
+ const existingSkills = registry ? (registry.skills || []) : [];
653
+ const duplicateMatches = [];
654
+ const conflictMatches = [];
655
+
656
+ existingSkills.forEach((existing) => {
657
+ const dupCodes = detectDuplicateReasonCodes(candidateSkill, existing);
658
+ if (dupCodes.length > 0) {
659
+ duplicateMatches.push({ skillId: existing.id, reasonCodes: dupCodes });
660
+ }
661
+ const conflictCodes = detectConflictReasonCodes(candidateSkill, existing);
662
+ if (conflictCodes.length > 0) {
663
+ conflictMatches.push({ skillId: existing.id, reasonCodes: conflictCodes });
664
+ }
665
+ });
666
+
667
+ let decision = "allow";
668
+ if (conflictMatches.length > 0) {
669
+ decision = "block";
670
+ } else if (duplicateMatches.length > 0 && duplicateMatches.some((m) => m.reasonCodes.includes("same_capability_binding") || m.reasonCodes.includes("high_description_overlap"))) {
671
+ decision = "merge_recommended";
672
+ } else if (duplicateMatches.length > 0) {
673
+ decision = "warn";
674
+ }
675
+
676
+ const reasonCodes = [
677
+ ...(duplicateMatches.flatMap((m) => m.reasonCodes)),
678
+ ...(conflictMatches.flatMap((m) => m.reasonCodes)),
679
+ ...secCodes,
680
+ ];
681
+
682
+ return {
683
+ schemaVersion: HYGIENE_SCHEMA_VERSION,
684
+ contract: "avorelo.skillIntakeCheck.v1",
685
+ generatedAt: new Date().toISOString(),
686
+ candidatePath: candidateSkillPath,
687
+ candidateSkillId: candidateSkill.id,
688
+ candidateSkillName: candidateSkill.name,
689
+ decision,
690
+ reasonCodes: [...new Set(reasonCodes)],
691
+ message: intakeDecisionMessage(decision, duplicateMatches, conflictMatches),
692
+ duplicateMatches,
693
+ conflictMatches,
694
+ securityFlags: secCodes,
695
+ redacted: true,
696
+ };
697
+ }
698
+
699
+ function intakeDecisionMessage(decision, duplicateMatches, conflictMatches) {
700
+ if (decision === "block") return `Candidate skill conflicts with ${conflictMatches.length} existing skill(s). Review conflicts before admission.`;
701
+ if (decision === "merge_recommended") return `Candidate skill overlaps significantly with ${duplicateMatches.length} existing skill(s). Consider merging or extracting unique contributions.`;
702
+ if (decision === "warn") return `Candidate skill has minor overlap with ${duplicateMatches.length} existing skill(s). Review before adding.`;
703
+ return "Candidate skill passes hygiene checks.";
704
+ }
705
+
706
+ function summarizeSkillHygiene(hygiene) {
707
+ const s = hygiene.summary || {};
708
+ return {
709
+ hygieneStatus: hygiene.hygieneStatus,
710
+ totalSkills: s.totalSkills,
711
+ duplicateGroups: s.duplicateGroups,
712
+ conflictGroups: s.conflictGroups,
713
+ shadowCandidates: s.shadowCandidates,
714
+ acceptedContributions: s.acceptedContributions,
715
+ contextHeavySkills: s.contextHeavySkills,
716
+ unsafeSkills: s.unsafeSkills,
717
+ routeEligibleAfterHygiene: s.routeEligibleAfterHygiene,
718
+ topReasonCodes: hygiene.topReasonCodes || [],
719
+ nextAction: hygiene.nextAction,
720
+ };
721
+ }
722
+
723
+ function formatSkillHygieneText(hygiene) {
724
+ const s = hygiene.summary || {};
725
+ const lines = [
726
+ `Skills Hygiene: ${(hygiene.hygieneStatus || "clean").toUpperCase()} · deduped=${s.duplicateGroups || 0} · shadow=${s.shadowCandidates || 0} · contributions=${s.acceptedContributions || 0} · conflicts=${s.conflictGroups || 0}`,
727
+ ` total=${s.totalSkills || 0} · context_heavy=${s.contextHeavySkills || 0} · unsafe=${s.unsafeSkills || 0} · eligible_after_hygiene=${s.routeEligibleAfterHygiene || 0}`,
728
+ ];
729
+
730
+ if (hygiene.topReasonCodes?.length) {
731
+ lines.push(` top_reason_codes: ${hygiene.topReasonCodes.slice(0, 5).join(", ")}`);
732
+ }
733
+
734
+ if (hygiene.duplicateGroups?.length) {
735
+ lines.push("");
736
+ lines.push("Duplicate Groups:");
737
+ hygiene.duplicateGroups.forEach((g) => {
738
+ lines.push(` [${g.skillIds.join(" vs ")}] — ${Object.values(g.reasonCodes).flat().join(", ")}`);
739
+ });
740
+ }
741
+
742
+ if (hygiene.shadowCandidates?.length) {
743
+ lines.push("");
744
+ lines.push("Shadow Candidates:");
745
+ hygiene.shadowCandidates.forEach((sc) => {
746
+ const contrib = sc.acceptedContributions?.length || 0;
747
+ lines.push(` ${sc.skillId} → shadow under ${sc.primarySkillId} (contributions=${contrib})`);
748
+ });
749
+ }
750
+
751
+ if (hygiene.conflictGroups?.length) {
752
+ lines.push("");
753
+ lines.push("Conflict Groups:");
754
+ hygiene.conflictGroups.forEach((g) => {
755
+ lines.push(` [${g.skillIds.join(" vs ")}] — ${g.reasonCodes.join(", ")}`);
756
+ });
757
+ }
758
+
759
+ lines.push("");
760
+ lines.push(hygiene.nextAction || "");
761
+ return lines.join("\n");
762
+ }
763
+
764
+ function formatSkillIntakeCheckText(check) {
765
+ const lines = [
766
+ `Skill Intake Check: ${(check.decision || "allow").toUpperCase()} — ${check.candidateSkillId || check.candidatePath}`,
767
+ ` ${check.message}`,
768
+ ];
769
+
770
+ if (check.duplicateMatches?.length) {
771
+ lines.push(` duplicate_matches: ${check.duplicateMatches.map((m) => m.skillId).join(", ")}`);
772
+ }
773
+ if (check.conflictMatches?.length) {
774
+ lines.push(` conflict_matches: ${check.conflictMatches.map((m) => m.skillId).join(", ")}`);
775
+ }
776
+ if (check.securityFlags?.length) {
777
+ lines.push(` security_flags: ${check.securityFlags.join(", ")}`);
778
+ }
779
+ return lines.join("\n");
780
+ }
781
+
782
+ const criticalCodes = new Set([
783
+ "hidden_unicode",
784
+ "policy_bypass_phrase",
785
+ "unsafe_auto_execution_phrase",
786
+ "blocked_or_unreviewed_source",
787
+ ]);
788
+
789
+ module.exports = {
790
+ HYGIENE_CONTRACT,
791
+ HYGIENE_SCHEMA_VERSION,
792
+ buildSkillHygiene,
793
+ buildSkillIntakeCheck,
794
+ selectPrimarySkillForTask,
795
+ buildBoundedContributions,
796
+ buildRouteHygiene,
797
+ summarizeSkillHygiene,
798
+ formatSkillHygieneText,
799
+ formatSkillIntakeCheckText,
800
+ detectDuplicateReasonCodes,
801
+ detectConflictReasonCodes,
802
+ detectContextHeavyReasonCodes,
803
+ detectSecurityHygieneReasonCodes,
804
+ isSkillUnsafe,
805
+ };