@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.
- package/.agent-src/commands/agent-handoff.md +14 -10
- package/.agent-src/commands/agents.md +1 -1
- package/.agent-src/commands/bug-fix.md +1 -1
- package/.agent-src/commands/bug-investigate.md +2 -2
- package/.agent-src/commands/chat-history/import.md +166 -0
- package/.agent-src/commands/chat-history/learn.md +178 -0
- package/.agent-src/commands/chat-history/show.md +17 -18
- package/.agent-src/commands/chat-history.md +26 -25
- package/.agent-src/commands/compress.md +12 -0
- package/.agent-src/commands/context/create.md +2 -2
- package/.agent-src/commands/context.md +1 -1
- package/.agent-src/commands/copilot-agents.md +1 -1
- package/.agent-src/commands/council/default.md +21 -12
- package/.agent-src/commands/council.md +1 -1
- package/.agent-src/commands/create-pr.md +28 -8
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature/dev.md +3 -3
- package/.agent-src/commands/feature.md +1 -1
- package/.agent-src/commands/fix/seeder.md +2 -2
- package/.agent-src/commands/fix.md +1 -1
- package/.agent-src/commands/jira-ticket.md +1 -1
- package/.agent-src/commands/judge.md +2 -2
- package/.agent-src/commands/memory.md +1 -1
- package/.agent-src/commands/mode.md +5 -5
- package/.agent-src/commands/module.md +1 -1
- package/.agent-src/commands/onboard.md +4 -4
- package/.agent-src/commands/optimize/augmentignore.md +1 -1
- package/.agent-src/commands/optimize-prompt.md +61 -0
- package/.agent-src/commands/optimize.md +1 -1
- package/.agent-src/commands/override.md +1 -1
- package/.agent-src/commands/review-changes.md +1 -1
- package/.agent-src/commands/review-routing.md +1 -1
- package/.agent-src/commands/roadmap.md +1 -1
- package/.agent-src/commands/set-cost-profile.md +3 -3
- package/.agent-src/commands/sync-agent-settings.md +2 -2
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/commands/tests/create.md +2 -2
- package/.agent-src/commands/tests.md +1 -1
- package/.agent-src/commands/threat-model.md +4 -4
- package/.agent-src/contexts/authority/commit-mechanics.md +14 -1
- package/.agent-src/contexts/authority/destructive-mechanics.md +14 -1
- package/.agent-src/contexts/authority/scope-mechanics.md +5 -0
- package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +76 -0
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +4 -4
- package/.agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +98 -0
- package/.agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +93 -0
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +125 -9
- package/.agent-src/contexts/execution/autonomy-mechanics.md +44 -0
- package/.agent-src/contexts/model-recommendations.md +2 -2
- package/.agent-src/contexts/override-system.md +1 -1
- package/.agent-src/personas/product-owner.md +2 -2
- package/.agent-src/personas/qa.md +1 -1
- package/.agent-src/rules/agent-authority.md +5 -6
- package/.agent-src/rules/agent-docs.md +11 -53
- package/.agent-src/rules/analysis-skill-routing.md +10 -40
- package/.agent-src/rules/architecture.md +6 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +5 -0
- package/.agent-src/rules/artifact-engagement-recording.md +23 -59
- package/.agent-src/rules/ask-when-uncertain.md +24 -47
- package/.agent-src/rules/augment-portability.md +14 -62
- package/.agent-src/rules/augment-source-of-truth.md +10 -1
- package/.agent-src/rules/autonomous-execution.md +17 -98
- package/.agent-src/rules/capture-learnings.md +9 -80
- package/.agent-src/rules/cli-output-handling.md +12 -42
- package/.agent-src/rules/command-suggestion-policy.md +25 -73
- package/.agent-src/rules/commit-conventions.md +9 -58
- package/.agent-src/rules/commit-policy.md +16 -47
- package/.agent-src/rules/context-hygiene.md +5 -0
- package/.agent-src/rules/direct-answers.md +21 -42
- package/.agent-src/rules/docker-commands.md +11 -45
- package/.agent-src/rules/docs-sync.md +10 -56
- package/.agent-src/rules/downstream-changes.md +5 -0
- package/.agent-src/rules/e2e-testing.md +9 -44
- package/.agent-src/rules/guidelines.md +13 -75
- package/.agent-src/rules/improve-before-implement.md +10 -2
- package/.agent-src/rules/language-and-tone.md +35 -69
- package/.agent-src/rules/laravel-translations.md +11 -40
- package/.agent-src/rules/markdown-safe-codeblocks.md +4 -0
- package/.agent-src/rules/minimal-safe-diff.md +4 -0
- package/.agent-src/rules/missing-tool-handling.md +4 -0
- package/.agent-src/rules/model-recommendation.md +9 -61
- package/.agent-src/rules/no-attribution-footers.md +53 -0
- package/.agent-src/rules/no-cheap-questions.md +11 -27
- package/.agent-src/rules/no-council-references.md +76 -0
- package/.agent-src/rules/no-roadmap-references.md +8 -1
- package/.agent-src/rules/non-destructive-by-default.md +13 -43
- package/.agent-src/rules/onboarding-gate.md +9 -117
- package/.agent-src/rules/package-ci-checks.md +10 -37
- package/.agent-src/rules/php-coding.md +10 -55
- package/.agent-src/rules/preservation-guard.md +9 -0
- package/.agent-src/rules/review-routing-awareness.md +9 -97
- package/.agent-src/rules/reviewer-awareness.md +8 -83
- package/.agent-src/rules/roadmap-progress-sync.md +7 -170
- package/.agent-src/rules/role-mode-adherence.md +6 -2
- package/.agent-src/rules/rule-type-governance.md +8 -66
- package/.agent-src/rules/runtime-safety.md +5 -0
- package/.agent-src/rules/scope-control.md +17 -62
- package/.agent-src/rules/security-sensitive-stop.md +7 -1
- package/.agent-src/rules/size-enforcement.md +6 -1
- package/.agent-src/rules/skill-improvement-trigger.md +9 -49
- package/.agent-src/rules/skill-quality.md +7 -64
- package/.agent-src/rules/slash-command-routing-policy.md +11 -63
- package/.agent-src/rules/think-before-action.md +22 -87
- package/.agent-src/rules/token-efficiency.md +10 -74
- package/.agent-src/rules/token-optimizer-maintenance.md +68 -0
- package/.agent-src/rules/tool-safety.md +4 -0
- package/.agent-src/rules/ui-audit-gate.md +25 -61
- package/.agent-src/rules/upstream-proposal.md +9 -67
- package/.agent-src/rules/user-interaction.md +25 -95
- package/.agent-src/rules/verify-before-complete.md +1 -1
- package/.agent-src/skills/agent-docs-writing/SKILL.md +1 -1
- package/.agent-src/skills/ai-council/SKILL.md +69 -5
- package/.agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/.agent-src/skills/analysis-skill-router/SKILL.md +3 -3
- package/.agent-src/skills/artisan-commands/SKILL.md +2 -2
- package/.agent-src/skills/authz-review/SKILL.md +1 -1
- package/.agent-src/skills/aws-infrastructure/SKILL.md +5 -5
- package/.agent-src/skills/blast-radius-analyzer/SKILL.md +8 -8
- package/.agent-src/skills/bug-analyzer/SKILL.md +5 -5
- package/.agent-src/skills/code-refactoring/SKILL.md +4 -4
- package/.agent-src/skills/code-review/SKILL.md +2 -2
- package/.agent-src/skills/command-writing/SKILL.md +11 -0
- package/.agent-src/skills/composer-packages/SKILL.md +2 -2
- package/.agent-src/skills/context-authoring/SKILL.md +11 -0
- package/.agent-src/skills/context-document/SKILL.md +1 -1
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +23 -0
- package/.agent-src/skills/copilot-config/SKILL.md +1 -1
- package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
- package/.agent-src/skills/dependency-upgrade/SKILL.md +2 -2
- package/.agent-src/skills/devcontainer/SKILL.md +2 -2
- package/.agent-src/skills/developer-like-execution/SKILL.md +1 -1
- package/.agent-src/skills/docker/SKILL.md +1 -1
- package/.agent-src/skills/dto-creator/SKILL.md +1 -1
- package/.agent-src/skills/estimate-ticket/SKILL.md +2 -2
- package/.agent-src/skills/fe-design/SKILL.md +4 -4
- package/.agent-src/skills/feature-planning/SKILL.md +5 -5
- package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
- package/.agent-src/skills/laravel/SKILL.md +1 -1
- package/.agent-src/skills/laravel-notifications/SKILL.md +5 -5
- package/.agent-src/skills/laravel-pennant/SKILL.md +1 -1
- package/.agent-src/skills/laravel-pulse/SKILL.md +4 -4
- package/.agent-src/skills/laravel-reverb/SKILL.md +2 -2
- package/.agent-src/skills/laravel-scheduling/SKILL.md +1 -1
- package/.agent-src/skills/md-language-check/SKILL.md +1 -1
- package/.agent-src/skills/migration-creator/SKILL.md +7 -7
- package/.agent-src/skills/multi-tenancy/SKILL.md +8 -8
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
- package/.agent-src/skills/performance-analysis/SKILL.md +3 -3
- package/.agent-src/skills/pest-testing/SKILL.md +6 -6
- package/.agent-src/skills/php-service/SKILL.md +2 -2
- package/.agent-src/skills/project-analysis-hypothesis-driven/SKILL.md +3 -3
- package/.agent-src/skills/project-analysis-react/SKILL.md +1 -1
- package/.agent-src/skills/project-analysis-symfony/SKILL.md +1 -1
- package/.agent-src/skills/project-analysis-zend-laminas/SKILL.md +2 -2
- package/.agent-src/skills/project-analyzer/SKILL.md +4 -4
- package/.agent-src/skills/prompt-optimizer/SKILL.md +108 -0
- package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
- package/.agent-src/skills/rule-writing/SKILL.md +33 -0
- package/.agent-src/skills/sentry-integration/SKILL.md +1 -1
- package/.agent-src/skills/skill-writing/SKILL.md +14 -0
- package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
- package/.agent-src/skills/terraform/SKILL.md +2 -2
- package/.agent-src/skills/terragrunt/SKILL.md +8 -8
- package/.agent-src/skills/test-performance/SKILL.md +5 -5
- package/.agent-src/skills/threat-modeling/SKILL.md +2 -2
- package/.agent-src/skills/token-optimizer/SKILL.md +110 -0
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
- package/.agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/templates/AGENTS.md +1 -1
- package/.agent-src/templates/agent-settings.md +25 -41
- package/.agent-src/templates/contexts/tenant-boundaries.md +2 -2
- package/.agent-src/templates/contexts.md +1 -1
- package/.agent-src/templates/copilot-instructions.md +21 -0
- package/.agent-src/templates/copilot-review-instructions.md +76 -0
- package/.agent-src/templates/features.md +1 -1
- package/.agent-src/templates/rule.md +127 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +7 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +0 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +0 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +2 -3
- package/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +11 -4
- package/AGENTS.md +71 -3
- package/CHANGELOG.md +180 -3
- package/README.md +24 -23
- package/config/agent-settings.template.yml +63 -23
- package/config/gitignore-block.txt +11 -4
- package/docs/architecture.md +84 -3
- package/docs/catalog.md +23 -11
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/agent-memory-contract.md +1 -1
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/context-paths.md +2 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/file-ownership-matrix.json +678 -609
- package/docs/contracts/hook-architecture-v1.md +8 -1
- package/docs/contracts/iron-law-overrides.txt +25 -0
- package/docs/contracts/kernel-membership.md +273 -0
- package/docs/contracts/load-context-schema.md +26 -11
- package/docs/contracts/memory-visibility-v1.md +8 -24
- package/docs/contracts/pilot/agent-authority.md +24 -0
- package/docs/contracts/pilot/direct-answers.md +70 -0
- package/docs/contracts/pilot/language-and-tone.md +63 -0
- package/docs/contracts/rule-classification.md +170 -0
- package/docs/contracts/rule-router.md +153 -0
- package/docs/customization.md +18 -7
- package/docs/decisions/ADR-001-kernel-swap-deferred.md +109 -0
- package/docs/decisions/ADR-002-kernel-bucket-overrides.md +124 -0
- package/docs/decisions/ADR-rule-kernel-and-router.md +122 -0
- package/docs/getting-started.md +19 -27
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +176 -0
- package/docs/guidelines/agent-infra/rule-type-governance.md +73 -0
- package/docs/guidelines/agent-infra/size-and-scope.md +13 -2
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +119 -0
- package/docs/guidelines/augment-portability-patterns.md +68 -0
- package/docs/guidelines/php/php-coding-patterns.md +62 -0
- package/docs/hook-payload-capture.md +221 -0
- package/docs/migrations/commands-1.15.0.md +17 -12
- package/docs/skills-catalog.md +5 -4
- package/llms.txt +4 -3
- package/package.json +1 -1
- package/scripts/_p43_bodies.py +235 -0
- package/scripts/_p43_compress.py +118 -0
- package/scripts/_p4_migrate.py +199 -0
- package/scripts/_pilot_council_question.py +57 -0
- package/scripts/_pilot_measure.py +53 -0
- package/scripts/agent-config +1 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/clients.py +1 -1
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/pricing.py +10 -9
- package/scripts/ai_council/session.py +107 -5
- package/scripts/build_linear_digest.py +3 -5
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/chat_history.py +952 -596
- package/scripts/check_always_budget.py +39 -6
- package/scripts/check_compressed_paths.py +213 -0
- package/scripts/check_compression.py +15 -0
- package/scripts/check_context_paths.py +1 -0
- package/scripts/check_council_layout.py +105 -0
- package/scripts/check_council_references.py +145 -0
- package/scripts/check_portability.py +2 -0
- package/scripts/check_references.py +14 -2
- package/scripts/check_token_optimizer_freshness.py +131 -0
- package/scripts/compile_router.py +148 -0
- package/scripts/compress.py +219 -11
- package/scripts/council_cli.py +63 -9
- package/scripts/council_prune.py +81 -0
- package/scripts/count_token_optimizer_usage.sh +54 -0
- package/scripts/hook_manifest.yaml +33 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/dispatch_hook.py +35 -0
- package/scripts/hooks_status.py +12 -1
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.sh +81 -2
- package/scripts/iron_law_sha.py +98 -0
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +2 -1
- package/scripts/lint_load_context.py +35 -5
- package/scripts/measure_rule_budget.py +314 -0
- package/scripts/prototype_lint_contradictions.py +150 -0
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/schemas/rule.schema.json +55 -6
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +359 -7
- package/scripts/smoke_path_resolution.py +93 -0
- package/scripts/update_prices.py +3 -3
- package/scripts/validate_frontmatter.py +41 -1
- package/.agent-src/commands/chat-history/checkpoint.md +0 -126
- package/.agent-src/commands/chat-history/clear.md +0 -103
- package/.agent-src/commands/chat-history/resume.md +0 -183
- package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +0 -72
- package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +0 -79
- package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +0 -87
- package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +0 -62
- package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +0 -78
- package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +0 -85
- package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +0 -65
- package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +0 -78
- package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +0 -53
- package/.agent-src/rules/chat-history-cadence.md +0 -143
- package/.agent-src/rules/chat-history-ownership.md +0 -124
- package/.agent-src/rules/chat-history-visibility.md +0 -97
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
- package/scripts/check_phase_coupling.py +0 -148
- /package/{docs → .agent-src/contexts}/contracts/artifact-engagement-flow.md +0 -0
- /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": {
|
|
30
|
-
|
|
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": {
|
|
35
|
-
|
|
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
|
|
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,
|
package/scripts/skill_linter.py
CHANGED
|
@@ -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 >
|
|
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
|
-
|
|
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
|
-
|
|
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())
|