@event4u/agent-config 1.16.0 → 1.17.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/direct-answers.md +34 -49
- 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 +45 -52
- package/.agent-src/rules/no-roadmap-references.md +73 -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 +10 -71
- 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/.claude-plugin/marketplace.json +22 -11
- package/AGENTS.md +2 -2
- package/CHANGELOG.md +90 -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 +178 -0
- package/docs/contracts/load-context-schema.md +1 -3
- 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/asking-and-brevity-examples.md +100 -0
- package/package.json +1 -1
- package/scripts/_one_off_phase4_dispatch_latency.py +108 -0
- package/scripts/_one_off_phase6_trigger_jaccard.py +92 -0
- package/scripts/_phase2_shim_helper.py +109 -0
- package/scripts/agent-config +10 -0
- package/scripts/ai_council/_one_off_2a4_acceptance.py +208 -0
- package/scripts/ai_council/_one_off_context_layer_v1_estimate.py +67 -0
- package/scripts/ai_council/_one_off_context_layer_v1_review.py +292 -0
- package/scripts/ai_council/_one_off_followups_review.py +259 -0
- package/scripts/ai_council/_one_off_nondestructive_inline_audit.py +209 -0
- package/scripts/ai_council/_one_off_phase_2a_budget_rebalance.py +257 -0
- package/scripts/ai_council/_one_off_phase_2a_post_revert.py +197 -0
- package/scripts/ai_council/_one_off_rule_hardening_v1.py +251 -0
- package/scripts/ai_council/_one_off_structural_open_questions.py +232 -0
- package/scripts/ai_council/_one_off_structural_optimization.py +144 -0
- package/scripts/ai_council/_one_off_structural_v3_gaps.py +252 -0
- package/scripts/ai_council/_one_off_structural_v3_review.py +240 -0
- package/scripts/check_always_budget.py +363 -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_phase_coupling.py +148 -0
- package/scripts/check_portability.py +2 -0
- package/scripts/check_references.py +29 -2
- package/scripts/check_safety_floor_untouched.py +125 -0
- package/scripts/command_suggester/loader.py +4 -1
- package/scripts/compress.py +59 -13
- package/scripts/generate_index.py +6 -2
- package/scripts/generate_ownership_matrix.py +323 -0
- package/scripts/hooks/augment-roadmap-progress.sh +57 -0
- package/scripts/install.py +49 -28
- package/scripts/lint_no_new_atomic_commands.py +12 -11
- 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/skill_linter.py +1 -0
- package/scripts/sync_agent_settings.py +25 -2
- package/scripts/update_counts.py +7 -0
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Context-file path & orphan checker.
|
|
3
|
+
|
|
4
|
+
Validates that every `*.md` under `.agent-src.uncompressed/contexts/`:
|
|
5
|
+
|
|
6
|
+
1. Lives in a locked sub-tree (or is one of six grandfathered root files).
|
|
7
|
+
2. Does not collide on basename with another context file in another sub-tree.
|
|
8
|
+
3. Is referenced by at least one rule, skill, command, or other context
|
|
9
|
+
(via `load_context:` frontmatter or a markdown body path mention).
|
|
10
|
+
|
|
11
|
+
Contract: docs/contracts/context-paths.md
|
|
12
|
+
Roadmap: road-to-structural-optimization.md § 0.6
|
|
13
|
+
|
|
14
|
+
Exit codes: 0 = clean, 1 = violations, 3 = internal error.
|
|
15
|
+
"""
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
import argparse
|
|
19
|
+
import json
|
|
20
|
+
import sys
|
|
21
|
+
from dataclasses import asdict, dataclass
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
25
|
+
CONTEXTS_ROOT = ROOT / ".agent-src.uncompressed" / "contexts"
|
|
26
|
+
|
|
27
|
+
# Sub-trees that may contain context files. Update in lock-step with
|
|
28
|
+
# docs/contracts/context-paths.md whenever a roadmap revision adds one.
|
|
29
|
+
LOCKED_SUBTREES = (
|
|
30
|
+
"communication/rules-always",
|
|
31
|
+
"communication/rules-auto",
|
|
32
|
+
"judges",
|
|
33
|
+
"analysis",
|
|
34
|
+
"skills",
|
|
35
|
+
"chat-history",
|
|
36
|
+
"execution",
|
|
37
|
+
"authority",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
# Files allowed to remain at the contexts root. Anything else at the root
|
|
41
|
+
# fails the path check. New context files MUST live in a sub-tree.
|
|
42
|
+
GRANDFATHERED_ROOT_FILES = frozenset({
|
|
43
|
+
"augment-infrastructure.md",
|
|
44
|
+
"documentation-hierarchy.md",
|
|
45
|
+
"model-recommendations.md",
|
|
46
|
+
"override-system.md",
|
|
47
|
+
"skills-and-commands.md",
|
|
48
|
+
"subagent-configuration.md",
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
# Directories whose content we scan for references to a context file.
|
|
52
|
+
# `agents/roadmaps` is included because in-flight roadmap docs are
|
|
53
|
+
# legitimate referrers during multi-phase rollouts — a context can land
|
|
54
|
+
# in phase N while its first rule/skill referrer lands in phase N+k.
|
|
55
|
+
# Without this scan dir, every newly-introduced context would orphan
|
|
56
|
+
# until the consuming artefact lands, blocking phase-by-phase commits.
|
|
57
|
+
REFERENCE_SCAN_DIRS = (
|
|
58
|
+
".agent-src.uncompressed/rules",
|
|
59
|
+
".agent-src.uncompressed/skills",
|
|
60
|
+
".agent-src.uncompressed/commands",
|
|
61
|
+
".agent-src.uncompressed/contexts",
|
|
62
|
+
"agents/roadmaps",
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass
|
|
67
|
+
class Violation:
|
|
68
|
+
file: str
|
|
69
|
+
kind: str # "out-of-tree" | "root-not-grandfathered" | "collision" | "orphan"
|
|
70
|
+
detail: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _collect_contexts(root: Path) -> list[Path]:
|
|
74
|
+
if not (root / CONTEXTS_ROOT.relative_to(ROOT)).exists():
|
|
75
|
+
return []
|
|
76
|
+
return sorted((root / CONTEXTS_ROOT.relative_to(ROOT)).rglob("*.md"))
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _check_path(ctx: Path, contexts_root: Path) -> Violation | None:
|
|
80
|
+
rel = ctx.relative_to(contexts_root)
|
|
81
|
+
parts = rel.parts
|
|
82
|
+
if len(parts) == 1:
|
|
83
|
+
if parts[0] not in GRANDFATHERED_ROOT_FILES:
|
|
84
|
+
return Violation(
|
|
85
|
+
file=str(ctx),
|
|
86
|
+
kind="root-not-grandfathered",
|
|
87
|
+
detail=(f"new file at contexts/ root — must live in one of "
|
|
88
|
+
f"{sorted(LOCKED_SUBTREES)} or be added to "
|
|
89
|
+
"GRANDFATHERED_ROOT_FILES via a roadmap revision"),
|
|
90
|
+
)
|
|
91
|
+
return None
|
|
92
|
+
subtree = "/".join(parts[:-1])
|
|
93
|
+
# Allow nested matches: e.g. communication/rules-always/foo/bar.md still
|
|
94
|
+
# lives under "communication/rules-always". Direct prefix match suffices.
|
|
95
|
+
for allowed in LOCKED_SUBTREES:
|
|
96
|
+
if subtree == allowed or subtree.startswith(allowed + "/"):
|
|
97
|
+
return None
|
|
98
|
+
return Violation(
|
|
99
|
+
file=str(ctx),
|
|
100
|
+
kind="out-of-tree",
|
|
101
|
+
detail=(f"sub-tree '{subtree}' is not in LOCKED_SUBTREES — see "
|
|
102
|
+
"docs/contracts/context-paths.md to add a new sub-tree"),
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _check_collisions(contexts: list[Path], contexts_root: Path) -> list[Violation]:
|
|
107
|
+
by_name: dict[str, list[Path]] = {}
|
|
108
|
+
for ctx in contexts:
|
|
109
|
+
by_name.setdefault(ctx.name, []).append(ctx)
|
|
110
|
+
out: list[Violation] = []
|
|
111
|
+
for name, paths in by_name.items():
|
|
112
|
+
if len(paths) <= 1:
|
|
113
|
+
continue
|
|
114
|
+
rels = sorted(str(p.relative_to(contexts_root)) for p in paths)
|
|
115
|
+
for p in paths:
|
|
116
|
+
out.append(Violation(
|
|
117
|
+
file=str(p),
|
|
118
|
+
kind="collision",
|
|
119
|
+
detail=f"basename '{name}' shared with: {', '.join(r for r in rels if r != str(p.relative_to(contexts_root)))}",
|
|
120
|
+
))
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _build_reference_corpus(root: Path) -> str:
|
|
125
|
+
chunks: list[str] = []
|
|
126
|
+
for d in REFERENCE_SCAN_DIRS:
|
|
127
|
+
base = root / d
|
|
128
|
+
if not base.exists():
|
|
129
|
+
continue
|
|
130
|
+
for f in base.rglob("*.md"):
|
|
131
|
+
try:
|
|
132
|
+
chunks.append(f.read_text(encoding="utf-8"))
|
|
133
|
+
except OSError:
|
|
134
|
+
continue
|
|
135
|
+
return "\n".join(chunks)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _check_orphans(contexts: list[Path], corpus: str, root: Path) -> list[Violation]:
|
|
139
|
+
out: list[Violation] = []
|
|
140
|
+
for ctx in contexts:
|
|
141
|
+
rel_src = str(ctx.relative_to(root)) # .agent-src.uncompressed/contexts/...
|
|
142
|
+
rel_short = rel_src.split("contexts/", 1)[-1] # judges/persona-voice-rubric.md
|
|
143
|
+
candidates = (rel_src, f"contexts/{rel_short}", rel_short)
|
|
144
|
+
# Exclude self-references: drop this file's own content from the
|
|
145
|
+
# corpus check by reading the file and subtracting its substring.
|
|
146
|
+
try:
|
|
147
|
+
own_text = ctx.read_text(encoding="utf-8")
|
|
148
|
+
except OSError:
|
|
149
|
+
own_text = ""
|
|
150
|
+
external_corpus = corpus.replace(own_text, "") if own_text else corpus
|
|
151
|
+
if not any(c in external_corpus for c in candidates):
|
|
152
|
+
out.append(Violation(
|
|
153
|
+
file=str(ctx),
|
|
154
|
+
kind="orphan",
|
|
155
|
+
detail="not referenced by any rule, skill, command, or other context",
|
|
156
|
+
))
|
|
157
|
+
return out
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def scan(root: Path) -> list[Violation]:
|
|
161
|
+
contexts_root = root / CONTEXTS_ROOT.relative_to(ROOT)
|
|
162
|
+
contexts = _collect_contexts(root)
|
|
163
|
+
violations: list[Violation] = []
|
|
164
|
+
for ctx in contexts:
|
|
165
|
+
v = _check_path(ctx, contexts_root)
|
|
166
|
+
if v:
|
|
167
|
+
violations.append(v)
|
|
168
|
+
violations.extend(_check_collisions(contexts, contexts_root))
|
|
169
|
+
corpus = _build_reference_corpus(root)
|
|
170
|
+
violations.extend(_check_orphans(contexts, corpus, root))
|
|
171
|
+
return violations
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def format_text(violations: list[Violation]) -> str:
|
|
175
|
+
if not violations:
|
|
176
|
+
return "✅ No context-path violations."
|
|
177
|
+
lines = [f"❌ Found {len(violations)} context-path violation(s):\n"]
|
|
178
|
+
for v in violations:
|
|
179
|
+
lines.append(f" 🔴 [{v.kind}] {v.file}\n {v.detail}")
|
|
180
|
+
return "\n".join(lines)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def main() -> int:
|
|
184
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
185
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
186
|
+
parser.add_argument("--root", type=Path, default=ROOT)
|
|
187
|
+
args = parser.parse_args()
|
|
188
|
+
try:
|
|
189
|
+
violations = scan(args.root)
|
|
190
|
+
except Exception as e: # pragma: no cover
|
|
191
|
+
print(f"Internal error: {e}", file=sys.stderr)
|
|
192
|
+
return 3
|
|
193
|
+
if args.format == "json":
|
|
194
|
+
print(json.dumps([asdict(v) for v in violations], indent=2))
|
|
195
|
+
else:
|
|
196
|
+
print(format_text(violations))
|
|
197
|
+
return 1 if violations else 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == "__main__":
|
|
201
|
+
sys.exit(main())
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""No-roadmap-references checker.
|
|
3
|
+
|
|
4
|
+
Stable artifacts (rules, skills, commands, contexts, guidelines, AGENTS.md,
|
|
5
|
+
README, copilot-instructions) must NOT cite a specific roadmap file in
|
|
6
|
+
`agents/roadmaps/`. Roadmap files are transient — archived, skipped, or
|
|
7
|
+
deleted as work completes — and stable artifacts citing them rot.
|
|
8
|
+
|
|
9
|
+
Allowed: directory mentions (`agents/roadmaps/`, `agents/roadmaps/archive/`,
|
|
10
|
+
`agents/roadmaps/skipped/`). Forbidden: specific `*.md` files inside those
|
|
11
|
+
directories.
|
|
12
|
+
|
|
13
|
+
Contract: .agent-src.uncompressed/rules/no-roadmap-references.md
|
|
14
|
+
|
|
15
|
+
Exit codes: 0 = clean, 1 = violations, 3 = internal error.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import json
|
|
21
|
+
import re
|
|
22
|
+
import sys
|
|
23
|
+
from dataclasses import asdict, dataclass
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
27
|
+
|
|
28
|
+
# Stable artefact trees — every `*.md` below MUST be free of roadmap-file
|
|
29
|
+
# citations. Directory mentions stay allowed (the regex below excludes them).
|
|
30
|
+
STABLE_TREES = (
|
|
31
|
+
".agent-src.uncompressed/rules",
|
|
32
|
+
".agent-src.uncompressed/skills",
|
|
33
|
+
".agent-src.uncompressed/commands",
|
|
34
|
+
".agent-src.uncompressed/contexts",
|
|
35
|
+
".agent-src.uncompressed/templates",
|
|
36
|
+
".agent-src.uncompressed/personas",
|
|
37
|
+
"agents/contexts",
|
|
38
|
+
"docs/guidelines",
|
|
39
|
+
"docs/contracts",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
# Stable single-file artefacts at well-known paths.
|
|
43
|
+
STABLE_FILES = (
|
|
44
|
+
"AGENTS.md",
|
|
45
|
+
"README.md",
|
|
46
|
+
"copilot-instructions.md",
|
|
47
|
+
"docs/architecture.md",
|
|
48
|
+
"docs/customization.md",
|
|
49
|
+
"docs/getting-started.md",
|
|
50
|
+
"docs/catalog.md",
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Roadmap-file pattern: any `*.md` file under `agents/roadmaps/` at any
|
|
54
|
+
# depth (including `archive/`, `skipped/`, and nested topical subfolders
|
|
55
|
+
# like `agent-memory/`). Directory-only mentions (`agents/roadmaps/`
|
|
56
|
+
# with trailing slash, no filename) and placeholder mentions like
|
|
57
|
+
# `agents/roadmaps/<file>.md` (angle-bracket placeholder) do NOT match.
|
|
58
|
+
ROADMAP_FILE_RE = re.compile(
|
|
59
|
+
r"agents/roadmaps/(?:[a-z0-9][a-z0-9_-]*/)*[a-z0-9][a-z0-9_-]*\.md",
|
|
60
|
+
re.IGNORECASE,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Files that may legitimately quote forbidden patterns inside backticks for
|
|
64
|
+
# documentation purposes — the rule itself, the companion CI script docs,
|
|
65
|
+
# and the contract doc that names the rule.
|
|
66
|
+
SELF_DOCUMENTING_ALLOWLIST = frozenset({
|
|
67
|
+
".agent-src.uncompressed/rules/no-roadmap-references.md",
|
|
68
|
+
"docs/guidelines/agent-infra/no-roadmap-references.md",
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
@dataclass
|
|
73
|
+
class Violation:
|
|
74
|
+
file: str
|
|
75
|
+
line: int
|
|
76
|
+
match: str
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _scan_file(path: Path, root: Path) -> list[Violation]:
|
|
80
|
+
rel = str(path.relative_to(root))
|
|
81
|
+
if rel in SELF_DOCUMENTING_ALLOWLIST:
|
|
82
|
+
return []
|
|
83
|
+
try:
|
|
84
|
+
text = path.read_text(encoding="utf-8")
|
|
85
|
+
except OSError:
|
|
86
|
+
return []
|
|
87
|
+
out: list[Violation] = []
|
|
88
|
+
in_fence = False
|
|
89
|
+
for n, line in enumerate(text.splitlines(), start=1):
|
|
90
|
+
# Skip fenced code blocks — path listings inside ``` are functional
|
|
91
|
+
# constants (command contracts, runtime checks), not link rot.
|
|
92
|
+
stripped = line.lstrip()
|
|
93
|
+
if stripped.startswith("```"):
|
|
94
|
+
in_fence = not in_fence
|
|
95
|
+
continue
|
|
96
|
+
if in_fence:
|
|
97
|
+
continue
|
|
98
|
+
for m in ROADMAP_FILE_RE.finditer(line):
|
|
99
|
+
out.append(Violation(file=rel, line=n, match=m.group(0)))
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _collect_targets(root: Path) -> list[Path]:
|
|
104
|
+
targets: list[Path] = []
|
|
105
|
+
for d in STABLE_TREES:
|
|
106
|
+
base = root / d
|
|
107
|
+
if not base.exists():
|
|
108
|
+
continue
|
|
109
|
+
targets.extend(sorted(base.rglob("*.md")))
|
|
110
|
+
for f in STABLE_FILES:
|
|
111
|
+
p = root / f
|
|
112
|
+
if p.exists():
|
|
113
|
+
targets.append(p)
|
|
114
|
+
return targets
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def scan(root: Path) -> list[Violation]:
|
|
118
|
+
out: list[Violation] = []
|
|
119
|
+
for path in _collect_targets(root):
|
|
120
|
+
out.extend(_scan_file(path, root))
|
|
121
|
+
return out
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def format_text(violations: list[Violation]) -> str:
|
|
125
|
+
if not violations:
|
|
126
|
+
return "✅ No roadmap-file references in stable artifacts."
|
|
127
|
+
lines = [f"❌ Found {len(violations)} roadmap reference(s) in stable artifacts:\n"]
|
|
128
|
+
for v in violations:
|
|
129
|
+
lines.append(f" 🔴 {v.file}:{v.line} → {v.match}")
|
|
130
|
+
lines.append(
|
|
131
|
+
"\nPromote the durable conclusion to agents/contexts/ and cite that "
|
|
132
|
+
"instead. See .agent-src.uncompressed/rules/no-roadmap-references.md."
|
|
133
|
+
)
|
|
134
|
+
return "\n".join(lines)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> int:
|
|
138
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
139
|
+
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
140
|
+
parser.add_argument("--root", type=Path, default=ROOT)
|
|
141
|
+
args = parser.parse_args()
|
|
142
|
+
try:
|
|
143
|
+
violations = scan(args.root)
|
|
144
|
+
except Exception as e: # pragma: no cover
|
|
145
|
+
print(f"Internal error: {e}", file=sys.stderr)
|
|
146
|
+
return 3
|
|
147
|
+
if args.format == "json":
|
|
148
|
+
print(json.dumps([asdict(v) for v in violations], indent=2))
|
|
149
|
+
else:
|
|
150
|
+
print(format_text(violations))
|
|
151
|
+
return 1 if violations else 0
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
if __name__ == "__main__":
|
|
155
|
+
sys.exit(main())
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Phase 6 → Phase 2B coupling guard (Phase 0.3.3).
|
|
3
|
+
|
|
4
|
+
Re-runs the audit recorded in `agents/roadmaps/phase6-2b-coupling.md`
|
|
5
|
+
on every CI run. Fails the build if any of the 13 Phase-2B target
|
|
6
|
+
rules introduces a reference to one of the three Phase-6-owned rules
|
|
7
|
+
(`chat-history-cadence`, `chat-history-ownership`,
|
|
8
|
+
`chat-history-visibility`) — by rule name, `load_context:` entry, or
|
|
9
|
+
body link / cite.
|
|
10
|
+
|
|
11
|
+
Excluded from the coupling probe (separate infrastructure, not
|
|
12
|
+
reshaped by Phase 6):
|
|
13
|
+
|
|
14
|
+
- The CLI dispatcher subcommand `./agent-config chat-history:hook`
|
|
15
|
+
and any other `chat-history:*` colon-suffix command surface.
|
|
16
|
+
|
|
17
|
+
Exit codes: 0 = decoupling intact, 1 = coupling detected,
|
|
18
|
+
3 = internal error (target rule missing, unreadable file).
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
29
|
+
SRC_RULES = REPO_ROOT / ".agent-src.uncompressed" / "rules"
|
|
30
|
+
COMP_RULES = REPO_ROOT / ".agent-src" / "rules"
|
|
31
|
+
|
|
32
|
+
# Phase 2B priority list — see road-to-structural-optimization.md § Phase 2 → 2B.
|
|
33
|
+
TARGET_RULES: tuple[str, ...] = (
|
|
34
|
+
"roadmap-progress-sync",
|
|
35
|
+
"user-interaction",
|
|
36
|
+
"augment-source-of-truth",
|
|
37
|
+
"command-suggestion-policy",
|
|
38
|
+
"artifact-engagement-recording",
|
|
39
|
+
"review-routing-awareness",
|
|
40
|
+
"autonomous-execution",
|
|
41
|
+
"docs-sync",
|
|
42
|
+
"cli-output-handling",
|
|
43
|
+
"augment-portability",
|
|
44
|
+
"ui-audit-gate",
|
|
45
|
+
"skill-quality",
|
|
46
|
+
"package-ci-checks",
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Phase 6 owns these three rule names. Match must be a *rule reference*,
|
|
50
|
+
# not a *dispatcher reference* (chat-history:hook etc).
|
|
51
|
+
PHASE6_RULES: tuple[str, ...] = (
|
|
52
|
+
"chat-history-cadence",
|
|
53
|
+
"chat-history-ownership",
|
|
54
|
+
"chat-history-visibility",
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
# Rule-reference pattern: rule name not immediately followed by `:` (which
|
|
58
|
+
# would mark it as a CLI subcommand like `chat-history:hook`). Allows
|
|
59
|
+
# trailing word-boundary characters typical of Markdown / YAML contexts.
|
|
60
|
+
_RULE_REF_RE = re.compile(
|
|
61
|
+
r"\bchat-history-(?:cadence|ownership|visibility)\b(?!:)"
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _scan(file: Path) -> list[tuple[int, str]]:
|
|
66
|
+
"""Return [(line_no, line)] of rule-reference matches in `file`."""
|
|
67
|
+
if not file.is_file():
|
|
68
|
+
return []
|
|
69
|
+
hits: list[tuple[int, str]] = []
|
|
70
|
+
for i, line in enumerate(file.read_text(encoding="utf-8").splitlines(), 1):
|
|
71
|
+
if _RULE_REF_RE.search(line):
|
|
72
|
+
hits.append((i, line.strip()))
|
|
73
|
+
return hits
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _check_surface(label: str, base: Path) -> tuple[int, list[str]]:
|
|
77
|
+
"""Scan `base` for all 13 Phase-2B targets; return (#hits, formatted lines)."""
|
|
78
|
+
if not base.is_dir():
|
|
79
|
+
return 0, [f"❌ {label} dir missing: {base}"]
|
|
80
|
+
out: list[str] = []
|
|
81
|
+
total = 0
|
|
82
|
+
for rule in TARGET_RULES:
|
|
83
|
+
path = base / f"{rule}.md"
|
|
84
|
+
if not path.is_file():
|
|
85
|
+
return 0, [f"❌ target rule missing: {path}"]
|
|
86
|
+
hits = _scan(path)
|
|
87
|
+
if not hits:
|
|
88
|
+
continue
|
|
89
|
+
total += len(hits)
|
|
90
|
+
for line_no, line in hits:
|
|
91
|
+
out.append(f" {label}/{rule}.md:{line_no} {line}")
|
|
92
|
+
return total, out
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def main() -> int:
|
|
96
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
97
|
+
parser.add_argument(
|
|
98
|
+
"--quiet",
|
|
99
|
+
action="store_true",
|
|
100
|
+
help="suppress the per-rule breakdown when no coupling found",
|
|
101
|
+
)
|
|
102
|
+
args = parser.parse_args()
|
|
103
|
+
|
|
104
|
+
hits_src, lines_src = _check_surface("uncompressed", SRC_RULES)
|
|
105
|
+
if any(line.startswith("❌") for line in lines_src):
|
|
106
|
+
for line in lines_src:
|
|
107
|
+
print(line, file=sys.stderr)
|
|
108
|
+
return 3
|
|
109
|
+
hits_comp, lines_comp = _check_surface("compressed", COMP_RULES)
|
|
110
|
+
if any(line.startswith("❌") for line in lines_comp):
|
|
111
|
+
for line in lines_comp:
|
|
112
|
+
print(line, file=sys.stderr)
|
|
113
|
+
return 3
|
|
114
|
+
|
|
115
|
+
total = hits_src + hits_comp
|
|
116
|
+
|
|
117
|
+
if total == 0:
|
|
118
|
+
if not args.quiet:
|
|
119
|
+
print(
|
|
120
|
+
f"✅ Phase 6 → 2B decoupling intact: 0 rule-references "
|
|
121
|
+
f"across {len(TARGET_RULES)} Phase-2B targets "
|
|
122
|
+
f"(uncompressed + compressed surfaces)."
|
|
123
|
+
)
|
|
124
|
+
print(
|
|
125
|
+
" probe: rule names, load_context: entries, body "
|
|
126
|
+
"link/cite — dispatcher subcommand chat-history:hook "
|
|
127
|
+
"excluded by design."
|
|
128
|
+
)
|
|
129
|
+
return 0
|
|
130
|
+
|
|
131
|
+
print(
|
|
132
|
+
f"❌ Phase 6 → 2B coupling detected: {total} rule-reference(s) "
|
|
133
|
+
f"across Phase-2B targets:"
|
|
134
|
+
)
|
|
135
|
+
for line in lines_src + lines_comp:
|
|
136
|
+
print(line)
|
|
137
|
+
print(
|
|
138
|
+
"\n Action: see agents/roadmaps/phase6-2b-coupling.md. "
|
|
139
|
+
"Either drop the new reference, migrate it to the dispatcher "
|
|
140
|
+
"(chat-history:hook), or trigger the >0-hits branch in 0.3.2 "
|
|
141
|
+
"(Phase 6 ships call-signature contract before Phase 2B "
|
|
142
|
+
"touches the coupled rule)."
|
|
143
|
+
)
|
|
144
|
+
return 1
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
sys.exit(main())
|
|
@@ -315,6 +315,7 @@ _TASK_FENCE_RE = re.compile(r"^\s*task\s+([a-z][a-z0-9:_-]*)\b")
|
|
|
315
315
|
# by the task-invocation detector (but still scanned for layer 1 + 2).
|
|
316
316
|
_TASK_DETECTOR_SKIP = (
|
|
317
317
|
"rules/augment-portability.md",
|
|
318
|
+
"contexts/communication/rules-auto/augment-portability-mechanics.md",
|
|
318
319
|
)
|
|
319
320
|
|
|
320
321
|
|
|
@@ -422,6 +423,7 @@ _CLI_INVOCATION_MAP: list[tuple[re.Pattern, str]] = [
|
|
|
422
423
|
# own help, the portability rule that defines the mapping).
|
|
423
424
|
_CLI_DETECTOR_SKIP = (
|
|
424
425
|
"rules/augment-portability.md",
|
|
426
|
+
"contexts/communication/rules-auto/augment-portability-mechanics.md",
|
|
425
427
|
)
|
|
426
428
|
|
|
427
429
|
|
|
@@ -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():
|
|
@@ -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())
|