@event4u/agent-config 6.0.0 → 6.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 (378) hide show
  1. package/.claude-plugin/marketplace.json +5 -5
  2. package/CHANGELOG.md +167 -440
  3. package/README.md +3 -3
  4. package/dist/agent-src/commands/agent-handoff.md +5 -4
  5. package/dist/agent-src/commands/agent-status.md +1 -0
  6. package/dist/agent-src/commands/agents/audit.md +1 -0
  7. package/dist/agent-src/commands/agents/init.md +3 -0
  8. package/dist/agent-src/commands/agents/optimize.md +1 -0
  9. package/dist/agent-src/commands/agents/user/accept.md +1 -0
  10. package/dist/agent-src/commands/agents/user/init.md +1 -0
  11. package/dist/agent-src/commands/agents/user/review.md +1 -0
  12. package/dist/agent-src/commands/agents/user/show.md +1 -0
  13. package/dist/agent-src/commands/agents/user/update.md +1 -0
  14. package/dist/agent-src/commands/agents/user.md +1 -0
  15. package/dist/agent-src/commands/agents.md +1 -0
  16. package/dist/agent-src/commands/analytics/prune.md +3 -2
  17. package/dist/agent-src/commands/analytics/show.md +3 -2
  18. package/dist/agent-src/commands/analytics.md +3 -2
  19. package/dist/agent-src/commands/analyze-reference-repo.md +1 -0
  20. package/dist/agent-src/commands/bug-fix.md +1 -0
  21. package/dist/agent-src/commands/bug-investigate.md +1 -0
  22. package/dist/agent-src/commands/challenge-me/vision.md +3 -2
  23. package/dist/agent-src/commands/challenge-me/with-docs.md +3 -2
  24. package/dist/agent-src/commands/challenge-me.md +3 -2
  25. package/dist/agent-src/commands/chat-history/import.md +9 -9
  26. package/dist/agent-src/commands/chat-history.md +32 -30
  27. package/dist/agent-src/commands/check-current-md.md +1 -0
  28. package/dist/agent-src/commands/commit/in-chunks.md +1 -0
  29. package/dist/agent-src/commands/commit.md +1 -0
  30. package/dist/agent-src/commands/condense.md +1 -0
  31. package/dist/agent-src/commands/context/create.md +1 -0
  32. package/dist/agent-src/commands/context/refactor.md +1 -0
  33. package/dist/agent-src/commands/context.md +1 -0
  34. package/dist/agent-src/commands/cost-report.md +5 -4
  35. package/dist/agent-src/commands/council/analysis.md +3 -2
  36. package/dist/agent-src/commands/council/debate.md +5 -4
  37. package/dist/agent-src/commands/council/default.md +3 -2
  38. package/dist/agent-src/commands/council/design.md +3 -2
  39. package/dist/agent-src/commands/council/optimize.md +3 -2
  40. package/dist/agent-src/commands/council/pr.md +3 -2
  41. package/dist/agent-src/commands/council.md +4 -3
  42. package/dist/agent-src/commands/e2e-heal.md +1 -0
  43. package/dist/agent-src/commands/e2e-plan.md +1 -0
  44. package/dist/agent-src/commands/estimate-ticket.md +1 -0
  45. package/dist/agent-src/commands/feature/dev.md +1 -0
  46. package/dist/agent-src/commands/feature/explore.md +1 -0
  47. package/dist/agent-src/commands/feature/plan.md +6 -6
  48. package/dist/agent-src/commands/feature/refactor.md +1 -0
  49. package/dist/agent-src/commands/feature/roadmap.md +1 -0
  50. package/dist/agent-src/commands/feature.md +1 -0
  51. package/dist/agent-src/commands/fix/ci.md +1 -0
  52. package/dist/agent-src/commands/fix/portability.md +1 -0
  53. package/dist/agent-src/commands/fix/pr-comments.md +147 -15
  54. package/dist/agent-src/commands/fix/refs.md +1 -0
  55. package/dist/agent-src/commands/fix/seeder.md +1 -0
  56. package/dist/agent-src/commands/fix.md +8 -8
  57. package/dist/agent-src/commands/ghostwriter/delete.md +1 -0
  58. package/dist/agent-src/commands/ghostwriter/fetch.md +1 -0
  59. package/dist/agent-src/commands/ghostwriter/list.md +1 -0
  60. package/dist/agent-src/commands/ghostwriter/show.md +1 -0
  61. package/dist/agent-src/commands/ghostwriter/write.md +1 -0
  62. package/dist/agent-src/commands/ghostwriter.md +1 -0
  63. package/dist/agent-src/commands/grill-me.md +3 -2
  64. package/dist/agent-src/commands/image/analyse.md +1 -0
  65. package/dist/agent-src/commands/image/create.md +1 -0
  66. package/dist/agent-src/commands/image/verify.md +1 -0
  67. package/dist/agent-src/commands/image.md +1 -0
  68. package/dist/agent-src/commands/implement-ticket.md +1 -0
  69. package/dist/agent-src/commands/jira-ticket.md +1 -0
  70. package/dist/agent-src/commands/judge/on-diff.md +1 -0
  71. package/dist/agent-src/commands/judge/solo.md +1 -0
  72. package/dist/agent-src/commands/judge/steps.md +1 -0
  73. package/dist/agent-src/commands/judge.md +1 -0
  74. package/dist/agent-src/commands/knowledge/cross-repo.md +1 -0
  75. package/dist/agent-src/commands/knowledge/forget.md +1 -0
  76. package/dist/agent-src/commands/knowledge/ingest.md +1 -0
  77. package/dist/agent-src/commands/knowledge/list.md +1 -0
  78. package/dist/agent-src/commands/knowledge.md +1 -0
  79. package/dist/agent-src/commands/memory/add.md +8 -6
  80. package/dist/agent-src/commands/memory/learn-low-impact.md +3 -2
  81. package/dist/agent-src/commands/memory/load.md +7 -7
  82. package/dist/agent-src/commands/memory/mine-session.md +39 -12
  83. package/dist/agent-src/commands/memory/promote.md +3 -2
  84. package/dist/agent-src/commands/memory/propose.md +7 -6
  85. package/dist/agent-src/commands/memory.md +3 -2
  86. package/dist/agent-src/commands/mode.md +1 -0
  87. package/dist/agent-src/commands/module/create.md +1 -0
  88. package/dist/agent-src/commands/module/explore.md +1 -0
  89. package/dist/agent-src/commands/module.md +1 -0
  90. package/dist/agent-src/commands/optimize/agents-dir.md +1 -0
  91. package/dist/agent-src/commands/optimize/augmentignore.md +1 -0
  92. package/dist/agent-src/commands/optimize/rtk.md +1 -0
  93. package/dist/agent-src/commands/optimize/skills.md +1 -0
  94. package/dist/agent-src/commands/optimize-prompt.md +1 -0
  95. package/dist/agent-src/commands/optimize.md +1 -0
  96. package/dist/agent-src/commands/orchestrate.md +1 -0
  97. package/dist/agent-src/commands/override/create.md +1 -0
  98. package/dist/agent-src/commands/override/manage.md +1 -0
  99. package/dist/agent-src/commands/override.md +1 -0
  100. package/dist/agent-src/commands/package-reset.md +1 -0
  101. package/dist/agent-src/commands/package-test.md +1 -0
  102. package/dist/agent-src/commands/post-as/ghostwriter.md +1 -0
  103. package/dist/agent-src/commands/post-as/me.md +1 -0
  104. package/dist/agent-src/commands/post-as.md +1 -0
  105. package/dist/agent-src/commands/pr/create/description-only.md +1 -0
  106. package/dist/agent-src/commands/pr/create.md +25 -0
  107. package/dist/agent-src/commands/prediction-pool.md +1 -0
  108. package/dist/agent-src/commands/prepare-for-review.md +1 -0
  109. package/dist/agent-src/commands/profile/activate.md +1 -0
  110. package/dist/agent-src/commands/profile/deactivate.md +1 -0
  111. package/dist/agent-src/commands/profile/show.md +1 -0
  112. package/dist/agent-src/commands/profile.md +1 -0
  113. package/dist/agent-src/commands/project-analyze.md +1 -0
  114. package/dist/agent-src/commands/project-health.md +1 -0
  115. package/dist/agent-src/commands/quality-fix.md +1 -0
  116. package/dist/agent-src/commands/refine-ticket.md +1 -0
  117. package/dist/agent-src/commands/research/deep.md +1 -0
  118. package/dist/agent-src/commands/research/report.md +1 -0
  119. package/dist/agent-src/commands/research.md +1 -0
  120. package/dist/agent-src/commands/review-changes.md +1 -0
  121. package/dist/agent-src/commands/review-routing.md +1 -0
  122. package/dist/agent-src/commands/roadmap/ai-council.md +1 -0
  123. package/dist/agent-src/commands/roadmap/create.md +1 -0
  124. package/dist/agent-src/commands/roadmap/process-full.md +1 -0
  125. package/dist/agent-src/commands/roadmap/process-phase.md +1 -0
  126. package/dist/agent-src/commands/roadmap/process-step.md +1 -0
  127. package/dist/agent-src/commands/roadmap.md +1 -0
  128. package/dist/agent-src/commands/rule-compliance-audit.md +1 -0
  129. package/dist/agent-src/commands/security-audit-config.md +84 -0
  130. package/dist/agent-src/commands/set-cost-profile.md +1 -0
  131. package/dist/agent-src/commands/skill/preview.md +1 -0
  132. package/dist/agent-src/commands/skill.md +1 -0
  133. package/dist/agent-src/commands/skills/discover.md +1 -0
  134. package/dist/agent-src/commands/skills.md +1 -0
  135. package/dist/agent-src/commands/sync-agent-settings.md +1 -0
  136. package/dist/agent-src/commands/sync-gitignore/fix.md +1 -0
  137. package/dist/agent-src/commands/sync-gitignore.md +1 -0
  138. package/dist/agent-src/commands/tests/create.md +1 -0
  139. package/dist/agent-src/commands/tests/execute.md +1 -0
  140. package/dist/agent-src/commands/tests.md +1 -0
  141. package/dist/agent-src/commands/threat-model.md +1 -0
  142. package/dist/agent-src/commands/update-form-request-messages.md +1 -0
  143. package/dist/agent-src/commands/upstream-contribute.md +1 -0
  144. package/dist/agent-src/commands/video/from-script.md +1 -0
  145. package/dist/agent-src/commands/video/from-song.md +1 -0
  146. package/dist/agent-src/commands/video/scene.md +1 -0
  147. package/dist/agent-src/commands/video/stitch.md +1 -0
  148. package/dist/agent-src/commands/video/storyboard.md +1 -0
  149. package/dist/agent-src/commands/video.md +1 -0
  150. package/dist/agent-src/commands/work.md +1 -0
  151. package/dist/agent-src/contexts/augment-infrastructure.md +1 -1
  152. package/dist/agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +1 -1
  153. package/dist/agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +2 -2
  154. package/dist/agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +6 -6
  155. package/dist/agent-src/contexts/contracts/consumer-agents-md-guide.md +2 -2
  156. package/dist/agent-src/contexts/execution/rdp-gate.md +75 -0
  157. package/dist/agent-src/contexts/subagent-configuration.md +1 -0
  158. package/dist/agent-src/personas/advisors/contrarian.md +1 -1
  159. package/dist/agent-src/personas/advisors/executor.md +1 -1
  160. package/dist/agent-src/personas/advisors/expansionist.md +1 -1
  161. package/dist/agent-src/personas/advisors/first-principles.md +1 -1
  162. package/dist/agent-src/personas/advisors/outsider.md +1 -1
  163. package/dist/agent-src/rules/autonomous-execution.md +12 -0
  164. package/dist/agent-src/rules/external-reference-deep-dive.md +1 -1
  165. package/dist/agent-src/rules/git-history-discipline.md +47 -1
  166. package/dist/agent-src/rules/improve-before-implement.md +12 -0
  167. package/dist/agent-src/rules/lethal-trifecta-guard.md +80 -0
  168. package/dist/agent-src/rules/no-pr-progress-comments.md +3 -4
  169. package/dist/agent-src/rules/notes-first-reasoning.md +71 -0
  170. package/dist/agent-src/rules/roadmap-progress-sync.md +48 -31
  171. package/dist/agent-src/rules/security-sensitive-stop.md +14 -1
  172. package/dist/agent-src/rules/source-confidentiality.md +97 -0
  173. package/dist/agent-src/rules/think-before-action.md +9 -1
  174. package/dist/agent-src/rules/untrusted-input-defense.md +76 -0
  175. package/dist/agent-src/scripts/archive_completed_roadmaps.py +171 -0
  176. package/dist/agent-src/skills/adversarial-review/SKILL.md +14 -0
  177. package/dist/agent-src/skills/agent-security-review/SKILL.md +113 -0
  178. package/dist/agent-src/skills/agent-security-review/evals/triggers.json +51 -0
  179. package/dist/agent-src/skills/ai-council/SKILL.md +3 -3
  180. package/dist/agent-src/skills/async-python-patterns/SKILL.md +1 -1
  181. package/dist/agent-src/skills/blast-radius-analyzer/SKILL.md +12 -11
  182. package/dist/agent-src/skills/command-routing/SKILL.md +1 -1
  183. package/dist/agent-src/skills/complexity-first-planning/SKILL.md +96 -0
  184. package/dist/agent-src/skills/complexity-first-planning/evals/triggers.json +16 -0
  185. package/dist/agent-src/skills/copilot-config/SKILL.md +3 -4
  186. package/dist/agent-src/skills/defense-in-depth/SKILL.md +1 -1
  187. package/dist/agent-src/skills/developer-like-execution/SKILL.md +5 -4
  188. package/dist/agent-src/skills/error-handling-patterns/SKILL.md +1 -1
  189. package/dist/agent-src/skills/feature-planning/SKILL.md +2 -2
  190. package/dist/agent-src/skills/mcp-builder/SKILL.md +1 -1
  191. package/dist/agent-src/skills/memory-consolidation/SKILL.md +63 -17
  192. package/dist/agent-src/skills/prompt-engineering-patterns/SKILL.md +1 -1
  193. package/dist/agent-src/skills/readme-writing-package/SKILL.md +1 -1
  194. package/dist/agent-src/skills/reasoning-orchestrator/SKILL.md +119 -0
  195. package/dist/agent-src/skills/reasoning-orchestrator/evals/triggers.json +16 -0
  196. package/dist/agent-src/skills/receiving-code-review/SKILL.md +6 -6
  197. package/dist/agent-src/skills/refine-prompt/SKILL.md +1 -1
  198. package/dist/agent-src/skills/refine-ticket/SKILL.md +1 -1
  199. package/dist/agent-src/skills/repomix-packer/SKILL.md +1 -1
  200. package/dist/agent-src/skills/secrets-management/SKILL.md +1 -1
  201. package/dist/agent-src/skills/subagent-orchestration/SKILL.md +10 -3
  202. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  203. package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +1 -1
  204. package/dist/agent-src/skills/token-optimizer/SKILL.md +1 -1
  205. package/dist/agent-src/templates/agents/.gitattributes.fragment +0 -1
  206. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +4 -4
  207. package/dist/agent-src/templates/scripts/check_memory.py +1 -2
  208. package/dist/agent-src/templates/scripts/check_memory_proposal.py +1 -1
  209. package/dist/agent-src/templates/scripts/memory_lookup.py +148 -289
  210. package/dist/agent-src/templates/scripts/memory_report.py +132 -2
  211. package/dist/agent-src/templates/scripts/memory_signal.py +7 -9
  212. package/dist/agent-src/templates/scripts/memory_status.py +25 -206
  213. package/dist/agent-src/templates/scripts/work_engine/directives/backend/memory.py +6 -6
  214. package/dist/agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +3 -3
  215. package/dist/agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +0 -1
  216. package/dist/cli/agent-config.js +31 -300
  217. package/dist/cli/agent-config.js.map +1 -1
  218. package/dist/cli/commands/commands.js +10 -5
  219. package/dist/cli/commands/commands.js.map +1 -1
  220. package/dist/cli/discovery/loadManifest.js.map +1 -1
  221. package/dist/cli/main.js +309 -0
  222. package/dist/cli/main.js.map +1 -0
  223. package/dist/discovery/deprecation-report.md +1 -1
  224. package/dist/discovery/discovery-manifest.json +645 -342
  225. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  226. package/dist/discovery/discovery-manifest.summary.md +8 -5
  227. package/dist/discovery/orphan-report.md +1 -1
  228. package/dist/discovery/packs.json +149 -37
  229. package/dist/discovery/trust-report.md +3 -3
  230. package/dist/discovery/workspaces.json +61 -36
  231. package/dist/mcp/registry-manifest.json +4 -4
  232. package/dist/router.json +1 -1
  233. package/dist/server/routes/wizard.js +4 -3
  234. package/dist/server/routes/wizard.js.map +1 -1
  235. package/dist/server/schemas/settings.js +18 -0
  236. package/dist/server/schemas/settings.js.map +1 -1
  237. package/docs/MIGRATION.md +1 -1
  238. package/docs/adrs/cost/0001-hard-stop-hook.md +5 -5
  239. package/docs/adrs/memory/0001-consumer-side-snapshot.md +15 -7
  240. package/docs/adrs/memory/README.md +6 -5
  241. package/docs/adrs/router/0001-three-tier-routing.md +2 -2
  242. package/docs/adrs/schema/0001-json-schema-frontmatter.md +2 -2
  243. package/docs/adrs/smoke/0001-per-tier-smoke-scripts.md +5 -5
  244. package/docs/adrs/telegraph/0001-default-off-until-bench.md +3 -3
  245. package/docs/architecture.md +9 -9
  246. package/docs/archive/CHANGELOG-pre-2.2.0.md +30 -30
  247. package/docs/archive/CHANGELOG-pre-2.25.0.md +1 -1
  248. package/docs/archive/CHANGELOG-pre-4.5.0.md +1 -1
  249. package/docs/archive/CHANGELOG-pre-6.0.0.md +473 -0
  250. package/docs/benchmark.md +54 -53
  251. package/docs/benchmarks.md +2 -2
  252. package/docs/case-studies/{frontend-design-vs-ui-ux-pro-max.md → frontend-design-positioning.md} +4 -4
  253. package/docs/catalog.md +20 -13
  254. package/docs/command-flows.md +90 -92
  255. package/docs/contracts/adr-layout.md +2 -3
  256. package/docs/contracts/adr-level-6-productization.md +1 -1
  257. package/docs/contracts/ai-council-config.md +42 -7
  258. package/docs/contracts/command-clusters.md +1 -1
  259. package/docs/contracts/cost-enforcement.md +1 -1
  260. package/docs/contracts/cost-summary-schema.md +1 -1
  261. package/docs/contracts/daily-workspace.md +1 -0
  262. package/docs/contracts/discovery-manifest.schema.json +4 -2
  263. package/docs/contracts/explain-modes.md +1 -1
  264. package/docs/contracts/implement-ticket-flow.md +6 -7
  265. package/docs/contracts/mcp-tool-inventory.md +10 -10
  266. package/docs/contracts/measurement-baseline.md +1 -1
  267. package/docs/contracts/memory-visibility-v1.md +1 -5
  268. package/docs/contracts/namespace.md +1 -1
  269. package/docs/contracts/persona-schema.md +1 -1
  270. package/docs/contracts/rule-interactions.md +1 -1
  271. package/docs/contracts/smoke-contracts.md +1 -1
  272. package/docs/contracts/universal-skills.md +0 -1
  273. package/docs/contracts/workspace-boundary.md +84 -0
  274. package/docs/customization.md +3 -3
  275. package/docs/decisions/ADR-009-event4u-namespace.md +1 -1
  276. package/docs/decisions/ADR-013-discovery-frontmatter-contract.md +1 -1
  277. package/docs/decisions/ADR-026-explain-mode-translation.md +1 -1
  278. package/docs/decisions/ADR-088-no-external-runtime-federation.md +26 -27
  279. package/docs/decisions/ADR-090-visibility-command-frontmatter-field.md +95 -0
  280. package/docs/decisions/ADR-091-split-meta-capability-packs.md +113 -0
  281. package/docs/decisions/ADR-092-defer-command-tier-alias-removal.md +93 -0
  282. package/docs/decisions/ADR-093-ai-council-config-user-global.md +111 -0
  283. package/docs/decisions/ADR-094-agent-memory-layer-removal.md +94 -0
  284. package/docs/decisions/ADR-095-workspace-boundary-contract.md +108 -0
  285. package/docs/decisions/INDEX.md +6 -0
  286. package/docs/development.md +5 -7
  287. package/docs/getting-started.md +4 -4
  288. package/docs/guidelines/agent-infra/5w2h-analysis.md +1 -1
  289. package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
  290. package/docs/guidelines/agent-infra/corpus-grounding-authoring.md +1 -1
  291. package/docs/guidelines/agent-infra/critical-thinking.md +1 -1
  292. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +1 -5
  293. package/docs/guidelines/agent-infra/first-principles.md +1 -1
  294. package/docs/guidelines/agent-infra/frontier-reasoning-operating-profile.md +164 -0
  295. package/docs/guidelines/agent-infra/inversion-thinking.md +1 -1
  296. package/docs/guidelines/agent-infra/ios-simulator-guide.md +9 -14
  297. package/docs/guidelines/agent-infra/mcp-request-signing.md +19 -22
  298. package/docs/guidelines/agent-infra/memory-access.md +25 -31
  299. package/docs/guidelines/agent-infra/mental-models.md +1 -1
  300. package/docs/guidelines/agent-infra/model-recommendation.md +29 -0
  301. package/docs/guidelines/agent-infra/scqa-framework.md +3 -3
  302. package/docs/guidelines/agent-infra/security-lint-containment.md +81 -0
  303. package/docs/guidelines/agent-infra/six-hats.md +1 -1
  304. package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
  305. package/docs/guidelines/agent-infra/untrusted-input-spotlighting.md +72 -0
  306. package/docs/installation.md +1 -1
  307. package/docs/mcp.md +2 -2
  308. package/docs/parity/{bench-ruflo.json → bench-external.json} +10 -10
  309. package/docs/parity/{ruflo.md → external-runtime.md} +9 -9
  310. package/docs/quality.md +3 -3
  311. package/docs/safety.md +3 -3
  312. package/docs/skills-catalog.md +4 -1
  313. package/llms.txt +3 -0
  314. package/package.json +1 -1
  315. package/src/config/agent-settings.template.yml +65 -3
  316. package/src/config/discovery/packs.yml +29 -0
  317. package/src/config/discovery/workspaces.yml +3 -1
  318. package/src/config/gitignore-block.txt +6 -0
  319. package/src/scripts/__pycache__/validate_frontmatter.cpython-312.pyc +0 -0
  320. package/src/scripts/_cli/cmd_doctor.py +99 -13
  321. package/src/scripts/_lib/__pycache__/__init__.cpython-312.pyc +0 -0
  322. package/src/scripts/_lib/__pycache__/agent_src.cpython-312.pyc +0 -0
  323. package/src/scripts/_lib/bench_ab_scoring_v2.py +227 -0
  324. package/src/scripts/_lib/global_deploy_inventory.py +39 -9
  325. package/src/scripts/_lib/link_crypto.py +206 -0
  326. package/src/scripts/_lib/security_lint.py +228 -0
  327. package/src/scripts/ai_council/clients.py +2 -2
  328. package/src/scripts/ai_council/config.py +55 -0
  329. package/src/scripts/audit_adr_coverage.py +0 -2
  330. package/src/scripts/audit_command_surface.py +18 -5
  331. package/src/scripts/audit_mcp_tools.py +2 -2
  332. package/src/scripts/audit_skill_descriptions.py +2 -2
  333. package/src/scripts/bench_ab_clone.py +62 -12
  334. package/src/scripts/bench_ab_task_runner.py +475 -30
  335. package/src/scripts/bench_ab_v2_run.py +247 -0
  336. package/src/scripts/bench_ab_v2_stats.py +347 -0
  337. package/src/scripts/bench_run.py +1 -1
  338. package/src/scripts/build_discovery_manifest.py +10 -0
  339. package/src/scripts/check_bite_sized_granularity.py +1 -2
  340. package/src/scripts/check_memory.py +49 -63
  341. package/src/scripts/check_memory_proposal.py +1 -1
  342. package/src/scripts/check_no_external_sources.py +101 -0
  343. package/src/scripts/check_references.py +2 -0
  344. package/src/scripts/cost_by_conversation.py +1 -1
  345. package/src/scripts/council_cli.py +28 -14
  346. package/src/scripts/external_sources_denylist.json +91 -0
  347. package/src/scripts/hook_manifest.yaml +14 -6
  348. package/src/scripts/injection_scan_hook.py +145 -0
  349. package/src/scripts/install-hooks.sh +11 -0
  350. package/src/scripts/install.py +88 -13
  351. package/src/scripts/lint_agent_security.py +112 -0
  352. package/src/scripts/lint_bench_ab.py +5 -4
  353. package/src/scripts/lint_command_tiers.py +63 -22
  354. package/src/scripts/lint_discovery_vocabulary.py +2 -0
  355. package/src/scripts/lint_empty_roadmaps.py +80 -0
  356. package/src/scripts/lint_hidden_unicode.py +132 -0
  357. package/src/scripts/lint_instruction_smuggling.py +107 -0
  358. package/src/scripts/lint_marketplace.py +1 -1
  359. package/src/scripts/lint_mcp_config_security.py +124 -0
  360. package/src/scripts/lint_skill_frontmatter_safety.py +144 -0
  361. package/src/scripts/lint_workspace_boundary.py +122 -0
  362. package/src/scripts/mcp_server/consumer_tool_catalog.json +2 -3
  363. package/src/scripts/mcp_server/tools.py +8 -32
  364. package/src/scripts/memory_lookup.py +27 -296
  365. package/src/scripts/memory_report.py +1 -23
  366. package/src/scripts/memory_signal.py +6 -53
  367. package/src/scripts/memory_status.py +25 -206
  368. package/src/scripts/mine_session.py +118 -41
  369. package/src/scripts/pack_dependency_allowlist.json +2 -2
  370. package/src/scripts/render_benchmark_md.py +141 -52
  371. package/src/scripts/schemas/command.schema.json +6 -1
  372. package/src/scripts/security_audit_config.py +153 -0
  373. package/dist/agent-src/commands/chat-history/learn.md +0 -184
  374. package/dist/agent-src/commands/chat-history/show.md +0 -113
  375. package/dist/agent-src/commands/fix/pr-bot-comments.md +0 -157
  376. package/dist/agent-src/commands/fix/pr-developer-comments.md +0 -163
  377. package/dist/agent-src/templates/agents/memory/architecture-decisions.example.yml +0 -95
  378. package/docs/contracts/agent-memory-contract.md +0 -159
@@ -136,11 +136,18 @@ def reap_stale(
136
136
  anchor: Path,
137
137
  current_files: set[str],
138
138
  inventory: dict,
139
+ dry_run: bool = False,
139
140
  ) -> list[Path]:
140
141
  """Delete previously-deployed files that the current deploy dropped.
141
142
 
142
143
  Returns the absolute paths actually deleted. Mutates nothing in
143
144
  ``inventory`` — callers record the new state via :func:`record_deploy`.
145
+
146
+ ``dry_run=True`` computes and returns the would-delete set (only paths
147
+ that currently exist on disk) WITHOUT unlinking anything or pruning
148
+ empty directories — the preview surface for ``install.py --dry-run``.
149
+ The selection logic (orphan diff, containment proof, directory guard)
150
+ is identical to the live path, so the preview is exact.
144
151
  """
145
152
  entry = inventory.get("tools", {}).get(tool_id)
146
153
  if not isinstance(entry, dict):
@@ -169,6 +176,11 @@ def reap_stale(
169
176
  continue
170
177
  if target.is_dir() and not target.is_symlink():
171
178
  continue # never delete directories
179
+ if dry_run:
180
+ # Preview: report only what is actually on disk and would go.
181
+ if target.exists() or target.is_symlink():
182
+ deleted.append(target)
183
+ continue
172
184
  try:
173
185
  target.unlink()
174
186
  except FileNotFoundError:
@@ -191,26 +203,41 @@ def reap_stale(
191
203
  return deleted
192
204
 
193
205
 
194
- def bootstrap_reap_tagged(
206
+ def reap_tagged_orphans(
195
207
  anchor: Path,
196
208
  dest_subs: list[str],
197
209
  current_files: set[str],
198
210
  package_tag: str,
211
+ dry_run: bool = False,
199
212
  ) -> list[Path]:
200
- """First-run reaping for PRE-inventory installs (marker-based ownership).
201
-
202
- Existing installs in the wild predate the inventory sidecar, so
203
- :func:`reap_stale` has nothing to diff against on the first upgraded
204
- deploy the legacy mess (renamed skills, retired command-as-skill
205
- entries, the 2026-05-13 colon-named shapes) would rot forever. Every
206
- deployed ``.md`` however carries the injected ``package:`` frontmatter
207
- tag (install P5.1), which is exactly the ownership proof reaping needs.
213
+ """Marker-based reaping of package-tagged orphans runs EVERY deploy.
214
+
215
+ This is the self-healing reaping path, complementary to
216
+ :func:`reap_stale` (which can only diff against the *previous*
217
+ inventory). It is the **only** path with ownership proof independent
218
+ of inventory history: every deployed ``.md`` carries the injected
219
+ ``package:`` frontmatter tag (install P5.1), so a tagged file absent
220
+ from the current bundle is provably our orphan regardless of whether
221
+ any inventory ever recorded it.
222
+
223
+ Why it must run every deploy, not just on first-run: an install that
224
+ predates the inventory sidecar never recorded its files, so once a
225
+ tool *does* get an inventory entry, :func:`reap_stale` has no record
226
+ of those legacy files to diff against — they would rot forever (the
227
+ renamed skills, retired command-as-skill entries, 2026-05-13
228
+ colon-named shapes, and post-6.0.0 command renames like
229
+ ``create-pr`` → ``pr/create``). Running this sweep unconditionally
230
+ closes that gap; it is idempotent (a clean tree yields no deletions).
208
231
 
209
232
  Deletes ``.md`` files under ``<anchor>/<dest_sub>`` that (a) carry
210
233
  ``package: <package_tag>`` in their frontmatter and (b) are not in the
211
234
  current expected file set; then prunes empty directories. Untagged
212
235
  files (user-authored skills in shared anchors) are never touched.
213
236
  Returns the absolute paths deleted.
237
+
238
+ ``dry_run=True`` returns the would-delete set (tagged orphans actually
239
+ present on disk) WITHOUT unlinking or pruning — the preview surface for
240
+ ``install.py --dry-run``. Selection logic is identical to the live path.
214
241
  """
215
242
  anchor_resolved = anchor.expanduser().resolve()
216
243
  deleted: list[Path] = []
@@ -242,6 +269,9 @@ def bootstrap_reap_tagged(
242
269
  line.strip() == needle for line in block.splitlines()
243
270
  ):
244
271
  continue
272
+ if dry_run:
273
+ deleted.append(md)
274
+ continue
245
275
  try:
246
276
  md.unlink()
247
277
  except OSError:
@@ -0,0 +1,206 @@
1
+ """link_crypto — encrypt/decrypt stored third-party package links.
2
+
3
+ Why this exists
4
+ ---------------
5
+ This package never stores a *readable* link to, or name of, an external
6
+ source that inspired an idea (see the source-confidentiality sweep). Where a
7
+ source link genuinely has to be retained — e.g. the upstream URL + pin in
8
+ ``agents/settings/contexts/skills-provenance.yml`` for license / refresh
9
+ bookkeeping — it is stored **encrypted**, never in plaintext.
10
+
11
+ Key resolution (per the maintainer's contract)
12
+ ----------------------------------------------
13
+ The symmetric key lives in ``.agent-settings.yml`` under
14
+ ``secrets.link_encryption_key`` and is **never committed** (the file is
15
+ gitignored). It is read in this order:
16
+
17
+ 1. **Project** — ``<project-root>/.agent-settings.yml``.
18
+ 2. **User-global** — ``~/.event4u/agent-config/agent-settings.yml``
19
+ (with the legacy-path fallback used by the rest of the suite).
20
+
21
+ ``encrypt`` uses the first key it finds (project preferred). ``decrypt`` tries
22
+ the project key first and, only if that fails to authenticate, falls back to
23
+ the user-global key — matching "try the project key, if it doesn't work use
24
+ the global one".
25
+
26
+ Threat model
27
+ ------------
28
+ The goal is **repo confidentiality**: someone browsing the committed tree (or
29
+ the published npm tarball / plugin mirror) must not be able to read which
30
+ external packages were used. It is authenticated symmetric encryption built
31
+ from the Python standard library only (PBKDF2-HMAC-SHA256 key derivation, an
32
+ HMAC-SHA256 counter-mode keystream, encrypt-then-MAC with HMAC-SHA256). No
33
+ third-party crypto dependency is added (scope-control). This is not intended
34
+ to withstand an offline attacker who already holds the key file.
35
+
36
+ Token format
37
+ ------------
38
+ ``ENC1:<base64( salt[16] || nonce[16] || ciphertext || tag[32] )>``
39
+ """
40
+
41
+ from __future__ import annotations
42
+
43
+ import base64
44
+ import hashlib
45
+ import hmac
46
+ import os
47
+ import re
48
+ import secrets
49
+ import sys
50
+ from pathlib import Path
51
+
52
+ MAGIC = "ENC1"
53
+ _SALT_LEN = 16
54
+ _NONCE_LEN = 16
55
+ _TAG_LEN = 32
56
+ _PBKDF2_ITERS = 200_000
57
+ _KEY_PATH = "secrets.link_encryption_key"
58
+ _USER_GLOBAL = Path.home() / ".event4u" / "agent-config" / "agent-settings.yml"
59
+ # Legacy user-global location kept readable for older installs.
60
+ _USER_GLOBAL_LEGACY = Path.home() / ".agent-config" / "agent-settings.yml"
61
+
62
+
63
+ # --------------------------------------------------------------------------- #
64
+ # Core crypto (stdlib only)
65
+ # --------------------------------------------------------------------------- #
66
+ def _derive(key: str, salt: bytes) -> tuple[bytes, bytes]:
67
+ dk = hashlib.pbkdf2_hmac("sha256", key.encode("utf-8"), salt, _PBKDF2_ITERS, dklen=64)
68
+ return dk[:32], dk[32:] # (enc_key, mac_key)
69
+
70
+
71
+ def _keystream(enc_key: bytes, nonce: bytes, n: int) -> bytes:
72
+ out = bytearray()
73
+ counter = 0
74
+ while len(out) < n:
75
+ out += hmac.new(enc_key, nonce + counter.to_bytes(8, "big"), hashlib.sha256).digest()
76
+ counter += 1
77
+ return bytes(out[:n])
78
+
79
+
80
+ def encrypt(plaintext: str, key: str) -> str:
81
+ """Encrypt ``plaintext`` with ``key`` → an ``ENC1:`` token."""
82
+ if not key:
83
+ raise ValueError("empty encryption key")
84
+ salt = secrets.token_bytes(_SALT_LEN)
85
+ nonce = secrets.token_bytes(_NONCE_LEN)
86
+ enc_key, mac_key = _derive(key, salt)
87
+ pt = plaintext.encode("utf-8")
88
+ ct = bytes(a ^ b for a, b in zip(pt, _keystream(enc_key, nonce, len(pt))))
89
+ tag = hmac.new(mac_key, salt + nonce + ct, hashlib.sha256).digest()
90
+ return f"{MAGIC}:" + base64.b64encode(salt + nonce + ct + tag).decode("ascii")
91
+
92
+
93
+ def is_token(value: str) -> bool:
94
+ return isinstance(value, str) and value.startswith(f"{MAGIC}:")
95
+
96
+
97
+ def _decrypt_one(token: str, key: str) -> str:
98
+ raw = base64.b64decode(token[len(MAGIC) + 1:])
99
+ salt, nonce, rest = raw[:_SALT_LEN], raw[_SALT_LEN:_SALT_LEN + _NONCE_LEN], raw[_SALT_LEN + _NONCE_LEN:]
100
+ ct, tag = rest[:-_TAG_LEN], rest[-_TAG_LEN:]
101
+ enc_key, mac_key = _derive(key, salt)
102
+ expected = hmac.new(mac_key, salt + nonce + ct, hashlib.sha256).digest()
103
+ if not hmac.compare_digest(expected, tag):
104
+ raise ValueError("authentication failed (wrong key or corrupt token)")
105
+ return bytes(a ^ b for a, b in zip(ct, _keystream(enc_key, nonce, len(ct)))).decode("utf-8")
106
+
107
+
108
+ def decrypt(token: str, keys: str | list[str]) -> str:
109
+ """Decrypt ``token``, trying each key in order (project first, then global)."""
110
+ if not is_token(token):
111
+ raise ValueError("not an ENC1 token")
112
+ candidates = [keys] if isinstance(keys, str) else list(keys)
113
+ candidates = [k for k in candidates if k]
114
+ if not candidates:
115
+ raise ValueError("no decryption key available")
116
+ last: Exception | None = None
117
+ for k in candidates:
118
+ try:
119
+ return _decrypt_one(token, k)
120
+ except Exception as exc: # noqa: BLE001 — try next key
121
+ last = exc
122
+ raise ValueError(f"decryption failed with all configured keys: {last}")
123
+
124
+
125
+ # --------------------------------------------------------------------------- #
126
+ # Key resolution from .agent-settings.yml (project → user-global)
127
+ # --------------------------------------------------------------------------- #
128
+ def _read_key_from(path: Path | None) -> str | None:
129
+ if not path or not path.is_file():
130
+ return None
131
+ # Minimal, dependency-free scalar read so this works even where PyYAML is
132
+ # absent. Matches `link_encryption_key:` at any indentation.
133
+ pat = re.compile(r'^\s*link_encryption_key:\s*["\']?([^"\'#\s]+)')
134
+ for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
135
+ m = pat.match(line)
136
+ if m:
137
+ return m.group(1)
138
+ return None
139
+
140
+
141
+ def project_key(project_root: Path | str | None = None) -> str | None:
142
+ root = Path(project_root) if project_root else Path.cwd()
143
+ return _read_key_from(root / ".agent-settings.yml")
144
+
145
+
146
+ def global_key() -> str | None:
147
+ return _read_key_from(_USER_GLOBAL) or _read_key_from(_USER_GLOBAL_LEGACY)
148
+
149
+
150
+ def resolve_keys(project_root: Path | str | None = None) -> list[str]:
151
+ """Ordered, de-duplicated key list: project first, then user-global.
152
+
153
+ An ``EVENT4U_LINK_KEY`` environment variable, if set, is consulted last as
154
+ a CI/automation escape hatch.
155
+ """
156
+ keys: list[str] = []
157
+ for k in (project_key(project_root), global_key(), os.environ.get("EVENT4U_LINK_KEY")):
158
+ if k and k not in keys:
159
+ keys.append(k)
160
+ return keys
161
+
162
+
163
+ # --------------------------------------------------------------------------- #
164
+ # CLI
165
+ # --------------------------------------------------------------------------- #
166
+ def _cli(argv: list[str]) -> int:
167
+ import argparse
168
+
169
+ p = argparse.ArgumentParser(prog="link_crypto", description=__doc__.split("\n", 1)[0])
170
+ sub = p.add_subparsers(dest="cmd", required=True)
171
+ pe = sub.add_parser("encrypt", help="encrypt a plaintext value (reads stdin if no --value)")
172
+ pe.add_argument("--value")
173
+ pd = sub.add_parser("decrypt", help="decrypt an ENC1 token (reads stdin if no --value)")
174
+ pd.add_argument("--value")
175
+ sub.add_parser("keygen", help="generate a fresh base64 key for .agent-settings.yml")
176
+ sub.add_parser("keystatus", help="report which key sources resolve (no secrets printed)")
177
+ args = p.parse_args(argv)
178
+
179
+ if args.cmd == "keygen":
180
+ print(base64.urlsafe_b64encode(secrets.token_bytes(32)).decode("ascii").rstrip("="))
181
+ return 0
182
+
183
+ if args.cmd == "keystatus":
184
+ print(f"project key: {'present' if project_key() else 'absent'}")
185
+ print(f"user-global key: {'present' if global_key() else 'absent'}")
186
+ print(f"env EVENT4U_LINK_KEY: {'present' if os.environ.get('EVENT4U_LINK_KEY') else 'absent'}")
187
+ print(f"resolved key count: {len(resolve_keys())}")
188
+ return 0
189
+
190
+ value = args.value if args.value is not None else sys.stdin.read().strip()
191
+ keys = resolve_keys()
192
+ if not keys:
193
+ sys.stderr.write(
194
+ "error: no link_encryption_key found in project or user-global "
195
+ ".agent-settings.yml (secrets.link_encryption_key)\n"
196
+ )
197
+ return 2
198
+ if args.cmd == "encrypt":
199
+ print(encrypt(value, keys[0]))
200
+ else:
201
+ print(decrypt(value, keys))
202
+ return 0
203
+
204
+
205
+ if __name__ == "__main__":
206
+ raise SystemExit(_cli(sys.argv[1:]))
@@ -0,0 +1,228 @@
1
+ #!/usr/bin/env python3
2
+ """Shared helpers for the agent-security corpus linters (road-to-security-pillar.md P1).
3
+
4
+ Implements the **false-positive containment convention** (P1.5) so the
5
+ self-audit linters can scan a corpus that legitimately *contains* attack
6
+ strings as teaching material, without the allowlist-growth death-spiral:
7
+
8
+ 1. **Fenced-block exemption** — content inside a ```` ```security-example ````
9
+ fence is skipped by every check. Grep-auditable, scoped to the block.
10
+ 2. **Confidence weighting** — a match in a doc / example / template / evals
11
+ file scores at 0.25x; below the FAIL threshold it is a WARN, not an error.
12
+ 3. **Per-file pragma** — ``<!-- security-lint: allow <check> "<reason>" -->``
13
+ anywhere in the file suppresses one check for that file. Reasons are
14
+ mandatory and counted; crossing PRAGMA_CAP entries repo-wide means the
15
+ linter is wrong (escalate per autonomous-execution), not "add another".
16
+
17
+ There is **no global allowlist** — that is the rejected pattern.
18
+
19
+ The module is import-only (no side effects). Each linter builds its findings
20
+ with :func:`scan` + the predicates here, then calls :func:`report`.
21
+ """
22
+ from __future__ import annotations
23
+
24
+ import re
25
+ from dataclasses import dataclass
26
+ from pathlib import Path
27
+
28
+ # repo root, resolved from src/scripts/_lib/security_lint.py
29
+ ROOT = Path(__file__).resolve().parents[3]
30
+
31
+ PRAGMA_CAP = 20
32
+ EXAMPLE_FENCE_LANG = "security-example"
33
+
34
+ # Shown in every linter's --help (P1.5 reference obligation).
35
+ GUIDELINE = "docs/guidelines/agent-infra/security-lint-containment.md"
36
+ GUIDELINE_EPILOG = (
37
+ "False-positive containment (fenced security-example block, confidence "
38
+ "weighting, per-file `security-lint: allow` pragma — no global allowlist): "
39
+ f"see {GUIDELINE}."
40
+ )
41
+
42
+ # Source-of-truth roots scanned by the self-audit linters.
43
+ DEFAULT_SCAN_ROOTS = ("src/skills", "src/rules", "src/agent-src", "src/domains")
44
+
45
+ # A path is "example/teaching" (0.25x weight) when it lives under docs/ or
46
+ # evals/, or its name marks it as an example/template/fixture.
47
+ _EXAMPLE_PATH = re.compile(
48
+ r"(^|/)(docs|evals|tests?|fixtures?)(/|$)|example|template|sample|/_template",
49
+ re.IGNORECASE,
50
+ )
51
+
52
+ _PRAGMA = re.compile(
53
+ r"<!--\s*security-lint:\s*allow\s+(?P<check>[\w-]+)\s+"
54
+ r"\"(?P<reason>[^\"]+)\"\s*-->"
55
+ )
56
+
57
+ _FENCE = re.compile(r"^(\s*)(`{3,}|~{3,})\s*([\w-]*)\s*$")
58
+
59
+ SEVERITY_RANK = {"LOW": 1, "MED": 2, "HIGH": 3}
60
+
61
+
62
+ @dataclass(frozen=True)
63
+ class Finding:
64
+ """One linter hit. ``weight`` is the confidence multiplier (1.0 or 0.25)."""
65
+
66
+ path: str # repo-relative
67
+ line: int # 1-based; 0 = file-level
68
+ check: str # stable check id, also the pragma key
69
+ severity: str # HIGH | MED | LOW
70
+ message: str
71
+ weight: float = 1.0
72
+
73
+ @property
74
+ def is_fail(self) -> bool:
75
+ """A HIGH-severity, full-weight finding fails the build."""
76
+ return self.severity == "HIGH" and self.weight >= 1.0
77
+
78
+
79
+ def is_example_path(rel_path: str) -> bool:
80
+ return bool(_EXAMPLE_PATH.search(rel_path))
81
+
82
+
83
+ def path_weight(rel_path: str) -> float:
84
+ return 0.25 if is_example_path(rel_path) else 1.0
85
+
86
+
87
+ @dataclass
88
+ class ScannedFile:
89
+ """A file pre-split into lines with a fence/pragma mask the linters reuse."""
90
+
91
+ path: Path
92
+ rel: str
93
+ lines: list[str]
94
+ # per-line flags (1-based index → flag); index 0 unused
95
+ in_example_fence: list[bool]
96
+ in_any_fence: list[bool]
97
+ pragmas: dict[str, str] # check id → reason (first 15 lines)
98
+ weight: float
99
+
100
+ def pragma_allows(self, check: str) -> bool:
101
+ return check in self.pragmas
102
+
103
+ def iter_lines(self, *, skip_example_fence: bool = True,
104
+ skip_any_fence: bool = False):
105
+ """Yield (lineno, text) honouring the fence masks."""
106
+ for i, text in enumerate(self.lines, start=1):
107
+ if skip_example_fence and self.in_example_fence[i]:
108
+ continue
109
+ if skip_any_fence and self.in_any_fence[i]:
110
+ continue
111
+ yield i, text
112
+
113
+
114
+ def scan_file(path: Path) -> ScannedFile:
115
+ if path.is_absolute():
116
+ try:
117
+ rel = path.relative_to(ROOT).as_posix()
118
+ except ValueError:
119
+ rel = path.name # outside the package root (e.g. a consumer-config audit)
120
+ else:
121
+ rel = path.as_posix()
122
+ raw = path.read_text(encoding="utf-8", errors="surrogatepass")
123
+ lines = raw.splitlines()
124
+ n = len(lines)
125
+ in_example = [False] * (n + 1)
126
+ in_any = [False] * (n + 1)
127
+
128
+ fence_open = False
129
+ fence_marker = ""
130
+ fence_is_example = False
131
+ for i, text in enumerate(lines, start=1):
132
+ m = _FENCE.match(text)
133
+ if m and not fence_open:
134
+ fence_open = True
135
+ fence_marker = m.group(2)[0]
136
+ fence_is_example = m.group(3) == EXAMPLE_FENCE_LANG
137
+ in_any[i] = True
138
+ in_example[i] = fence_is_example
139
+ continue
140
+ if fence_open:
141
+ in_any[i] = True
142
+ in_example[i] = fence_is_example
143
+ # closing fence: same marker char, 3+ long, no info string
144
+ cm = _FENCE.match(text)
145
+ if cm and cm.group(2)[0] == fence_marker and cm.group(3) == "":
146
+ fence_open = False
147
+ fence_is_example = False
148
+
149
+ # Pragmas are explicit, grep-auditable opt-out markers — honour them
150
+ # anywhere in the file (a long frontmatter can push the body past line 15).
151
+ pragmas: dict[str, str] = {}
152
+ for text in lines:
153
+ for m in _PRAGMA.finditer(text):
154
+ pragmas[m.group("check")] = m.group("reason")
155
+
156
+ return ScannedFile(
157
+ path=path,
158
+ rel=rel,
159
+ lines=lines,
160
+ in_example_fence=in_example,
161
+ in_any_fence=in_any,
162
+ pragmas=pragmas,
163
+ weight=path_weight(rel),
164
+ )
165
+
166
+
167
+ def scan_path(path: Path, base: Path) -> ScannedFile:
168
+ """scan_file, but compute ``rel`` relative to an arbitrary ``base`` root.
169
+
170
+ Used by the consumer-facing audit (P3.1) to scan a target repo's config
171
+ instead of this package's ``src/``.
172
+ """
173
+ sf = scan_file(path)
174
+ try:
175
+ rel = path.resolve().relative_to(base.resolve()).as_posix()
176
+ except ValueError:
177
+ rel = path.name
178
+ return ScannedFile(
179
+ path=sf.path, rel=rel, lines=sf.lines,
180
+ in_example_fence=sf.in_example_fence, in_any_fence=sf.in_any_fence,
181
+ pragmas=sf.pragmas, weight=path_weight(rel),
182
+ )
183
+
184
+
185
+ def iter_corpus(roots=DEFAULT_SCAN_ROOTS, exts=(".md",)):
186
+ """Yield ScannedFile for every matching file under the given roots."""
187
+ for root in roots:
188
+ base = ROOT / root
189
+ if not base.exists():
190
+ continue
191
+ for path in sorted(base.rglob("*")):
192
+ if path.is_file() and path.suffix in exts:
193
+ yield scan_file(path)
194
+
195
+
196
+ def report(findings: list[Finding], *, check_label: str) -> int:
197
+ """Print findings grouped by severity; return an exit code.
198
+
199
+ Exit 1 iff at least one finding ``is_fail`` (HIGH + full weight). WARN-level
200
+ (weighted-down or < HIGH) findings print but never fail the build.
201
+ """
202
+ if not findings:
203
+ print(f"✅ {check_label}: clean ({_corpus_note()}).")
204
+ return 0
205
+
206
+ fails = [f for f in findings if f.is_fail]
207
+ warns = [f for f in findings if not f.is_fail]
208
+
209
+ for f in sorted(findings, key=lambda x: (-SEVERITY_RANK.get(x.severity, 0), x.path, x.line)):
210
+ glyph = "\U0001f534" if f.is_fail else "⚠️"
211
+ loc = f"{f.path}:{f.line}" if f.line else f.path
212
+ wnote = "" if f.weight >= 1.0 else f" (weight {f.weight:g})"
213
+ print(f" {glyph} [{f.severity}] {f.check} — {loc}{wnote}: {f.message}")
214
+
215
+ print()
216
+ if fails:
217
+ print(
218
+ f"❌ {check_label}: {len(fails)} blocking finding(s), "
219
+ f"{len(warns)} warning(s). Fix, or mark a true teaching example with a "
220
+ f"```{EXAMPLE_FENCE_LANG} fence or a `security-lint: allow` pragma."
221
+ )
222
+ return 1
223
+ print(f"⚠️ {check_label}: {len(warns)} warning(s), 0 blocking.")
224
+ return 0
225
+
226
+
227
+ def _corpus_note() -> str:
228
+ return "scanned " + ", ".join(DEFAULT_SCAN_ROOTS)
@@ -324,7 +324,7 @@ class GeminiClient(ExternalAIClient):
324
324
  if api_key is None:
325
325
  raise RuntimeError(
326
326
  "GeminiClient requires explicit api_key or injected client. "
327
- "Use `api_key_ref: env:GEMINI_API_KEY` in agents/settings/.ai-council.yml."
327
+ "Use `api_key_ref: env:GEMINI_API_KEY` in ~/.event4u/agent-config/settings/.ai-council.yml."
328
328
  )
329
329
  try:
330
330
  from google import genai # type: ignore[import-not-found]
@@ -658,7 +658,7 @@ class CliClient(ExternalAIClient):
658
658
  raise CliClientError(
659
659
  f"{type(self).__name__}: binary {self.default_binary!r} "
660
660
  f"not found on PATH. Install the provider CLI or set "
661
- f"`members.{self.name}.binary:` in agents/settings/.ai-council.yml."
661
+ f"`members.{self.name}.binary:` in ~/.event4u/agent-config/settings/.ai-council.yml."
662
662
  )
663
663
  self.binary = resolved
664
664
 
@@ -456,6 +456,61 @@ class CouncilConfig:
456
456
  source_path: Path | None = None
457
457
 
458
458
 
459
+ #: Dotfile name for the council config in any scope.
460
+ COUNCIL_CONFIG_RELNAME = ".ai-council.yml"
461
+
462
+ #: User-global location, relative to ``event4u_root()`` — under ``settings/``
463
+ #: alongside the other per-user config (``.agent-settings.yml``,
464
+ #: ``.agent-user.yml``). This is exactly the path the browser setup wizard
465
+ #: reads/writes (``<writeRoot>/settings/.ai-council.yml`` in
466
+ #: ``src/server/routes/wizard.ts``), so a council configured in the wizard is
467
+ #: the same file the CLI reads.
468
+ COUNCIL_CONFIG_USER_GLOBAL_REL = "settings/.ai-council.yml"
469
+
470
+ #: Env var pinning the council config to an explicit absolute path, ahead
471
+ #: of the project → user-global search. Mirrors ``EVENT4U_CONFIG_HOME`` but
472
+ #: targets the config file itself (tests / power users).
473
+ COUNCIL_CONFIG_ENV = "AI_COUNCIL_CONFIG"
474
+
475
+
476
+ def resolve_config_path(project_root: Path, *, env: dict | None = None) -> Path:
477
+ """Resolve which ``.ai-council.yml`` the council reads.
478
+
479
+ Precedence (first match wins):
480
+
481
+ 1. ``$AI_COUNCIL_CONFIG`` — explicit absolute override (tests / power
482
+ users). Honoured even when the target is absent, so a typo surfaces
483
+ as "create it here" instead of a silent fallback.
484
+ 2. Project-local ``<project_root>/agents/settings/.ai-council.yml`` — a
485
+ consumer project that checks in its own council config.
486
+ 3. User-global ``~/.event4u/agent-config/settings/.ai-council.yml`` (with
487
+ the legacy ``~/.config/agent-config/`` read-fallback) — the canonical
488
+ per-user location, configured once for every project the developer
489
+ works in, and the exact file the setup wizard reads/writes.
490
+
491
+ Always returns a ``Path`` (never ``None``): when nothing exists yet it
492
+ returns the user-global write target, so callers' ``.exists()`` gate and
493
+ "create it at <path>" messaging both point at the global location.
494
+ """
495
+ env_map = env if env is not None else os.environ
496
+ override = env_map.get(COUNCIL_CONFIG_ENV)
497
+ if override:
498
+ return Path(override).expanduser()
499
+ project_path = (
500
+ project_root / "agents" / "settings" / COUNCIL_CONFIG_RELNAME
501
+ )
502
+ if project_path.exists():
503
+ return project_path
504
+ found = user_global_paths.resolve_with_fallback(
505
+ COUNCIL_CONFIG_USER_GLOBAL_REL, env=env,
506
+ )
507
+ if found is not None:
508
+ return found
509
+ return user_global_paths.write_target(
510
+ COUNCIL_CONFIG_USER_GLOBAL_REL, env=env,
511
+ )
512
+
513
+
459
514
  def load_council_config(path: Path) -> CouncilConfig:
460
515
  """Load and validate the council YAML at ``path``."""
461
516
  if not path.exists():
@@ -33,8 +33,6 @@ AREAS: dict[str, dict[str, str]] = {
33
33
  "scope": "router.json shape, tier semantics, dispatch precedence."},
34
34
  "smoke": {"contract": "smoke-contracts.md",
35
35
  "scope": "Per-tier smoke contracts, baseline locks, regression gates."},
36
- "memory": {"contract": "agent-memory-contract.md",
37
- "scope": "Memory MCP, propose / promote / poison flow, runtime-trust scoring."},
38
36
  }
39
37
 
40
38
  NAMED = re.compile(r"^(\d{4})-([a-z0-9-]+)\.md$")