@event4u/agent-config 1.13.0 → 1.15.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 +4 -1
- package/.agent-src/commands/agent-status.md +3 -0
- package/.agent-src/commands/agents-audit.md +4 -0
- package/.agent-src/commands/agents-cleanup.md +6 -1
- package/.agent-src/commands/agents-prepare.md +3 -0
- package/.agent-src/commands/analyze-reference-repo.md +4 -0
- package/.agent-src/commands/bug-fix.md +7 -3
- package/.agent-src/commands/bug-investigate.md +4 -0
- package/.agent-src/commands/chat-history-checkpoint.md +126 -0
- package/.agent-src/commands/chat-history-clear.md +6 -1
- package/.agent-src/commands/chat-history-resume.md +7 -2
- package/.agent-src/commands/chat-history.md +7 -2
- package/.agent-src/commands/check-current-md.md +137 -0
- package/.agent-src/commands/commit-in-chunks.md +118 -0
- package/.agent-src/commands/commit.md +4 -0
- package/.agent-src/commands/compress.md +37 -2
- package/.agent-src/commands/context-create.md +4 -0
- package/.agent-src/commands/context-refactor.md +4 -0
- package/.agent-src/commands/copilot-agents-init.md +3 -0
- package/.agent-src/commands/copilot-agents-optimize.md +3 -0
- package/.agent-src/commands/create-pr-description.md +4 -0
- package/.agent-src/commands/create-pr.md +4 -0
- package/.agent-src/commands/do-and-judge.md +4 -1
- package/.agent-src/commands/do-in-steps.md +3 -0
- package/.agent-src/commands/e2e-heal.md +4 -0
- package/.agent-src/commands/e2e-plan.md +4 -0
- package/.agent-src/commands/estimate-ticket.md +4 -1
- package/.agent-src/commands/feature-dev.md +4 -0
- package/.agent-src/commands/feature-explore.md +4 -0
- package/.agent-src/commands/feature-plan.md +4 -0
- package/.agent-src/commands/feature-refactor.md +4 -0
- package/.agent-src/commands/feature-roadmap.md +6 -0
- package/.agent-src/commands/fix-ci.md +4 -0
- package/.agent-src/commands/fix-portability.md +5 -2
- package/.agent-src/commands/fix-pr-bot-comments.md +4 -0
- package/.agent-src/commands/fix-pr-comments.md +4 -0
- package/.agent-src/commands/fix-pr-developer-comments.md +4 -0
- package/.agent-src/commands/fix-references.md +3 -0
- package/.agent-src/commands/fix-seeder.md +4 -0
- package/.agent-src/commands/implement-ticket.md +39 -13
- package/.agent-src/commands/jira-ticket.md +4 -0
- package/.agent-src/commands/judge.md +3 -0
- package/.agent-src/commands/memory-add.md +5 -3
- package/.agent-src/commands/memory-full.md +5 -2
- package/.agent-src/commands/memory-promote.md +7 -6
- package/.agent-src/commands/mode.md +3 -0
- package/.agent-src/commands/module-create.md +4 -0
- package/.agent-src/commands/module-explore.md +4 -0
- package/.agent-src/commands/onboard.md +33 -0
- package/.agent-src/commands/optimize-agents.md +4 -0
- package/.agent-src/commands/optimize-augmentignore.md +12 -0
- package/.agent-src/commands/optimize-rtk-filters.md +3 -0
- package/.agent-src/commands/optimize-skills.md +4 -0
- package/.agent-src/commands/override-create.md +4 -0
- package/.agent-src/commands/override-manage.md +4 -0
- package/.agent-src/commands/package-reset.md +3 -0
- package/.agent-src/commands/package-test.md +3 -0
- package/.agent-src/commands/prepare-for-review.md +4 -0
- package/.agent-src/commands/project-analyze.md +4 -0
- package/.agent-src/commands/project-health.md +4 -0
- package/.agent-src/commands/propose-memory.md +6 -8
- package/.agent-src/commands/quality-fix.md +4 -0
- package/.agent-src/commands/refine-ticket.md +12 -7
- package/.agent-src/commands/review-changes.md +39 -8
- package/.agent-src/commands/review-routing.md +4 -0
- package/.agent-src/commands/roadmap-create.md +18 -0
- package/.agent-src/commands/roadmap-execute.md +14 -1
- package/.agent-src/commands/rule-compliance-audit.md +4 -0
- package/.agent-src/commands/set-cost-profile.md +11 -0
- package/.agent-src/commands/sync-agent-settings.md +12 -0
- package/.agent-src/commands/sync-gitignore.md +3 -0
- package/.agent-src/commands/tests-create.md +4 -0
- package/.agent-src/commands/tests-execute.md +6 -3
- package/.agent-src/commands/threat-model.md +4 -0
- package/.agent-src/commands/update-form-request-messages.md +4 -0
- package/.agent-src/commands/upstream-contribute.md +4 -0
- package/.agent-src/commands/work.md +161 -0
- package/.agent-src/guidelines/agent-infra/engineering-memory-data-format.md +2 -6
- package/.agent-src/guidelines/agent-infra/layered-settings.md +0 -1
- package/.agent-src/guidelines/agent-infra/memory-access.md +0 -7
- package/.agent-src/guidelines/agent-infra/role-contracts.md +2 -4
- package/.agent-src/guidelines/agent-infra/self-improvement-pipeline.md +0 -1
- package/.agent-src/guidelines/php/patterns/strategy.md +180 -2
- package/.agent-src/personas/README.md +0 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +7 -2
- package/.agent-src/rules/artifact-engagement-recording.md +133 -0
- package/.agent-src/rules/ask-when-uncertain.md +18 -13
- package/.agent-src/rules/augment-portability.md +64 -37
- package/.agent-src/rules/autonomous-execution.md +158 -0
- package/.agent-src/rules/chat-history-cadence.md +109 -0
- package/.agent-src/rules/chat-history-ownership.md +123 -0
- package/.agent-src/rules/chat-history-visibility.md +96 -0
- package/.agent-src/rules/cli-output-handling.md +27 -4
- package/.agent-src/rules/command-suggestion.md +134 -0
- package/.agent-src/rules/commit-policy.md +109 -0
- package/.agent-src/rules/direct-answers.md +114 -0
- package/.agent-src/rules/docs-sync.md +36 -0
- package/.agent-src/rules/downstream-changes.md +10 -9
- package/.agent-src/rules/improve-before-implement.md +9 -6
- package/.agent-src/rules/language-and-tone.md +85 -6
- package/.agent-src/rules/non-destructive-by-default.md +117 -0
- package/.agent-src/rules/package-ci-checks.md +4 -0
- package/.agent-src/rules/preservation-guard.md +20 -0
- package/.agent-src/rules/roadmap-progress-sync.md +159 -27
- package/.agent-src/rules/role-mode-adherence.md +1 -1
- package/.agent-src/rules/scope-control.md +42 -1
- package/.agent-src/rules/size-enforcement.md +2 -3
- package/.agent-src/rules/skill-quality.md +3 -8
- package/.agent-src/rules/ui-audit-before-build.md +106 -0
- package/.agent-src/rules/user-interaction.md +107 -51
- package/.agent-src/scripts/update_roadmap_progress.py +73 -9
- package/.agent-src/skills/blade-ui/SKILL.md +47 -3
- package/.agent-src/skills/command-routing/SKILL.md +32 -0
- package/.agent-src/skills/command-writing/SKILL.md +52 -2
- package/.agent-src/skills/description-assist/SKILL.md +21 -0
- package/.agent-src/skills/estimate-ticket/SKILL.md +0 -1
- package/.agent-src/skills/existing-ui-audit/SKILL.md +202 -0
- package/.agent-src/skills/fe-design/SKILL.md +78 -61
- package/.agent-src/skills/file-editor/SKILL.md +9 -0
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +4 -0
- package/.agent-src/skills/flux/SKILL.md +31 -4
- package/.agent-src/skills/guideline-writing/SKILL.md +24 -2
- package/.agent-src/skills/learning-to-rule-or-skill/SKILL.md +51 -9
- package/.agent-src/skills/livewire/SKILL.md +49 -4
- package/.agent-src/skills/md-language-check/SKILL.md +103 -0
- package/.agent-src/skills/php-coder/SKILL.md +24 -0
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +121 -0
- package/.agent-src/skills/refine-prompt/SKILL.md +220 -0
- package/.agent-src/skills/refine-ticket/SKILL.md +32 -28
- package/.agent-src/skills/roadmap-management/SKILL.md +24 -11
- package/.agent-src/skills/rule-writing/SKILL.md +23 -1
- package/.agent-src/skills/skill-writing/SKILL.md +3 -5
- package/.agent-src/skills/upstream-contribute/SKILL.md +3 -3
- package/.agent-src/skills/using-git-worktrees/SKILL.md +3 -1
- package/.agent-src/templates/AGENTS.md +24 -6
- package/.agent-src/templates/agent-settings.md +149 -0
- package/.agent-src/templates/roadmaps.md +11 -4
- package/.agent-src/templates/scripts/implement_ticket/__init__.py +63 -26
- package/.agent-src/templates/scripts/implement_ticket/__main__.py +8 -2
- package/.agent-src/templates/scripts/memory_lookup.py +1 -1
- package/.agent-src/templates/scripts/telemetry/__init__.py +42 -0
- package/.agent-src/templates/scripts/telemetry/aggregator.py +154 -0
- package/.agent-src/templates/scripts/telemetry/boundary.py +171 -0
- package/.agent-src/templates/scripts/telemetry/engagement.py +238 -0
- package/.agent-src/templates/scripts/telemetry/report_renderer.py +170 -0
- package/.agent-src/templates/scripts/telemetry/settings.py +112 -0
- package/.agent-src/templates/scripts/telemetry_record.py +166 -0
- package/.agent-src/templates/scripts/telemetry_report.py +161 -0
- package/.agent-src/templates/scripts/telemetry_status.py +142 -0
- package/.agent-src/templates/scripts/work_engine/__init__.py +58 -0
- package/.agent-src/templates/scripts/work_engine/__main__.py +9 -0
- package/.agent-src/templates/scripts/work_engine/cli.py +195 -0
- package/.agent-src/templates/scripts/work_engine/cli_args.py +116 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/delivery_state.py +10 -3
- package/.agent-src/templates/scripts/work_engine/directives/__init__.py +33 -0
- package/.agent-src/templates/scripts/work_engine/directives/backend/__init__.py +98 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/analyze.py +1 -1
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/implement.py +3 -3
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/memory.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/plan.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/backend/refine.py +396 -0
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/report.py +37 -5
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/test.py +2 -2
- package/.agent-src/templates/scripts/{implement_ticket/steps → work_engine/directives/backend}/verify.py +2 -2
- package/.agent-src/templates/scripts/work_engine/directives/mixed/__init__.py +116 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/contract.py +254 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/stitch.py +229 -0
- package/.agent-src/templates/scripts/work_engine/directives/mixed/ui.py +231 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/__init__.py +113 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/_passthrough.py +44 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/apply.py +241 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/audit.py +414 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/design.py +335 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/polish.py +510 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui/review.py +468 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/__init__.py +119 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/_skipped.py +37 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/apply.py +165 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/refine.py +66 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/report.py +62 -0
- package/.agent-src/templates/scripts/work_engine/directives/ui_trivial/test.py +115 -0
- package/.agent-src/templates/scripts/work_engine/dispatcher.py +331 -0
- package/.agent-src/templates/scripts/work_engine/emitters.py +43 -0
- package/.agent-src/templates/scripts/work_engine/errors.py +19 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +76 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +54 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +32 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +103 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +42 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +49 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/directive_set_guard.py +53 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/halt_surface_audit.py +50 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/state_shape_validation.py +52 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/trace.py +84 -0
- package/.agent-src/templates/scripts/work_engine/hooks/context.py +66 -0
- package/.agent-src/templates/scripts/work_engine/hooks/events.py +44 -0
- package/.agent-src/templates/scripts/work_engine/hooks/exceptions.py +79 -0
- package/.agent-src/templates/scripts/work_engine/hooks/registry.py +60 -0
- package/.agent-src/templates/scripts/work_engine/hooks/runner.py +73 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +141 -0
- package/.agent-src/templates/scripts/work_engine/input_builders.py +163 -0
- package/.agent-src/templates/scripts/work_engine/intent/__init__.py +47 -0
- package/.agent-src/templates/scripts/work_engine/intent/classify.py +280 -0
- package/.agent-src/templates/scripts/work_engine/migration/__init__.py +8 -0
- package/.agent-src/templates/scripts/work_engine/migration/v0_to_v1.py +231 -0
- package/.agent-src/templates/scripts/{implement_ticket → work_engine}/persona_policy.py +1 -1
- package/.agent-src/templates/scripts/work_engine/resolvers/__init__.py +22 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/diff.py +106 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/file.py +113 -0
- package/.agent-src/templates/scripts/work_engine/resolvers/prompt.py +90 -0
- package/.agent-src/templates/scripts/work_engine/scoring/__init__.py +14 -0
- package/.agent-src/templates/scripts/work_engine/scoring/confidence.py +300 -0
- package/.agent-src/templates/scripts/work_engine/stack/__init__.py +31 -0
- package/.agent-src/templates/scripts/work_engine/stack/detect.py +187 -0
- package/.agent-src/templates/scripts/work_engine/state.py +641 -0
- package/.agent-src/templates/scripts/work_engine/state_io.py +202 -0
- package/.claude-plugin/marketplace.json +105 -2
- package/AGENTS.md +38 -8
- package/CHANGELOG.md +609 -0
- package/README.md +136 -14
- package/config/agent-settings.template.yml +45 -0
- package/config/gitignore-block.txt +4 -0
- package/docs/MIGRATION.md +122 -0
- package/docs/architecture.md +111 -35
- package/docs/contracts/STABILITY.md +95 -0
- package/docs/contracts/adr-chat-history-split.md +132 -0
- package/docs/contracts/adr-command-suggestion.md +146 -0
- package/docs/contracts/adr-implement-ticket-runtime.md +122 -0
- package/docs/contracts/adr-product-ui-track.md +384 -0
- package/docs/contracts/adr-prompt-driven-execution.md +187 -0
- package/docs/contracts/agent-memory-contract.md +149 -0
- package/docs/contracts/artifact-engagement-flow.md +262 -0
- package/docs/contracts/command-clusters.md +126 -0
- package/docs/contracts/command-suggestion-flow.md +148 -0
- package/docs/contracts/implement-ticket-flow.md +628 -0
- package/docs/contracts/linear-ai-rules-inclusion.md +143 -0
- package/docs/contracts/linear-ai-three-layers.md +131 -0
- package/docs/contracts/rule-interactions.md +107 -0
- package/docs/contracts/rule-interactions.yml +142 -0
- package/docs/contracts/ui-stack-extension.md +236 -0
- package/docs/contracts/ui-track-flow.md +338 -0
- package/docs/development.md +1 -1
- package/docs/getting-started.md +3 -3
- package/docs/installation.md +124 -2
- package/docs/migrations/commands-1.15.0.md +112 -0
- package/docs/showcase.md +204 -0
- package/docs/ui-track-mental-model.md +121 -0
- package/package.json +1 -1
- package/scripts/agent-config +199 -0
- package/scripts/audit_cloud_compatibility.py +288 -0
- package/scripts/build_cloud_bundle.py +458 -0
- package/scripts/build_linear_digest.py +263 -0
- package/scripts/chat_history.py +796 -7
- package/scripts/check_compression.py +139 -0
- package/scripts/check_iron_law_prominence.py +143 -0
- package/scripts/check_md_language.py +159 -0
- package/scripts/check_portability.py +38 -0
- package/scripts/check_public_links.py +185 -0
- package/scripts/check_references.py +1 -0
- package/scripts/check_reply_consistency.py +140 -0
- package/scripts/command_suggester/__init__.py +51 -0
- package/scripts/command_suggester/cooldown.py +132 -0
- package/scripts/command_suggester/loader.py +70 -0
- package/scripts/command_suggester/match.py +180 -0
- package/scripts/command_suggester/rank.py +120 -0
- package/scripts/command_suggester/render.py +86 -0
- package/scripts/command_suggester/sanitize.py +113 -0
- package/scripts/command_suggester/settings.py +125 -0
- package/scripts/command_suggester/types.py +78 -0
- package/scripts/hooks/augment-chat-history.sh +56 -0
- package/scripts/install-hooks.sh +67 -0
- package/scripts/install.py +150 -33
- package/scripts/lint_marketplace.py +27 -0
- package/scripts/lint_no_new_atomic_commands.py +179 -0
- package/scripts/lint_rule_interactions.py +149 -0
- package/scripts/memory_lookup.py +1 -1
- package/scripts/migrate_command_suggestions.py +151 -0
- package/scripts/release.py +297 -64
- package/scripts/schemas/command.schema.json +41 -0
- package/scripts/skill_linter.py +81 -0
- package/scripts/sync_agent_settings.py +42 -12
- package/scripts/update_counts.py +10 -0
- package/templates/consumer-settings/augment-cli-hooks.json +54 -0
- package/templates/consumer-settings/claude-settings.json +55 -1
- package/.agent-src/rules/chat-history.md +0 -171
- package/.agent-src/templates/scripts/implement_ticket/cli.py +0 -171
- package/.agent-src/templates/scripts/implement_ticket/dispatcher.py +0 -134
- package/.agent-src/templates/scripts/implement_ticket/steps/__init__.py +0 -49
- package/.agent-src/templates/scripts/implement_ticket/steps/refine.py +0 -140
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Public-link checker for the agent-config public surface.
|
|
4
|
+
|
|
5
|
+
Scans the public-surface files (README.md, AGENTS.md, docs/architecture.md)
|
|
6
|
+
for markdown links into `docs/contracts/`, then validates each link against
|
|
7
|
+
the `stability:` frontmatter declared by the target file (per
|
|
8
|
+
`docs/contracts/STABILITY.md`).
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- target stability=stable → OK (no marker required).
|
|
12
|
+
- target stability=beta → OK; warns if surrounding text has no
|
|
13
|
+
visible "(beta)" marker.
|
|
14
|
+
- target stability=experimental → ERROR. Public surface MUST NOT link
|
|
15
|
+
to experimental contracts.
|
|
16
|
+
- target outside docs/contracts/ but referenced for contract-shaped
|
|
17
|
+
intent (links into agents/contexts/*.md from public files) → ERROR.
|
|
18
|
+
- target file missing → ERROR.
|
|
19
|
+
- target file under docs/contracts/ without `stability:` frontmatter
|
|
20
|
+
(except STABILITY.md itself) → ERROR.
|
|
21
|
+
|
|
22
|
+
Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
|
|
23
|
+
|
|
24
|
+
Usage:
|
|
25
|
+
python3 scripts/check_public_links.py
|
|
26
|
+
python3 scripts/check_public_links.py --list # list contracts + levels
|
|
27
|
+
python3 scripts/check_public_links.py --json # machine-readable
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import json
|
|
34
|
+
import re
|
|
35
|
+
import sys
|
|
36
|
+
from dataclasses import dataclass, asdict
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
40
|
+
PUBLIC_FILES = [Path("README.md"), Path("AGENTS.md"), Path("docs/architecture.md")]
|
|
41
|
+
CONTRACTS_DIR = Path("docs/contracts")
|
|
42
|
+
STABILITY_FILE = CONTRACTS_DIR / "STABILITY.md"
|
|
43
|
+
|
|
44
|
+
LINK_RE = re.compile(r"\[(?P<text>[^\]]+)\]\((?P<href>[^)\s]+)(?:\s+\"[^\"]*\")?\)")
|
|
45
|
+
FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?)\n---\s*\n", re.DOTALL)
|
|
46
|
+
STABILITY_RE = re.compile(r"^stability:\s*(\w+)\s*$", re.MULTILINE)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class Violation:
|
|
51
|
+
file: str
|
|
52
|
+
line: int
|
|
53
|
+
href: str
|
|
54
|
+
reason: str
|
|
55
|
+
severity: str # "error" | "warning"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def read_stability(path: Path) -> str | None:
|
|
59
|
+
if not path.exists():
|
|
60
|
+
return None
|
|
61
|
+
txt = path.read_text(encoding="utf-8")
|
|
62
|
+
m = FRONTMATTER_RE.match(txt)
|
|
63
|
+
if not m:
|
|
64
|
+
return None
|
|
65
|
+
sm = STABILITY_RE.search(m.group(1))
|
|
66
|
+
return sm.group(1) if sm else None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def collect_contracts() -> dict[Path, str | None]:
|
|
70
|
+
out: dict[Path, str | None] = {}
|
|
71
|
+
for p in sorted((ROOT / CONTRACTS_DIR).glob("*.md")):
|
|
72
|
+
rel = p.relative_to(ROOT)
|
|
73
|
+
out[rel] = read_stability(p)
|
|
74
|
+
return out
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def resolve(public_file: Path, href: str) -> Path | None:
|
|
78
|
+
href = href.split("#", 1)[0]
|
|
79
|
+
if not href or href.startswith(("http://", "https://", "mailto:", "tel:")):
|
|
80
|
+
return None
|
|
81
|
+
if href.startswith("/"):
|
|
82
|
+
return Path(href.lstrip("/"))
|
|
83
|
+
return (public_file.parent / href).resolve().relative_to(ROOT.resolve())
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def scan_file(public_file: Path, contracts: dict[Path, str | None]) -> list[Violation]:
|
|
87
|
+
abs_path = ROOT / public_file
|
|
88
|
+
if not abs_path.exists():
|
|
89
|
+
return []
|
|
90
|
+
violations: list[Violation] = []
|
|
91
|
+
for lineno, line in enumerate(abs_path.read_text(encoding="utf-8").splitlines(), 1):
|
|
92
|
+
for m in LINK_RE.finditer(line):
|
|
93
|
+
href = m.group("href")
|
|
94
|
+
text = m.group("text")
|
|
95
|
+
try:
|
|
96
|
+
target = resolve(public_file, href)
|
|
97
|
+
except ValueError:
|
|
98
|
+
continue
|
|
99
|
+
if target is None:
|
|
100
|
+
continue
|
|
101
|
+
if target.parts[:2] == ("agents", "contexts") and target.suffix == ".md":
|
|
102
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
103
|
+
"public surface MUST NOT link into agents/contexts/ — move target to docs/contracts/",
|
|
104
|
+
"error"))
|
|
105
|
+
continue
|
|
106
|
+
if target.parts[:2] != ("docs", "contracts") or target.suffix != ".md":
|
|
107
|
+
continue
|
|
108
|
+
if target == STABILITY_FILE:
|
|
109
|
+
continue
|
|
110
|
+
if target not in contracts:
|
|
111
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
112
|
+
f"target not found: {target}", "error"))
|
|
113
|
+
continue
|
|
114
|
+
level = contracts[target]
|
|
115
|
+
if level is None:
|
|
116
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
117
|
+
f"target missing 'stability:' frontmatter: {target}", "error"))
|
|
118
|
+
continue
|
|
119
|
+
if level == "experimental":
|
|
120
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
121
|
+
f"public surface MUST NOT link to experimental contract: {target}",
|
|
122
|
+
"error"))
|
|
123
|
+
continue
|
|
124
|
+
if level == "beta":
|
|
125
|
+
window = line.lower()
|
|
126
|
+
if "(beta)" not in window and "[beta]" not in window:
|
|
127
|
+
violations.append(Violation(str(public_file), lineno, href,
|
|
128
|
+
f"link to beta contract '{target}' lacks visible (beta) marker",
|
|
129
|
+
"warning"))
|
|
130
|
+
return violations
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def main() -> int:
|
|
134
|
+
ap = argparse.ArgumentParser()
|
|
135
|
+
ap.add_argument("--list", action="store_true", help="list contracts + stability levels")
|
|
136
|
+
ap.add_argument("--json", action="store_true", help="machine-readable output")
|
|
137
|
+
ap.add_argument("--strict", action="store_true",
|
|
138
|
+
help="fail on warnings as well as errors (default: errors only)")
|
|
139
|
+
args = ap.parse_args()
|
|
140
|
+
|
|
141
|
+
contracts = collect_contracts()
|
|
142
|
+
if args.list:
|
|
143
|
+
for p, lvl in contracts.items():
|
|
144
|
+
print(f" {lvl or '(no frontmatter)':14} {p}")
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
missing_fm = [p for p, lvl in contracts.items() if lvl is None and p != STABILITY_FILE]
|
|
148
|
+
violations: list[Violation] = []
|
|
149
|
+
for p in missing_fm:
|
|
150
|
+
violations.append(Violation(str(p), 0, "(self)",
|
|
151
|
+
"missing 'stability:' frontmatter required by docs/contracts/STABILITY.md",
|
|
152
|
+
"error"))
|
|
153
|
+
for f in PUBLIC_FILES:
|
|
154
|
+
violations.extend(scan_file(f, contracts))
|
|
155
|
+
|
|
156
|
+
if args.json:
|
|
157
|
+
print(json.dumps([asdict(v) for v in violations], indent=2))
|
|
158
|
+
else:
|
|
159
|
+
errors = [v for v in violations if v.severity == "error"]
|
|
160
|
+
warnings = [v for v in violations if v.severity == "warning"]
|
|
161
|
+
for v in violations:
|
|
162
|
+
icon = "❌" if v.severity == "error" else "⚠️ "
|
|
163
|
+
loc = f"{v.file}:{v.line}" if v.line else v.file
|
|
164
|
+
print(f"{icon} {loc} {v.href}\n → {v.reason}")
|
|
165
|
+
if not violations:
|
|
166
|
+
print(f"✅ public-link check clean — {len(contracts)} contracts scanned, "
|
|
167
|
+
f"{len(PUBLIC_FILES)} public files clean")
|
|
168
|
+
else:
|
|
169
|
+
print(f"\nsummary: {len(errors)} error(s), {len(warnings)} warning(s)")
|
|
170
|
+
|
|
171
|
+
has_errors = any(v.severity == "error" for v in violations)
|
|
172
|
+
has_warnings = any(v.severity == "warning" for v in violations)
|
|
173
|
+
if has_errors:
|
|
174
|
+
return 1
|
|
175
|
+
if has_warnings and args.strict:
|
|
176
|
+
return 1
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
try:
|
|
182
|
+
sys.exit(main())
|
|
183
|
+
except Exception as e:
|
|
184
|
+
print(f"❌ internal error: {e}", file=sys.stderr)
|
|
185
|
+
sys.exit(3)
|
|
@@ -78,6 +78,7 @@ EXAMPLE_PATH_PATTERNS = [
|
|
|
78
78
|
re.compile(r"agents/overrides/"), # override examples
|
|
79
79
|
re.compile(r"commands/old-cmd"), # example placeholder
|
|
80
80
|
re.compile(r"agents/README"), # README reference (may not exist in package)
|
|
81
|
+
re.compile(r"agents/index[\w.-]*\.md"), # planned auto-generated artefact index (F5)
|
|
81
82
|
re.compile(r"agents/docs/"), # project-specific docs (not in package)
|
|
82
83
|
re.compile(r"agents/contexts/"), # project-specific contexts (not in package)
|
|
83
84
|
re.compile(r"agents/gates"), # project-specific policy docs
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""check_reply_consistency.py — enforce user-interaction.md Iron Laws.
|
|
3
|
+
|
|
4
|
+
Single-Source Recommendation Line: a reply with numbered options must
|
|
5
|
+
have ONE bolded `Recommendation: N` / `Empfehlung: N` line, no inline
|
|
6
|
+
`(recommended)` / `(rec)` / `(empfohlen)` tag next to options, and the
|
|
7
|
+
recommended number must appear in the option block.
|
|
8
|
+
|
|
9
|
+
Modes:
|
|
10
|
+
--stdin / --file <path> Validate a single draft (all rules).
|
|
11
|
+
--scan-dir <dir> Scan .md tree for legacy inline-tag regression.
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
0 ok · 2 inline tag · 3 multi-rec · 4 rec-not-in-options
|
|
15
|
+
5 options-without-rec (strict) · 6 scan-dir found · 9 usage error
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
OPTION_LINE_RE = re.compile(r"^\s*>?\s*(\d+)\.\s+\S")
|
|
25
|
+
REC_LINE_RE = re.compile(
|
|
26
|
+
r"(?:Recommendation|Empfehlung)\s*:\s*(\d+)\b", re.IGNORECASE
|
|
27
|
+
)
|
|
28
|
+
TAG_RE = re.compile(r"\((?:recommended|rec|empfohlen)\)", re.IGNORECASE)
|
|
29
|
+
CODESPAN_RE = re.compile(r"`[^`\n]*`")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _strip_codespans(line: str) -> str:
|
|
33
|
+
return CODESPAN_RE.sub("``", line)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def find_inline_tag(text: str) -> tuple[int, str] | None:
|
|
37
|
+
"""Return (line_no, raw_line) of the first numbered-option line carrying
|
|
38
|
+
an inline (recommended)-class tag outside code spans, or None."""
|
|
39
|
+
for idx, raw in enumerate(text.splitlines(), start=1):
|
|
40
|
+
stripped = _strip_codespans(raw)
|
|
41
|
+
if not OPTION_LINE_RE.match(stripped):
|
|
42
|
+
continue
|
|
43
|
+
if TAG_RE.search(stripped):
|
|
44
|
+
return idx, raw.strip()
|
|
45
|
+
return None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def find_option_blocks(text: str) -> list[list[int]]:
|
|
49
|
+
"""Group consecutive numbered-option lines into blocks; return list of
|
|
50
|
+
blocks, each a list of the numbers found in that block."""
|
|
51
|
+
blocks: list[list[int]] = []
|
|
52
|
+
current: list[int] = []
|
|
53
|
+
for raw in text.splitlines():
|
|
54
|
+
m = OPTION_LINE_RE.match(raw)
|
|
55
|
+
if m:
|
|
56
|
+
current.append(int(m.group(1)))
|
|
57
|
+
else:
|
|
58
|
+
if len(current) >= 2:
|
|
59
|
+
blocks.append(current)
|
|
60
|
+
current = []
|
|
61
|
+
if len(current) >= 2:
|
|
62
|
+
blocks.append(current)
|
|
63
|
+
return blocks
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def validate(text: str, strict: bool = False) -> tuple[int, str]:
|
|
67
|
+
"""Run rules. Returns (exit_code, human_message)."""
|
|
68
|
+
tag = find_inline_tag(text)
|
|
69
|
+
if tag:
|
|
70
|
+
line_no, snippet = tag
|
|
71
|
+
return 2, f"line {line_no}: inline tag on numbered option — {snippet!r}"
|
|
72
|
+
|
|
73
|
+
blocks = find_option_blocks(text)
|
|
74
|
+
rec_numbers = [int(n) for n in REC_LINE_RE.findall(text)]
|
|
75
|
+
|
|
76
|
+
if not blocks:
|
|
77
|
+
return 0, "ok (no numbered options block)"
|
|
78
|
+
|
|
79
|
+
if not rec_numbers:
|
|
80
|
+
if strict:
|
|
81
|
+
return 5, "numbered options without Recommendation:/Empfehlung: line"
|
|
82
|
+
return 0, "ok (options without recommendation; non-strict)"
|
|
83
|
+
|
|
84
|
+
distinct = sorted(set(rec_numbers))
|
|
85
|
+
if len(distinct) > 1:
|
|
86
|
+
return 3, f"multiple distinct recommendation numbers: {distinct}"
|
|
87
|
+
|
|
88
|
+
rec_num = distinct[0]
|
|
89
|
+
for block in blocks:
|
|
90
|
+
if rec_num in block:
|
|
91
|
+
return 0, f"ok (recommendation {rec_num} matches option block)"
|
|
92
|
+
return 4, f"recommendation {rec_num} not present in any option block"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def cmd_scan_dir(root: Path) -> int:
|
|
96
|
+
if not root.is_dir():
|
|
97
|
+
print(f"error: not a directory: {root}", file=sys.stderr)
|
|
98
|
+
return 9
|
|
99
|
+
violations: list[tuple[Path, int, str]] = []
|
|
100
|
+
for md in sorted(root.rglob("*.md")):
|
|
101
|
+
text = md.read_text(encoding="utf-8")
|
|
102
|
+
for idx, raw in enumerate(text.splitlines(), start=1):
|
|
103
|
+
stripped = _strip_codespans(raw)
|
|
104
|
+
if OPTION_LINE_RE.match(stripped) and TAG_RE.search(stripped):
|
|
105
|
+
violations.append((md, idx, raw.strip()))
|
|
106
|
+
if violations:
|
|
107
|
+
for path, line, snippet in violations:
|
|
108
|
+
print(f" 🔴 {path}:{line} — inline-tag — {snippet}", file=sys.stderr)
|
|
109
|
+
print(f"\n❌ {len(violations)} legacy-pattern violation(s)", file=sys.stderr)
|
|
110
|
+
return 6
|
|
111
|
+
print(f"✅ No legacy (recommended) tags found under {root}")
|
|
112
|
+
return 0
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def main(argv: list[str] | None = None) -> int:
|
|
116
|
+
p = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
|
|
117
|
+
g = p.add_mutually_exclusive_group(required=True)
|
|
118
|
+
g.add_argument("--stdin", action="store_true", help="read draft from stdin")
|
|
119
|
+
g.add_argument("--file", type=Path, help="read draft from file")
|
|
120
|
+
g.add_argument("--scan-dir", type=Path, help="scan dir for legacy inline tags")
|
|
121
|
+
p.add_argument("--strict", action="store_true",
|
|
122
|
+
help="numbered options REQUIRE recommendation line (rule 5)")
|
|
123
|
+
p.add_argument("-v", "--verbose", action="store_true")
|
|
124
|
+
args = p.parse_args(argv)
|
|
125
|
+
|
|
126
|
+
if args.scan_dir:
|
|
127
|
+
return cmd_scan_dir(args.scan_dir)
|
|
128
|
+
|
|
129
|
+
text = sys.stdin.read() if args.stdin else args.file.read_text(encoding="utf-8")
|
|
130
|
+
code, msg = validate(text, strict=args.strict)
|
|
131
|
+
if code == 0:
|
|
132
|
+
if args.verbose:
|
|
133
|
+
print(f"✅ {msg}")
|
|
134
|
+
return 0
|
|
135
|
+
print(f"❌ [exit {code}] {msg}", file=sys.stderr)
|
|
136
|
+
return code
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
if __name__ == "__main__":
|
|
140
|
+
sys.exit(main())
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Context-aware command suggestion engine.
|
|
2
|
+
|
|
3
|
+
Public API exposed for the always-on `command-suggestion` rule and for
|
|
4
|
+
tests. The engine is **deterministic** and **read-only**: it scores
|
|
5
|
+
candidate commands against a user message + recent context, applies
|
|
6
|
+
ranking, suppresses cooled-down suggestions, and renders a numbered
|
|
7
|
+
options block. It never executes a command — the user pick is what
|
|
8
|
+
triggers the standard slash flow.
|
|
9
|
+
|
|
10
|
+
See `agents/contexts/command-suggestion-eligibility.md` for the
|
|
11
|
+
locked eligibility table and `road-to-context-aware-command-suggestion`
|
|
12
|
+
for the full design.
|
|
13
|
+
"""
|
|
14
|
+
from .types import CommandSpec, Match, Settings, CooldownState
|
|
15
|
+
from .loader import load_commands
|
|
16
|
+
from .match import match
|
|
17
|
+
from .rank import rank
|
|
18
|
+
from .cooldown import (
|
|
19
|
+
apply_cooldown,
|
|
20
|
+
CooldownStore,
|
|
21
|
+
detect_disable_directive,
|
|
22
|
+
is_explicit_slash_invocation,
|
|
23
|
+
)
|
|
24
|
+
from .render import render
|
|
25
|
+
from .sanitize import (
|
|
26
|
+
sanitize_context,
|
|
27
|
+
sanitize_message,
|
|
28
|
+
strip_code_blocks,
|
|
29
|
+
strip_suggestion_echo,
|
|
30
|
+
)
|
|
31
|
+
from .settings import load_settings
|
|
32
|
+
|
|
33
|
+
__all__ = [
|
|
34
|
+
"CommandSpec",
|
|
35
|
+
"Match",
|
|
36
|
+
"Settings",
|
|
37
|
+
"CooldownState",
|
|
38
|
+
"CooldownStore",
|
|
39
|
+
"load_commands",
|
|
40
|
+
"load_settings",
|
|
41
|
+
"match",
|
|
42
|
+
"rank",
|
|
43
|
+
"apply_cooldown",
|
|
44
|
+
"detect_disable_directive",
|
|
45
|
+
"is_explicit_slash_invocation",
|
|
46
|
+
"render",
|
|
47
|
+
"sanitize_context",
|
|
48
|
+
"sanitize_message",
|
|
49
|
+
"strip_code_blocks",
|
|
50
|
+
"strip_suggestion_echo",
|
|
51
|
+
]
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
"""Suppress recently-shown suggestions per conversation.
|
|
2
|
+
|
|
3
|
+
Cooldown key is `(command_name, evidence)` so two distinct triggers
|
|
4
|
+
for the same command (e.g. `/commit` from "git status shows changes"
|
|
5
|
+
vs. from "save this to git") track separately. The user explicitly
|
|
6
|
+
invoking a command via `/command` clears that command's cooldown so
|
|
7
|
+
the next genuine match surfaces immediately.
|
|
8
|
+
|
|
9
|
+
The store is in-memory; persistence is the agent's job (conversation
|
|
10
|
+
state). Phase 5 wires the per-conversation `disabled_for_conversation`
|
|
11
|
+
flag into the same store.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import time
|
|
17
|
+
from typing import Mapping
|
|
18
|
+
|
|
19
|
+
from .types import CommandSpec, CooldownState, Match, Settings
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
_DURATION_RE = re.compile(r"^\s*(\d+)\s*([smhd])\s*$", re.IGNORECASE)
|
|
23
|
+
_DISABLE_DIRECTIVE_RE = re.compile(
|
|
24
|
+
r"(?:^|\s)/command-suggestion-(off|on)\b", re.IGNORECASE
|
|
25
|
+
)
|
|
26
|
+
_EXPLICIT_SLASH_RE = re.compile(r"^\s*/[A-Za-z][A-Za-z0-9_-]*\b")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def is_explicit_slash_invocation(message: str) -> bool:
|
|
30
|
+
"""Return True when the message starts with an explicit ``/command``.
|
|
31
|
+
|
|
32
|
+
Per the `command-suggestion` rule, explicit slash invocations
|
|
33
|
+
bypass the suggestion layer entirely \u2014 they're handled by
|
|
34
|
+
`slash-commands` directly. The engine should not score in that
|
|
35
|
+
case. Helper exposed for the runtime caller and the GT-CS4
|
|
36
|
+
golden.
|
|
37
|
+
"""
|
|
38
|
+
if not message:
|
|
39
|
+
return False
|
|
40
|
+
return bool(_EXPLICIT_SLASH_RE.match(message))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def detect_disable_directive(message: str) -> bool | None:
|
|
44
|
+
"""Detect a `/command-suggestion-off` / `-on` directive in the user message.
|
|
45
|
+
|
|
46
|
+
Returns ``True`` to disable for the rest of the conversation,
|
|
47
|
+
``False`` to re-enable, ``None`` when no directive is present.
|
|
48
|
+
The latest occurrence in the message wins (order-stable on tie).
|
|
49
|
+
Mutating the `CooldownStore` is the caller's responsibility — this
|
|
50
|
+
helper stays pure so tests don't have to fake time.
|
|
51
|
+
"""
|
|
52
|
+
if not message:
|
|
53
|
+
return None
|
|
54
|
+
last: bool | None = None
|
|
55
|
+
for m in _DISABLE_DIRECTIVE_RE.finditer(message):
|
|
56
|
+
last = m.group(1).lower() == "off"
|
|
57
|
+
return last
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def parse_cooldown(value: str | None, default_seconds: int) -> int:
|
|
61
|
+
"""Convert `'10m'` / `'30s'` / `'1h'` / `'2d'` to seconds.
|
|
62
|
+
|
|
63
|
+
Returns ``default_seconds`` for any malformed or missing input —
|
|
64
|
+
keeping the runtime fail-soft. The schema validator caps the
|
|
65
|
+
string length, so we never see absurd inputs in practice.
|
|
66
|
+
"""
|
|
67
|
+
if not value:
|
|
68
|
+
return default_seconds
|
|
69
|
+
m = _DURATION_RE.match(str(value))
|
|
70
|
+
if not m:
|
|
71
|
+
return default_seconds
|
|
72
|
+
n, unit = int(m.group(1)), m.group(2).lower()
|
|
73
|
+
factor = {"s": 1, "m": 60, "h": 3600, "d": 86400}[unit]
|
|
74
|
+
return n * factor
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class CooldownStore:
|
|
78
|
+
"""Thin wrapper around `CooldownState` with time-aware helpers.
|
|
79
|
+
|
|
80
|
+
Tests inject a fixed `now` to make decay deterministic; runtime
|
|
81
|
+
leaves it as `time.time`.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
def __init__(self, state: CooldownState | None = None, *, now=time.time):
|
|
85
|
+
self.state = state or CooldownState()
|
|
86
|
+
self._now = now
|
|
87
|
+
|
|
88
|
+
def is_cooled_down(
|
|
89
|
+
self, command: str, evidence: str, *, window_seconds: int
|
|
90
|
+
) -> bool:
|
|
91
|
+
last = self.state.last_shown.get((command, evidence))
|
|
92
|
+
if last is None:
|
|
93
|
+
return False
|
|
94
|
+
return (self._now() - last) < window_seconds
|
|
95
|
+
|
|
96
|
+
def record_shown(self, matches: list[Match]) -> None:
|
|
97
|
+
ts = self._now()
|
|
98
|
+
for m in matches:
|
|
99
|
+
self.state.last_shown[(m.command, m.evidence)] = ts
|
|
100
|
+
|
|
101
|
+
def record_explicit_invocation(self, command: str) -> None:
|
|
102
|
+
"""Clear the cooldown when the user explicitly types `/command`.
|
|
103
|
+
|
|
104
|
+
We drop every entry for that command (across all evidences)
|
|
105
|
+
so a deliberate invocation always produces a clean slate.
|
|
106
|
+
"""
|
|
107
|
+
ts = self._now()
|
|
108
|
+
self.state.explicit_invocations[command] = ts
|
|
109
|
+
keys_to_drop = [
|
|
110
|
+
k for k in self.state.last_shown if k[0] == command
|
|
111
|
+
]
|
|
112
|
+
for k in keys_to_drop:
|
|
113
|
+
del self.state.last_shown[k]
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def apply_cooldown(
|
|
117
|
+
matches: list[Match],
|
|
118
|
+
store: CooldownStore,
|
|
119
|
+
settings: Settings,
|
|
120
|
+
specs_by_name: Mapping[str, CommandSpec],
|
|
121
|
+
) -> list[Match]:
|
|
122
|
+
if store.state.disabled_for_conversation:
|
|
123
|
+
return []
|
|
124
|
+
out: list[Match] = []
|
|
125
|
+
for m in matches:
|
|
126
|
+
spec = specs_by_name.get(m.command)
|
|
127
|
+
per_cmd = spec.cooldown if spec else None
|
|
128
|
+
window = parse_cooldown(per_cmd, settings.cooldown_seconds)
|
|
129
|
+
if store.is_cooled_down(m.command, m.evidence, window_seconds=window):
|
|
130
|
+
continue
|
|
131
|
+
out.append(m)
|
|
132
|
+
return out
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Read command frontmatter into `CommandSpec` instances.
|
|
2
|
+
|
|
3
|
+
Reuses the package's stdlib-only `validate_frontmatter.parse_frontmatter`
|
|
4
|
+
so the loader and the linter agree on what counts as well-formed.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from .types import CommandSpec
|
|
13
|
+
|
|
14
|
+
# Sibling stdlib parser — same one the linter calls.
|
|
15
|
+
_SCRIPTS_DIR = Path(__file__).resolve().parent.parent
|
|
16
|
+
sys.path.insert(0, str(_SCRIPTS_DIR))
|
|
17
|
+
from validate_frontmatter import parse_frontmatter # noqa: E402
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def load_commands(commands_dir: Path) -> list[CommandSpec]:
|
|
21
|
+
"""Load every `*.md` under ``commands_dir`` as a `CommandSpec`.
|
|
22
|
+
|
|
23
|
+
Files without a `suggestion` block are loaded as `eligible=False`
|
|
24
|
+
with empty rationale — keeps tests deterministic on legacy data.
|
|
25
|
+
Bad frontmatter is skipped silently; the linter is the gate, not
|
|
26
|
+
this loader.
|
|
27
|
+
"""
|
|
28
|
+
specs: list[CommandSpec] = []
|
|
29
|
+
for path in sorted(commands_dir.glob("*.md")):
|
|
30
|
+
text = path.read_text(encoding="utf-8")
|
|
31
|
+
data, _offset = parse_frontmatter(text)
|
|
32
|
+
if data is None:
|
|
33
|
+
continue
|
|
34
|
+
name = str(data.get("name") or path.stem)
|
|
35
|
+
description = str(data.get("description") or "")
|
|
36
|
+
spec = _spec_from_data(name, description, data.get("suggestion"))
|
|
37
|
+
specs.append(spec)
|
|
38
|
+
return specs
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _spec_from_data(
|
|
42
|
+
name: str, description: str, suggestion: Any
|
|
43
|
+
) -> CommandSpec:
|
|
44
|
+
if not isinstance(suggestion, dict):
|
|
45
|
+
return CommandSpec(name=name, description=description, eligible=False)
|
|
46
|
+
eligible = suggestion.get("eligible") is True
|
|
47
|
+
if not eligible:
|
|
48
|
+
return CommandSpec(
|
|
49
|
+
name=name,
|
|
50
|
+
description=description,
|
|
51
|
+
eligible=False,
|
|
52
|
+
rationale=str(suggestion.get("rationale") or ""),
|
|
53
|
+
)
|
|
54
|
+
floor = suggestion.get("confidence_floor")
|
|
55
|
+
floor_f: float | None
|
|
56
|
+
try:
|
|
57
|
+
floor_f = float(floor) if floor is not None else None
|
|
58
|
+
except (TypeError, ValueError):
|
|
59
|
+
floor_f = None
|
|
60
|
+
cooldown = suggestion.get("cooldown")
|
|
61
|
+
cooldown_s = str(cooldown) if cooldown is not None else None
|
|
62
|
+
return CommandSpec(
|
|
63
|
+
name=name,
|
|
64
|
+
description=description,
|
|
65
|
+
eligible=True,
|
|
66
|
+
trigger_description=str(suggestion.get("trigger_description") or ""),
|
|
67
|
+
trigger_context=str(suggestion.get("trigger_context") or ""),
|
|
68
|
+
confidence_floor=floor_f,
|
|
69
|
+
cooldown=cooldown_s,
|
|
70
|
+
)
|