@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,78 @@
|
|
|
1
|
+
"""Type definitions for the command suggestion engine.
|
|
2
|
+
|
|
3
|
+
Plain dataclasses — no third-party deps. Kept in a sibling module so
|
|
4
|
+
match/rank/cooldown/render can import without cycles.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass, field
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class CommandSpec:
|
|
14
|
+
"""Loaded command metadata that drives matching.
|
|
15
|
+
|
|
16
|
+
Fields mirror the `suggestion:` frontmatter block plus the
|
|
17
|
+
command's `name` and `description`. Ineligible commands are
|
|
18
|
+
represented with `eligible=False` and are never returned by the
|
|
19
|
+
matcher; the loader keeps them so cross-referencing stays simple.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
name: str
|
|
23
|
+
description: str
|
|
24
|
+
eligible: bool
|
|
25
|
+
trigger_description: str = ""
|
|
26
|
+
trigger_context: str = ""
|
|
27
|
+
rationale: str = ""
|
|
28
|
+
confidence_floor: Optional[float] = None
|
|
29
|
+
cooldown: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(frozen=True)
|
|
33
|
+
class Match:
|
|
34
|
+
"""A scored candidate. `score` is 0.0–1.0 inclusive.
|
|
35
|
+
|
|
36
|
+
`matched_trigger` is "description" | "context" | "both" and lets
|
|
37
|
+
the renderer surface why a command surfaced. `evidence` is the
|
|
38
|
+
short substring that fired (debugging / golden tests / explain).
|
|
39
|
+
`has_structural_bonus` is True when a heavy-signal pattern (ticket
|
|
40
|
+
key, file path, glob) co-occurred in the message — the ranker
|
|
41
|
+
treats those as specific enough to bypass vague-input suppression.
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
command: str
|
|
45
|
+
score: float
|
|
46
|
+
matched_trigger: str
|
|
47
|
+
evidence: str
|
|
48
|
+
has_structural_bonus: bool = False
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class Settings:
|
|
53
|
+
"""Runtime knobs read from `.agent-settings.yml`.
|
|
54
|
+
|
|
55
|
+
Defaults match the "open decisions" leans in the roadmap.
|
|
56
|
+
Per-command frontmatter values override the global floor /
|
|
57
|
+
cooldown.
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
enabled: bool = True
|
|
61
|
+
confidence_floor: float = 0.6
|
|
62
|
+
cooldown_seconds: int = 600 # 10m
|
|
63
|
+
max_options: int = 4
|
|
64
|
+
blocklist: tuple[str, ...] = ()
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@dataclass
|
|
68
|
+
class CooldownState:
|
|
69
|
+
"""Per-conversation cooldown tracker — mutable on purpose."""
|
|
70
|
+
|
|
71
|
+
last_shown: dict[tuple[str, str], float] = field(default_factory=dict)
|
|
72
|
+
"""Key: (command_name, trigger_evidence). Value: unix timestamp."""
|
|
73
|
+
|
|
74
|
+
explicit_invocations: dict[str, float] = field(default_factory=dict)
|
|
75
|
+
"""Commands the user explicitly typed; clears their cooldown."""
|
|
76
|
+
|
|
77
|
+
disabled_for_conversation: bool = False
|
|
78
|
+
"""Set by the `/command-suggestion-off` directive (Phase 5)."""
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Augment Code lifecycle-hook trampoline for chat-history.
|
|
3
|
+
#
|
|
4
|
+
# Augment requires hook scripts to use the .sh extension and live at
|
|
5
|
+
# either a system path (/etc/augment/...) or user scope
|
|
6
|
+
# (~/.augment/...). This trampoline lives at user scope and dispatches
|
|
7
|
+
# every event to whichever workspace fired it, so a single install
|
|
8
|
+
# covers every project that has ./agent-config available.
|
|
9
|
+
#
|
|
10
|
+
# Behaviour:
|
|
11
|
+
# - Read the JSON event from stdin into a buffer.
|
|
12
|
+
# - Extract workspace_roots[0]; bail silently when missing.
|
|
13
|
+
# - cd into that workspace; bail silently when it is not a directory
|
|
14
|
+
# or does not contain ./agent-config.
|
|
15
|
+
# - Re-pipe the original JSON into
|
|
16
|
+
# ./agent-config chat-history:hook --platform augment
|
|
17
|
+
# so chat_history.py can run the platform mapping.
|
|
18
|
+
# - Always exit 0 — chat-history must never block the agent loop.
|
|
19
|
+
|
|
20
|
+
set -u
|
|
21
|
+
|
|
22
|
+
EVENT_DATA="$(cat)"
|
|
23
|
+
|
|
24
|
+
# Extract workspace_roots[0] using whichever JSON tool is available.
|
|
25
|
+
WORKSPACE=""
|
|
26
|
+
if command -v jq >/dev/null 2>&1; then
|
|
27
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" \
|
|
28
|
+
| jq -r '.workspace_roots[0] // empty' 2>/dev/null)"
|
|
29
|
+
elif command -v python3 >/dev/null 2>&1; then
|
|
30
|
+
WORKSPACE="$(printf '%s' "$EVENT_DATA" | python3 -c '
|
|
31
|
+
import json, sys
|
|
32
|
+
try:
|
|
33
|
+
data = json.load(sys.stdin)
|
|
34
|
+
except Exception:
|
|
35
|
+
sys.exit(0)
|
|
36
|
+
roots = data.get("workspace_roots") or []
|
|
37
|
+
if roots:
|
|
38
|
+
print(roots[0])
|
|
39
|
+
' 2>/dev/null)"
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
if [ -z "$WORKSPACE" ] || [ ! -d "$WORKSPACE" ]; then
|
|
43
|
+
exit 0
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
cd "$WORKSPACE" 2>/dev/null || exit 0
|
|
47
|
+
|
|
48
|
+
if [ ! -x ./agent-config ]; then
|
|
49
|
+
exit 0
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
printf '%s' "$EVENT_DATA" \
|
|
53
|
+
| ./agent-config chat-history:hook --platform augment \
|
|
54
|
+
>/dev/null 2>&1 || true
|
|
55
|
+
|
|
56
|
+
exit 0
|
package/scripts/install-hooks.sh
CHANGED
|
@@ -27,3 +27,70 @@ EOF
|
|
|
27
27
|
|
|
28
28
|
chmod +x "$HOOKS_DIR/pre-push"
|
|
29
29
|
echo "✅ Pre-push hook installed."
|
|
30
|
+
|
|
31
|
+
# Pre-commit: marketplace consistency -----------------------------------------
|
|
32
|
+
#
|
|
33
|
+
# Distribution manifests (.claude-plugin/marketplace.json) drift silently —
|
|
34
|
+
# adding a skill on disk without updating the manifest renders it invisible to
|
|
35
|
+
# Claude Code Plugin Marketplace consumers. CI catches it, but a structural
|
|
36
|
+
# pre-commit gate stops the bad commit from landing in the first place.
|
|
37
|
+
# Runtime is ~40 ms; always-on is cheaper than scoped detection.
|
|
38
|
+
|
|
39
|
+
cat > "$HOOKS_DIR/pre-commit" << 'EOF'
|
|
40
|
+
#!/usr/bin/env bash
|
|
41
|
+
# Pre-commit hook: verify .claude-plugin/marketplace.json lists every skill
|
|
42
|
+
# that exists on disk under .claude/skills/.
|
|
43
|
+
|
|
44
|
+
python3 scripts/lint_marketplace.py
|
|
45
|
+
status=$?
|
|
46
|
+
|
|
47
|
+
if [ $status -ne 0 ]; then
|
|
48
|
+
echo ""
|
|
49
|
+
echo "❌ Commit blocked — .claude-plugin/marketplace.json is out of sync."
|
|
50
|
+
echo " Add the missing skill to the manifest (or remove the stale entry),"
|
|
51
|
+
echo " then re-stage and commit. To bypass for an unrelated WIP commit:"
|
|
52
|
+
echo " git commit --no-verify"
|
|
53
|
+
exit 1
|
|
54
|
+
fi
|
|
55
|
+
EOF
|
|
56
|
+
|
|
57
|
+
chmod +x "$HOOKS_DIR/pre-commit"
|
|
58
|
+
echo "✅ Pre-commit hook installed."
|
|
59
|
+
|
|
60
|
+
# Chat-history bridge hooks ----------------------------------------------------
|
|
61
|
+
#
|
|
62
|
+
# Augment IDE plugin (and any other agent surface without native chat
|
|
63
|
+
# lifecycle hooks) cannot fire SessionStart/Stop/PostToolUse. Git hooks
|
|
64
|
+
# are the platform-agnostic lifecycle surface that fires regardless of
|
|
65
|
+
# IDE — every commit, merge, checkout, and rewrite turns into a phase
|
|
66
|
+
# boundary in .agent-chat-history when an agent session is active.
|
|
67
|
+
#
|
|
68
|
+
# The hooks are silent no-ops when no agent session is active (the
|
|
69
|
+
# chat_history.py hook-append script returns "skipped_no_sidecar" with
|
|
70
|
+
# exit 0) and `|| true` belt-and-suspenders ensures git operations are
|
|
71
|
+
# never blocked.
|
|
72
|
+
|
|
73
|
+
write_chat_history_hook() {
|
|
74
|
+
local name="$1"
|
|
75
|
+
local phase_tag="$2"
|
|
76
|
+
cat > "$HOOKS_DIR/$name" << EOF
|
|
77
|
+
#!/usr/bin/env bash
|
|
78
|
+
# $name: append a phase boundary to .agent-chat-history when an agent
|
|
79
|
+
# session is active. Silent no-op otherwise. Never blocks git.
|
|
80
|
+
|
|
81
|
+
if [ -x ./agent-config ]; then
|
|
82
|
+
ref="\$(git rev-parse --short HEAD 2>/dev/null || echo unknown)"
|
|
83
|
+
payload="{\"phase\":\"$phase_tag\",\"source\":\"git-hook:\$ref\"}"
|
|
84
|
+
./agent-config chat-history:checkpoint --payload "\$payload" \
|
|
85
|
+
>/dev/null 2>&1 || true
|
|
86
|
+
fi
|
|
87
|
+
exit 0
|
|
88
|
+
EOF
|
|
89
|
+
chmod +x "$HOOKS_DIR/$name"
|
|
90
|
+
echo "✅ $name hook installed."
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
write_chat_history_hook "post-commit" "git:post-commit"
|
|
94
|
+
write_chat_history_hook "post-merge" "git:post-merge"
|
|
95
|
+
write_chat_history_hook "post-checkout" "git:post-checkout"
|
|
96
|
+
write_chat_history_hook "post-rewrite" "git:post-rewrite"
|
package/scripts/install.py
CHANGED
|
@@ -251,45 +251,67 @@ def _yaml_scalar(value: str) -> str:
|
|
|
251
251
|
def _replace_template_value(template: str, dotted_path: str, value: str) -> str:
|
|
252
252
|
"""Replace the default value for a dotted-path key in the YAML template.
|
|
253
253
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
254
|
+
Convenience wrapper: formats *value* as a YAML scalar (via
|
|
255
|
+
:func:`_yaml_scalar`) and delegates to :func:`_replace_template_value_raw`.
|
|
256
|
+
"""
|
|
257
|
+
return _replace_template_value_raw(template, dotted_path, _yaml_scalar(value))
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _replace_template_value_raw(template: str, dotted_path: str, raw_yaml: str) -> str:
|
|
261
|
+
"""Replace the value at *dotted_path* with the pre-formatted *raw_yaml*.
|
|
262
|
+
|
|
263
|
+
Handles arbitrary nesting depth. The template uses 2-space indents;
|
|
264
|
+
parent sections are tracked by indent level so the leaf scalar is
|
|
265
|
+
only replaced when every parent matches the dotted path.
|
|
266
|
+
|
|
267
|
+
Comments and indentation are preserved. Returns *template* unchanged
|
|
268
|
+
if the path cannot be located.
|
|
257
269
|
"""
|
|
258
270
|
parts = dotted_path.split(".")
|
|
259
|
-
if
|
|
260
|
-
|
|
261
|
-
elif len(parts) == 2:
|
|
262
|
-
section, key = parts[0], parts[1]
|
|
263
|
-
else:
|
|
264
|
-
return template # deeper nesting not supported in current schema
|
|
271
|
+
if not parts:
|
|
272
|
+
return template
|
|
265
273
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
274
|
+
sections = parts[:-1]
|
|
275
|
+
key = parts[-1]
|
|
276
|
+
target_indent = " " * len(sections)
|
|
277
|
+
|
|
278
|
+
header_re = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*):\s*$")
|
|
279
|
+
scalar_re = re.compile(r"^(\s*)([A-Za-z_][A-Za-z0-9_]*):\s*\S.*$")
|
|
280
|
+
|
|
281
|
+
# Stack of section names by depth; None entries mean "not yet seen
|
|
282
|
+
# at this depth" or "left this section". For path a.b.c we need
|
|
283
|
+
# current_path == ['a', 'b'] when scanning for key 'c' at indent 4.
|
|
284
|
+
current_path: list[str | None] = [None] * len(sections)
|
|
271
285
|
|
|
272
|
-
|
|
286
|
+
lines = template.splitlines()
|
|
273
287
|
for idx, line in enumerate(lines):
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if m_section:
|
|
277
|
-
current_section = m_section.group(1)
|
|
288
|
+
stripped = line.strip()
|
|
289
|
+
if not stripped or stripped.startswith("#"):
|
|
278
290
|
continue
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
291
|
+
|
|
292
|
+
m_header = header_re.match(line)
|
|
293
|
+
if m_header:
|
|
294
|
+
indent = m_header.group(1)
|
|
295
|
+
name = m_header.group(2)
|
|
296
|
+
depth = len(indent) // 2
|
|
297
|
+
if depth < len(sections):
|
|
298
|
+
current_path[depth] = name
|
|
299
|
+
# Reset deeper levels — we just entered a new sub-tree.
|
|
300
|
+
for d in range(depth + 1, len(sections)):
|
|
301
|
+
current_path[d] = None
|
|
302
|
+
continue
|
|
303
|
+
|
|
304
|
+
m_scalar = scalar_re.match(line)
|
|
305
|
+
if not m_scalar:
|
|
306
|
+
continue
|
|
307
|
+
indent = m_scalar.group(1)
|
|
308
|
+
name = m_scalar.group(2)
|
|
309
|
+
if name != key or indent != target_indent:
|
|
310
|
+
continue
|
|
311
|
+
if current_path != list(sections):
|
|
312
|
+
continue
|
|
313
|
+
lines[idx] = f"{indent}{key}: {raw_yaml}"
|
|
314
|
+
return "\n".join(lines) + ("\n" if template.endswith("\n") else "")
|
|
293
315
|
return template
|
|
294
316
|
|
|
295
317
|
|
|
@@ -436,6 +458,92 @@ def ensure_augment_bridge(project_root: Path, force: bool) -> None:
|
|
|
436
458
|
merge_json_file(project_root / ".augment" / "settings.json", bridge, force, ".augment/settings.json")
|
|
437
459
|
|
|
438
460
|
|
|
461
|
+
# Augment lifecycle hooks live at user scope (~/.augment/settings.json) per
|
|
462
|
+
# https://docs.augmentcode.com/cli/hooks — that is the only path read by both
|
|
463
|
+
# the CLI and the IDE plugins (VSCode, IntelliJ). Project-local
|
|
464
|
+
# .augment/settings.json is plugin enablement, not hooks.
|
|
465
|
+
AUGMENT_USER_DIR = Path.home() / ".augment"
|
|
466
|
+
AUGMENT_USER_HOOKS_DIR = AUGMENT_USER_DIR / "hooks"
|
|
467
|
+
AUGMENT_TRAMPOLINE_NAME = "augment-chat-history.sh"
|
|
468
|
+
AUGMENT_HOOK_EVENTS = ("SessionStart", "SessionEnd", "Stop", "PostToolUse")
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def ensure_augment_user_hooks(package_root: Path, force: bool) -> None:
|
|
472
|
+
"""Deploy the Augment lifecycle-hook trampoline at user scope.
|
|
473
|
+
|
|
474
|
+
Augment hook scripts must use the .sh extension and be referenced by
|
|
475
|
+
absolute path; user scope is the only surface that fires for both the
|
|
476
|
+
CLI and the IDE plugins. This installs once per developer (not per
|
|
477
|
+
project) — the trampoline reads workspace_roots from the event payload
|
|
478
|
+
and dispatches into whichever project is active at hook-fire time.
|
|
479
|
+
"""
|
|
480
|
+
src = package_root / "scripts" / "hooks" / AUGMENT_TRAMPOLINE_NAME
|
|
481
|
+
if not src.exists():
|
|
482
|
+
skip(f"augment trampoline missing in package: {src}")
|
|
483
|
+
return
|
|
484
|
+
|
|
485
|
+
AUGMENT_USER_HOOKS_DIR.mkdir(parents=True, exist_ok=True)
|
|
486
|
+
dst = AUGMENT_USER_HOOKS_DIR / AUGMENT_TRAMPOLINE_NAME
|
|
487
|
+
|
|
488
|
+
src_text = src.read_text(encoding="utf-8")
|
|
489
|
+
if dst.exists() and dst.read_text(encoding="utf-8") == src_text and not force:
|
|
490
|
+
skip(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} already up to date")
|
|
491
|
+
else:
|
|
492
|
+
dst.write_text(src_text, encoding="utf-8")
|
|
493
|
+
dst.chmod(0o755)
|
|
494
|
+
success(f"~/.augment/hooks/{AUGMENT_TRAMPOLINE_NAME} installed")
|
|
495
|
+
|
|
496
|
+
hook_entry = {
|
|
497
|
+
"hooks": [
|
|
498
|
+
{
|
|
499
|
+
"type": "command",
|
|
500
|
+
"command": str(dst),
|
|
501
|
+
},
|
|
502
|
+
],
|
|
503
|
+
}
|
|
504
|
+
settings_patch: dict = {"hooks": {event: [hook_entry] for event in AUGMENT_HOOK_EVENTS}}
|
|
505
|
+
merge_json_file(
|
|
506
|
+
AUGMENT_USER_DIR / "settings.json",
|
|
507
|
+
settings_patch,
|
|
508
|
+
force,
|
|
509
|
+
"~/.augment/settings.json",
|
|
510
|
+
)
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
def _chat_history_hook_block(platform: str) -> dict:
|
|
514
|
+
"""Single hook entry that calls ./agent-config chat-history:hook --platform <name>."""
|
|
515
|
+
return {
|
|
516
|
+
"hooks": [
|
|
517
|
+
{
|
|
518
|
+
"type": "command",
|
|
519
|
+
"command": f"./agent-config chat-history:hook --platform {platform}",
|
|
520
|
+
},
|
|
521
|
+
],
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def ensure_claude_bridge(project_root: Path, force: bool) -> None:
|
|
526
|
+
"""Deploy .claude/settings.json with plugin enablement and chat-history hooks.
|
|
527
|
+
|
|
528
|
+
Hooks dispatch to scripts/chat_history.py via the project-root ./agent-config
|
|
529
|
+
wrapper. They are no-ops when chat_history.enabled is false in
|
|
530
|
+
.agent-settings.yml. Idempotent: reruns merge cleanly without duplicating
|
|
531
|
+
entries (deep_merge replaces hook arrays rather than appending).
|
|
532
|
+
"""
|
|
533
|
+
claude_hook = _chat_history_hook_block("claude")
|
|
534
|
+
bridge = {
|
|
535
|
+
"enabledPlugins": {"agent-conf@event4u": True},
|
|
536
|
+
"hooks": {
|
|
537
|
+
"SessionStart": [claude_hook],
|
|
538
|
+
"UserPromptSubmit": [claude_hook],
|
|
539
|
+
"PostToolUse": [claude_hook],
|
|
540
|
+
"Stop": [claude_hook],
|
|
541
|
+
"SessionEnd": [claude_hook],
|
|
542
|
+
},
|
|
543
|
+
}
|
|
544
|
+
merge_json_file(project_root / ".claude" / "settings.json", bridge, force, ".claude/settings.json")
|
|
545
|
+
|
|
546
|
+
|
|
439
547
|
def ensure_copilot_bridge(project_root: Path, force: bool) -> None:
|
|
440
548
|
target = project_root / ".github" / "plugin" / "marketplace.json"
|
|
441
549
|
|
|
@@ -474,6 +582,11 @@ def parse_options(argv: list[str]) -> argparse.Namespace:
|
|
|
474
582
|
)
|
|
475
583
|
parser.add_argument("--force", action="store_true", help="overwrite existing files")
|
|
476
584
|
parser.add_argument("--skip-bridges", action="store_true", help="only create .agent-settings.yml")
|
|
585
|
+
parser.add_argument(
|
|
586
|
+
"--augment-user-hooks",
|
|
587
|
+
action="store_true",
|
|
588
|
+
help="also deploy ~/.augment/settings.json + ~/.augment/hooks/ (user-scope, all projects)",
|
|
589
|
+
)
|
|
477
590
|
parser.add_argument("--project", default=None, help="project root (default: cwd or PROJECT_ROOT env)")
|
|
478
591
|
parser.add_argument("--package", default=None, help="package root (default: auto-detect under project)")
|
|
479
592
|
parser.add_argument("--quiet", action="store_true", help="suppress info/success output (warnings/errors still shown)")
|
|
@@ -516,8 +629,12 @@ def main(argv: list[str]) -> int:
|
|
|
516
629
|
if not opts.skip_bridges:
|
|
517
630
|
ensure_vscode_bridge(project_root, package_type, opts.force)
|
|
518
631
|
ensure_augment_bridge(project_root, opts.force)
|
|
632
|
+
ensure_claude_bridge(project_root, opts.force)
|
|
519
633
|
ensure_copilot_bridge(project_root, opts.force)
|
|
520
634
|
|
|
635
|
+
if opts.augment_user_hooks:
|
|
636
|
+
ensure_augment_user_hooks(package_root, opts.force)
|
|
637
|
+
|
|
521
638
|
if not QUIET:
|
|
522
639
|
print()
|
|
523
640
|
success("Done.")
|
|
@@ -10,6 +10,8 @@ shape used by anthropics/skills:
|
|
|
10
10
|
- metadata must have description + version
|
|
11
11
|
- metadata.version must match package.json (single source of truth)
|
|
12
12
|
- every plugins[].skills[] entry must exist on disk and carry a SKILL.md
|
|
13
|
+
- every SKILL.md on disk under .claude/skills/ must be listed in some
|
|
14
|
+
plugin's skills[] (drift detection)
|
|
13
15
|
|
|
14
16
|
Exit codes: 0 = clean, 1 = problems found, 3 = internal error.
|
|
15
17
|
"""
|
|
@@ -23,6 +25,7 @@ from pathlib import Path
|
|
|
23
25
|
ROOT = Path(".")
|
|
24
26
|
MARKETPLACE = ROOT / ".claude-plugin" / "marketplace.json"
|
|
25
27
|
PACKAGE_JSON = ROOT / "package.json"
|
|
28
|
+
CLAUDE_SKILLS_DIR = ROOT / ".claude" / "skills"
|
|
26
29
|
|
|
27
30
|
|
|
28
31
|
def fail(errors: list[str]) -> int:
|
|
@@ -121,6 +124,30 @@ def main() -> int:
|
|
|
121
124
|
if not skill_md.exists():
|
|
122
125
|
errors.append(f"{entry} has no SKILL.md: `{path}`")
|
|
123
126
|
|
|
127
|
+
# Reverse-completeness: every SKILL.md on disk under .claude/skills/
|
|
128
|
+
# must appear in some plugin's skills[]. Catches the drift where new
|
|
129
|
+
# skills are generated but never added to the marketplace manifest.
|
|
130
|
+
listed: set[str] = set()
|
|
131
|
+
for plugin in plugins:
|
|
132
|
+
if not isinstance(plugin, dict):
|
|
133
|
+
continue
|
|
134
|
+
for path in plugin.get("skills", []):
|
|
135
|
+
if isinstance(path, str):
|
|
136
|
+
listed.add(path.removeprefix("./"))
|
|
137
|
+
|
|
138
|
+
if CLAUDE_SKILLS_DIR.exists():
|
|
139
|
+
for skill_dir in sorted(CLAUDE_SKILLS_DIR.iterdir()):
|
|
140
|
+
if not skill_dir.is_dir():
|
|
141
|
+
continue
|
|
142
|
+
if not (skill_dir / "SKILL.md").exists():
|
|
143
|
+
continue
|
|
144
|
+
rel = f".claude/skills/{skill_dir.name}"
|
|
145
|
+
if rel not in listed:
|
|
146
|
+
errors.append(
|
|
147
|
+
f"skill exists on disk but is not listed in marketplace.json: "
|
|
148
|
+
f"`./{rel}`"
|
|
149
|
+
)
|
|
150
|
+
|
|
124
151
|
if errors:
|
|
125
152
|
return fail(errors)
|
|
126
153
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Atomic-command linter for the command-collapse policy.
|
|
4
|
+
|
|
5
|
+
Reads the locked verb clusters from `docs/contracts/command-clusters.md`,
|
|
6
|
+
finds every command file under `.agent-src.uncompressed/commands/` that
|
|
7
|
+
was **added** since `--baseline` (default: `main`), and requires each
|
|
8
|
+
new file to declare either:
|
|
9
|
+
|
|
10
|
+
- `cluster: <locked-name>` (file is a cluster entry or sub-command), or
|
|
11
|
+
- `superseded_by: <slug>` (file is a deprecation shim).
|
|
12
|
+
|
|
13
|
+
Modifications to pre-existing files are NOT flagged — only additions.
|
|
14
|
+
This stops the atomic surface from growing without forcing every existing
|
|
15
|
+
command into a Phase 1 cluster (most aren't in Phase 1).
|
|
16
|
+
|
|
17
|
+
Exit codes: 0 = clean, 1 = violations found, 3 = internal error.
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
python3 scripts/lint_no_new_atomic_commands.py
|
|
21
|
+
python3 scripts/lint_no_new_atomic_commands.py --baseline origin/main
|
|
22
|
+
python3 scripts/lint_no_new_atomic_commands.py --all # ignore baseline
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import re
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
from dataclasses import dataclass
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
35
|
+
COMMANDS_DIR = Path(".agent-src.uncompressed/commands")
|
|
36
|
+
CLUSTER_CONTRACT = Path("docs/contracts/command-clusters.md")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass
|
|
40
|
+
class Violation:
|
|
41
|
+
file: str
|
|
42
|
+
reason: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_locked_clusters() -> set[str]:
|
|
46
|
+
"""Parse the Phase 1 cluster table from the locked contract."""
|
|
47
|
+
text = (ROOT / CLUSTER_CONTRACT).read_text(encoding="utf-8")
|
|
48
|
+
# Locate the Phase 1 table; cluster names sit in backticks in column 1.
|
|
49
|
+
in_phase_1 = False
|
|
50
|
+
clusters: set[str] = set()
|
|
51
|
+
for line in text.splitlines():
|
|
52
|
+
if line.startswith("## Phase 1 clusters"):
|
|
53
|
+
in_phase_1 = True
|
|
54
|
+
continue
|
|
55
|
+
if in_phase_1 and line.startswith("## "):
|
|
56
|
+
break
|
|
57
|
+
if in_phase_1:
|
|
58
|
+
m = re.match(r"\|\s*`([a-z][a-z0-9-]*)`\s*\|", line)
|
|
59
|
+
if m:
|
|
60
|
+
clusters.add(m.group(1))
|
|
61
|
+
if not clusters:
|
|
62
|
+
print(
|
|
63
|
+
f"❌ Could not parse Phase 1 cluster table from {CLUSTER_CONTRACT}",
|
|
64
|
+
file=sys.stderr,
|
|
65
|
+
)
|
|
66
|
+
sys.exit(3)
|
|
67
|
+
return clusters
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def added_command_files(baseline: str) -> list[Path]:
|
|
71
|
+
"""Files under commands/ added (status A) since baseline."""
|
|
72
|
+
try:
|
|
73
|
+
result = subprocess.run(
|
|
74
|
+
["git", "diff", "--name-only", "--diff-filter=A",
|
|
75
|
+
f"{baseline}...HEAD", "--", str(COMMANDS_DIR)],
|
|
76
|
+
capture_output=True, text=True, cwd=ROOT, timeout=15,
|
|
77
|
+
)
|
|
78
|
+
except (FileNotFoundError, subprocess.TimeoutExpired) as exc:
|
|
79
|
+
print(f"❌ git diff failed: {exc}", file=sys.stderr)
|
|
80
|
+
sys.exit(3)
|
|
81
|
+
if result.returncode != 0:
|
|
82
|
+
print(f"❌ git diff exit {result.returncode}: {result.stderr}",
|
|
83
|
+
file=sys.stderr)
|
|
84
|
+
sys.exit(3)
|
|
85
|
+
files = [Path(p) for p in result.stdout.splitlines()
|
|
86
|
+
if p.endswith(".md") and p != ""]
|
|
87
|
+
# Also include untracked (newly added, uncommitted) files.
|
|
88
|
+
try:
|
|
89
|
+
wt = subprocess.run(
|
|
90
|
+
["git", "status", "--porcelain", "--", str(COMMANDS_DIR)],
|
|
91
|
+
capture_output=True, text=True, cwd=ROOT, timeout=10,
|
|
92
|
+
)
|
|
93
|
+
for line in wt.stdout.splitlines():
|
|
94
|
+
if len(line) < 4:
|
|
95
|
+
continue
|
|
96
|
+
status = line[:2]
|
|
97
|
+
if status.strip() not in ("A", "??", "AM"):
|
|
98
|
+
continue
|
|
99
|
+
path = line[3:].strip().split(" -> ")[-1]
|
|
100
|
+
if path.endswith(".md"):
|
|
101
|
+
p = Path(path)
|
|
102
|
+
if p not in files:
|
|
103
|
+
files.append(p)
|
|
104
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
105
|
+
pass
|
|
106
|
+
return files
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def all_command_files() -> list[Path]:
|
|
110
|
+
return sorted((ROOT / COMMANDS_DIR).glob("*.md"))
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
114
|
+
text = path.read_text(encoding="utf-8")
|
|
115
|
+
if not text.startswith("---"):
|
|
116
|
+
return {}
|
|
117
|
+
end = text.find("\n---", 3)
|
|
118
|
+
if end == -1:
|
|
119
|
+
return {}
|
|
120
|
+
fm: dict[str, str] = {}
|
|
121
|
+
for line in text[3:end].splitlines():
|
|
122
|
+
if ":" in line:
|
|
123
|
+
k, _, v = line.partition(":")
|
|
124
|
+
fm[k.strip()] = v.strip()
|
|
125
|
+
return fm
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def check_file(path: Path, clusters: set[str]) -> Violation | None:
|
|
129
|
+
abs_path = path if path.is_absolute() else ROOT / path
|
|
130
|
+
if not abs_path.exists():
|
|
131
|
+
return None # deleted file, nothing to check
|
|
132
|
+
fm = parse_frontmatter(abs_path)
|
|
133
|
+
if "superseded_by" in fm:
|
|
134
|
+
return None # shim — exempt
|
|
135
|
+
cluster = fm.get("cluster")
|
|
136
|
+
if not cluster:
|
|
137
|
+
return Violation(str(path),
|
|
138
|
+
"missing `cluster:` frontmatter "
|
|
139
|
+
f"(allowed: {sorted(clusters)})")
|
|
140
|
+
if cluster not in clusters:
|
|
141
|
+
return Violation(str(path),
|
|
142
|
+
f"`cluster: {cluster}` is not a locked cluster "
|
|
143
|
+
f"(allowed: {sorted(clusters)})")
|
|
144
|
+
return None
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def main() -> int:
|
|
148
|
+
ap = argparse.ArgumentParser(description=__doc__)
|
|
149
|
+
ap.add_argument("--baseline", default="main",
|
|
150
|
+
help="git ref to diff against (default: main)")
|
|
151
|
+
ap.add_argument("--all", action="store_true",
|
|
152
|
+
help="check every command file, not just changed ones")
|
|
153
|
+
args = ap.parse_args()
|
|
154
|
+
|
|
155
|
+
clusters = load_locked_clusters()
|
|
156
|
+
targets = (all_command_files() if args.all
|
|
157
|
+
else added_command_files(args.baseline))
|
|
158
|
+
if not targets:
|
|
159
|
+
print(f"✅ No new commands added under {COMMANDS_DIR} "
|
|
160
|
+
f"(baseline: {args.baseline}).")
|
|
161
|
+
return 0
|
|
162
|
+
|
|
163
|
+
violations = [v for v in (check_file(p, clusters) for p in targets)
|
|
164
|
+
if v is not None]
|
|
165
|
+
if violations:
|
|
166
|
+
print(f"❌ {len(violations)} newly-added atomic command(s) violate "
|
|
167
|
+
f"the command-cluster policy:")
|
|
168
|
+
for v in violations:
|
|
169
|
+
print(f" • {v.file} — {v.reason}")
|
|
170
|
+
print(f"\nSee docs/contracts/command-clusters.md for the locked "
|
|
171
|
+
f"cluster names and frontmatter contract.")
|
|
172
|
+
return 1
|
|
173
|
+
print(f"✅ {len(targets)} newly-added command(s) all declare a valid "
|
|
174
|
+
f"`cluster:` (or `superseded_by:`).")
|
|
175
|
+
return 0
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
if __name__ == "__main__":
|
|
179
|
+
sys.exit(main())
|