@event4u/agent-config 1.19.0 → 1.21.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 (297) hide show
  1. package/.agent-src/commands/agent-handoff.md +14 -10
  2. package/.agent-src/commands/agents.md +1 -1
  3. package/.agent-src/commands/bug-fix.md +1 -1
  4. package/.agent-src/commands/bug-investigate.md +2 -2
  5. package/.agent-src/commands/chat-history/import.md +166 -0
  6. package/.agent-src/commands/chat-history/learn.md +178 -0
  7. package/.agent-src/commands/chat-history/show.md +17 -18
  8. package/.agent-src/commands/chat-history.md +26 -25
  9. package/.agent-src/commands/compress.md +12 -0
  10. package/.agent-src/commands/context/create.md +2 -2
  11. package/.agent-src/commands/context.md +1 -1
  12. package/.agent-src/commands/copilot-agents.md +1 -1
  13. package/.agent-src/commands/council/default.md +21 -12
  14. package/.agent-src/commands/council.md +1 -1
  15. package/.agent-src/commands/create-pr.md +28 -8
  16. package/.agent-src/commands/e2e-heal.md +1 -1
  17. package/.agent-src/commands/e2e-plan.md +1 -1
  18. package/.agent-src/commands/feature/dev.md +3 -3
  19. package/.agent-src/commands/feature.md +1 -1
  20. package/.agent-src/commands/fix/seeder.md +2 -2
  21. package/.agent-src/commands/fix.md +1 -1
  22. package/.agent-src/commands/jira-ticket.md +1 -1
  23. package/.agent-src/commands/judge.md +2 -2
  24. package/.agent-src/commands/memory.md +1 -1
  25. package/.agent-src/commands/mode.md +5 -5
  26. package/.agent-src/commands/module.md +1 -1
  27. package/.agent-src/commands/onboard.md +4 -4
  28. package/.agent-src/commands/optimize/augmentignore.md +1 -1
  29. package/.agent-src/commands/optimize-prompt.md +61 -0
  30. package/.agent-src/commands/optimize.md +1 -1
  31. package/.agent-src/commands/override.md +1 -1
  32. package/.agent-src/commands/review-changes.md +1 -1
  33. package/.agent-src/commands/review-routing.md +1 -1
  34. package/.agent-src/commands/roadmap.md +1 -1
  35. package/.agent-src/commands/set-cost-profile.md +3 -3
  36. package/.agent-src/commands/sync-agent-settings.md +2 -2
  37. package/.agent-src/commands/sync-gitignore.md +1 -1
  38. package/.agent-src/commands/tests/create.md +2 -2
  39. package/.agent-src/commands/tests.md +1 -1
  40. package/.agent-src/commands/threat-model.md +4 -4
  41. package/.agent-src/contexts/authority/commit-mechanics.md +14 -1
  42. package/.agent-src/contexts/authority/destructive-mechanics.md +14 -1
  43. package/.agent-src/contexts/authority/scope-mechanics.md +5 -0
  44. package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +76 -0
  45. package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
  46. package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +4 -4
  47. package/.agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +98 -0
  48. package/.agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +93 -0
  49. package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +125 -9
  50. package/.agent-src/contexts/execution/autonomy-mechanics.md +44 -0
  51. package/.agent-src/contexts/model-recommendations.md +2 -2
  52. package/.agent-src/contexts/override-system.md +1 -1
  53. package/.agent-src/personas/product-owner.md +2 -2
  54. package/.agent-src/personas/qa.md +1 -1
  55. package/.agent-src/rules/agent-authority.md +5 -6
  56. package/.agent-src/rules/agent-docs.md +11 -53
  57. package/.agent-src/rules/analysis-skill-routing.md +10 -40
  58. package/.agent-src/rules/architecture.md +6 -1
  59. package/.agent-src/rules/artifact-drafting-protocol.md +5 -0
  60. package/.agent-src/rules/artifact-engagement-recording.md +23 -59
  61. package/.agent-src/rules/ask-when-uncertain.md +24 -47
  62. package/.agent-src/rules/augment-portability.md +14 -62
  63. package/.agent-src/rules/augment-source-of-truth.md +10 -1
  64. package/.agent-src/rules/autonomous-execution.md +17 -98
  65. package/.agent-src/rules/capture-learnings.md +9 -80
  66. package/.agent-src/rules/cli-output-handling.md +12 -42
  67. package/.agent-src/rules/command-suggestion-policy.md +25 -73
  68. package/.agent-src/rules/commit-conventions.md +9 -58
  69. package/.agent-src/rules/commit-policy.md +16 -47
  70. package/.agent-src/rules/context-hygiene.md +5 -0
  71. package/.agent-src/rules/direct-answers.md +21 -42
  72. package/.agent-src/rules/docker-commands.md +11 -45
  73. package/.agent-src/rules/docs-sync.md +10 -56
  74. package/.agent-src/rules/downstream-changes.md +5 -0
  75. package/.agent-src/rules/e2e-testing.md +9 -44
  76. package/.agent-src/rules/guidelines.md +13 -75
  77. package/.agent-src/rules/improve-before-implement.md +10 -2
  78. package/.agent-src/rules/language-and-tone.md +35 -69
  79. package/.agent-src/rules/laravel-translations.md +11 -40
  80. package/.agent-src/rules/markdown-safe-codeblocks.md +4 -0
  81. package/.agent-src/rules/minimal-safe-diff.md +4 -0
  82. package/.agent-src/rules/missing-tool-handling.md +4 -0
  83. package/.agent-src/rules/model-recommendation.md +9 -61
  84. package/.agent-src/rules/no-attribution-footers.md +53 -0
  85. package/.agent-src/rules/no-cheap-questions.md +11 -27
  86. package/.agent-src/rules/no-council-references.md +76 -0
  87. package/.agent-src/rules/no-roadmap-references.md +8 -1
  88. package/.agent-src/rules/non-destructive-by-default.md +13 -43
  89. package/.agent-src/rules/onboarding-gate.md +9 -117
  90. package/.agent-src/rules/package-ci-checks.md +10 -37
  91. package/.agent-src/rules/php-coding.md +10 -55
  92. package/.agent-src/rules/preservation-guard.md +9 -0
  93. package/.agent-src/rules/review-routing-awareness.md +9 -97
  94. package/.agent-src/rules/reviewer-awareness.md +8 -83
  95. package/.agent-src/rules/roadmap-progress-sync.md +7 -170
  96. package/.agent-src/rules/role-mode-adherence.md +6 -2
  97. package/.agent-src/rules/rule-type-governance.md +8 -66
  98. package/.agent-src/rules/runtime-safety.md +5 -0
  99. package/.agent-src/rules/scope-control.md +17 -62
  100. package/.agent-src/rules/security-sensitive-stop.md +7 -1
  101. package/.agent-src/rules/size-enforcement.md +6 -1
  102. package/.agent-src/rules/skill-improvement-trigger.md +9 -49
  103. package/.agent-src/rules/skill-quality.md +7 -64
  104. package/.agent-src/rules/slash-command-routing-policy.md +11 -63
  105. package/.agent-src/rules/think-before-action.md +22 -87
  106. package/.agent-src/rules/token-efficiency.md +10 -74
  107. package/.agent-src/rules/token-optimizer-maintenance.md +68 -0
  108. package/.agent-src/rules/tool-safety.md +4 -0
  109. package/.agent-src/rules/ui-audit-gate.md +25 -61
  110. package/.agent-src/rules/upstream-proposal.md +9 -67
  111. package/.agent-src/rules/user-interaction.md +25 -95
  112. package/.agent-src/rules/verify-before-complete.md +1 -1
  113. package/.agent-src/skills/agent-docs-writing/SKILL.md +1 -1
  114. package/.agent-src/skills/ai-council/SKILL.md +69 -5
  115. package/.agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
  116. package/.agent-src/skills/analysis-skill-router/SKILL.md +3 -3
  117. package/.agent-src/skills/artisan-commands/SKILL.md +2 -2
  118. package/.agent-src/skills/authz-review/SKILL.md +1 -1
  119. package/.agent-src/skills/aws-infrastructure/SKILL.md +5 -5
  120. package/.agent-src/skills/blast-radius-analyzer/SKILL.md +8 -8
  121. package/.agent-src/skills/bug-analyzer/SKILL.md +5 -5
  122. package/.agent-src/skills/code-refactoring/SKILL.md +4 -4
  123. package/.agent-src/skills/code-review/SKILL.md +2 -2
  124. package/.agent-src/skills/command-writing/SKILL.md +11 -0
  125. package/.agent-src/skills/composer-packages/SKILL.md +2 -2
  126. package/.agent-src/skills/context-authoring/SKILL.md +11 -0
  127. package/.agent-src/skills/context-document/SKILL.md +1 -1
  128. package/.agent-src/skills/copilot-agents-optimization/SKILL.md +23 -0
  129. package/.agent-src/skills/copilot-config/SKILL.md +1 -1
  130. package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
  131. package/.agent-src/skills/dependency-upgrade/SKILL.md +2 -2
  132. package/.agent-src/skills/devcontainer/SKILL.md +2 -2
  133. package/.agent-src/skills/developer-like-execution/SKILL.md +1 -1
  134. package/.agent-src/skills/docker/SKILL.md +1 -1
  135. package/.agent-src/skills/dto-creator/SKILL.md +1 -1
  136. package/.agent-src/skills/estimate-ticket/SKILL.md +2 -2
  137. package/.agent-src/skills/fe-design/SKILL.md +4 -4
  138. package/.agent-src/skills/feature-planning/SKILL.md +5 -5
  139. package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
  140. package/.agent-src/skills/laravel/SKILL.md +1 -1
  141. package/.agent-src/skills/laravel-notifications/SKILL.md +5 -5
  142. package/.agent-src/skills/laravel-pennant/SKILL.md +1 -1
  143. package/.agent-src/skills/laravel-pulse/SKILL.md +4 -4
  144. package/.agent-src/skills/laravel-reverb/SKILL.md +2 -2
  145. package/.agent-src/skills/laravel-scheduling/SKILL.md +1 -1
  146. package/.agent-src/skills/md-language-check/SKILL.md +1 -1
  147. package/.agent-src/skills/migration-creator/SKILL.md +7 -7
  148. package/.agent-src/skills/multi-tenancy/SKILL.md +8 -8
  149. package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
  150. package/.agent-src/skills/performance-analysis/SKILL.md +3 -3
  151. package/.agent-src/skills/pest-testing/SKILL.md +6 -6
  152. package/.agent-src/skills/php-service/SKILL.md +2 -2
  153. package/.agent-src/skills/project-analysis-hypothesis-driven/SKILL.md +3 -3
  154. package/.agent-src/skills/project-analysis-react/SKILL.md +1 -1
  155. package/.agent-src/skills/project-analysis-symfony/SKILL.md +1 -1
  156. package/.agent-src/skills/project-analysis-zend-laminas/SKILL.md +2 -2
  157. package/.agent-src/skills/project-analyzer/SKILL.md +4 -4
  158. package/.agent-src/skills/prompt-optimizer/SKILL.md +108 -0
  159. package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
  160. package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
  161. package/.agent-src/skills/rule-writing/SKILL.md +33 -0
  162. package/.agent-src/skills/sentry-integration/SKILL.md +1 -1
  163. package/.agent-src/skills/skill-writing/SKILL.md +14 -0
  164. package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
  165. package/.agent-src/skills/terraform/SKILL.md +2 -2
  166. package/.agent-src/skills/terragrunt/SKILL.md +8 -8
  167. package/.agent-src/skills/test-performance/SKILL.md +5 -5
  168. package/.agent-src/skills/threat-modeling/SKILL.md +2 -2
  169. package/.agent-src/skills/token-optimizer/SKILL.md +110 -0
  170. package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
  171. package/.agent-src/skills/universal-project-analysis/SKILL.md +1 -1
  172. package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
  173. package/.agent-src/templates/AGENTS.md +1 -1
  174. package/.agent-src/templates/agent-settings.md +25 -41
  175. package/.agent-src/templates/contexts/tenant-boundaries.md +2 -2
  176. package/.agent-src/templates/contexts.md +1 -1
  177. package/.agent-src/templates/copilot-instructions.md +21 -0
  178. package/.agent-src/templates/copilot-review-instructions.md +76 -0
  179. package/.agent-src/templates/features.md +1 -1
  180. package/.agent-src/templates/rule.md +127 -0
  181. package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +7 -5
  182. package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +0 -4
  183. package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +0 -4
  184. package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
  185. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
  186. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
  187. package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +2 -3
  188. package/.agent-src/templates/skill.md +30 -1
  189. package/.claude-plugin/marketplace.json +11 -4
  190. package/AGENTS.md +71 -3
  191. package/CHANGELOG.md +180 -3
  192. package/README.md +24 -23
  193. package/config/agent-settings.template.yml +63 -23
  194. package/config/gitignore-block.txt +11 -4
  195. package/docs/architecture.md +84 -3
  196. package/docs/catalog.md +23 -11
  197. package/docs/contracts/adr-chat-history-split.md +10 -1
  198. package/docs/contracts/agent-memory-contract.md +1 -1
  199. package/docs/contracts/command-clusters.md +1 -1
  200. package/docs/contracts/context-paths.md +2 -1
  201. package/docs/contracts/cross-wing-handoff.md +133 -0
  202. package/docs/contracts/file-ownership-matrix.json +678 -609
  203. package/docs/contracts/hook-architecture-v1.md +8 -1
  204. package/docs/contracts/iron-law-overrides.txt +25 -0
  205. package/docs/contracts/kernel-membership.md +273 -0
  206. package/docs/contracts/load-context-schema.md +26 -11
  207. package/docs/contracts/memory-visibility-v1.md +8 -24
  208. package/docs/contracts/pilot/agent-authority.md +24 -0
  209. package/docs/contracts/pilot/direct-answers.md +70 -0
  210. package/docs/contracts/pilot/language-and-tone.md +63 -0
  211. package/docs/contracts/rule-classification.md +170 -0
  212. package/docs/contracts/rule-router.md +153 -0
  213. package/docs/customization.md +18 -7
  214. package/docs/decisions/ADR-001-kernel-swap-deferred.md +109 -0
  215. package/docs/decisions/ADR-002-kernel-bucket-overrides.md +124 -0
  216. package/docs/decisions/ADR-rule-kernel-and-router.md +122 -0
  217. package/docs/getting-started.md +19 -27
  218. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
  219. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +176 -0
  220. package/docs/guidelines/agent-infra/rule-type-governance.md +73 -0
  221. package/docs/guidelines/agent-infra/size-and-scope.md +13 -2
  222. package/docs/guidelines/agent-infra/skill-quality-checklist.md +119 -0
  223. package/docs/guidelines/augment-portability-patterns.md +68 -0
  224. package/docs/guidelines/php/php-coding-patterns.md +62 -0
  225. package/docs/hook-payload-capture.md +221 -0
  226. package/docs/migrations/commands-1.15.0.md +17 -12
  227. package/docs/skills-catalog.md +5 -4
  228. package/llms.txt +4 -3
  229. package/package.json +1 -1
  230. package/scripts/_p43_bodies.py +235 -0
  231. package/scripts/_p43_compress.py +118 -0
  232. package/scripts/_p4_migrate.py +199 -0
  233. package/scripts/_pilot_council_question.py +57 -0
  234. package/scripts/_pilot_measure.py +53 -0
  235. package/scripts/agent-config +1 -1
  236. package/scripts/ai_council/_default_prices.py +4 -4
  237. package/scripts/ai_council/clients.py +1 -1
  238. package/scripts/ai_council/modes.py +3 -4
  239. package/scripts/ai_council/pricing.py +10 -9
  240. package/scripts/ai_council/session.py +107 -5
  241. package/scripts/build_linear_digest.py +3 -5
  242. package/scripts/build_rule_trigger_matrix.py +1 -9
  243. package/scripts/chat_history.py +952 -596
  244. package/scripts/check_always_budget.py +39 -6
  245. package/scripts/check_compressed_paths.py +213 -0
  246. package/scripts/check_compression.py +15 -0
  247. package/scripts/check_context_paths.py +1 -0
  248. package/scripts/check_council_layout.py +105 -0
  249. package/scripts/check_council_references.py +145 -0
  250. package/scripts/check_portability.py +2 -0
  251. package/scripts/check_references.py +14 -2
  252. package/scripts/check_token_optimizer_freshness.py +131 -0
  253. package/scripts/compile_router.py +148 -0
  254. package/scripts/compress.py +219 -11
  255. package/scripts/council_cli.py +63 -9
  256. package/scripts/council_prune.py +81 -0
  257. package/scripts/count_token_optimizer_usage.sh +54 -0
  258. package/scripts/hook_manifest.yaml +33 -0
  259. package/scripts/hooks/augment-chat-history.sh +10 -0
  260. package/scripts/hooks/cowork-dispatcher.sh +98 -0
  261. package/scripts/hooks/dispatch_hook.py +35 -0
  262. package/scripts/hooks_status.py +12 -1
  263. package/scripts/install-hooks.sh +2 -2
  264. package/scripts/install.sh +81 -2
  265. package/scripts/iron_law_sha.py +98 -0
  266. package/scripts/lint_handoffs.py +214 -0
  267. package/scripts/lint_hook_manifest.py +2 -1
  268. package/scripts/lint_load_context.py +35 -5
  269. package/scripts/measure_rule_budget.py +314 -0
  270. package/scripts/prototype_lint_contradictions.py +150 -0
  271. package/scripts/redact_hook_capture.py +148 -0
  272. package/scripts/schemas/rule.schema.json +55 -6
  273. package/scripts/schemas/skill.schema.json +5 -0
  274. package/scripts/skill_linter.py +359 -7
  275. package/scripts/smoke_path_resolution.py +93 -0
  276. package/scripts/update_prices.py +3 -3
  277. package/scripts/validate_frontmatter.py +41 -1
  278. package/.agent-src/commands/chat-history/checkpoint.md +0 -126
  279. package/.agent-src/commands/chat-history/clear.md +0 -103
  280. package/.agent-src/commands/chat-history/resume.md +0 -183
  281. package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +0 -72
  282. package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +0 -79
  283. package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +0 -87
  284. package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +0 -62
  285. package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +0 -78
  286. package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +0 -85
  287. package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +0 -65
  288. package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +0 -78
  289. package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +0 -53
  290. package/.agent-src/rules/chat-history-cadence.md +0 -143
  291. package/.agent-src/rules/chat-history-ownership.md +0 -124
  292. package/.agent-src/rules/chat-history-visibility.md +0 -97
  293. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
  294. package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
  295. package/scripts/check_phase_coupling.py +0 -148
  296. /package/{docs → .agent-src/contexts}/contracts/artifact-engagement-flow.md +0 -0
  297. /package/{docs → .agent-src/contexts}/contracts/command-suggestion-flow.md +0 -0
@@ -26,18 +26,67 @@
26
26
  },
27
27
  "load_context": {
28
28
  "type": "array",
29
- "items": {"type": "string", "pattern": "\\.md$"},
30
- "description": "Lazy on-demand context references. Path rules and budget caps enforced by scripts/lint_load_context.py. Contract: docs/contracts/load-context-schema.md."
29
+ "items": {
30
+ "type": "string",
31
+ "pattern": "^((\\.\\./)*contexts/|agents/contexts/|\\.agent-src/contexts/)[^\\s]+\\.md$",
32
+ "description": "Logical name (preferred — `contexts/<area>/<file>.md`) or project-local (`agents/contexts/<file>.md`). The `.agent-src.uncompressed/` prefix is rejected by the regex; the rewriter (`scripts/compress.py::_rewrite_paths`) resolves logical names at compress time. Rewritten relative forms (`../contexts/...`, `../../contexts/...`) are accepted so the linter passes on the compressed mirror in CI."
33
+ },
34
+ "description": "Lazy on-demand context references. Use logical names rooted at the source (e.g. `contexts/execution/foo.md`); the `.agent-src.uncompressed/` prefix is forbidden by the regex (road-to-path-fixes.md P5.3). Path rules and budget caps enforced by scripts/lint_load_context.py. Contract: docs/contracts/load-context-schema.md."
31
35
  },
32
36
  "load_context_eager": {
33
37
  "type": "array",
34
- "items": {"type": "string", "pattern": "\\.md$"},
35
- "description": "Eager auto-loaded context references. Counts against the per-rule char budget; enforced by scripts/lint_load_context.py."
38
+ "items": {
39
+ "type": "string",
40
+ "pattern": "^((\\.\\./)*contexts/|agents/contexts/|\\.agent-src/contexts/)[^\\s]+\\.md$",
41
+ "description": "Same logical-name rule as `load_context`."
42
+ },
43
+ "description": "Eager auto-loaded context references. Same logical-name rule as `load_context`. Counts against the per-rule char budget; enforced by scripts/lint_load_context.py."
36
44
  },
37
45
  "tier": {
38
46
  "type": "string",
39
- "enum": ["1", "2a", "2b", "3", "safety-floor", "mechanical-already"],
40
- "description": "Hardening tier per road-to-rule-hardening.md. Optional today, recommended for new rules. Tracked in agents/contexts/rule-trigger-matrix.md. Tier 3 rules also referenced in agents/contexts/tier-3-dispositions.md."
47
+ "enum": ["1", "2a", "2b", "3", "safety-floor", "mechanical-already", "kernel", "tier-1", "tier-2"],
48
+ "description": "Hardening tier. Legacy values (1/2a/2b/3/safety-floor/mechanical-already) accepted; new router-canonical values (kernel/tier-1/tier-2) introduced by road-to-kernel-and-router.md Phase 4."
49
+ },
50
+ "triggers": {
51
+ "type": "array",
52
+ "items": {
53
+ "type": "object",
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "keyword": {"type": "string"},
57
+ "phrase": {"type": "string"},
58
+ "intent": {"type": "string"},
59
+ "file_pattern": {"type": "string"},
60
+ "path_prefix": {"type": "string", "description": "Literal path-prefix match pattern the host evaluates against the file the agent is editing — NOT a file reference. The rewriter leaves it verbatim. Source-of-truth rules that must fire on edits under `.agent-src.uncompressed/` keep that prefix here (the prefix ban applies only to `load_context:` and body links — see road-to-path-fixes.md P2.2 / AI-Council 2026-05-06)."},
61
+ "command": {"type": "string"},
62
+ "reason": {"type": "string"}
63
+ }
64
+ },
65
+ "description": "Router activation triggers (Phase 3 of road-to-kernel-and-router.md). Forbidden on kernel rules, required on non-kernel rules. Schema: docs/contracts/rule-router.md."
66
+ },
67
+ "routes_to": {
68
+ "type": "array",
69
+ "items": {"type": "string", "pattern": "^(skill|guideline|command|contract):"},
70
+ "description": "Router targets (skill / guideline / command / contract). Forbidden on kernel rules. Schema: docs/contracts/rule-router.md."
71
+ },
72
+ "profile": {
73
+ "type": "string",
74
+ "enum": ["minimal", "balanced", "full"],
75
+ "description": "Optional profile override; rare. Tier-derived default applies otherwise."
76
+ },
77
+ "validator_ignore": {
78
+ "type": "array",
79
+ "items": {
80
+ "type": "object",
81
+ "additionalProperties": false,
82
+ "required": ["type", "pattern"],
83
+ "properties": {
84
+ "type": {"type": "string", "enum": ["substring", "regex"]},
85
+ "pattern": {"type": "string", "minLength": 1},
86
+ "reason": {"type": "string", "description": "Human-readable rationale for the suppression — surfaced in audit logs."}
87
+ }
88
+ },
89
+ "description": "Per-rule allowlist consumed by the post-compression validator (scripts/check_compressed_paths.py). Rules that document forbidden path substrings as their subject matter (e.g. augment-portability, no-roadmap-references) declare the literal strings here so the gate does not flag itself. road-to-path-fixes.md P5.1."
41
90
  }
42
91
  }
43
92
  }
@@ -37,6 +37,11 @@
37
37
  "pattern": "^[a-z][a-z0-9-]*$"
38
38
  }
39
39
  },
40
+ "tier": {
41
+ "type": "string",
42
+ "enum": ["senior"],
43
+ "description": "Optional tier marker. `senior` opts the skill into the Senior-Tier Required Structure check (Context-First lead, Related Skills, Proactive Triggers, Output Artifacts) per .agent-src.uncompressed/rules/skill-quality.md."
44
+ },
40
45
  "execution": {
41
46
  "type": "object",
42
47
  "additionalProperties": false,
@@ -99,6 +99,17 @@ TYPE_PATTERN = re.compile(r'^type:\s*"?(always|auto)"?\s*$', re.MULTILINE)
99
99
  SOURCE_PATTERN = re.compile(r'^source:\s*"?(package|project)"?\s*$', re.MULTILINE)
100
100
  STATUS_PATTERN = re.compile(r'^status:\s*"?(active|deprecated|superseded)"?\s*$', re.MULTILINE)
101
101
  REPLACED_BY_PATTERN = re.compile(r'^replaced_by:\s*"?([\w-]+)"?\s*$', re.MULTILINE)
102
+ TIER_PATTERN = re.compile(r'^tier:\s*"?([\w-]+)"?\s*$', re.MULTILINE)
103
+
104
+ # --- Senior-tier required-block patterns (skill-quality.md § Senior-Tier Required Structure) ---
105
+ # Heading-only checks; detail-shape lives in skill-quality-mechanics.md.
106
+ SENIOR_RELATED_SKILLS_PATTERN = re.compile(r"^##\s+Related Skills\s*$", re.MULTILINE)
107
+ SENIOR_RELATED_WHEN_PATTERN = re.compile(r"\*\*WHEN to use this\*\*", re.IGNORECASE)
108
+ SENIOR_RELATED_WHEN_NOT_PATTERN = re.compile(r"\*\*WHEN NOT to use this\*\*", re.IGNORECASE)
109
+ SENIOR_PROACTIVE_PATTERN = re.compile(
110
+ r"^##\s+When the agent should load this\s*$", re.MULTILINE
111
+ )
112
+ SENIOR_OUTPUT_PATTERN = re.compile(r"^##\s+Output\s*$", re.MULTILINE)
102
113
  H1_PATTERN = re.compile(r"^# .+", re.MULTILINE)
103
114
  DOUBLE_BLANK_PATTERN = re.compile(r"\n{3,}")
104
115
 
@@ -106,6 +117,17 @@ VALID_RULE_TYPES = {"always", "auto"}
106
117
  VALID_RULE_SOURCES = {"package", "project"}
107
118
  VALID_STATUSES = {"active", "deprecated", "superseded"}
108
119
 
120
+ # --- Router schema (docs/contracts/rule-router.md) ---
121
+ ROUTER_ALLOWED_TRIGGER_KEYS = {"keyword", "phrase", "intent", "file_pattern",
122
+ "path_prefix", "command"}
123
+ ROUTER_ALLOWED_PROFILES = {"minimal", "balanced", "full"}
124
+ KERNEL_RULE_IDS: set[str] = {
125
+ "agent-authority", "ask-when-uncertain", "commit-policy",
126
+ "direct-answers", "language-and-tone", "no-cheap-questions",
127
+ "non-destructive-by-default", "scope-control",
128
+ "verify-before-complete",
129
+ }
130
+
109
131
  # --- Runtime execution metadata constants ---
110
132
  VALID_EXECUTION_TYPES = {"manual", "assisted", "automated"}
111
133
  VALID_EXECUTION_HANDLERS = {"none", "shell", "php", "node", "internal"}
@@ -209,6 +231,42 @@ def extract_sections(text: str) -> set[str]:
209
231
  return {match.group(1).strip() for match in SECTION_PATTERN.finditer(text)}
210
232
 
211
233
 
234
+ def _count_code_blocks(text: str) -> int:
235
+ """Return the number of fenced code blocks (``` … ```) in *text*."""
236
+ fence_count = 0
237
+ for line in text.splitlines():
238
+ stripped = line.lstrip()
239
+ if stripped.startswith("```"):
240
+ fence_count += 1
241
+ return fence_count // 2
242
+
243
+
244
+ def _fenced_content_ratio(text: str) -> float:
245
+ """Return the fraction of non-empty lines that sit inside fenced blocks.
246
+
247
+ Used as a structural signal: rules / files dominated by verbatim Iron-Law
248
+ blocks or worked examples score high and are exempted from raw line-count
249
+ warnings (council review 2026-05-06).
250
+ """
251
+ inside = False
252
+ fenced_lines = 0
253
+ non_empty = 0
254
+ for line in text.splitlines():
255
+ stripped = line.strip()
256
+ if stripped.startswith("```"):
257
+ inside = not inside
258
+ if stripped:
259
+ non_empty += 1
260
+ continue
261
+ if stripped:
262
+ non_empty += 1
263
+ if inside:
264
+ fenced_lines += 1
265
+ if non_empty == 0:
266
+ return 0.0
267
+ return fenced_lines / non_empty
268
+
269
+
212
270
  def extract_description(text: str) -> Optional[str]:
213
271
  frontmatter = FRONTMATTER_PATTERN.search(text)
214
272
  if not frontmatter:
@@ -415,6 +473,11 @@ def lint_skill(path: Path, text: str) -> LintResult:
415
473
  if execution is not None:
416
474
  issues.extend(lint_execution_metadata(execution))
417
475
 
476
+ # --- Senior-tier required-block check (skill-quality.md § Senior-Tier Required Structure) ---
477
+ tier_match = TIER_PATTERN.search(frontmatter)
478
+ if tier_match and tier_match.group(1) == "senior":
479
+ issues.extend(lint_senior_tier_blocks(text))
480
+
418
481
  procedure_block = find_procedure_block(text)
419
482
  if procedure_block is not None:
420
483
  if not procedure_block:
@@ -479,8 +542,12 @@ def lint_skill(path: Path, text: str) -> LintResult:
479
542
  suggestions.append("Add a requirement-checking or validation step before implementation")
480
543
 
481
544
  # --- Size check (see guidelines/agent-infra/size-and-scope.md) ---
545
+ # Threshold raised from 300 → 400 (council review 2026-05-06): reference-rich
546
+ # skills (quality-tools 411, ai-council 399, project-analyzer 341) legitimately
547
+ # exceed 300 lines without being split-candidates. Structural follow-up tracked
548
+ # in agents/roadmaps/road-to-structural-linter-reform.md.
482
549
  total_lines = len(text.splitlines())
483
- if total_lines > 300:
550
+ if total_lines > 400:
484
551
  issues.append(Issue("warning", "skill_too_large", f"Skill has {total_lines} lines; review for split (see size-and-scope guideline)"))
485
552
 
486
553
  # --- Pointer-only / guideline-dependent skill detection ---
@@ -537,6 +604,131 @@ def extract_frontmatter(text: str) -> Optional[str]:
537
604
  return match.group(1) if match else None
538
605
 
539
606
 
607
+ def _parse_yaml_list(frontmatter: str, key: str) -> Optional[list]:
608
+ """Parse a simple top-level YAML list `key:` from frontmatter.
609
+
610
+ Supports the two shapes we emit in rule frontmatter:
611
+ triggers:
612
+ - keyword: "foo"
613
+ - phrase: "bar baz"
614
+ routes_to:
615
+ - skill:php-coder
616
+ - guideline:agent-infra/asking-and-brevity-examples
617
+
618
+ Returns ``None`` if the key is absent (so the caller can distinguish
619
+ "missing" from "empty"); returns ``[]`` for an explicitly empty list.
620
+ """
621
+ lines = frontmatter.splitlines()
622
+ out: list = []
623
+ in_block = False
624
+ for line in lines:
625
+ if not in_block:
626
+ if line.startswith(f"{key}:"):
627
+ rhs = line[len(key) + 1:].strip()
628
+ if rhs in ("", "[]"):
629
+ if rhs == "[]":
630
+ return []
631
+ in_block = True
632
+ else:
633
+ return None # unexpected scalar shape
634
+ continue
635
+ if line.startswith(" - "):
636
+ item = line[4:].strip()
637
+ if ":" in item and not item.startswith(("'", '"')):
638
+ k, _, v = item.partition(":")
639
+ out.append({k.strip(): v.strip().strip('"').strip("'")})
640
+ else:
641
+ out.append(item.strip('"').strip("'"))
642
+ elif line.strip() == "" or line.startswith(" "):
643
+ continue
644
+ else:
645
+ break
646
+ return out if in_block else None
647
+
648
+
649
+ def lint_router_frontmatter(rule_id: str, frontmatter: str,
650
+ rule_type: Optional[str]) -> List[Issue]:
651
+ """Validate `triggers:` / `routes_to:` per docs/contracts/rule-router.md.
652
+
653
+ Strict checks (always errors): kernel rules MUST NOT carry router fields;
654
+ `triggers:` items must use one allowed key; `routes_to:` items must
655
+ follow `kind:id` with kind ∈ {skill, guideline} and the target file
656
+ must exist on disk.
657
+
658
+ Lenient checks (info-level until Phase 4 migrations land): non-kernel
659
+ rules without `triggers:` / `routes_to:` get an informational note,
660
+ not an error — the existing description-matching path still works.
661
+ """
662
+ issues: List[Issue] = []
663
+ triggers = _parse_yaml_list(frontmatter, "triggers")
664
+ routes_to = _parse_yaml_list(frontmatter, "routes_to")
665
+
666
+ is_kernel = rule_id in KERNEL_RULE_IDS or rule_type == "always"
667
+
668
+ if is_kernel:
669
+ if triggers is not None:
670
+ issues.append(Issue("error", "kernel_has_triggers",
671
+ "Kernel rules MUST NOT declare triggers: (kernel is unconditional)"))
672
+ if routes_to is not None:
673
+ issues.append(Issue("error", "kernel_has_routes_to",
674
+ "Kernel rules MUST NOT declare routes_to: (kernel body stays inline)"))
675
+ return issues
676
+
677
+ # Non-kernel rule path
678
+ if triggers is None:
679
+ issues.append(Issue("info", "router_triggers_missing",
680
+ "Non-kernel rule has no triggers: — falls back to description matching "
681
+ "until Phase 4 migration lands"))
682
+ else:
683
+ for idx, item in enumerate(triggers):
684
+ if not isinstance(item, dict) or len(item) != 1:
685
+ issues.append(Issue("error", "trigger_shape_invalid",
686
+ f"triggers[{idx}] must be a single-key mapping"))
687
+ continue
688
+ (k,) = item.keys()
689
+ if k not in ROUTER_ALLOWED_TRIGGER_KEYS:
690
+ allowed = ", ".join(sorted(ROUTER_ALLOWED_TRIGGER_KEYS))
691
+ issues.append(Issue("error", "trigger_key_unknown",
692
+ f"triggers[{idx}] key '{k}' not in allowed set ({allowed})"))
693
+
694
+ if routes_to is None:
695
+ issues.append(Issue("info", "router_routes_to_missing",
696
+ "Non-kernel rule has no routes_to: — body should migrate to skill / "
697
+ "guideline in Phase 4"))
698
+ else:
699
+ repo_root = Path(__file__).resolve().parent.parent
700
+ for idx, item in enumerate(routes_to):
701
+ if not isinstance(item, str) or ":" not in item:
702
+ issues.append(Issue("error", "route_shape_invalid",
703
+ f"routes_to[{idx}] must be 'kind:id'"))
704
+ continue
705
+ kind, _, target_id = item.partition(":")
706
+ if kind == "skill":
707
+ target = repo_root / ".agent-src.uncompressed" / "skills" / target_id / "SKILL.md"
708
+ elif kind == "guideline":
709
+ target = repo_root / "docs" / "guidelines" / f"{target_id}.md"
710
+ elif kind == "command":
711
+ target = repo_root / ".agent-src.uncompressed" / "commands" / f"{target_id}.md"
712
+ elif kind == "contract":
713
+ # Contracts live in two places: stable host docs in
714
+ # docs/contracts/ and load-bearing flows in
715
+ # .agent-src.uncompressed/contexts/contracts/ (road-to-path-fixes
716
+ # P4 / Council R2). Try both before failing.
717
+ target = repo_root / "docs" / "contracts" / f"{target_id}.md"
718
+ if not target.exists():
719
+ alt = repo_root / ".agent-src.uncompressed" / "contexts" / "contracts" / f"{target_id}.md"
720
+ if alt.exists():
721
+ target = alt
722
+ else:
723
+ issues.append(Issue("error", "route_kind_unknown",
724
+ f"routes_to[{idx}] kind '{kind}' must be 'skill', 'guideline', 'command', or 'contract'"))
725
+ continue
726
+ if not target.exists():
727
+ issues.append(Issue("error", "route_target_missing",
728
+ f"routes_to[{idx}] target '{item}' not found at {target}"))
729
+ return issues
730
+
731
+
540
732
  def extract_frontmatter_field(frontmatter: str, pattern: re.Pattern[str]) -> Optional[str]:
541
733
  match = pattern.search(frontmatter)
542
734
  return match.group(1).strip() if match else None
@@ -603,6 +795,57 @@ def parse_execution_block(frontmatter: str) -> Optional[dict]:
603
795
  return result
604
796
 
605
797
 
798
+ def lint_senior_tier_blocks(text: str) -> List[Issue]:
799
+ """Validate the four required blocks for `tier: senior` skills.
800
+
801
+ Per .agent-src.uncompressed/rules/skill-quality.md § Senior-Tier
802
+ Required Structure: Context-First lead (description), Related Skills
803
+ (with WHEN / WHEN NOT lists), Proactive Triggers, Output Artifacts.
804
+
805
+ The Context-First lead is checked structurally via description length
806
+ + content; here we enforce the three section blocks and the WHEN /
807
+ WHEN NOT two-list pattern inside Related Skills.
808
+ """
809
+ issues: List[Issue] = []
810
+
811
+ if not SENIOR_RELATED_SKILLS_PATTERN.search(text):
812
+ issues.append(Issue(
813
+ "error",
814
+ "missing_senior_related_skills",
815
+ "Senior-tier skill missing `## Related Skills` block (skill-quality.md § Senior-Tier Required Structure)",
816
+ ))
817
+ else:
818
+ related_block = extract_section_block(text, "Related Skills") or ""
819
+ if not SENIOR_RELATED_WHEN_PATTERN.search(related_block):
820
+ issues.append(Issue(
821
+ "error",
822
+ "missing_senior_related_when",
823
+ "Senior-tier `## Related Skills` block missing `**WHEN to use this**` list",
824
+ ))
825
+ if not SENIOR_RELATED_WHEN_NOT_PATTERN.search(related_block):
826
+ issues.append(Issue(
827
+ "error",
828
+ "missing_senior_related_when_not",
829
+ "Senior-tier `## Related Skills` block missing `**WHEN NOT to use this**` list",
830
+ ))
831
+
832
+ if not SENIOR_PROACTIVE_PATTERN.search(text):
833
+ issues.append(Issue(
834
+ "error",
835
+ "missing_senior_proactive_triggers",
836
+ "Senior-tier skill missing `## When the agent should load this` block",
837
+ ))
838
+
839
+ if not SENIOR_OUTPUT_PATTERN.search(text):
840
+ issues.append(Issue(
841
+ "error",
842
+ "missing_senior_output_artifacts",
843
+ "Senior-tier skill missing `## Output` block declaring artifact name + shape",
844
+ ))
845
+
846
+ return issues
847
+
848
+
606
849
  def lint_execution_metadata(execution: dict) -> List[Issue]:
607
850
  """Validate the execution block of a skill."""
608
851
  issues: List[Issue] = []
@@ -734,6 +977,9 @@ def lint_rule(path: Path, text: str) -> LintResult:
734
977
  f"Always-rule with topic-specific description ({', '.join(topic_keywords)}) — "
735
978
  f"consider auto type per rule-type-governance"))
736
979
 
980
+ # Router schema validation (docs/contracts/rule-router.md, Phase 3.3).
981
+ issues.extend(lint_router_frontmatter(path.stem, frontmatter, rule_type))
982
+
737
983
  # --- Structure checks ---
738
984
  # H1 heading
739
985
  if not H1_PATTERN.search(text):
@@ -750,14 +996,18 @@ def lint_rule(path: Path, text: str) -> LintResult:
750
996
  issues.append(Issue("warning", "double_blank_lines", "File contains double or triple blank lines"))
751
997
 
752
998
  # --- Content checks (see guidelines/agent-infra/size-and-scope.md) ---
999
+ # Length thresholds gated by fenced-content density (council review 2026-05-06):
1000
+ # rules dominated by verbatim Iron-Law blocks / worked examples are protected
1001
+ # from the > 40 / > 60 warnings. Hard error at 200 stays unconditional.
753
1002
  line_count = len([line for line in text.splitlines() if line.strip()])
754
1003
  total_lines = len(text.splitlines())
1004
+ fenced_ratio = _fenced_content_ratio(text)
755
1005
  if total_lines > 200:
756
1006
  issues.append(Issue("error", "rule_too_large", f"Rule has {total_lines} lines (hard limit: 200); must split or move to guideline"))
757
- elif line_count > 60:
758
- issues.append(Issue("warning", "long_rule", f"Rule has {line_count} non-empty lines; prefer < 60 (see size-and-scope guideline)"))
759
- elif line_count > 40:
760
- issues.append(Issue("warning", "long_rule", f"Rule has {line_count} non-empty lines; rules should be concise"))
1007
+ elif line_count > 60 and fenced_ratio < 0.30:
1008
+ issues.append(Issue("warning", "long_rule", f"Rule has {line_count} non-empty lines (fenced-content {fenced_ratio:.0%}); prefer < 60 (see size-and-scope guideline)"))
1009
+ elif line_count > 40 and fenced_ratio < 0.30:
1010
+ issues.append(Issue("warning", "long_rule", f"Rule has {line_count} non-empty lines (fenced-content {fenced_ratio:.0%}); rules should be concise"))
761
1011
 
762
1012
  for bad_sign in RULE_BAD_SIGNS:
763
1013
  if bad_sign in text:
@@ -902,9 +1152,16 @@ def lint_command(path: Path, text: str) -> LintResult:
902
1152
  issues.append(Issue("warning", "no_steps", "Command has no Steps section or numbered sub-headings"))
903
1153
 
904
1154
  # --- Size check (see guidelines/agent-infra/size-and-scope.md) ---
1155
+ # Word threshold (1000) gated by structural delegation signal (council review
1156
+ # 2026-05-06): well-factored orchestrators with ≥ 5 sub-sections AND ≥ 3 code
1157
+ # blocks are exempt — the size reflects dispatch breadth, not bloat.
905
1158
  word_count = len(text.split())
906
1159
  if word_count > 1000:
907
- issues.append(Issue("warning", "large_command", f"Command has {word_count} words (target: 200-600, max ~1000)"))
1160
+ section_count = len(sections)
1161
+ code_block_count = _count_code_blocks(text)
1162
+ delegation_signal = section_count >= 5 and code_block_count >= 3
1163
+ if not delegation_signal:
1164
+ issues.append(Issue("warning", "large_command", f"Command has {word_count} words (target: 200-600, max ~1000); {section_count} sub-sections, {code_block_count} code blocks — lacks delegation structure"))
908
1165
 
909
1166
  # File must end with exactly one newline
910
1167
  if not text.endswith("\n"):
@@ -1656,6 +1913,70 @@ def lint_governance(path: Path, text: str, artifact_type: str, repo_root: Path |
1656
1913
  return issues
1657
1914
 
1658
1915
 
1916
+ # --- Structural malice check (see road-to-suite-closure Phase 5) ---
1917
+ #
1918
+ # Five regex patterns scan skill / rule / command bodies for **structural**
1919
+ # (not semantic) malice. Findings surface as ``Issue("error",
1920
+ # "malice:<pattern>", "<line>:<matched>")`` so ``compute_exit_code`` can
1921
+ # emit exit code 3 (security-failure), distinct from 2 (build-failure).
1922
+ # Semantic checks (PII leakage, prompt injection) are deferred to v2.
1923
+
1924
+ # (a) credential exfil — curl|wget piping ${TOKEN}/${KEY}/${SECRET}/...
1925
+ # env vars or hitting ~/.aws/ ~/.ssh/ secrets.
1926
+ _MALICE_CRED_EXFIL = re.compile(
1927
+ r"\b(?:curl|wget)\b[^\n]*"
1928
+ r"(?:\$\{?[A-Z_]*(?:TOKEN|KEY|SECRET|PASSWORD|CREDENTIAL|API)[A-Z_]*\}?"
1929
+ r"|~/\.(?:aws|ssh)/)"
1930
+ )
1931
+ # (b) arbitrary execution — eval/exec over a network-fetched payload, or
1932
+ # `bash <(curl ...)` / `sh <(wget ...)` style remote-execution.
1933
+ _MALICE_REMOTE_EXEC = re.compile(
1934
+ r"(?:\b(?:eval|exec)\s*\([^)]*(?:curl|wget|requests\.get|urllib)"
1935
+ r"|\b(?:bash|sh|zsh)\s*<\s*\(\s*(?:curl|wget))"
1936
+ )
1937
+ # (c) force-push to a protected ref.
1938
+ _MALICE_FORCE_PUSH = re.compile(
1939
+ r"\bgit\s+push\b[^\n]*--force(?:-with-lease)?\b[^\n]*"
1940
+ r"\b(?:main|master|prod|production|release)\b"
1941
+ )
1942
+ # (d) world-readable secrets — chmod 0?[4567]xx on .pem/.key/.env files.
1943
+ _MALICE_CHMOD_SECRETS = re.compile(
1944
+ r"\bchmod\s+0?[4567]\d{2}\s+[^\n]*\.(?:pem|key|env)\b"
1945
+ )
1946
+ # (e) unbounded subprocess shell injection — shell=True interpolating ${VAR}.
1947
+ _MALICE_SHELL_INJECT = re.compile(
1948
+ r"\bsubprocess\.[A-Za-z_]+\s*\([^)]*shell\s*=\s*True[^)]*\$\{"
1949
+ )
1950
+
1951
+ _MALICE_PATTERNS: list[tuple[str, re.Pattern[str]]] = [
1952
+ ("cred_exfil", _MALICE_CRED_EXFIL),
1953
+ ("remote_exec", _MALICE_REMOTE_EXEC),
1954
+ ("force_push_protected", _MALICE_FORCE_PUSH),
1955
+ ("chmod_secrets", _MALICE_CHMOD_SECRETS),
1956
+ ("shell_injection", _MALICE_SHELL_INJECT),
1957
+ ]
1958
+
1959
+
1960
+ def check_structural_malice(text: str) -> List[Issue]:
1961
+ """Return one Issue per malice match. Empty list when clean.
1962
+
1963
+ Issue shape: ``Issue("error", f"malice:{name}", f"{line}:{matched}")``.
1964
+ The ``format_text`` renderer special-cases the ``malice:`` code prefix
1965
+ to emit ``<path>:<line>:malice:<pattern>:<matched>`` per Phase 5.2.
1966
+ """
1967
+ issues: List[Issue] = []
1968
+ for lineno, raw in enumerate(text.splitlines(), start=1):
1969
+ for name, pattern in _MALICE_PATTERNS:
1970
+ match = pattern.search(raw)
1971
+ if match:
1972
+ issues.append(Issue(
1973
+ severity="error",
1974
+ code=f"malice:{name}",
1975
+ message=f"{lineno}:{match.group(0).strip()}",
1976
+ ))
1977
+ return issues
1978
+
1979
+
1659
1980
  # --- Output-schema check (see road-to-trigger-evals Phase 3.5) ---
1660
1981
  #
1661
1982
  # Skills that freeze an output shape (`refine-ticket`, `estimate-ticket`)
@@ -1865,11 +2186,36 @@ def lint_file(path: Path, repo_root: Path | None = None) -> LintResult:
1865
2186
  result.issues.extend(schema_issues)
1866
2187
  result.status = classify_status(result.issues)
1867
2188
 
2189
+ # Post-processing: structural malice scan (errors). Skills, rules,
2190
+ # and commands carry executable patterns; guidelines/personas are
2191
+ # prose-only and skipped to keep noise low.
2192
+ if artifact_type in ("skill", "rule", "command"):
2193
+ malice_issues = check_structural_malice(text)
2194
+ if malice_issues:
2195
+ result.issues.extend(malice_issues)
2196
+ result.status = classify_status(result.issues)
2197
+
1868
2198
  return result
1869
2199
 
1870
2200
 
1871
2201
  def format_text(results: list[LintResult]) -> str:
1872
2202
  lines: list[str] = []
2203
+ # Phase 5.2: malice findings render in the spec shape
2204
+ # ``<path>:<line>:malice:<pattern>:<matched>`` ahead of the badge
2205
+ # block so security-failures are grep-able from the top.
2206
+ malice_total = 0
2207
+ for result in results:
2208
+ for issue in result.issues:
2209
+ if issue.code.startswith("malice:"):
2210
+ pattern_name = issue.code.split(":", 1)[1]
2211
+ lineno, _, matched = issue.message.partition(":")
2212
+ lines.append(
2213
+ f"{result.file}:{lineno}:malice:{pattern_name}:{matched}"
2214
+ )
2215
+ malice_total += 1
2216
+ if malice_total:
2217
+ lines.append("")
2218
+
1873
2219
  for result in results:
1874
2220
  badge = {"pass": "[PASS]", "pass_with_warnings": "[WARN]", "fail": "[FAIL]"}[result.status]
1875
2221
  lines.append(f"{badge} {result.file} ({result.artifact_type})")
@@ -1888,7 +2234,8 @@ def format_text(results: list[LintResult]) -> str:
1888
2234
  fails = sum(1 for r in results if r.status == "fail")
1889
2235
  warns = sum(1 for r in results if r.status == "pass_with_warnings")
1890
2236
  passes = sum(1 for r in results if r.status == "pass")
1891
- lines.append(f"Summary: {passes} pass, {warns} warn, {fails} fail, {total} total")
2237
+ suffix = f", {malice_total} malice" if malice_total else ""
2238
+ lines.append(f"Summary: {passes} pass, {warns} warn, {fails} fail, {total} total{suffix}")
1892
2239
  return "\n".join(lines)
1893
2240
 
1894
2241
 
@@ -2087,6 +2434,11 @@ def check_duplication(root: Path) -> list[LintResult]:
2087
2434
 
2088
2435
 
2089
2436
  def compute_exit_code(results: list[LintResult], strict_warnings: bool) -> int:
2437
+ # Phase 5.2: structural-malice findings emit exit code 3 (security-
2438
+ # failure), distinct from 2 (build-failure) so CI surfaces can split.
2439
+ for r in results:
2440
+ if any(issue.code.startswith("malice:") for issue in r.issues):
2441
+ return 3
2090
2442
  if any(r.status == "fail" for r in results):
2091
2443
  return 2
2092
2444
  if any(r.status == "pass_with_warnings" for r in results) and strict_warnings:
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ """Smoke-test path resolution against the package's own `.augment/` projection.
3
+
4
+ Per `agents/roadmaps/road-to-path-fixes.md` Phase 7 (Council Decision 3,
5
+ 2026-05-06): the package's `.augment/` tree has the same shape as the
6
+ `.augment/` tree a consumer would receive after `scripts/install.sh`.
7
+ If `load_context:` entries resolve cleanly here, they resolve cleanly
8
+ in any consumer.
9
+
10
+ What it does:
11
+ - Walks `.augment/rules/*.md`.
12
+ - Parses each rule's YAML frontmatter.
13
+ - Resolves every `load_context:` and `load_context_eager:` entry
14
+ against the rule file's directory.
15
+ - Reports any miss with a file:entry line.
16
+
17
+ Exit codes: 0 = all entries resolve, 1 = one or more misses, 3 = no
18
+ `.augment/rules/` directory found (run `task sync` first).
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import sys
23
+ from pathlib import Path
24
+
25
+ import yaml
26
+
27
+ ROOT = Path(__file__).resolve().parent.parent
28
+ AUGMENT_RULES = ROOT / ".augment" / "rules"
29
+
30
+
31
+ def _split_frontmatter(text: str):
32
+ if not text.startswith("---\n"):
33
+ return None
34
+ end = text.find("\n---\n", 4)
35
+ if end == -1:
36
+ return None
37
+ try:
38
+ fm = yaml.safe_load(text[4:end])
39
+ except yaml.YAMLError:
40
+ return None
41
+ return fm if isinstance(fm, dict) else {}
42
+
43
+
44
+ def _check_rule(rule_file: Path, misses: list[tuple[str, str]]) -> int:
45
+ fm = _split_frontmatter(rule_file.read_text(encoding="utf-8"))
46
+ if not fm:
47
+ return 0
48
+ checked = 0
49
+ rule_dir = rule_file.parent
50
+ for key in ("load_context", "load_context_eager"):
51
+ entries = fm.get(key) or []
52
+ if not isinstance(entries, list):
53
+ continue
54
+ for entry in entries:
55
+ if not isinstance(entry, str):
56
+ continue
57
+ checked += 1
58
+ target = (rule_dir / entry).resolve()
59
+ if not target.is_file():
60
+ misses.append((str(rule_file.relative_to(ROOT)), entry))
61
+ return checked
62
+
63
+
64
+ def main() -> int:
65
+ if not AUGMENT_RULES.is_dir():
66
+ print(
67
+ f"❌ {AUGMENT_RULES.relative_to(ROOT)} not found — run `task sync` first",
68
+ file=sys.stderr,
69
+ )
70
+ return 3
71
+
72
+ misses: list[tuple[str, str]] = []
73
+ rule_count = 0
74
+ entry_count = 0
75
+ for rule_file in sorted(AUGMENT_RULES.glob("*.md")):
76
+ rule_count += 1
77
+ entry_count += _check_rule(rule_file, misses)
78
+
79
+ if misses:
80
+ print(f"❌ {len(misses)} unresolved load_context entr(y/ies):")
81
+ for rule, entry in misses:
82
+ print(f" {rule} → {entry!r}")
83
+ return 1
84
+
85
+ print(
86
+ f"✅ smoke-path-resolution clean "
87
+ f"({rule_count} rules, {entry_count} load_context entr(y/ies) resolved)"
88
+ )
89
+ return 0
90
+
91
+
92
+ if __name__ == "__main__":
93
+ sys.exit(main())