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