@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
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 3.4 demo-shape linter — wrong / right / why per demo.
|
|
3
|
+
|
|
4
|
+
Cap: ≤ 100 LOC, stdlib only. Hooked into `task ci` via
|
|
5
|
+
`Taskfile.yml` ▸ `check-examples-shape`. Validates every
|
|
6
|
+
`docs/guidelines/agent-infra/*-demos.md`: frontmatter keys
|
|
7
|
+
(`demo_for:`, `layer: pattern-memory`, `prose_delta:` with before /
|
|
8
|
+
after char counts), and each `## Demo N` section having Wrong /
|
|
9
|
+
Right shape headings, a `**Failure mode:**` line, and a Why-it-works
|
|
10
|
+
explanation (heading or inline).
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
19
|
+
DEMO_GLOB = "docs/guidelines/agent-infra/*-demos.md"
|
|
20
|
+
REQUIRED_FM_KEYS = ("demo_for:", "layer: pattern-memory", "prose_delta:")
|
|
21
|
+
REQUIRED_FM_DELTA = ("rule_chars_before:", "rule_chars_after:")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _frontmatter(text: str) -> str:
|
|
25
|
+
if not text.startswith("---\n"):
|
|
26
|
+
return ""
|
|
27
|
+
end = text.find("\n---\n", 4)
|
|
28
|
+
return text[4:end] if end != -1 else ""
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _check_frontmatter(fm: str, problems: list[str]) -> None:
|
|
32
|
+
for key in (*REQUIRED_FM_KEYS, *REQUIRED_FM_DELTA):
|
|
33
|
+
if key not in fm:
|
|
34
|
+
problems.append(f"frontmatter missing: {key!r}")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _check_demo_sections(text: str, problems: list[str]) -> None:
|
|
38
|
+
demo_pat = re.compile(r"^## Demo \d+\b.*$", re.MULTILINE)
|
|
39
|
+
demo_starts = [m.start() for m in demo_pat.finditer(text)]
|
|
40
|
+
if not demo_starts:
|
|
41
|
+
problems.append("no '## Demo N — …' sections found")
|
|
42
|
+
return
|
|
43
|
+
bounds = demo_starts + [len(text)]
|
|
44
|
+
for i, start in enumerate(demo_starts):
|
|
45
|
+
section = text[start:bounds[i + 1]]
|
|
46
|
+
title = section.splitlines()[0]
|
|
47
|
+
if "### Wrong shape" not in section:
|
|
48
|
+
problems.append(f"{title!r}: missing '### Wrong shape'")
|
|
49
|
+
if "### Right shape" not in section:
|
|
50
|
+
problems.append(f"{title!r}: missing '### Right shape'")
|
|
51
|
+
if "**Failure mode:**" not in section:
|
|
52
|
+
problems.append(f"{title!r}: missing '**Failure mode:**' line")
|
|
53
|
+
has_why_section = "### Why it works" in section
|
|
54
|
+
has_why_inline = "**Why it works:**" in section
|
|
55
|
+
if not (has_why_section or has_why_inline):
|
|
56
|
+
problems.append(
|
|
57
|
+
f"{title!r}: missing 'Why it works' explanation "
|
|
58
|
+
"(### Why it works or **Why it works:** inline)"
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def lint_demo(path: Path) -> list[str]:
|
|
63
|
+
text = path.read_text(encoding="utf-8")
|
|
64
|
+
problems: list[str] = []
|
|
65
|
+
fm = _frontmatter(text)
|
|
66
|
+
if not fm:
|
|
67
|
+
problems.append("missing YAML frontmatter (--- block at top)")
|
|
68
|
+
else:
|
|
69
|
+
_check_frontmatter(fm, problems)
|
|
70
|
+
_check_demo_sections(text, problems)
|
|
71
|
+
return problems
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def main() -> int:
|
|
75
|
+
demos = sorted(REPO_ROOT.glob(DEMO_GLOB))
|
|
76
|
+
if not demos:
|
|
77
|
+
print(f"❌ no demo files matched {DEMO_GLOB}", file=sys.stderr)
|
|
78
|
+
return 1
|
|
79
|
+
failed = 0
|
|
80
|
+
for demo in demos:
|
|
81
|
+
rel = demo.relative_to(REPO_ROOT)
|
|
82
|
+
problems = lint_demo(demo)
|
|
83
|
+
if problems:
|
|
84
|
+
failed += 1
|
|
85
|
+
print(f"❌ {rel}", file=sys.stderr)
|
|
86
|
+
for p in problems:
|
|
87
|
+
print(f" - {p}", file=sys.stderr)
|
|
88
|
+
else:
|
|
89
|
+
print(f"✅ {rel}")
|
|
90
|
+
if failed:
|
|
91
|
+
print(f"\n❌ {failed} demo file(s) failed shape lint", file=sys.stderr)
|
|
92
|
+
return 1
|
|
93
|
+
print(f"\n✅ {len(demos)} demo file(s) shape-clean")
|
|
94
|
+
return 0
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
if __name__ == "__main__":
|
|
98
|
+
sys.exit(main())
|
|
@@ -43,24 +43,24 @@ class Violation:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
def load_locked_clusters() -> set[str]:
|
|
46
|
-
"""Parse the
|
|
46
|
+
"""Parse the locked cluster table from the contract."""
|
|
47
47
|
text = (ROOT / CLUSTER_CONTRACT).read_text(encoding="utf-8")
|
|
48
|
-
# Locate the
|
|
49
|
-
|
|
48
|
+
# Locate the locked-clusters table; cluster names sit in backticks in column 1.
|
|
49
|
+
in_table = False
|
|
50
50
|
clusters: set[str] = set()
|
|
51
51
|
for line in text.splitlines():
|
|
52
|
-
if line.startswith("##
|
|
53
|
-
|
|
52
|
+
if line.startswith("## Locked clusters"):
|
|
53
|
+
in_table = True
|
|
54
54
|
continue
|
|
55
|
-
if
|
|
55
|
+
if in_table and line.startswith("## "):
|
|
56
56
|
break
|
|
57
|
-
if
|
|
57
|
+
if in_table:
|
|
58
58
|
m = re.match(r"\|\s*`([a-z][a-z0-9-]*)`\s*\|", line)
|
|
59
59
|
if m:
|
|
60
60
|
clusters.add(m.group(1))
|
|
61
61
|
if not clusters:
|
|
62
62
|
print(
|
|
63
|
-
f"❌ Could not parse
|
|
63
|
+
f"❌ Could not parse locked-clusters table from {CLUSTER_CONTRACT}",
|
|
64
64
|
file=sys.stderr,
|
|
65
65
|
)
|
|
66
66
|
sys.exit(3)
|
|
@@ -83,7 +83,7 @@ def added_command_files(baseline: str) -> list[Path]:
|
|
|
83
83
|
file=sys.stderr)
|
|
84
84
|
sys.exit(3)
|
|
85
85
|
files = [Path(p) for p in result.stdout.splitlines()
|
|
86
|
-
if p.endswith(".md") and p != ""]
|
|
86
|
+
if p.endswith(".md") and p != "" and Path(p).name != "AGENTS.md"]
|
|
87
87
|
# Also include untracked (newly added, uncommitted) files.
|
|
88
88
|
try:
|
|
89
89
|
wt = subprocess.run(
|
|
@@ -97,7 +97,7 @@ def added_command_files(baseline: str) -> list[Path]:
|
|
|
97
97
|
if status.strip() not in ("A", "??", "AM"):
|
|
98
98
|
continue
|
|
99
99
|
path = line[3:].strip().split(" -> ")[-1]
|
|
100
|
-
if path.endswith(".md"):
|
|
100
|
+
if path.endswith(".md") and Path(path).name != "AGENTS.md":
|
|
101
101
|
p = Path(path)
|
|
102
102
|
if p not in files:
|
|
103
103
|
files.append(p)
|
|
@@ -107,7 +107,8 @@ def added_command_files(baseline: str) -> list[Path]:
|
|
|
107
107
|
|
|
108
108
|
|
|
109
109
|
def all_command_files() -> list[Path]:
|
|
110
|
-
return sorted((ROOT / COMMANDS_DIR).
|
|
110
|
+
return sorted(p for p in (ROOT / COMMANDS_DIR).rglob("*.md")
|
|
111
|
+
if p.name != "AGENTS.md")
|
|
111
112
|
|
|
112
113
|
|
|
113
114
|
def parse_frontmatter(path: Path) -> dict[str, str]:
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 5.2 roadmap-complexity linter.
|
|
3
|
+
|
|
4
|
+
Enforces the measurable subset of
|
|
5
|
+
`docs/contracts/roadmap-complexity-standard.md`:
|
|
6
|
+
|
|
7
|
+
- every `agents/roadmaps/*.md` declares `complexity: lightweight`
|
|
8
|
+
or `complexity: structural` in frontmatter;
|
|
9
|
+
- lightweight roadmaps have ≤ 600 total lines and ≤ 6 `## Phase N`
|
|
10
|
+
headings, and contain no `## Council Round N` / `### Verdict`
|
|
11
|
+
sections;
|
|
12
|
+
- structural roadmaps have no upper cap, but the tag must be
|
|
13
|
+
declared.
|
|
14
|
+
|
|
15
|
+
Cap: ≤ 150 LOC, stdlib only. Hooked into `task ci` via
|
|
16
|
+
`task lint-roadmap-complexity`.
|
|
17
|
+
"""
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import re
|
|
21
|
+
import sys
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
ROADMAP_GLOB = "agents/roadmaps/*.md"
|
|
26
|
+
LIGHTWEIGHT_LINE_CAP = 600
|
|
27
|
+
LIGHTWEIGHT_PHASE_CAP = 6
|
|
28
|
+
|
|
29
|
+
PHASE_PAT = re.compile(r"^## Phase \d+\b", re.MULTILINE)
|
|
30
|
+
COUNCIL_PAT = re.compile(r"^## Council Round \d+\b", re.MULTILINE)
|
|
31
|
+
VERDICT_PAT = re.compile(r"^### Verdict\b", re.MULTILINE)
|
|
32
|
+
COMPLEXITY_PAT = re.compile(
|
|
33
|
+
r"^complexity:\s*(lightweight|structural)\s*$", re.MULTILINE
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _frontmatter(text: str) -> str:
|
|
38
|
+
if not text.startswith("---\n"):
|
|
39
|
+
return ""
|
|
40
|
+
end = text.find("\n---\n", 4)
|
|
41
|
+
return text[4:end] if end != -1 else ""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _read_complexity(fm: str) -> str | None:
|
|
45
|
+
m = COMPLEXITY_PAT.search(fm)
|
|
46
|
+
return m.group(1) if m else None
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _check_lightweight(text: str, line_count: int, problems: list[str]) -> None:
|
|
50
|
+
if line_count > LIGHTWEIGHT_LINE_CAP:
|
|
51
|
+
problems.append(
|
|
52
|
+
f"lightweight cap exceeded: {line_count} lines "
|
|
53
|
+
f"(max {LIGHTWEIGHT_LINE_CAP}); consider tagging structural "
|
|
54
|
+
f"or trimming"
|
|
55
|
+
)
|
|
56
|
+
phases = len(PHASE_PAT.findall(text))
|
|
57
|
+
if phases > LIGHTWEIGHT_PHASE_CAP:
|
|
58
|
+
problems.append(
|
|
59
|
+
f"lightweight phase cap exceeded: {phases} phases "
|
|
60
|
+
f"(max {LIGHTWEIGHT_PHASE_CAP})"
|
|
61
|
+
)
|
|
62
|
+
if COUNCIL_PAT.search(text):
|
|
63
|
+
problems.append(
|
|
64
|
+
"lightweight roadmap contains '## Council Round N' "
|
|
65
|
+
"block — council debates belong in structural roadmaps"
|
|
66
|
+
)
|
|
67
|
+
if VERDICT_PAT.search(text):
|
|
68
|
+
problems.append(
|
|
69
|
+
"lightweight roadmap contains '### Verdict' block — "
|
|
70
|
+
"council verdicts belong in structural roadmaps"
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def lint_roadmap(path: Path) -> list[str]:
|
|
75
|
+
text = path.read_text(encoding="utf-8")
|
|
76
|
+
line_count = text.count("\n") + (1 if text and not text.endswith("\n") else 0)
|
|
77
|
+
problems: list[str] = []
|
|
78
|
+
fm = _frontmatter(text)
|
|
79
|
+
complexity = _read_complexity(fm) if fm else None
|
|
80
|
+
if complexity is None:
|
|
81
|
+
problems.append(
|
|
82
|
+
"missing 'complexity:' frontmatter "
|
|
83
|
+
"(must declare 'lightweight' or 'structural')"
|
|
84
|
+
)
|
|
85
|
+
return problems
|
|
86
|
+
if complexity == "lightweight":
|
|
87
|
+
_check_lightweight(text, line_count, problems)
|
|
88
|
+
return problems
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def main() -> int:
|
|
92
|
+
roadmaps = sorted(REPO_ROOT.glob(ROADMAP_GLOB))
|
|
93
|
+
if not roadmaps:
|
|
94
|
+
print(f"❌ no roadmaps matched {ROADMAP_GLOB}", file=sys.stderr)
|
|
95
|
+
return 1
|
|
96
|
+
failed = 0
|
|
97
|
+
summary: list[tuple[str, str]] = []
|
|
98
|
+
for roadmap in roadmaps:
|
|
99
|
+
rel = roadmap.relative_to(REPO_ROOT)
|
|
100
|
+
problems = lint_roadmap(roadmap)
|
|
101
|
+
text = roadmap.read_text(encoding="utf-8")
|
|
102
|
+
complexity = _read_complexity(_frontmatter(text)) or "untagged"
|
|
103
|
+
summary.append((str(rel), complexity))
|
|
104
|
+
if problems:
|
|
105
|
+
failed += 1
|
|
106
|
+
print(f"❌ {rel} [{complexity}]", file=sys.stderr)
|
|
107
|
+
for p in problems:
|
|
108
|
+
print(f" - {p}", file=sys.stderr)
|
|
109
|
+
else:
|
|
110
|
+
print(f"✅ {rel} [{complexity}]")
|
|
111
|
+
print()
|
|
112
|
+
light = sum(1 for _, c in summary if c == "lightweight")
|
|
113
|
+
structural = sum(1 for _, c in summary if c == "structural")
|
|
114
|
+
untagged = sum(1 for _, c in summary if c == "untagged")
|
|
115
|
+
print(
|
|
116
|
+
f"summary: {light} lightweight · {structural} structural · "
|
|
117
|
+
f"{untagged} untagged · {len(summary)} total"
|
|
118
|
+
)
|
|
119
|
+
if failed:
|
|
120
|
+
print(f"\n❌ {failed} roadmap(s) failed complexity lint", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
print(f"\n✅ {len(roadmaps)} roadmap(s) complexity-clean")
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
if __name__ == "__main__":
|
|
127
|
+
sys.exit(main())
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Platform-agnostic hook for the `onboarding-gate` rule.
|
|
3
|
+
|
|
4
|
+
Reads `.agent-settings.yml` from the consumer repo and writes a
|
|
5
|
+
deterministic state file the rule body can cite as the source of
|
|
6
|
+
truth for "do I need to prompt the user about /onboard?".
|
|
7
|
+
|
|
8
|
+
Output is written to `agents/state/onboarding-gate.json` with:
|
|
9
|
+
{
|
|
10
|
+
"required": <bool>, // true → rule fires on first turn
|
|
11
|
+
"reason": "<string>", // why this state was set
|
|
12
|
+
"checked_at": "<iso8601>", // last hook run
|
|
13
|
+
"settings_present": <bool> // .agent-settings.yml exists
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
Exit code is **always 0**. Hooks must never block the agent loop.
|
|
17
|
+
|
|
18
|
+
Output discipline:
|
|
19
|
+
- stdout: nothing (Augment surfaces stdout to the user)
|
|
20
|
+
- stderr: one short line in --verbose mode, otherwise silent
|
|
21
|
+
|
|
22
|
+
CLI:
|
|
23
|
+
python3 scripts/onboarding_gate_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 re
|
|
31
|
+
import sys
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
SETTINGS_FILE = ".agent-settings.yml"
|
|
35
|
+
STATE_DIR = Path("agents") / "state"
|
|
36
|
+
STATE_FILE = STATE_DIR / "onboarding-gate.json"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _read_onboarded(settings_path: Path) -> tuple[bool, str]:
|
|
40
|
+
"""Return (required, reason) — minimal, dependency-free YAML parsing.
|
|
41
|
+
|
|
42
|
+
We only need a single key under the `onboarding:` block. Full YAML is
|
|
43
|
+
overkill (and would pull in a runtime dep). We scan line-by-line for
|
|
44
|
+
`onboarded: <bool>` inside the `onboarding:` section.
|
|
45
|
+
"""
|
|
46
|
+
if not settings_path.is_file():
|
|
47
|
+
return (False, "settings_file_missing") # legacy: do not block
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
text = settings_path.read_text(encoding="utf-8")
|
|
51
|
+
except OSError:
|
|
52
|
+
return (False, "settings_file_unreadable")
|
|
53
|
+
|
|
54
|
+
in_onboarding = False
|
|
55
|
+
onboarded_value: str | None = None
|
|
56
|
+
for raw in text.splitlines():
|
|
57
|
+
line = raw.rstrip()
|
|
58
|
+
if not line or line.lstrip().startswith("#"):
|
|
59
|
+
continue
|
|
60
|
+
if re.match(r"^onboarding\s*:\s*$", line):
|
|
61
|
+
in_onboarding = True
|
|
62
|
+
continue
|
|
63
|
+
if in_onboarding:
|
|
64
|
+
# Section ends when a top-level (non-indented) key starts.
|
|
65
|
+
if line and not line.startswith((" ", "\t")):
|
|
66
|
+
break
|
|
67
|
+
m = re.match(r"^\s+onboarded\s*:\s*(\S+)\s*(?:#.*)?$", line)
|
|
68
|
+
if m:
|
|
69
|
+
onboarded_value = m.group(1).strip().lower()
|
|
70
|
+
|
|
71
|
+
if onboarded_value is None:
|
|
72
|
+
return (False, "key_missing") # legacy / pre-rule project
|
|
73
|
+
if onboarded_value in ("true", "yes", "on"):
|
|
74
|
+
return (False, "already_onboarded")
|
|
75
|
+
if onboarded_value in ("false", "no", "off"):
|
|
76
|
+
return (True, "explicit_false")
|
|
77
|
+
return (False, f"unknown_value:{onboarded_value}")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _write_state(consumer_root: Path, required: bool, reason: str,
|
|
81
|
+
settings_present: bool) -> None:
|
|
82
|
+
"""Write `agents/state/onboarding-gate.json` atomically."""
|
|
83
|
+
state_dir = consumer_root / STATE_DIR
|
|
84
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
payload = {
|
|
86
|
+
"required": required,
|
|
87
|
+
"reason": reason,
|
|
88
|
+
"checked_at": _dt.datetime.now(_dt.timezone.utc).isoformat(
|
|
89
|
+
timespec="seconds"),
|
|
90
|
+
"settings_present": settings_present,
|
|
91
|
+
}
|
|
92
|
+
target = consumer_root / STATE_FILE
|
|
93
|
+
tmp = target.with_suffix(".json.tmp")
|
|
94
|
+
tmp.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
95
|
+
tmp.replace(target)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def run(*, consumer_root: Path, verbose: bool = False) -> int:
|
|
99
|
+
settings_path = consumer_root / SETTINGS_FILE
|
|
100
|
+
settings_present = settings_path.is_file()
|
|
101
|
+
try:
|
|
102
|
+
required, reason = _read_onboarded(settings_path)
|
|
103
|
+
except Exception: # pragma: no cover — defensive
|
|
104
|
+
required, reason = (False, "hook_error")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
_write_state(consumer_root, required, reason, settings_present)
|
|
108
|
+
except OSError:
|
|
109
|
+
if verbose:
|
|
110
|
+
print("onboarding-gate-hook: state write failed",
|
|
111
|
+
file=sys.stderr)
|
|
112
|
+
return 0 # never block
|
|
113
|
+
|
|
114
|
+
if verbose:
|
|
115
|
+
print(f"onboarding-gate-hook: required={required} reason={reason}",
|
|
116
|
+
file=sys.stderr)
|
|
117
|
+
return 0
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main(argv: list[str] | None = None) -> int:
|
|
121
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
122
|
+
parser.add_argument("--platform", default="generic",
|
|
123
|
+
help="informational platform tag")
|
|
124
|
+
parser.add_argument("--verbose", action="store_true",
|
|
125
|
+
help="emit one stderr line per invocation")
|
|
126
|
+
args = parser.parse_args(argv)
|
|
127
|
+
# Drain stdin so callers piping JSON don't block on a SIGPIPE on
|
|
128
|
+
# platforms that strictly require stdin to be consumed.
|
|
129
|
+
try:
|
|
130
|
+
sys.stdin.read()
|
|
131
|
+
except Exception:
|
|
132
|
+
pass
|
|
133
|
+
return run(consumer_root=Path.cwd(), verbose=args.verbose)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
if __name__ == "__main__": # pragma: no cover
|
|
137
|
+
sys.exit(main())
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Platform-agnostic PostToolUse hook for the `roadmap-progress-sync` rule.
|
|
3
|
+
|
|
4
|
+
Reads a JSON event from stdin (Augment / Claude / Cursor / Cline /
|
|
5
|
+
Windsurf / Gemini PostToolUse-shaped envelopes), decides whether the
|
|
6
|
+
tool call wrote to a roadmap file under `agents/roadmaps/`, and — when
|
|
7
|
+
it did — re-runs `update_roadmap_progress.py` so the dashboard stays
|
|
8
|
+
in sync without depending on agent self-discipline.
|
|
9
|
+
|
|
10
|
+
Exit code is **always 0**. Hooks must never block the agent loop; the
|
|
11
|
+
worst-case is a no-op when stdin is malformed or the regenerator is
|
|
12
|
+
missing.
|
|
13
|
+
|
|
14
|
+
Output discipline:
|
|
15
|
+
- stdout: nothing (Augment would surface stdout to the user)
|
|
16
|
+
- stderr: one short line in --verbose mode, otherwise silent
|
|
17
|
+
|
|
18
|
+
CLI:
|
|
19
|
+
python3 scripts/roadmap_progress_hook.py [--platform NAME] [--verbose]
|
|
20
|
+
|
|
21
|
+
The `--platform` flag is informational only — the filter logic reads
|
|
22
|
+
the same field names across platforms (tool_name, tool_input.path,
|
|
23
|
+
file_changes[].path).
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import subprocess
|
|
30
|
+
import sys
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
# Tools whose successful execution can write to a roadmap file. We keep
|
|
34
|
+
# the list explicit so an unknown tool name (e.g. a new MCP tool that
|
|
35
|
+
# happens to mention a roadmap path in its input) does not trigger a
|
|
36
|
+
# spurious regeneration.
|
|
37
|
+
WRITE_TOOLS = frozenset({
|
|
38
|
+
"str-replace-editor",
|
|
39
|
+
"save-file",
|
|
40
|
+
"remove-files",
|
|
41
|
+
# Claude Code / Cursor naming variants — kept for cross-platform
|
|
42
|
+
# parity if this hook is ever wired beyond Augment.
|
|
43
|
+
"Edit",
|
|
44
|
+
"Write",
|
|
45
|
+
"MultiEdit",
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
ROADMAP_PREFIX = "agents/roadmaps/"
|
|
49
|
+
# Paths under these subtrees are tracked but not part of the open list
|
|
50
|
+
# the dashboard summarises — regenerating on every archived edit would
|
|
51
|
+
# be wasteful. The check still fires on the parent dir itself.
|
|
52
|
+
ROADMAP_EXCLUDED_PARTS = frozenset({"archive", "skipped"})
|
|
53
|
+
DASHBOARD_PATH = "agents/roadmaps-progress.md"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _candidate_paths(payload: dict) -> list[str]:
|
|
57
|
+
"""Pull every plausible file path out of a PostToolUse payload."""
|
|
58
|
+
out: list[str] = []
|
|
59
|
+
fc = payload.get("file_changes")
|
|
60
|
+
if isinstance(fc, list):
|
|
61
|
+
for entry in fc:
|
|
62
|
+
if isinstance(entry, dict):
|
|
63
|
+
p = entry.get("path")
|
|
64
|
+
if isinstance(p, str) and p:
|
|
65
|
+
out.append(p)
|
|
66
|
+
ti = payload.get("tool_input")
|
|
67
|
+
if isinstance(ti, dict):
|
|
68
|
+
for key in ("path", "file_path", "target_file"):
|
|
69
|
+
v = ti.get(key)
|
|
70
|
+
if isinstance(v, str) and v:
|
|
71
|
+
out.append(v)
|
|
72
|
+
return out
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_roadmap_touch(path: str) -> bool:
|
|
76
|
+
"""Return True if `path` is a roadmap file we should react to."""
|
|
77
|
+
norm = path.lstrip("./").replace("\\", "/")
|
|
78
|
+
if not norm.startswith(ROADMAP_PREFIX):
|
|
79
|
+
return False
|
|
80
|
+
if norm == DASHBOARD_PATH:
|
|
81
|
+
# Defensive — the dashboard sits at agents/roadmaps-progress.md,
|
|
82
|
+
# NOT inside agents/roadmaps/. The prefix check above already
|
|
83
|
+
# excludes it, but keep this explicit so a future relocation
|
|
84
|
+
# cannot turn the hook into an infinite loop.
|
|
85
|
+
return False
|
|
86
|
+
rest = norm[len(ROADMAP_PREFIX):]
|
|
87
|
+
parts = rest.split("/")
|
|
88
|
+
if len(parts) >= 2 and parts[0] in ROADMAP_EXCLUDED_PARTS:
|
|
89
|
+
return False
|
|
90
|
+
if not norm.endswith(".md"):
|
|
91
|
+
return False
|
|
92
|
+
return True
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _resolve_regenerator(consumer_root: Path) -> Path | None:
|
|
96
|
+
"""Find the regenerator script — package-shipped or installed copy."""
|
|
97
|
+
for candidate in (
|
|
98
|
+
consumer_root / ".augment" / "scripts" / "update_roadmap_progress.py",
|
|
99
|
+
consumer_root / ".agent-src" / "scripts" / "update_roadmap_progress.py",
|
|
100
|
+
consumer_root / ".agent-src.uncompressed" / "scripts" / "update_roadmap_progress.py",
|
|
101
|
+
):
|
|
102
|
+
if candidate.is_file():
|
|
103
|
+
return candidate
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
|
|
108
|
+
payload: dict = {}
|
|
109
|
+
if stdin_text.strip():
|
|
110
|
+
try:
|
|
111
|
+
decoded = json.loads(stdin_text)
|
|
112
|
+
if isinstance(decoded, dict):
|
|
113
|
+
payload = decoded
|
|
114
|
+
except json.JSONDecodeError:
|
|
115
|
+
return 0 # malformed stdin → silent no-op, never block
|
|
116
|
+
|
|
117
|
+
tool = payload.get("tool_name") or payload.get("toolName") or payload.get("tool")
|
|
118
|
+
if not isinstance(tool, str) or tool not in WRITE_TOOLS:
|
|
119
|
+
return 0
|
|
120
|
+
|
|
121
|
+
paths = _candidate_paths(payload)
|
|
122
|
+
if not any(_is_roadmap_touch(p) for p in paths):
|
|
123
|
+
return 0
|
|
124
|
+
|
|
125
|
+
script = _resolve_regenerator(consumer_root)
|
|
126
|
+
if script is None:
|
|
127
|
+
if verbose:
|
|
128
|
+
print("roadmap-progress-hook: regenerator not found, skipping",
|
|
129
|
+
file=sys.stderr)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
subprocess.run(
|
|
134
|
+
[sys.executable, str(script)],
|
|
135
|
+
cwd=consumer_root, check=False,
|
|
136
|
+
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
|
|
137
|
+
timeout=30,
|
|
138
|
+
)
|
|
139
|
+
except (OSError, subprocess.SubprocessError):
|
|
140
|
+
pass # never propagate regenerator failures into the agent loop
|
|
141
|
+
|
|
142
|
+
if verbose:
|
|
143
|
+
print(f"roadmap-progress-hook: regenerated for tool={tool}",
|
|
144
|
+
file=sys.stderr)
|
|
145
|
+
return 0
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def main(argv: list[str] | None = None) -> int:
|
|
149
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
150
|
+
parser.add_argument("--platform", default="generic",
|
|
151
|
+
help="informational platform tag (augment/claude/...)")
|
|
152
|
+
parser.add_argument("--verbose", action="store_true",
|
|
153
|
+
help="emit one stderr line per invocation")
|
|
154
|
+
args = parser.parse_args(argv)
|
|
155
|
+
return run(sys.stdin.read(), consumer_root=Path.cwd(), verbose=args.verbose)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
if __name__ == "__main__": # pragma: no cover
|
|
159
|
+
sys.exit(main())
|
|
@@ -9,7 +9,8 @@
|
|
|
9
9
|
"properties": {
|
|
10
10
|
"name": {
|
|
11
11
|
"type": "string",
|
|
12
|
-
"pattern": "^[a-z][a-z0-9-]
|
|
12
|
+
"pattern": "^[a-z][a-z0-9-]*(:[a-z][a-z0-9-]*)?$",
|
|
13
|
+
"$comment": "Top-level commands use the bare slug (`commit`). Nested cluster commands under `commands/<cluster>/<sub>.md` use the colon form (`council:default`) to mirror Claude Code's `/cluster:sub` rendering. Directory slug for `.claude/skills/` is the hyphenated form (`council-default`), generated by compress.py."
|
|
13
14
|
},
|
|
14
15
|
"description": {
|
|
15
16
|
"type": "string",
|
|
@@ -40,8 +41,8 @@
|
|
|
40
41
|
},
|
|
41
42
|
"superseded_by": {
|
|
42
43
|
"type": "string",
|
|
43
|
-
"pattern": "^[a-z][a-z0-9-]*( [a-z][a-z0-9-]*)?$",
|
|
44
|
-
"description": "Set on deprecation shims. Format: '<cluster> <sub>' (e.g. 'fix ci'). See docs/contracts/command-clusters.md § Deprecation shim contract."
|
|
44
|
+
"pattern": "^[a-z][a-z0-9-]*( (--[a-z][a-z0-9-]*|[a-z][a-z0-9-]*))?$",
|
|
45
|
+
"description": "Set on deprecation shims. Format: '<cluster> <sub>' (e.g. 'fix ci') or '<cluster> --<flag>' for flag-clusters (e.g. 'commit --in-chunks'). See docs/contracts/command-clusters.md § Deprecation shim contract."
|
|
45
46
|
},
|
|
46
47
|
"deprecated_in": {
|
|
47
48
|
"type": "string",
|
|
@@ -33,6 +33,11 @@
|
|
|
33
33
|
"type": "array",
|
|
34
34
|
"items": {"type": "string", "pattern": "\\.md$"},
|
|
35
35
|
"description": "Eager auto-loaded context references. Counts against the per-rule char budget; enforced by scripts/lint_load_context.py."
|
|
36
|
+
},
|
|
37
|
+
"tier": {
|
|
38
|
+
"type": "string",
|
|
39
|
+
"enum": ["1", "2a", "2b", "3", "safety-floor", "mechanical-already"],
|
|
40
|
+
"description": "Hardening tier per road-to-rule-hardening.md. Optional today, recommended for new rules. Tracked in agents/contexts/rule-trigger-matrix.md. Tier 3 rules also referenced in agents/contexts/tier-3-dispositions.md."
|
|
36
41
|
}
|
|
37
42
|
}
|
|
38
43
|
}
|
package/scripts/skill_linter.py
CHANGED
|
@@ -886,6 +886,7 @@ def lint_command(path: Path, text: str) -> LintResult:
|
|
|
886
886
|
"error", "shim_missing_warning",
|
|
887
887
|
"Deprecation shim must contain a one-line warning matching "
|
|
888
888
|
"'⚠️ /<old-name> is deprecated; use /<cluster> <sub> instead.'"
|
|
889
|
+
" (or '/<cluster> --<flag>' for flag-clusters)"
|
|
889
890
|
" (see docs/contracts/command-clusters.md § Deprecation shim contract)"
|
|
890
891
|
))
|
|
891
892
|
|
|
@@ -136,8 +136,31 @@ def _append_unknown(body: str, user_flat: dict[str, object], known: set[str]) ->
|
|
|
136
136
|
|
|
137
137
|
|
|
138
138
|
def render_target(template_body: str, user_data: dict) -> str:
|
|
139
|
-
"""Return the desired `.agent-settings.yml` body for the given user data.
|
|
140
|
-
|
|
139
|
+
"""Return the desired `.agent-settings.yml` body for the given user data.
|
|
140
|
+
|
|
141
|
+
The trailing ``_user:`` block (emitted by :func:`_append_unknown`) is
|
|
142
|
+
already in dotted-key form on every read after the first sync. Re-
|
|
143
|
+
flattening it would prepend another ``_user.`` segment on every run
|
|
144
|
+
and accumulate forever, so we strip the wrapper and merge its
|
|
145
|
+
contents straight into the flat dict.
|
|
146
|
+
"""
|
|
147
|
+
if user_data:
|
|
148
|
+
user_only = user_data.pop("_user", None) if isinstance(user_data, dict) else None
|
|
149
|
+
user_flat = _flatten(user_data)
|
|
150
|
+
if isinstance(user_only, dict):
|
|
151
|
+
for key, value in user_only.items():
|
|
152
|
+
# Dotted keys round-trip verbatim — never re-flatten them.
|
|
153
|
+
if isinstance(key, str):
|
|
154
|
+
# Heal legacy corruption: pre-fix syncs prepended a
|
|
155
|
+
# `_user.` segment per run, so a key may carry an
|
|
156
|
+
# arbitrary number of them. Strip them all back to
|
|
157
|
+
# the original leaf path.
|
|
158
|
+
healed = key
|
|
159
|
+
while healed.startswith("_user."):
|
|
160
|
+
healed = healed[len("_user."):]
|
|
161
|
+
user_flat[healed] = value
|
|
162
|
+
else:
|
|
163
|
+
user_flat = {}
|
|
141
164
|
known = _template_keys(template_body)
|
|
142
165
|
body = _apply_user_values(template_body, user_flat)
|
|
143
166
|
return _append_unknown(body, user_flat, known)
|
package/scripts/update_counts.py
CHANGED
|
@@ -37,6 +37,13 @@ def count(kind: str) -> int:
|
|
|
37
37
|
if not pdir.exists():
|
|
38
38
|
return 0
|
|
39
39
|
return sum(1 for f in pdir.glob("*.md") if f.name != "README.md")
|
|
40
|
+
if kind == "commands":
|
|
41
|
+
# Commands may be flat (`commands/<name>.md`) or nested under a
|
|
42
|
+
# cluster directory (`commands/<cluster>/<sub>.md`). Walk the tree
|
|
43
|
+
# and skip the AGENTS.md reference orchestrator.
|
|
44
|
+
return sum(
|
|
45
|
+
1 for f in (SRC / kind).rglob("*.md") if f.name != "AGENTS.md"
|
|
46
|
+
)
|
|
40
47
|
return sum(1 for _ in (SRC / kind).glob("*.md"))
|
|
41
48
|
|
|
42
49
|
|