@event4u/agent-config 1.16.0 → 1.18.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/{agents-audit.md → agents/audit.md} +4 -3
- package/.agent-src/commands/{agents-cleanup.md → agents/cleanup.md} +12 -6
- package/.agent-src/commands/{agents-prepare.md → agents/prepare.md} +4 -3
- package/.agent-src/commands/agents.md +46 -0
- package/.agent-src/commands/{chat-history-checkpoint.md → chat-history/checkpoint.md} +4 -4
- package/.agent-src/commands/{chat-history-clear.md → chat-history/clear.md} +4 -4
- package/.agent-src/commands/{chat-history-resume.md → chat-history/resume.md} +4 -4
- package/.agent-src/commands/chat-history/show.md +107 -0
- package/.agent-src/commands/chat-history.md +33 -89
- package/.agent-src/commands/{commit-in-chunks.md → commit/in-chunks.md} +15 -13
- package/.agent-src/commands/commit.md +22 -2
- package/.agent-src/commands/{context-create.md → context/create.md} +4 -3
- package/.agent-src/commands/{context-refactor.md → context/refactor.md} +4 -3
- package/.agent-src/commands/context.md +44 -0
- package/.agent-src/commands/{copilot-agents-init.md → copilot-agents/init.md} +4 -3
- package/.agent-src/commands/{copilot-agents-optimize.md → copilot-agents/optimize.md} +4 -3
- package/.agent-src/commands/copilot-agents.md +44 -0
- package/.agent-src/commands/council/default.md +221 -0
- package/.agent-src/commands/{council-design.md → council/design.md} +6 -5
- package/.agent-src/commands/{council-optimize.md → council/optimize.md} +7 -6
- package/.agent-src/commands/{council-pr.md → council/pr.md} +6 -5
- package/.agent-src/commands/council.md +47 -212
- package/.agent-src/commands/{create-pr-description.md → create-pr/description-only.md} +4 -2
- package/.agent-src/commands/create-pr.md +26 -5
- package/.agent-src/commands/{feature-dev.md → feature/dev.md} +5 -10
- package/.agent-src/commands/{feature-explore.md → feature/explore.md} +4 -8
- package/.agent-src/commands/{feature-plan.md → feature/plan.md} +4 -8
- package/.agent-src/commands/{feature-refactor.md → feature/refactor.md} +4 -8
- package/.agent-src/commands/{feature-roadmap.md → feature/roadmap.md} +6 -10
- package/.agent-src/commands/feature.md +6 -12
- package/.agent-src/commands/{fix-ci.md → fix/ci.md} +4 -8
- package/.agent-src/commands/{fix-portability.md → fix/portability.md} +4 -8
- package/.agent-src/commands/{fix-pr-bot-comments.md → fix/pr-bots.md} +4 -8
- package/.agent-src/commands/{fix-pr-developer-comments.md → fix/pr-developers.md} +4 -8
- package/.agent-src/commands/{fix-pr-comments.md → fix/pr.md} +7 -11
- package/.agent-src/commands/{fix-references.md → fix/refs.md} +4 -8
- package/.agent-src/commands/{fix-seeder.md → fix/seeder.md} +4 -8
- package/.agent-src/commands/fix.md +7 -13
- package/.agent-src/commands/{do-and-judge.md → judge/on-diff.md} +4 -3
- package/.agent-src/commands/judge/solo.md +90 -0
- package/.agent-src/commands/{do-in-steps.md → judge/steps.md} +4 -3
- package/.agent-src/commands/judge.md +35 -70
- package/.agent-src/commands/{memory-add.md → memory/add.md} +4 -3
- package/.agent-src/commands/{memory-full.md → memory/load.md} +4 -3
- package/.agent-src/commands/{memory-promote.md → memory/promote.md} +4 -3
- package/.agent-src/commands/{propose-memory.md → memory/propose.md} +4 -3
- package/.agent-src/commands/memory.md +48 -0
- package/.agent-src/commands/{module-create.md → module/create.md} +4 -3
- package/.agent-src/commands/{module-explore.md → module/explore.md} +4 -3
- package/.agent-src/commands/module.md +44 -0
- package/.agent-src/commands/{optimize-agents.md → optimize/agents.md} +4 -8
- package/.agent-src/commands/{optimize-augmentignore.md → optimize/augmentignore.md} +4 -9
- package/.agent-src/commands/{optimize-rtk-filters.md → optimize/rtk.md} +4 -8
- package/.agent-src/commands/{optimize-skills.md → optimize/skills.md} +4 -8
- package/.agent-src/commands/optimize.md +4 -10
- package/.agent-src/commands/{override-create.md → override/create.md} +4 -3
- package/.agent-src/commands/{override-manage.md → override/manage.md} +4 -3
- package/.agent-src/commands/override.md +44 -0
- package/.agent-src/commands/{roadmap-create.md → roadmap/create.md} +4 -3
- package/.agent-src/commands/{roadmap-execute.md → roadmap/execute.md} +4 -3
- package/.agent-src/commands/roadmap.md +44 -0
- package/.agent-src/commands/{tests-create.md → tests/create.md} +4 -3
- package/.agent-src/commands/{tests-execute.md → tests/execute.md} +4 -3
- package/.agent-src/commands/tests.md +44 -0
- package/.agent-src/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +72 -0
- package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +79 -0
- package/.agent-src/contexts/communication/rules-auto/augment-source-of-truth-mechanics.md +98 -0
- package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +87 -0
- package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +62 -0
- package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +78 -0
- package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +85 -0
- package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +65 -0
- package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +78 -0
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +62 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +55 -0
- package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +53 -0
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +77 -0
- package/.agent-src/contexts/judges/no-consolidate-rationale.md +102 -0
- package/.agent-src/contexts/judges/persona-voice-rubric.md +140 -0
- package/.agent-src/rules/artifact-engagement-recording.md +13 -69
- package/.agent-src/rules/ask-when-uncertain.md +27 -42
- package/.agent-src/rules/augment-portability.md +15 -61
- package/.agent-src/rules/augment-source-of-truth.md +27 -93
- package/.agent-src/rules/cli-output-handling.md +10 -76
- package/.agent-src/rules/command-suggestion-policy.md +18 -59
- package/.agent-src/rules/commit-conventions.md +17 -14
- package/.agent-src/rules/context-hygiene.md +6 -0
- package/.agent-src/rules/direct-answers.md +35 -59
- package/.agent-src/rules/docker-commands.md +5 -5
- package/.agent-src/rules/docs-sync.md +15 -69
- package/.agent-src/rules/language-and-tone.md +48 -72
- package/.agent-src/rules/missing-tool-handling.md +28 -22
- package/.agent-src/rules/no-cheap-questions.md +39 -53
- package/.agent-src/rules/no-roadmap-references.md +73 -0
- package/.agent-src/rules/onboarding-gate.md +7 -0
- package/.agent-src/rules/package-ci-checks.md +21 -61
- package/.agent-src/rules/preservation-guard.md +64 -29
- package/.agent-src/rules/review-routing-awareness.md +24 -43
- package/.agent-src/rules/roadmap-progress-sync.md +31 -65
- package/.agent-src/rules/rule-type-governance.md +28 -0
- package/.agent-src/rules/security-sensitive-stop.md +8 -8
- package/.agent-src/rules/skill-quality.md +16 -48
- package/.agent-src/rules/slash-command-routing-policy.md +7 -4
- package/.agent-src/rules/think-before-action.md +52 -42
- package/.agent-src/rules/tool-safety.md +19 -16
- package/.agent-src/rules/ui-audit-gate.md +24 -38
- package/.agent-src/rules/user-interaction.md +13 -68
- package/.agent-src/skills/ai-council/SKILL.md +2 -0
- package/.agent-src/skills/api-testing/SKILL.md +1 -1
- package/.agent-src/skills/check-refs/SKILL.md +59 -40
- package/.agent-src/skills/conventional-commits-writing/SKILL.md +86 -28
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +5 -5
- package/.agent-src/skills/developer-like-execution/SKILL.md +4 -4
- package/.agent-src/skills/finishing-a-development-branch/SKILL.md +101 -65
- package/.agent-src/skills/flux/SKILL.md +30 -10
- package/.agent-src/skills/github-ci/SKILL.md +2 -2
- package/.agent-src/skills/judge-code-quality/SKILL.md +7 -8
- package/.agent-src/skills/judge-security-auditor/SKILL.md +4 -5
- package/.agent-src/skills/judge-test-coverage/SKILL.md +3 -4
- package/.agent-src/skills/lint-skills/SKILL.md +57 -39
- package/.agent-src/skills/md-language-check/SKILL.md +61 -39
- package/.agent-src/skills/override-management/SKILL.md +5 -5
- package/.agent-src/skills/quality-tools/SKILL.md +2 -2
- package/.agent-src/skills/react-shadcn-ui/SKILL.md +116 -43
- package/.agent-src/skills/readme-reviewer/SKILL.md +30 -29
- package/.agent-src/skills/readme-writing/SKILL.md +78 -53
- package/.agent-src/skills/readme-writing-package/SKILL.md +50 -47
- package/.agent-src/skills/receiving-code-review/SKILL.md +52 -47
- package/.agent-src/skills/refine-prompt/SKILL.md +0 -1
- package/.agent-src/skills/requesting-code-review/SKILL.md +35 -30
- package/.agent-src/skills/security/SKILL.md +7 -2
- package/.agent-src/skills/security-audit/SKILL.md +7 -3
- package/.agent-src/skills/systematic-debugging/SKILL.md +68 -60
- package/.agent-src/skills/test-driven-development/SKILL.md +59 -57
- package/.agent-src/skills/test-performance/SKILL.md +0 -1
- package/.agent-src/skills/traefik/SKILL.md +4 -4
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +28 -26
- package/.agent-src/templates/roadmaps.md +4 -0
- package/.claude-plugin/marketplace.json +22 -11
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +125 -1
- package/README.md +18 -17
- package/docs/architecture.md +4 -6
- package/docs/catalog.md +67 -39
- package/docs/contracts/STABILITY.md +13 -7
- package/docs/contracts/adr-chat-history-split.md +1 -3
- package/docs/contracts/adr-command-suggestion.md +0 -2
- package/docs/contracts/adr-implement-ticket-runtime.md +1 -2
- package/docs/contracts/adr-product-ui-track.md +3 -6
- package/docs/contracts/adr-prompt-driven-execution.md +3 -4
- package/docs/contracts/agent-memory-contract.md +6 -11
- package/docs/contracts/artifact-engagement-flow.md +6 -9
- package/docs/contracts/command-clusters.md +56 -46
- package/docs/contracts/command-suggestion-flow.md +1 -3
- package/docs/contracts/context-paths.md +99 -0
- package/docs/contracts/file-ownership-matrix.json +6722 -0
- package/docs/contracts/file-ownership-matrix.md +134 -0
- package/docs/contracts/implement-ticket-flow.md +6 -9
- package/docs/contracts/linear-ai-rules-inclusion.md +0 -1
- package/docs/contracts/linear-ai-three-layers.md +0 -2
- package/docs/contracts/load-context-budget-model.md +258 -0
- package/docs/contracts/load-context-schema.md +21 -3
- package/docs/contracts/roadmap-complexity-standard.md +137 -0
- package/docs/contracts/rule-interactions.md +0 -1
- package/docs/contracts/rule-priority-hierarchy.md +1 -1
- package/docs/contracts/ui-track-flow.md +7 -17
- package/docs/customization.md +2 -0
- package/docs/getting-started.md +5 -4
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
- package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +100 -0
- package/docs/guidelines/agent-infra/direct-answers-demos.md +145 -0
- package/docs/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
- package/package.json +1 -1
- package/scripts/_phase2_shim_helper.py +109 -0
- package/scripts/agent-config +30 -0
- package/scripts/ai_council/one_off_archive/2026-05/README.md +45 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_2a4_acceptance.py +208 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_budget_v2_audit.py +206 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_context_layer_v1_estimate.py +67 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_context_layer_v1_review.py +292 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_followups_review.py +259 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_nondestructive_inline_audit.py +209 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase4_dispatch_latency.py +108 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase6_trigger_jaccard.py +92 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase_2a_budget_rebalance.py +257 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_phase_2a_post_revert.py +197 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_rule_hardening_v1.py +251 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_open_questions.py +232 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_optimization.py +144 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_v3_gaps.py +252 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_structural_v3_review.py +240 -0
- package/scripts/build_rule_trigger_matrix.py +360 -0
- package/scripts/check_always_budget.py +402 -45
- package/scripts/check_cluster_patterns.py +159 -0
- package/scripts/check_command_count_messaging.py +14 -7
- package/scripts/check_context_paths.py +201 -0
- package/scripts/check_no_roadmap_refs.py +155 -0
- package/scripts/check_one_off_location.py +81 -0
- package/scripts/check_phase_coupling.py +148 -0
- package/scripts/check_portability.py +2 -0
- package/scripts/check_references.py +35 -2
- package/scripts/check_safety_floor_untouched.py +125 -0
- package/scripts/command_suggester/loader.py +4 -1
- package/scripts/compress.py +64 -15
- package/scripts/context_hygiene_hook.py +173 -0
- package/scripts/generate_index.py +6 -2
- package/scripts/generate_ownership_matrix.py +323 -0
- package/scripts/hooks/augment-context-hygiene.sh +55 -0
- package/scripts/hooks/augment-onboarding-gate.sh +55 -0
- package/scripts/hooks/augment-roadmap-progress.sh +57 -0
- package/scripts/install.py +105 -45
- package/scripts/lint_examples.py +98 -0
- package/scripts/lint_no_new_atomic_commands.py +12 -11
- package/scripts/lint_roadmap_complexity.py +127 -0
- package/scripts/onboarding_gate_hook.py +137 -0
- package/scripts/requirements-evals.txt +1 -0
- package/scripts/roadmap_progress_hook.py +159 -0
- package/scripts/schemas/command.schema.json +4 -3
- package/scripts/schemas/rule.schema.json +5 -0
- package/scripts/skill_linter.py +1 -0
- package/scripts/sync_agent_settings.py +25 -2
- package/scripts/update_counts.py +7 -0
- /package/scripts/ai_council/{_one_off_rebalancing_audit.py → one_off_archive/2026-05/_one_off_rebalancing_audit.py} +0 -0
- /package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py} +0 -0
|
@@ -101,6 +101,14 @@ EXAMPLE_PATH_PATTERNS = [
|
|
|
101
101
|
re.compile(r"skills/[\w-]+/SKILL\.md"), # example skill paths in commands
|
|
102
102
|
re.compile(r"\{"), # template placeholders like {module}
|
|
103
103
|
re.compile(r"\.compression-hashes\.json"), # JSON file, not .md
|
|
104
|
+
# Forward references inside in-flight planning docs (road-to-
|
|
105
|
+
# structural-optimization.md and its companion spike protocols).
|
|
106
|
+
# Each pattern below is removed once the matching phase lands.
|
|
107
|
+
re.compile(r"structural-optimization-3a-spike\.md"), # 3a.0.2
|
|
108
|
+
re.compile(r"contexts/judges/no-consolidate-rationale"), # 3a.0.2 abort
|
|
109
|
+
re.compile(r"contexts/judges/judge-shared-procedure"), # 3a.1
|
|
110
|
+
re.compile(r"contexts/analysis/project-analysis-core-procedure"), # 3b.1
|
|
111
|
+
re.compile(r"agents/roadmaps/phase6-non-overlap-evidence"), # 6.1 conditional
|
|
104
112
|
]
|
|
105
113
|
|
|
106
114
|
|
|
@@ -118,8 +126,20 @@ def collect_artifacts(root: Path) -> dict[str, set[str]]:
|
|
|
118
126
|
arts["skills"].add(d.name)
|
|
119
127
|
for f in (augment / "rules").glob("*.md") if (augment / "rules").exists() else []:
|
|
120
128
|
arts["rules"].add(f.stem)
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
cmd_dir = augment / "commands"
|
|
130
|
+
if cmd_dir.exists():
|
|
131
|
+
for f in cmd_dir.rglob("*.md"):
|
|
132
|
+
if f.name == "AGENTS.md":
|
|
133
|
+
continue
|
|
134
|
+
# Top-level: bare stem ("commit"). Nested: cluster-sub ("council-default")
|
|
135
|
+
# AND the cluster:sub form, since references may use either.
|
|
136
|
+
rel = f.relative_to(cmd_dir).with_suffix("")
|
|
137
|
+
parts = rel.parts
|
|
138
|
+
if len(parts) == 1:
|
|
139
|
+
arts["commands"].add(parts[0])
|
|
140
|
+
else:
|
|
141
|
+
arts["commands"].add("-".join(parts))
|
|
142
|
+
arts["commands"].add(":".join(parts))
|
|
123
143
|
gdir = augment / "guidelines"
|
|
124
144
|
if gdir.exists():
|
|
125
145
|
for f in gdir.rglob("*.md"):
|
|
@@ -225,6 +245,13 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
|
|
|
225
245
|
if any(p.search(raw_ref) for p in EXAMPLE_PATH_PATTERNS):
|
|
226
246
|
continue
|
|
227
247
|
|
|
248
|
+
# Skip references into directories already excluded from scanning
|
|
249
|
+
# (gitignored audit trails, archived roadmaps). Files there are
|
|
250
|
+
# not committed, so existence checks would always fail in CI.
|
|
251
|
+
if any(raw_ref.startswith(skip + "/") or raw_ref == skip
|
|
252
|
+
for skip in SKIP_DIRS):
|
|
253
|
+
continue
|
|
254
|
+
|
|
228
255
|
resolved = False
|
|
229
256
|
# Try raw ref as-is from root (covers .agent-src/..., agents/..., etc.)
|
|
230
257
|
if (root / raw_ref).exists():
|
|
@@ -247,6 +274,12 @@ def check_file(filepath: Path, artifacts: dict[str, set[str]], root: Path) -> Li
|
|
|
247
274
|
if (prefix / rel).exists():
|
|
248
275
|
resolved = True
|
|
249
276
|
break
|
|
277
|
+
# `agents/state/*.json` are runtime hook state files —
|
|
278
|
+
# gitignored, written by hooks at session/turn time, never
|
|
279
|
+
# committed. Prose references to them are descriptive, not
|
|
280
|
+
# checkable file paths.
|
|
281
|
+
if not resolved and raw_ref.startswith("agents/state/"):
|
|
282
|
+
resolved = True
|
|
250
283
|
if not resolved:
|
|
251
284
|
broken.append(BrokenRef(
|
|
252
285
|
file=str(filepath), line=i, ref=m.group(1),
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Safety-floor exclusion linter (Phase 2A.0 of road-to-structural-optimization).
|
|
3
|
+
|
|
4
|
+
Per Q3=A locked decision (council Round 3, 2026-05-03), the four
|
|
5
|
+
safety-floor always-rules are out of scope for Phase 2A slimming:
|
|
6
|
+
|
|
7
|
+
- non-destructive-by-default
|
|
8
|
+
- commit-policy
|
|
9
|
+
- scope-control
|
|
10
|
+
- verify-before-complete
|
|
11
|
+
|
|
12
|
+
This linter compares HEAD against a baseline ref (default: ``main``)
|
|
13
|
+
and fails CI if any of those four rule files were modified by the
|
|
14
|
+
working branch.
|
|
15
|
+
|
|
16
|
+
Lift via the two-gate rollback documented in
|
|
17
|
+
``agents/roadmaps/road-to-structural-optimization.md`` § Phase 2A
|
|
18
|
+
Abort/rollback.
|
|
19
|
+
|
|
20
|
+
Exit codes: 0 = clean (or skipped — see ``--skip-if-no-baseline``),
|
|
21
|
+
1 = safety-floor file modified, 3 = internal error.
|
|
22
|
+
"""
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
import argparse
|
|
26
|
+
import subprocess
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
31
|
+
RULES_DIR_REL = ".agent-src.uncompressed/rules"
|
|
32
|
+
SAFETY_FLOOR = (
|
|
33
|
+
"non-destructive-by-default.md",
|
|
34
|
+
"commit-policy.md",
|
|
35
|
+
"scope-control.md",
|
|
36
|
+
"verify-before-complete.md",
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _run_git(args: list[str]) -> tuple[int, str]:
|
|
41
|
+
proc = subprocess.run(
|
|
42
|
+
["git", *args],
|
|
43
|
+
cwd=REPO_ROOT,
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
check=False,
|
|
47
|
+
)
|
|
48
|
+
return proc.returncode, (proc.stdout or "") + (proc.stderr or "")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _baseline_exists(ref: str) -> bool:
|
|
52
|
+
code, _ = _run_git(["rev-parse", "--verify", "--quiet", ref])
|
|
53
|
+
return code == 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _changed_files(baseline: str) -> list[str]:
|
|
57
|
+
code, output = _run_git(["diff", "--name-only", f"{baseline}...HEAD"])
|
|
58
|
+
if code != 0:
|
|
59
|
+
raise RuntimeError(f"git diff failed: {output}")
|
|
60
|
+
return [line.strip() for line in output.splitlines() if line.strip()]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def main() -> int:
|
|
64
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
65
|
+
parser.add_argument(
|
|
66
|
+
"--baseline",
|
|
67
|
+
default="origin/main",
|
|
68
|
+
help="Baseline ref (default: origin/main)",
|
|
69
|
+
)
|
|
70
|
+
parser.add_argument(
|
|
71
|
+
"--skip-if-no-baseline",
|
|
72
|
+
action="store_true",
|
|
73
|
+
help="Exit 0 silently if baseline ref does not exist (local dev)",
|
|
74
|
+
)
|
|
75
|
+
args = parser.parse_args()
|
|
76
|
+
|
|
77
|
+
if not _baseline_exists(args.baseline):
|
|
78
|
+
if args.skip_if_no_baseline:
|
|
79
|
+
print(f"ℹ️ baseline {args.baseline} not found — skipped")
|
|
80
|
+
return 0
|
|
81
|
+
# Fallback: try plain `main`
|
|
82
|
+
if _baseline_exists("main"):
|
|
83
|
+
args.baseline = "main"
|
|
84
|
+
else:
|
|
85
|
+
print(
|
|
86
|
+
f"❌ baseline {args.baseline} (and `main`) not found. "
|
|
87
|
+
"Pass --skip-if-no-baseline to silence in local dev.",
|
|
88
|
+
file=sys.stderr,
|
|
89
|
+
)
|
|
90
|
+
return 3
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
changed = _changed_files(args.baseline)
|
|
94
|
+
except RuntimeError as exc:
|
|
95
|
+
print(f"❌ {exc}", file=sys.stderr)
|
|
96
|
+
return 3
|
|
97
|
+
|
|
98
|
+
floor_paths = {f"{RULES_DIR_REL}/{name}" for name in SAFETY_FLOOR}
|
|
99
|
+
breaches = sorted(p for p in changed if p in floor_paths)
|
|
100
|
+
|
|
101
|
+
if breaches:
|
|
102
|
+
print(
|
|
103
|
+
"❌ Safety-floor rule(s) modified — Phase 2A is not allowed to "
|
|
104
|
+
"touch these (Q3=A locked decision):",
|
|
105
|
+
file=sys.stderr,
|
|
106
|
+
)
|
|
107
|
+
for path in breaches:
|
|
108
|
+
print(f" {path}", file=sys.stderr)
|
|
109
|
+
print(
|
|
110
|
+
"\n Lift via the two-gate rollback documented in "
|
|
111
|
+
"agents/roadmaps/road-to-structural-optimization.md "
|
|
112
|
+
"§ Phase 2A Abort/rollback.",
|
|
113
|
+
file=sys.stderr,
|
|
114
|
+
)
|
|
115
|
+
return 1
|
|
116
|
+
|
|
117
|
+
print(
|
|
118
|
+
f"✅ Safety-floor untouched ({len(SAFETY_FLOOR)} rules guarded "
|
|
119
|
+
f"vs. {args.baseline})."
|
|
120
|
+
)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == "__main__":
|
|
125
|
+
sys.exit(main())
|
|
@@ -26,7 +26,10 @@ def load_commands(commands_dir: Path) -> list[CommandSpec]:
|
|
|
26
26
|
this loader.
|
|
27
27
|
"""
|
|
28
28
|
specs: list[CommandSpec] = []
|
|
29
|
-
for path in sorted(commands_dir.
|
|
29
|
+
for path in sorted(commands_dir.rglob("*.md")):
|
|
30
|
+
# Skip cluster authoring docs — not commands.
|
|
31
|
+
if path.name == "AGENTS.md":
|
|
32
|
+
continue
|
|
30
33
|
text = path.read_text(encoding="utf-8")
|
|
31
34
|
data, _offset = parse_frontmatter(text)
|
|
32
35
|
if data is None:
|
package/scripts/compress.py
CHANGED
|
@@ -312,6 +312,29 @@ def generate_gemini_md() -> None:
|
|
|
312
312
|
print(" ✅ Created GEMINI.md → AGENTS.md symlink")
|
|
313
313
|
|
|
314
314
|
|
|
315
|
+
def _command_slug(source_file: Path) -> str:
|
|
316
|
+
"""Return the flat .claude/skills/ slug for a command source file.
|
|
317
|
+
|
|
318
|
+
Top-level commands keep their stem (`commit.md` → `commit`). Nested
|
|
319
|
+
commands flatten the relative path with `-` (`council/default.md` →
|
|
320
|
+
`council-default`). Keeps slug collisions out of `.claude/skills/`
|
|
321
|
+
while preserving native nested invocation in `.agent-src/commands/`.
|
|
322
|
+
"""
|
|
323
|
+
rel = source_file.relative_to(COMMANDS_SOURCE)
|
|
324
|
+
return "-".join(rel.with_suffix("").parts)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _iter_commands():
|
|
328
|
+
"""Yield (source_file, slug) for every command .md file (recursive)."""
|
|
329
|
+
if not COMMANDS_SOURCE.exists():
|
|
330
|
+
return
|
|
331
|
+
for source_file in sorted(COMMANDS_SOURCE.rglob("*.md")):
|
|
332
|
+
# Skip the cluster AGENTS.md authoring doc (not a command).
|
|
333
|
+
if source_file.name == "AGENTS.md":
|
|
334
|
+
continue
|
|
335
|
+
yield source_file, _command_slug(source_file)
|
|
336
|
+
|
|
337
|
+
|
|
315
338
|
def generate_claude_skills() -> None:
|
|
316
339
|
"""Create .claude/skills/ symlinks for ALL skills in .agent-src/skills/.
|
|
317
340
|
"""
|
|
@@ -321,16 +344,14 @@ def generate_claude_skills() -> None:
|
|
|
321
344
|
|
|
322
345
|
# All skill directories in .agent-src/skills/
|
|
323
346
|
skills = sorted([d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()])
|
|
324
|
-
# All command
|
|
325
|
-
|
|
326
|
-
if COMMANDS_SOURCE.exists():
|
|
327
|
-
command_names = {f.stem for f in COMMANDS_SOURCE.glob("*.md")}
|
|
347
|
+
# All command slugs (to protect from stale cleanup)
|
|
348
|
+
command_slugs = {slug for _, slug in _iter_commands()}
|
|
328
349
|
|
|
329
350
|
CLAUDE_SKILLS_DIR.mkdir(parents=True, exist_ok=True)
|
|
330
351
|
|
|
331
352
|
# Clean stale symlinks (but not converted commands or README)
|
|
332
353
|
for item in CLAUDE_SKILLS_DIR.iterdir():
|
|
333
|
-
if item.is_symlink() and item.name not in skills and item.name not in
|
|
354
|
+
if item.is_symlink() and item.name not in skills and item.name not in command_slugs and item.name != "README.md":
|
|
334
355
|
item.unlink()
|
|
335
356
|
|
|
336
357
|
count = 0
|
|
@@ -357,11 +378,15 @@ def extract_description_from_md(content: str) -> str:
|
|
|
357
378
|
|
|
358
379
|
|
|
359
380
|
def generate_claude_commands() -> None:
|
|
360
|
-
"""Create .claude/skills/{
|
|
381
|
+
"""Create .claude/skills/{slug}/SKILL.md symlinks for ALL Augment commands.
|
|
361
382
|
|
|
362
383
|
Commands in .agent-src/commands/ are the single source of truth.
|
|
363
384
|
They must include name: and disable-model-invocation: true in frontmatter
|
|
364
385
|
(added once, then maintained as part of the command file).
|
|
386
|
+
|
|
387
|
+
Top-level commands use their filename stem as the slug. Nested
|
|
388
|
+
cluster commands (e.g. `commands/council/default.md`) are flattened
|
|
389
|
+
to `council-default` so directories never collide in `.claude/skills/`.
|
|
365
390
|
"""
|
|
366
391
|
if not COMMANDS_SOURCE.exists():
|
|
367
392
|
print(" ⚠️ .agent-src/commands/ not found — skipping commands")
|
|
@@ -374,32 +399,53 @@ def generate_claude_commands() -> None:
|
|
|
374
399
|
if SKILLS_SOURCE.exists():
|
|
375
400
|
skill_names = {d.name for d in SKILLS_SOURCE.iterdir() if d.is_dir()}
|
|
376
401
|
|
|
402
|
+
# Track current command slugs for stale-directory cleanup
|
|
403
|
+
current_slugs: set[str] = set()
|
|
377
404
|
count = 0
|
|
378
405
|
skipped = 0
|
|
379
|
-
for source_file in
|
|
380
|
-
name = source_file.stem
|
|
381
|
-
|
|
406
|
+
for source_file, slug in _iter_commands():
|
|
382
407
|
# Skip if a real skill with the same name exists — skill takes priority
|
|
383
|
-
if
|
|
408
|
+
if slug in skill_names:
|
|
384
409
|
skipped += 1
|
|
385
410
|
continue
|
|
386
411
|
|
|
412
|
+
current_slugs.add(slug)
|
|
413
|
+
|
|
387
414
|
# Create skill directory (real dir, symlinked SKILL.md inside)
|
|
388
|
-
skill_dir = CLAUDE_SKILLS_DIR /
|
|
415
|
+
skill_dir = CLAUDE_SKILLS_DIR / slug
|
|
389
416
|
skill_dir.mkdir(parents=True, exist_ok=True)
|
|
390
417
|
|
|
391
418
|
skill_file = skill_dir / "SKILL.md"
|
|
392
419
|
if skill_file.exists() or skill_file.is_symlink():
|
|
393
420
|
skill_file.unlink()
|
|
394
421
|
|
|
395
|
-
# Symlink: .claude/skills/{
|
|
396
|
-
|
|
422
|
+
# Symlink: .claude/skills/{slug}/SKILL.md → ../../../.agent-src/commands/<rel-path>
|
|
423
|
+
rel_path = source_file.relative_to(COMMANDS_SOURCE)
|
|
424
|
+
rel_target = Path("../../../.agent-src/commands") / rel_path
|
|
397
425
|
skill_file.symlink_to(rel_target)
|
|
398
426
|
count += 1
|
|
399
427
|
|
|
428
|
+
# Clean stale command skill directories — real dirs from removed commands.
|
|
429
|
+
# Only delete if the directory contains exactly the SKILL.md symlink we created.
|
|
430
|
+
removed_dirs = 0
|
|
431
|
+
for item in CLAUDE_SKILLS_DIR.iterdir():
|
|
432
|
+
if not item.is_dir() or item.is_symlink():
|
|
433
|
+
continue
|
|
434
|
+
if item.name in skill_names or item.name in current_slugs:
|
|
435
|
+
continue
|
|
436
|
+
skill_md = item / "SKILL.md"
|
|
437
|
+
if skill_md.is_symlink():
|
|
438
|
+
entries = list(item.iterdir())
|
|
439
|
+
if len(entries) == 1 and entries[0].name == "SKILL.md":
|
|
440
|
+
skill_md.unlink()
|
|
441
|
+
item.rmdir()
|
|
442
|
+
removed_dirs += 1
|
|
443
|
+
|
|
400
444
|
msg = f" ✅ Created {count} command symlinks in .claude/skills/"
|
|
401
445
|
if skipped:
|
|
402
446
|
msg += f" ({skipped} skipped — same-name skill exists)"
|
|
447
|
+
if removed_dirs:
|
|
448
|
+
msg += f" ({removed_dirs} stale dirs removed)"
|
|
403
449
|
print(msg)
|
|
404
450
|
|
|
405
451
|
|
|
@@ -515,8 +561,11 @@ def project_to_augment() -> None:
|
|
|
515
561
|
dst.symlink_to(Path("..") / ".agent-src" / name)
|
|
516
562
|
print(f" ✅ Symlinked .augment/{name} → ../.agent-src/{name}")
|
|
517
563
|
|
|
518
|
-
# Cleanup: remove any stray top-level entries in .augment/ that are no longer projected
|
|
519
|
-
|
|
564
|
+
# Cleanup: remove any stray top-level entries in .augment/ that are no longer projected.
|
|
565
|
+
# `state` holds runtime state files written by hooks (onboarding-gate,
|
|
566
|
+
# context-hygiene, …) and must survive sync — it is regenerated by
|
|
567
|
+
# the next hook fire, not by compress.
|
|
568
|
+
known = set(AUGMENT_SYMLINK_DIRS) | set(AUGMENT_SYMLINK_FILES) | {"rules", "state"}
|
|
520
569
|
for item in AUGMENT_DIR.iterdir():
|
|
521
570
|
if item.name in known:
|
|
522
571
|
continue
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Platform-agnostic PostToolUse hook for the `context-hygiene` rule.
|
|
3
|
+
|
|
4
|
+
Maintains a deterministic state file the rule body cites for the
|
|
5
|
+
freshness threshold, the 3-failure stop, and tool-loop detection. The
|
|
6
|
+
agent's job shrinks from "remember three counters" to "read this file
|
|
7
|
+
before responding".
|
|
8
|
+
|
|
9
|
+
Output: `agents/state/context-hygiene.json`
|
|
10
|
+
{
|
|
11
|
+
"tool_calls": <int>, // running PostToolUse count
|
|
12
|
+
"consecutive_same_tool": <int>, // includes the latest call
|
|
13
|
+
"last_tool": "<name>",
|
|
14
|
+
"tool_history": [..., last 5 names],
|
|
15
|
+
"loop_detected": <bool>, // ≥ 3 same tool in a row
|
|
16
|
+
"freshness_threshold": <int|null>, // 20/40/60 milestone hit
|
|
17
|
+
"checked_at": "<iso8601>"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
Exit code is always 0.
|
|
21
|
+
|
|
22
|
+
CLI:
|
|
23
|
+
python3 scripts/context_hygiene_hook.py [--platform NAME] [--verbose]
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import datetime as _dt
|
|
29
|
+
import json
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
STATE_DIR = Path("agents") / "state"
|
|
34
|
+
STATE_FILE = STATE_DIR / "context-hygiene.json"
|
|
35
|
+
|
|
36
|
+
LOOP_THRESHOLD = 3 # 3+ consecutive same-tool calls
|
|
37
|
+
HISTORY_DEPTH = 5
|
|
38
|
+
FRESHNESS_MILESTONES = (20, 40, 60)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _load_state(target: Path) -> dict:
|
|
42
|
+
if not target.is_file():
|
|
43
|
+
return {
|
|
44
|
+
"tool_calls": 0,
|
|
45
|
+
"consecutive_same_tool": 0,
|
|
46
|
+
"last_tool": None,
|
|
47
|
+
"tool_history": [],
|
|
48
|
+
"loop_detected": False,
|
|
49
|
+
"freshness_threshold": None,
|
|
50
|
+
}
|
|
51
|
+
try:
|
|
52
|
+
decoded = json.loads(target.read_text(encoding="utf-8"))
|
|
53
|
+
if isinstance(decoded, dict):
|
|
54
|
+
return decoded
|
|
55
|
+
except (OSError, json.JSONDecodeError):
|
|
56
|
+
pass
|
|
57
|
+
# Corrupt — start fresh, never block.
|
|
58
|
+
return {
|
|
59
|
+
"tool_calls": 0,
|
|
60
|
+
"consecutive_same_tool": 0,
|
|
61
|
+
"last_tool": None,
|
|
62
|
+
"tool_history": [],
|
|
63
|
+
"loop_detected": False,
|
|
64
|
+
"freshness_threshold": None,
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _extract_tool(payload: dict) -> str | None:
|
|
69
|
+
for key in ("tool_name", "toolName", "tool"):
|
|
70
|
+
v = payload.get(key)
|
|
71
|
+
if isinstance(v, str) and v:
|
|
72
|
+
return v
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _milestone_hit(prev: int, curr: int) -> int | None:
|
|
77
|
+
"""Return the milestone crossed by going from `prev` to `curr`, else None."""
|
|
78
|
+
for ms in FRESHNESS_MILESTONES:
|
|
79
|
+
if prev < ms <= curr:
|
|
80
|
+
return ms
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _update(state: dict, tool: str | None) -> dict:
|
|
85
|
+
if tool is None:
|
|
86
|
+
# Non-tool event (e.g. malformed payload) — still mark we ran.
|
|
87
|
+
state["checked_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat(
|
|
88
|
+
timespec="seconds")
|
|
89
|
+
return state
|
|
90
|
+
|
|
91
|
+
prev_count = int(state.get("tool_calls") or 0)
|
|
92
|
+
curr_count = prev_count + 1
|
|
93
|
+
state["tool_calls"] = curr_count
|
|
94
|
+
|
|
95
|
+
last = state.get("last_tool")
|
|
96
|
+
if last == tool:
|
|
97
|
+
state["consecutive_same_tool"] = int(
|
|
98
|
+
state.get("consecutive_same_tool") or 0) + 1
|
|
99
|
+
else:
|
|
100
|
+
state["consecutive_same_tool"] = 1
|
|
101
|
+
state["last_tool"] = tool
|
|
102
|
+
|
|
103
|
+
hist = state.get("tool_history") or []
|
|
104
|
+
if not isinstance(hist, list):
|
|
105
|
+
hist = []
|
|
106
|
+
hist.append(tool)
|
|
107
|
+
state["tool_history"] = hist[-HISTORY_DEPTH:]
|
|
108
|
+
|
|
109
|
+
state["loop_detected"] = (
|
|
110
|
+
state["consecutive_same_tool"] >= LOOP_THRESHOLD)
|
|
111
|
+
|
|
112
|
+
ms = _milestone_hit(prev_count, curr_count)
|
|
113
|
+
if ms is not None:
|
|
114
|
+
state["freshness_threshold"] = ms
|
|
115
|
+
state["checked_at"] = _dt.datetime.now(_dt.timezone.utc).isoformat(
|
|
116
|
+
timespec="seconds")
|
|
117
|
+
return state
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def _write_state(consumer_root: Path, state: dict) -> None:
|
|
121
|
+
state_dir = consumer_root / STATE_DIR
|
|
122
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
target = consumer_root / STATE_FILE
|
|
124
|
+
tmp = target.with_suffix(".json.tmp")
|
|
125
|
+
tmp.write_text(json.dumps(state, indent=2) + "\n", encoding="utf-8")
|
|
126
|
+
tmp.replace(target)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
|
|
130
|
+
payload: dict = {}
|
|
131
|
+
if stdin_text.strip():
|
|
132
|
+
try:
|
|
133
|
+
decoded = json.loads(stdin_text)
|
|
134
|
+
if isinstance(decoded, dict):
|
|
135
|
+
payload = decoded
|
|
136
|
+
except json.JSONDecodeError:
|
|
137
|
+
pass # silent no-op, never block
|
|
138
|
+
|
|
139
|
+
target = consumer_root / STATE_FILE
|
|
140
|
+
state = _load_state(target)
|
|
141
|
+
state = _update(state, _extract_tool(payload))
|
|
142
|
+
|
|
143
|
+
try:
|
|
144
|
+
_write_state(consumer_root, state)
|
|
145
|
+
except OSError:
|
|
146
|
+
if verbose:
|
|
147
|
+
print("context-hygiene-hook: state write failed",
|
|
148
|
+
file=sys.stderr)
|
|
149
|
+
return 0
|
|
150
|
+
|
|
151
|
+
if verbose:
|
|
152
|
+
print(
|
|
153
|
+
f"context-hygiene-hook: tool_calls={state.get('tool_calls')} "
|
|
154
|
+
f"loop={state.get('loop_detected')} "
|
|
155
|
+
f"threshold={state.get('freshness_threshold')}",
|
|
156
|
+
file=sys.stderr,
|
|
157
|
+
)
|
|
158
|
+
return 0
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def main(argv: list[str] | None = None) -> int:
|
|
162
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
163
|
+
parser.add_argument("--platform", default="generic",
|
|
164
|
+
help="informational platform tag")
|
|
165
|
+
parser.add_argument("--verbose", action="store_true",
|
|
166
|
+
help="emit one stderr line per invocation")
|
|
167
|
+
args = parser.parse_args(argv)
|
|
168
|
+
return run(sys.stdin.read(), consumer_root=Path.cwd(),
|
|
169
|
+
verbose=args.verbose)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__": # pragma: no cover
|
|
173
|
+
sys.exit(main())
|
|
@@ -95,7 +95,10 @@ def _collect_rules() -> list[Entry]:
|
|
|
95
95
|
|
|
96
96
|
def _collect_commands() -> list[Entry]:
|
|
97
97
|
out = []
|
|
98
|
-
|
|
98
|
+
cmd_dir = SRC / "commands"
|
|
99
|
+
for cmd_md in sorted(cmd_dir.rglob("*.md")):
|
|
100
|
+
if cmd_md.name == "AGENTS.md":
|
|
101
|
+
continue
|
|
99
102
|
fm = _parse_frontmatter(cmd_md.read_text(encoding="utf-8"))
|
|
100
103
|
is_shim = bool(fm.get("superseded_by"))
|
|
101
104
|
extra = ""
|
|
@@ -103,12 +106,13 @@ def _collect_commands() -> list[Entry]:
|
|
|
103
106
|
extra = f"shim → /{fm['superseded_by']}"
|
|
104
107
|
elif fm.get("cluster"):
|
|
105
108
|
extra = f"cluster: {fm['cluster']}"
|
|
109
|
+
rel = cmd_md.relative_to(cmd_dir)
|
|
106
110
|
out.append(Entry(
|
|
107
111
|
kind="shim" if is_shim else "command",
|
|
108
112
|
name=fm.get("name") or cmd_md.stem,
|
|
109
113
|
description=_truncate(fm.get("description", "")),
|
|
110
114
|
extra=extra,
|
|
111
|
-
path=f".agent-src.uncompressed/commands/{
|
|
115
|
+
path=f".agent-src.uncompressed/commands/{rel}",
|
|
112
116
|
))
|
|
113
117
|
return out
|
|
114
118
|
|