@event4u/agent-config 1.17.0 → 1.19.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/council/default.md +74 -76
- package/.agent-src/commands/feature/roadmap.md +22 -0
- package/.agent-src/commands/roadmap/create.md +38 -6
- package/.agent-src/commands/roadmap/execute.md +36 -9
- package/.agent-src/rules/agent-authority.md +1 -0
- package/.agent-src/rules/agent-docs.md +1 -0
- package/.agent-src/rules/analysis-skill-routing.md +1 -0
- package/.agent-src/rules/architecture.md +1 -0
- package/.agent-src/rules/artifact-drafting-protocol.md +1 -0
- package/.agent-src/rules/artifact-engagement-recording.md +1 -0
- package/.agent-src/rules/ask-when-uncertain.md +1 -0
- package/.agent-src/rules/augment-portability.md +1 -0
- package/.agent-src/rules/augment-source-of-truth.md +1 -0
- package/.agent-src/rules/autonomous-execution.md +1 -0
- package/.agent-src/rules/capture-learnings.md +1 -0
- package/.agent-src/rules/chat-history-cadence.md +34 -0
- package/.agent-src/rules/chat-history-ownership.md +1 -0
- package/.agent-src/rules/chat-history-visibility.md +1 -0
- package/.agent-src/rules/cli-output-handling.md +2 -2
- package/.agent-src/rules/command-suggestion-policy.md +1 -0
- package/.agent-src/rules/commit-conventions.md +1 -0
- package/.agent-src/rules/commit-policy.md +1 -0
- package/.agent-src/rules/context-hygiene.md +28 -0
- package/.agent-src/rules/direct-answers.md +18 -26
- package/.agent-src/rules/docker-commands.md +1 -0
- package/.agent-src/rules/docs-sync.md +1 -0
- package/.agent-src/rules/downstream-changes.md +1 -0
- package/.agent-src/rules/e2e-testing.md +1 -0
- package/.agent-src/rules/guidelines.md +1 -0
- package/.agent-src/rules/improve-before-implement.md +1 -0
- package/.agent-src/rules/language-and-tone.md +1 -0
- package/.agent-src/rules/laravel-translations.md +1 -0
- package/.agent-src/rules/markdown-safe-codeblocks.md +1 -0
- package/.agent-src/rules/minimal-safe-diff.md +1 -0
- package/.agent-src/rules/missing-tool-handling.md +1 -0
- package/.agent-src/rules/model-recommendation.md +1 -0
- package/.agent-src/rules/no-cheap-questions.md +15 -21
- package/.agent-src/rules/no-roadmap-references.md +1 -0
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +33 -0
- package/.agent-src/rules/package-ci-checks.md +1 -0
- package/.agent-src/rules/php-coding.md +1 -0
- package/.agent-src/rules/preservation-guard.md +1 -0
- package/.agent-src/rules/review-routing-awareness.md +1 -0
- package/.agent-src/rules/reviewer-awareness.md +1 -0
- package/.agent-src/rules/roadmap-progress-sync.md +49 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +29 -0
- package/.agent-src/rules/runtime-safety.md +1 -0
- package/.agent-src/rules/scope-control.md +1 -0
- package/.agent-src/rules/security-sensitive-stop.md +1 -0
- package/.agent-src/rules/size-enforcement.md +1 -0
- package/.agent-src/rules/skill-improvement-trigger.md +1 -0
- package/.agent-src/rules/skill-quality.md +1 -0
- package/.agent-src/rules/slash-command-routing-policy.md +39 -0
- package/.agent-src/rules/think-before-action.md +1 -0
- package/.agent-src/rules/token-efficiency.md +1 -0
- package/.agent-src/rules/tool-safety.md +1 -0
- package/.agent-src/rules/ui-audit-gate.md +1 -0
- package/.agent-src/rules/upstream-proposal.md +1 -0
- package/.agent-src/rules/user-interaction.md +1 -0
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +16 -0
- package/.agent-src/templates/roadmaps.md +12 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +9 -0
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/decision_trace.py +163 -0
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/memory_visibility.py +111 -0
- package/.agent-src/templates/scripts/work_engine/hooks/settings.py +36 -0
- package/.agent-src/templates/scripts/work_engine/scoring/decision_trace.py +141 -0
- package/.agent-src/templates/scripts/work_engine/scoring/memory_visibility.py +125 -0
- package/.claude-plugin/marketplace.json +1 -1
- package/CHANGELOG.md +97 -0
- package/README.md +20 -20
- package/config/agent-settings.template.yml +23 -0
- package/docs/architecture.md +1 -1
- package/docs/catalog.md +5 -2
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +7 -0
- package/docs/contracts/hook-architecture-v1.md +213 -0
- package/docs/contracts/load-context-budget-model.md +80 -0
- package/docs/contracts/load-context-schema.md +20 -0
- package/docs/contracts/memory-visibility-v1.md +138 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/roadmap-complexity-standard.md +137 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +1 -0
- package/docs/development.md +4 -1
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +134 -0
- package/docs/guidelines/agent-infra/direct-answers-demos.md +145 -0
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/docs/guidelines/agent-infra/verify-before-complete-demos.md +128 -0
- package/package.json +1 -1
- package/scripts/agent-config +64 -0
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +24 -8
- package/scripts/ai_council/one_off_archive/2026-05/README.md +67 -0
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_budget_v2_audit.py +206 -0
- package/scripts/ai_council/{_one_off_roundtrip.py → one_off_archive/2026-05/_one_off_roundtrip.py} +13 -8
- package/scripts/ai_council/one_off_archive/2026-05/_one_off_tier_retrofit.py +180 -0
- package/scripts/ai_council/session.py +92 -0
- package/scripts/build_rule_trigger_matrix.py +360 -0
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +11 -1
- package/scripts/check_always_budget.py +46 -2
- package/scripts/check_one_off_location.py +81 -0
- package/scripts/check_references.py +6 -0
- package/scripts/compress.py +5 -2
- package/scripts/context_hygiene_hook.py +181 -0
- package/scripts/council_cli.py +357 -0
- package/scripts/hook_manifest.yaml +184 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-context-hygiene.sh +55 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/augment-onboarding-gate.sh +55 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +348 -0
- package/scripts/hooks/envelope.py +98 -0
- package/scripts/hooks/gemini-dispatcher.sh +117 -0
- package/scripts/hooks/state_io.py +122 -0
- package/scripts/hooks/windsurf-dispatcher.sh +123 -0
- package/scripts/hooks_status.py +146 -0
- package/scripts/install.py +728 -51
- package/scripts/install.sh +1 -1
- package/scripts/lint_examples.py +98 -0
- package/scripts/lint_hook_manifest.py +216 -0
- package/scripts/lint_one_off_age.py +184 -0
- package/scripts/lint_roadmap_complexity.py +127 -0
- package/scripts/lint_rule_tiers.py +78 -0
- package/scripts/lint_showcase_sessions.py +148 -0
- package/scripts/minimal_safe_diff_hook.py +245 -0
- package/scripts/onboarding_gate_hook.py +142 -0
- package/scripts/readme_linter.py +12 -3
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/schemas/rule.schema.json +5 -0
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/verify_before_complete_hook.py +216 -0
- /package/scripts/ai_council/{_one_off_2a4_acceptance.py → one_off_archive/2026-05/_one_off_2a4_acceptance.py} +0 -0
- /package/scripts/ai_council/{_one_off_context_layer_v1_estimate.py → one_off_archive/2026-05/_one_off_context_layer_v1_estimate.py} +0 -0
- /package/scripts/ai_council/{_one_off_context_layer_v1_review.py → one_off_archive/2026-05/_one_off_context_layer_v1_review.py} +0 -0
- /package/scripts/ai_council/{_one_off_followups_review.py → one_off_archive/2026-05/_one_off_followups_review.py} +0 -0
- /package/scripts/ai_council/{_one_off_nondestructive_inline_audit.py → one_off_archive/2026-05/_one_off_nondestructive_inline_audit.py} +0 -0
- /package/scripts/{_one_off_phase4_dispatch_latency.py → ai_council/one_off_archive/2026-05/_one_off_phase4_dispatch_latency.py} +0 -0
- /package/scripts/{_one_off_phase6_trigger_jaccard.py → ai_council/one_off_archive/2026-05/_one_off_phase6_trigger_jaccard.py} +0 -0
- /package/scripts/ai_council/{_one_off_phase_2a_budget_rebalance.py → one_off_archive/2026-05/_one_off_phase_2a_budget_rebalance.py} +0 -0
- /package/scripts/ai_council/{_one_off_phase_2a_post_revert.py → one_off_archive/2026-05/_one_off_phase_2a_post_revert.py} +0 -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_rule_hardening_v1.py → one_off_archive/2026-05/_one_off_rule_hardening_v1.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_open_questions.py → one_off_archive/2026-05/_one_off_structural_open_questions.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_optimization.py → one_off_archive/2026-05/_one_off_structural_optimization.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_v3_gaps.py → one_off_archive/2026-05/_one_off_structural_v3_gaps.py} +0 -0
- /package/scripts/ai_council/{_one_off_structural_v3_review.py → one_off_archive/2026-05/_one_off_structural_v3_review.py} +0 -0
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""One-off — tier-bulk-retrofit (Phase 2.1 + 2.2 of road-to-feedback-consolidation).
|
|
3
|
+
|
|
4
|
+
Parses agents/contexts/rule-trigger-matrix.md, emits tmp/tier-classification.md,
|
|
5
|
+
and inserts a `tier:` frontmatter key into every rule under
|
|
6
|
+
.agent-src.uncompressed/rules/. Idempotent — re-runs are a no-op when a rule
|
|
7
|
+
already declares the same tier value.
|
|
8
|
+
|
|
9
|
+
Lifecycle: scripts/_one_off/2026-05/. Purge eligible after 2026-08-04 per
|
|
10
|
+
docs/contracts/one-off-script-lifecycle.md.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import re
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
REPO = Path(__file__).resolve().parents[3]
|
|
19
|
+
MATRIX = REPO / "agents" / "contexts" / "rule-trigger-matrix.md"
|
|
20
|
+
RULES_DIR = REPO / ".agent-src.uncompressed" / "rules"
|
|
21
|
+
COMPRESSED_RULES_DIR = REPO / ".agent-src" / "rules"
|
|
22
|
+
SPREADSHEET = REPO / "tmp" / "tier-classification.md"
|
|
23
|
+
|
|
24
|
+
VALID_TIERS = {"1", "2a", "2b", "3", "safety-floor", "mechanical-already"}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def parse_matrix() -> dict[str, tuple[str, str]]:
|
|
28
|
+
"""Return {rule_filename: (tier, notes)} from the matrix table."""
|
|
29
|
+
out: dict[str, tuple[str, str]] = {}
|
|
30
|
+
full = MATRIX.read_text(encoding="utf-8")
|
|
31
|
+
# Slice between '## Matrix' and the next '## ' heading.
|
|
32
|
+
start = full.find("\n## Matrix\n")
|
|
33
|
+
if start == -1:
|
|
34
|
+
sys.exit("matrix: '## Matrix' section not found")
|
|
35
|
+
end = full.find("\n## ", start + 1)
|
|
36
|
+
text = full[start:end] if end != -1 else full[start:]
|
|
37
|
+
# Table rows look like: | `agent-authority.md` | always | 1468 | … | 3 | no | Priority index, … |
|
|
38
|
+
row_re = re.compile(
|
|
39
|
+
r"^\|\s*`([a-z0-9-]+\.md)`\s*\|" # rule filename
|
|
40
|
+
r"[^|]*\|" # type
|
|
41
|
+
r"[^|]*\|" # raw
|
|
42
|
+
r"[^|]*\|" # ext
|
|
43
|
+
r"[^|]*\|" # trigger
|
|
44
|
+
r"[^|]*\|" # obs
|
|
45
|
+
r"[^|]*\|" # enforce
|
|
46
|
+
r"[^|]*\|" # hook-cost
|
|
47
|
+
r"\s*([^|]+?)\s*\|" # tier
|
|
48
|
+
r"[^|]*\|" # dormant?
|
|
49
|
+
r"\s*(.*?)\s*\|\s*$", # notes
|
|
50
|
+
re.MULTILINE,
|
|
51
|
+
)
|
|
52
|
+
for m in row_re.finditer(text):
|
|
53
|
+
name, tier, notes = m.group(1), m.group(2).strip(), m.group(3).strip()
|
|
54
|
+
if tier not in VALID_TIERS:
|
|
55
|
+
sys.exit(f"unknown tier '{tier}' for {name}")
|
|
56
|
+
out[name] = (tier, notes)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def write_spreadsheet(classifications: dict[str, tuple[str, str]]) -> None:
|
|
61
|
+
SPREADSHEET.parent.mkdir(parents=True, exist_ok=True)
|
|
62
|
+
lines = [
|
|
63
|
+
"# Tier classification — Phase 2.1 of road-to-feedback-consolidation",
|
|
64
|
+
"",
|
|
65
|
+
"Source: `agents/contexts/rule-trigger-matrix.md` (manual classifications",
|
|
66
|
+
"in `scripts/build_rule_trigger_matrix.py`'s `CLASSIFICATION` table).",
|
|
67
|
+
"Generated by `scripts/_one_off/2026-05/_one_off_tier-retrofit.py`.",
|
|
68
|
+
"",
|
|
69
|
+
"Tier rubric: see `agents/contexts/hardening-pattern.md`.",
|
|
70
|
+
"",
|
|
71
|
+
f"Total: {len(classifications)} rules.",
|
|
72
|
+
"",
|
|
73
|
+
"| Rule | Tier | Rationale |",
|
|
74
|
+
"|---|---|---|",
|
|
75
|
+
]
|
|
76
|
+
for name in sorted(classifications):
|
|
77
|
+
tier, notes = classifications[name]
|
|
78
|
+
lines.append(f"| `{name}` | `{tier}` | {notes} |")
|
|
79
|
+
SPREADSHEET.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, str], str, str]:
|
|
83
|
+
"""Return (kv, raw_block, body). raw_block excludes the --- fences."""
|
|
84
|
+
if not text.startswith("---\n"):
|
|
85
|
+
return {}, "", text
|
|
86
|
+
end = text.find("\n---\n", 4)
|
|
87
|
+
if end == -1:
|
|
88
|
+
return {}, "", text
|
|
89
|
+
raw = text[4:end]
|
|
90
|
+
body = text[end + 5 :]
|
|
91
|
+
kv: dict[str, str] = {}
|
|
92
|
+
for line in raw.splitlines():
|
|
93
|
+
if ":" in line:
|
|
94
|
+
k, _, v = line.partition(":")
|
|
95
|
+
kv[k.strip()] = v.strip()
|
|
96
|
+
return kv, raw, body
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def apply_tier(rule_path: Path, tier: str) -> str:
|
|
100
|
+
"""Return one of: 'unchanged', 'inserted', 'updated'.
|
|
101
|
+
|
|
102
|
+
Tier is always written as a quoted string in YAML (`tier: "<value>"`) so the
|
|
103
|
+
schema enum check (string-only) holds for numeric tiers like `1` and `3`.
|
|
104
|
+
"""
|
|
105
|
+
text = rule_path.read_text(encoding="utf-8")
|
|
106
|
+
kv, raw, body = parse_frontmatter(text)
|
|
107
|
+
if not raw:
|
|
108
|
+
sys.exit(f"{rule_path}: no frontmatter found")
|
|
109
|
+
existing_raw = kv.get("tier")
|
|
110
|
+
existing = existing_raw.strip('"').strip("'") if existing_raw else None
|
|
111
|
+
quoted = f'"{tier}"'
|
|
112
|
+
target_line = f"tier: {quoted}"
|
|
113
|
+
if existing == tier and existing_raw == quoted:
|
|
114
|
+
return "unchanged"
|
|
115
|
+
new_lines: list[str] = []
|
|
116
|
+
inserted = False
|
|
117
|
+
for line in raw.splitlines():
|
|
118
|
+
new_lines.append(line)
|
|
119
|
+
if not inserted and line.startswith("type:"):
|
|
120
|
+
new_lines.append(target_line)
|
|
121
|
+
inserted = True
|
|
122
|
+
if existing is not None:
|
|
123
|
+
new_lines = [
|
|
124
|
+
l if not l.lstrip().startswith("tier:") else target_line
|
|
125
|
+
for l in new_lines
|
|
126
|
+
]
|
|
127
|
+
seen_tier = False
|
|
128
|
+
deduped: list[str] = []
|
|
129
|
+
for l in new_lines:
|
|
130
|
+
if l == target_line:
|
|
131
|
+
if seen_tier:
|
|
132
|
+
continue
|
|
133
|
+
seen_tier = True
|
|
134
|
+
deduped.append(l)
|
|
135
|
+
new_lines = deduped
|
|
136
|
+
result = "updated" if existing != tier or existing_raw != quoted else "unchanged"
|
|
137
|
+
else:
|
|
138
|
+
if not inserted:
|
|
139
|
+
new_lines.insert(0, target_line)
|
|
140
|
+
result = "inserted"
|
|
141
|
+
new_raw = "\n".join(new_lines)
|
|
142
|
+
rule_path.write_text(f"---\n{new_raw}\n---\n{body}", encoding="utf-8")
|
|
143
|
+
return result
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main() -> int:
|
|
147
|
+
classifications = parse_matrix()
|
|
148
|
+
if len(classifications) != 58:
|
|
149
|
+
sys.exit(f"expected 58 rules in matrix, got {len(classifications)}")
|
|
150
|
+
|
|
151
|
+
on_disk = {p.name for p in RULES_DIR.glob("*.md")}
|
|
152
|
+
missing = on_disk - classifications.keys()
|
|
153
|
+
extra = classifications.keys() - on_disk
|
|
154
|
+
if missing or extra:
|
|
155
|
+
sys.exit(f"matrix/disk mismatch: missing={missing} extra={extra}")
|
|
156
|
+
|
|
157
|
+
write_spreadsheet(classifications)
|
|
158
|
+
|
|
159
|
+
counts: dict[str, int] = {"unchanged": 0, "inserted": 0, "updated": 0}
|
|
160
|
+
mirror_counts = {"unchanged": 0, "inserted": 0, "updated": 0, "skipped": 0}
|
|
161
|
+
for name, (tier, _) in classifications.items():
|
|
162
|
+
result = apply_tier(RULES_DIR / name, tier)
|
|
163
|
+
counts[result] += 1
|
|
164
|
+
compressed = COMPRESSED_RULES_DIR / name
|
|
165
|
+
if compressed.exists():
|
|
166
|
+
mirror_counts[apply_tier(compressed, tier)] += 1
|
|
167
|
+
else:
|
|
168
|
+
mirror_counts["skipped"] += 1
|
|
169
|
+
print(
|
|
170
|
+
f"tier-retrofit: spreadsheet={SPREADSHEET.relative_to(REPO)} "
|
|
171
|
+
f"src(unchanged={counts['unchanged']} inserted={counts['inserted']} "
|
|
172
|
+
f"updated={counts['updated']}) "
|
|
173
|
+
f"mirror(unchanged={mirror_counts['unchanged']} inserted={mirror_counts['inserted']} "
|
|
174
|
+
f"updated={mirror_counts['updated']} skipped={mirror_counts['skipped']})"
|
|
175
|
+
)
|
|
176
|
+
return 0
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
if __name__ == "__main__":
|
|
180
|
+
raise SystemExit(main())
|
|
@@ -23,6 +23,8 @@ from __future__ import annotations
|
|
|
23
23
|
|
|
24
24
|
import datetime as _dt
|
|
25
25
|
import json
|
|
26
|
+
import re
|
|
27
|
+
import shutil
|
|
26
28
|
import sys
|
|
27
29
|
from dataclasses import dataclass, field
|
|
28
30
|
from pathlib import Path
|
|
@@ -33,6 +35,10 @@ from scripts.ai_council.orchestrator import render
|
|
|
33
35
|
|
|
34
36
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
35
37
|
SESSIONS_DIR = REPO_ROOT / "agents" / "council-sessions"
|
|
38
|
+
SETTINGS_FILE = REPO_ROOT / ".agent-settings.yml"
|
|
39
|
+
|
|
40
|
+
DEFAULT_RETENTION_DAYS = 14
|
|
41
|
+
_TS_RE = re.compile(r"^(\d{4})-(\d{2})-(\d{2})T(\d{2})-(\d{2})-(\d{2})Z$")
|
|
36
42
|
|
|
37
43
|
|
|
38
44
|
@dataclass
|
|
@@ -69,12 +75,90 @@ def _serialise_response(r: CouncilResponse) -> dict[str, object]:
|
|
|
69
75
|
}
|
|
70
76
|
|
|
71
77
|
|
|
78
|
+
def _load_retention_days(settings_path: Path | None = None) -> int:
|
|
79
|
+
"""Read `ai_council.session_retention_days` from `.agent-settings.yml`.
|
|
80
|
+
|
|
81
|
+
Returns `DEFAULT_RETENTION_DAYS` on any read/parse failure (missing
|
|
82
|
+
file, invalid YAML, missing key, non-int value). Pruning never
|
|
83
|
+
blocks the council on a settings error.
|
|
84
|
+
"""
|
|
85
|
+
path = settings_path or SETTINGS_FILE
|
|
86
|
+
if not path.exists():
|
|
87
|
+
return DEFAULT_RETENTION_DAYS
|
|
88
|
+
try:
|
|
89
|
+
import yaml # type: ignore[import-not-found]
|
|
90
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8")) or {}
|
|
91
|
+
except Exception: # noqa: BLE001 - never block on settings parse
|
|
92
|
+
return DEFAULT_RETENTION_DAYS
|
|
93
|
+
ai = data.get("ai_council") if isinstance(data, dict) else None
|
|
94
|
+
if not isinstance(ai, dict):
|
|
95
|
+
return DEFAULT_RETENTION_DAYS
|
|
96
|
+
raw = ai.get("session_retention_days", DEFAULT_RETENTION_DAYS)
|
|
97
|
+
try:
|
|
98
|
+
return int(raw)
|
|
99
|
+
except (TypeError, ValueError):
|
|
100
|
+
return DEFAULT_RETENTION_DAYS
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _parse_session_timestamp(name: str) -> _dt.datetime | None:
|
|
104
|
+
"""Parse `YYYY-MM-DDTHH-MM-SSZ` directory name to a UTC datetime."""
|
|
105
|
+
m = _TS_RE.match(name)
|
|
106
|
+
if not m:
|
|
107
|
+
return None
|
|
108
|
+
try:
|
|
109
|
+
y, mo, d, h, mi, s = (int(g) for g in m.groups())
|
|
110
|
+
return _dt.datetime(y, mo, d, h, mi, s, tzinfo=_dt.timezone.utc)
|
|
111
|
+
except ValueError:
|
|
112
|
+
return None
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def prune_old_sessions(
|
|
116
|
+
sessions_dir: Path,
|
|
117
|
+
retention_days: int,
|
|
118
|
+
*,
|
|
119
|
+
now: _dt.datetime | None = None,
|
|
120
|
+
) -> list[Path]:
|
|
121
|
+
"""Delete session subdirectories older than `retention_days`.
|
|
122
|
+
|
|
123
|
+
A session is "old" when its directory-name timestamp predates
|
|
124
|
+
`now - retention_days`. Non-matching names (e.g. JSON reports at
|
|
125
|
+
the root, custom folders) are skipped. Never raises — disk
|
|
126
|
+
failures are logged to stderr.
|
|
127
|
+
|
|
128
|
+
Returns the list of deleted directories. `retention_days <= 0`
|
|
129
|
+
disables pruning and returns an empty list.
|
|
130
|
+
"""
|
|
131
|
+
if retention_days <= 0 or not sessions_dir.exists():
|
|
132
|
+
return []
|
|
133
|
+
cutoff = (now or _dt.datetime.now(_dt.timezone.utc)) - _dt.timedelta(days=retention_days)
|
|
134
|
+
removed: list[Path] = []
|
|
135
|
+
try:
|
|
136
|
+
entries = list(sessions_dir.iterdir())
|
|
137
|
+
except OSError as exc: # noqa: BLE001 - never block the report
|
|
138
|
+
print(f"[council:session] prune iterdir failed: {exc}", file=sys.stderr)
|
|
139
|
+
return removed
|
|
140
|
+
for entry in entries:
|
|
141
|
+
if not entry.is_dir():
|
|
142
|
+
continue
|
|
143
|
+
ts = _parse_session_timestamp(entry.name)
|
|
144
|
+
if ts is None or ts >= cutoff:
|
|
145
|
+
continue
|
|
146
|
+
try:
|
|
147
|
+
shutil.rmtree(entry)
|
|
148
|
+
removed.append(entry)
|
|
149
|
+
except OSError as exc: # noqa: BLE001 - never block the report
|
|
150
|
+
print(f"[council:session] prune rmtree failed for {entry}: {exc}",
|
|
151
|
+
file=sys.stderr)
|
|
152
|
+
return removed
|
|
153
|
+
|
|
154
|
+
|
|
72
155
|
def save(
|
|
73
156
|
*,
|
|
74
157
|
manifest: SessionManifest,
|
|
75
158
|
responses: list[CouncilResponse] | Iterable[list[CouncilResponse]],
|
|
76
159
|
sessions_dir: Path | None = None,
|
|
77
160
|
timestamp: str | None = None,
|
|
161
|
+
retention_days: int | None = None,
|
|
78
162
|
) -> Path:
|
|
79
163
|
"""Persist a council call. Returns the session directory.
|
|
80
164
|
|
|
@@ -83,6 +167,11 @@ def save(
|
|
|
83
167
|
- `Iterable[list[CouncilResponse]]` — multi-round, one list per
|
|
84
168
|
round in execution order.
|
|
85
169
|
|
|
170
|
+
`retention_days` controls auto-pruning of older sibling sessions
|
|
171
|
+
after the new one is written. `None` reads the value from
|
|
172
|
+
`.agent-settings.yml` (`ai_council.session_retention_days`,
|
|
173
|
+
default `14`); `0` disables pruning.
|
|
174
|
+
|
|
86
175
|
Disk-write failures are surfaced via a stderr line but do not
|
|
87
176
|
raise; the caller's text report is the source of truth.
|
|
88
177
|
"""
|
|
@@ -141,4 +230,7 @@ def save(
|
|
|
141
230
|
except OSError as exc: # noqa: BLE001 - never block the report
|
|
142
231
|
print(f"[council:session] write failed: {exc}", file=sys.stderr)
|
|
143
232
|
|
|
233
|
+
days = _load_retention_days() if retention_days is None else retention_days
|
|
234
|
+
prune_old_sessions(base, days)
|
|
235
|
+
|
|
144
236
|
return session_dir
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Build agents/contexts/rule-trigger-matrix.md.
|
|
3
|
+
|
|
4
|
+
Emits a single matrix mapping every rule in `.agent-src.uncompressed/rules/`
|
|
5
|
+
to its trigger event, observability, enforcement surface, hook-cost
|
|
6
|
+
estimate, and Tier classification. Sourced from the Phase 1 inventory of
|
|
7
|
+
`road-to-rule-hardening.md` plus `road-to-context-layer-maturity.md`
|
|
8
|
+
Phase 1 (`load_context:` chains).
|
|
9
|
+
|
|
10
|
+
Exit 0 always; this is a generator, not a gate.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import json
|
|
15
|
+
import sys
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
import yaml
|
|
19
|
+
|
|
20
|
+
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
21
|
+
SRC_RULES = REPO_ROOT / ".agent-src.uncompressed" / "rules"
|
|
22
|
+
COMP_RULES = REPO_ROOT / ".agent-src" / "rules"
|
|
23
|
+
SRC_PREFIX = ".agent-src.uncompressed/"
|
|
24
|
+
COMP_PREFIX = ".agent-src/"
|
|
25
|
+
OUT = REPO_ROOT / "agents" / "contexts" / "rule-trigger-matrix.md"
|
|
26
|
+
|
|
27
|
+
# Classification table — one row per rule. See § Methodology in the
|
|
28
|
+
# generated file for the column meanings. Sourced from the Phase 1 audit;
|
|
29
|
+
# reviewed entries carry an empirical signal in `notes`.
|
|
30
|
+
#
|
|
31
|
+
# Columns: trigger, observability, enforcement, hook_cost, tier, dormant
|
|
32
|
+
#
|
|
33
|
+
# trigger: when the rule should fire
|
|
34
|
+
# observability: agent-only | hook | settings | mechanical-already
|
|
35
|
+
# enforcement: output | tool-call | state | hook | none
|
|
36
|
+
# hook_cost: low | medium | high | NA-mechanical | NA-soft
|
|
37
|
+
# tier: 1 | 2a | 2b | 3 | safety-floor | mechanical-already
|
|
38
|
+
# dormant: no | suspected | unknown
|
|
39
|
+
CLASSIFICATION: dict[str, dict] = {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def add(name, trigger, obs, enf, cost, tier, dormant="no", notes=""):
|
|
43
|
+
CLASSIFICATION[name] = dict(
|
|
44
|
+
trigger=trigger, observability=obs, enforcement=enf,
|
|
45
|
+
hook_cost=cost, tier=tier, dormant=dormant, notes=notes,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
# ── Always-rules — safety floor (out of scope for hardening) ──────────
|
|
50
|
+
add("non-destructive-by-default.md", "destructive-op intent", "agent-only",
|
|
51
|
+
"tool-call", "NA-soft", "safety-floor", notes="Safety floor — Iron Law, not modified")
|
|
52
|
+
add("commit-policy.md", "commit intent", "agent-only", "tool-call",
|
|
53
|
+
"NA-soft", "safety-floor", notes="Safety floor — never-ask Iron Law")
|
|
54
|
+
add("scope-control.md", "git-op / refactor intent", "agent-only", "tool-call",
|
|
55
|
+
"NA-soft", "safety-floor", notes="Safety floor — permission gate")
|
|
56
|
+
add("verify-before-complete.md", "completion claim", "agent-only", "output",
|
|
57
|
+
"low", "2b",
|
|
58
|
+
notes="Pre-PR/commit gate. Hookable: detect 'done'/'complete' in reply, require fresh test/quality output in same turn.")
|
|
59
|
+
|
|
60
|
+
# ── Always-rules — Iron-Law pre-send (Tier 3, soft by construction) ───
|
|
61
|
+
add("agent-authority.md", "every turn (router)", "agent-only", "none",
|
|
62
|
+
"NA-soft", "3", notes="Priority index, no trigger of its own")
|
|
63
|
+
add("ask-when-uncertain.md", "pre-send vague-detection", "agent-only", "output",
|
|
64
|
+
"NA-soft", "3", notes="One-question-per-turn — output-rewrite would be needed")
|
|
65
|
+
add("direct-answers.md", "pre-send (every reply)", "agent-only", "output",
|
|
66
|
+
"NA-soft", "3", notes="No-flattery + verify + brevity Iron Laws")
|
|
67
|
+
add("language-and-tone.md", "pre-send language detection", "agent-only", "output",
|
|
68
|
+
"medium", "3",
|
|
69
|
+
notes="Hook could detect German trigger words in last user msg + flag drift. Best-effort marker only.")
|
|
70
|
+
add("no-cheap-questions.md", "pre-send Q&A check", "agent-only", "output",
|
|
71
|
+
"NA-soft", "3", notes="Pre-send self-check, no platform surface")
|
|
72
|
+
|
|
73
|
+
# ── Auto-rules — Tier 1 candidates (mechanizable, deterministic) ──────
|
|
74
|
+
add("chat-history-cadence.md", "per-turn / per-tool / per-phase", "mechanical-already",
|
|
75
|
+
"hook", "NA-mechanical", "mechanical-already",
|
|
76
|
+
notes="PRECEDENT — heartbeat + chat_history.py + hooks. Reference pattern.")
|
|
77
|
+
add("chat-history-ownership.md", "first turn", "hook", "state",
|
|
78
|
+
"low", "1", notes="Detectable: ownership classification at session start")
|
|
79
|
+
add("chat-history-visibility.md", "heartbeat marker emit", "mechanical-already",
|
|
80
|
+
"hook", "NA-mechanical", "mechanical-already",
|
|
81
|
+
notes="Subprocess marker print is already mechanical")
|
|
82
|
+
add("onboarding-gate.md", "first turn (settings.onboarded == false)", "settings",
|
|
83
|
+
"state", "low", "1",
|
|
84
|
+
notes="Pilot candidate — frequency 100% on un-onboarded projects, binary verifiable")
|
|
85
|
+
add("roadmap-progress-sync.md", "file-edit on agents/roadmaps/**", "hook",
|
|
86
|
+
"tool-call", "low", "1",
|
|
87
|
+
notes="Pilot 1 (smallest hook). PostToolUse path filter; already documented in mechanics context.")
|
|
88
|
+
add("context-hygiene.md", "turn counter / tool-loop / topic shift", "hook",
|
|
89
|
+
"state", "medium", "1",
|
|
90
|
+
notes="Per-turn counter + tool-call repetition detector. Cross-platform persistence is the cost driver.")
|
|
91
|
+
add("size-enforcement.md", "file save on .agent-src.uncompressed/{skills,rules,commands}/**",
|
|
92
|
+
"mechanical-already", "tool-call", "NA-mechanical", "mechanical-already",
|
|
93
|
+
notes="Enforced by skill_linter.py + check_always_budget.py")
|
|
94
|
+
add("no-roadmap-references.md", "file save on stable artifacts", "mechanical-already",
|
|
95
|
+
"tool-call", "NA-mechanical", "mechanical-already",
|
|
96
|
+
notes="Enforced by scripts/check_no_roadmap_refs.py (CI gate)")
|
|
97
|
+
add("augment-portability.md", "file save on .agent-src/**", "mechanical-already",
|
|
98
|
+
"tool-call", "NA-mechanical", "mechanical-already",
|
|
99
|
+
notes="Enforced by scripts/check_portability.py")
|
|
100
|
+
add("augment-source-of-truth.md", "file save on .agent-src/ or .augment/",
|
|
101
|
+
"hook", "tool-call", "low", "1",
|
|
102
|
+
notes="Pre-write hook: refuse writes to generated dirs")
|
|
103
|
+
add("package-ci-checks.md", "pre-push to remote", "mechanical-already",
|
|
104
|
+
"hook", "NA-mechanical", "mechanical-already",
|
|
105
|
+
notes="task ci is the gate")
|
|
106
|
+
add("artifact-engagement-recording.md", "phase-step / task end", "mechanical-already",
|
|
107
|
+
"hook", "NA-mechanical", "mechanical-already",
|
|
108
|
+
notes="telemetry:record subprocess is already mechanical")
|
|
109
|
+
|
|
110
|
+
# ── Auto-rules — Tier 2a candidates (marker nudge) ────────────────────
|
|
111
|
+
add("model-recommendation.md", "task-start / topic-shift", "hook",
|
|
112
|
+
"output", "low", "2a",
|
|
113
|
+
notes="Phase 5 prototype target. Marker injection at first user msg + topic-change detection.")
|
|
114
|
+
add("capture-learnings.md", "task completion", "hook", "output",
|
|
115
|
+
"medium", "2a", notes="Post-task marker; learning detection is fuzzy")
|
|
116
|
+
add("skill-improvement-trigger.md", "task completion (settings.skill_improvement)",
|
|
117
|
+
"settings", "state", "low", "2a",
|
|
118
|
+
notes="Settings-flag observable; pipeline already exists")
|
|
119
|
+
add("commit-conventions.md", "commit message draft", "hook", "output",
|
|
120
|
+
"low", "2a", notes="Hook on /commit invocation, marker for conventional-commits format")
|
|
121
|
+
add("docs-sync.md", "file-edit on .augment/{skills,rules,commands}/**", "hook",
|
|
122
|
+
"tool-call", "medium", "2a",
|
|
123
|
+
notes="Detect add/rename/delete; remind to update count + cross-refs")
|
|
124
|
+
add("agent-docs.md", "file-edit on agents/docs/, AGENTS.md", "hook",
|
|
125
|
+
"tool-call", "medium", "2a", notes="Path-pattern based marker")
|
|
126
|
+
add("upstream-proposal.md", "skill/rule create event", "hook", "output",
|
|
127
|
+
"medium", "2a", notes="Marker after new artifact lands")
|
|
128
|
+
add("review-routing-awareness.md", "PR-prep / risk flagging", "hook",
|
|
129
|
+
"output", "medium", "2a",
|
|
130
|
+
notes="Marker when /create-pr or risk-tagging keywords detected")
|
|
131
|
+
add("reviewer-awareness.md", "PR-prep", "hook", "output",
|
|
132
|
+
"medium", "2a", notes="Reviewer-suggestion marker at PR creation")
|
|
133
|
+
add("security-sensitive-stop.md", "file-edit on auth/billing/secrets paths",
|
|
134
|
+
"hook", "tool-call", "low", "2a",
|
|
135
|
+
notes="Path-pattern based marker — strong candidate for low-cost hook")
|
|
136
|
+
add("cli-output-handling.md", "tool-call (verbose CLI)", "hook", "tool-call",
|
|
137
|
+
"low", "2a", notes="Pre-tool-call marker on git/test/lint invocations")
|
|
138
|
+
add("artifact-drafting-protocol.md", "skill/rule create or major rewrite",
|
|
139
|
+
"hook", "output", "medium", "2a",
|
|
140
|
+
notes="Marker on file-create in .agent-src.uncompressed/{skills,rules,commands}/")
|
|
141
|
+
add("missing-tool-handling.md", "tool failure (command not found)", "hook",
|
|
142
|
+
"output", "low", "2a", notes="Post-tool-failure marker — strong fit")
|
|
143
|
+
add("token-efficiency.md", "every reply / verbose-tool invocation", "hook",
|
|
144
|
+
"output", "medium", "2a", notes="Soft Iron Law; nudge via verbose-output detection")
|
|
145
|
+
add("rule-type-governance.md", "rule create/edit", "hook", "tool-call",
|
|
146
|
+
"low", "2a", notes="Linter could enforce; currently advisory")
|
|
147
|
+
add("role-mode-adherence.md", "settings.roles.active_role set", "settings",
|
|
148
|
+
"output", "low", "2a", notes="Mode marker emit at turn end")
|
|
149
|
+
|
|
150
|
+
# ── Auto-rules — Tier 2b (structured injection / gate) ────────────────
|
|
151
|
+
add("downstream-changes.md", "post-edit (callsite check)", "hook",
|
|
152
|
+
"tool-call", "high", "2b",
|
|
153
|
+
notes="Requires callsite analysis — codebase-retrieval-style query. High cost, high value.")
|
|
154
|
+
add("ui-audit-gate.md", "pre-edit on UI files (settings.state.ui_audit empty)",
|
|
155
|
+
"settings", "tool-call", "medium", "2b",
|
|
156
|
+
notes="Block edit until state.ui_audit populated")
|
|
157
|
+
add("preservation-guard.md", "skill/rule merge or compress", "hook",
|
|
158
|
+
"tool-call", "medium", "2b",
|
|
159
|
+
notes="Pre-merge structured check — diff-shape verifiable")
|
|
160
|
+
add("minimal-safe-diff.md", "every diff", "hook", "tool-call",
|
|
161
|
+
"high", "2b", notes="Diff-shape check; reformatting/drive-by detection is fuzzy")
|
|
162
|
+
add("improve-before-implement.md", "task-start (implementation intent)",
|
|
163
|
+
"hook", "output", "medium", "2b",
|
|
164
|
+
notes="Pre-implementation gate; could inject 'validated?' field requirement")
|
|
165
|
+
add("think-before-action.md", "pre-edit", "hook", "output",
|
|
166
|
+
"medium", "2b", notes="Pre-tool-call marker requiring analysis-first")
|
|
167
|
+
add("runtime-safety.md", "skill metadata change", "hook", "tool-call",
|
|
168
|
+
"low", "2b", notes="Linter-enforceable on skill frontmatter")
|
|
169
|
+
add("tool-safety.md", "skill creation (external tool decl)", "hook",
|
|
170
|
+
"tool-call", "low", "2b", notes="Allowlist-enforceable in skill linter")
|
|
171
|
+
add("skill-quality.md", "skill create/edit", "mechanical-already",
|
|
172
|
+
"tool-call", "NA-mechanical", "mechanical-already",
|
|
173
|
+
notes="Enforced by scripts/skill_linter.py")
|
|
174
|
+
add("markdown-safe-codeblocks.md", "markdown output with code", "hook",
|
|
175
|
+
"output", "medium", "2b", notes="Output-shape check; nesting detection")
|
|
176
|
+
|
|
177
|
+
# ── Auto-rules — Tier 3 (inherent soft / topic-only triggers) ─────────
|
|
178
|
+
add("autonomous-execution.md", "workflow decision (trivial vs blocking)",
|
|
179
|
+
"agent-only", "output", "NA-soft", "3",
|
|
180
|
+
notes="Disposition rule; trivial classification is judgment")
|
|
181
|
+
add("user-interaction.md", "pre-send (every Q&A reply)", "agent-only",
|
|
182
|
+
"output", "NA-soft", "3", notes="Numbered-options Iron Law")
|
|
183
|
+
add("guidelines.md", "before code edit (topic match)", "agent-only",
|
|
184
|
+
"output", "NA-soft", "3", notes="Generic 'check guidelines' nudge")
|
|
185
|
+
add("architecture.md", "new file/class/module creation", "agent-only",
|
|
186
|
+
"output", "NA-soft", "3", notes="Architectural decisions — judgment-bound")
|
|
187
|
+
add("php-coding.md", "PHP file edit", "agent-only", "output",
|
|
188
|
+
"NA-soft", "3", notes="Topic-matched coding guideline")
|
|
189
|
+
add("laravel-translations.md", "lang/ file edit", "hook", "tool-call",
|
|
190
|
+
"low", "2a", dormant="suspected",
|
|
191
|
+
notes="Path-pattern detectable but rare in this repo")
|
|
192
|
+
add("e2e-testing.md", "Playwright file edit", "agent-only", "output",
|
|
193
|
+
"NA-soft", "3", notes="Topic-matched")
|
|
194
|
+
add("docker-commands.md", "PHP CLI in Docker context", "agent-only",
|
|
195
|
+
"output", "NA-soft", "3", notes="Topic-matched")
|
|
196
|
+
|
|
197
|
+
# ── Suspected-dormant entries (per roadmap RH Phase 1 explicit list) ──
|
|
198
|
+
add("command-suggestion-policy.md", "user prompt match (engine-driven)",
|
|
199
|
+
"mechanical-already", "hook", "NA-mechanical", "mechanical-already",
|
|
200
|
+
dormant="suspected",
|
|
201
|
+
notes="Engine in scripts/command_suggester/ exists; live-fire signal unverified — needs telemetry pass")
|
|
202
|
+
add("slash-command-routing-policy.md", "user msg starts with /",
|
|
203
|
+
"hook", "tool-call", "low", "1", dormant="suspected",
|
|
204
|
+
notes="Pattern-detection; live-fire signal unverified")
|
|
205
|
+
add("analysis-skill-routing.md", "analysis skill picker", "agent-only",
|
|
206
|
+
"output", "NA-soft", "3", dormant="suspected",
|
|
207
|
+
notes="Skill-router; no observable surface today")
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def fm(path):
|
|
211
|
+
txt = path.read_text(encoding="utf-8")
|
|
212
|
+
if not txt.startswith("---\n"):
|
|
213
|
+
return {}
|
|
214
|
+
end = txt.find("\n---\n", 4)
|
|
215
|
+
if end == -1:
|
|
216
|
+
return {}
|
|
217
|
+
try:
|
|
218
|
+
return yaml.safe_load(txt[4:end]) or {}
|
|
219
|
+
except yaml.YAMLError:
|
|
220
|
+
return {}
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def to_comp(entry: str) -> Path:
|
|
224
|
+
if entry.startswith(SRC_PREFIX):
|
|
225
|
+
return REPO_ROOT / (COMP_PREFIX + entry[len(SRC_PREFIX):])
|
|
226
|
+
return REPO_ROOT / entry
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def walk(rule: Path):
|
|
230
|
+
seen: set[Path] = set()
|
|
231
|
+
chains: list[tuple[str, int]] = []
|
|
232
|
+
stack = [(rule, 0, "")]
|
|
233
|
+
while stack:
|
|
234
|
+
node, depth, _ = stack.pop()
|
|
235
|
+
for entry in (fm(node).get("load_context") or []) + (fm(node).get("load_context_eager") or []):
|
|
236
|
+
comp = to_comp(str(entry))
|
|
237
|
+
if depth + 1 > 2 or not comp.exists() or comp in seen:
|
|
238
|
+
continue
|
|
239
|
+
seen.add(comp)
|
|
240
|
+
chains.append((str(entry), comp.stat().st_size))
|
|
241
|
+
stack.append((comp, depth + 1, str(entry)))
|
|
242
|
+
return chains
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def emit():
|
|
246
|
+
rules = sorted(SRC_RULES.glob("*.md"))
|
|
247
|
+
rows = []
|
|
248
|
+
for r in rules:
|
|
249
|
+
f = fm(r)
|
|
250
|
+
rtype = f.get("type", "?")
|
|
251
|
+
comp = COMP_RULES / r.name
|
|
252
|
+
raw = comp.stat().st_size if comp.exists() else r.stat().st_size
|
|
253
|
+
ctx_chains = walk(comp if comp.exists() else r)
|
|
254
|
+
ext = raw + sum(s for _, s in ctx_chains)
|
|
255
|
+
rows.append((r.name, rtype, raw, ext, ctx_chains))
|
|
256
|
+
|
|
257
|
+
lines: list[str] = []
|
|
258
|
+
lines.append("# Rule Trigger Matrix")
|
|
259
|
+
lines.append("")
|
|
260
|
+
lines.append("**Source:** Phase 1 of `road-to-rule-hardening.md` (self-check audit) +")
|
|
261
|
+
lines.append("Phase 1 of `road-to-context-layer-maturity.md` (`load_context:` inventory).")
|
|
262
|
+
lines.append("**Generated by:** `scripts/build_rule_trigger_matrix.py` — re-run after rule")
|
|
263
|
+
lines.append("set changes. Manual classifications live in the script's `CLASSIFICATION`")
|
|
264
|
+
lines.append("table; size and context-chain columns are derived from the rule files.")
|
|
265
|
+
lines.append("")
|
|
266
|
+
lines.append("## Methodology")
|
|
267
|
+
lines.append("")
|
|
268
|
+
lines.append("| Column | Meaning |")
|
|
269
|
+
lines.append("|---|---|")
|
|
270
|
+
lines.append("| `type` | Frontmatter `type` (`always` / `auto`) |")
|
|
271
|
+
lines.append("| `raw` | Compressed rule size in chars (`.agent-src/rules/<name>`) |")
|
|
272
|
+
lines.append("| `ext` | Extended size under Model (b): raw + transitive `load_context` |")
|
|
273
|
+
lines.append("| `trigger` | Observable event that should activate the rule |")
|
|
274
|
+
lines.append("| `obs` | Where the trigger is observable: `hook` (platform hook), `settings` (`.agent-settings.yml` state), `agent-only` (in-head), `mechanical-already` (precedent — already enforced by a script) |")
|
|
275
|
+
lines.append("| `enforce` | Surface where the rule's effect lands: `output` / `tool-call` / `state` / `hook` / `none` |")
|
|
276
|
+
lines.append("| `hook-cost` | Engineering cost to mechanise across Augment + Claude Code: `low` (≤ 1 day, single hook script), `medium` (1–3 days, cross-platform persistence), `high` (≥ 3 days, semantic analysis or output rewrite), `NA-mechanical` (precedent — script exists), `NA-soft` (no platform mechanism plausible) |")
|
|
277
|
+
lines.append("| `tier` | Per RH roadmap: `1` mechanical · `2a` marker nudge · `2b` structured injection · `3` inherent soft · `safety-floor` (Iron-Law, never modified) · `mechanical-already` (precedent) |")
|
|
278
|
+
lines.append("| `dormant?` | Has the rule observably fired? `no` (yes, fires) · `suspected` (per RH Phase 1 explicit list) · `unknown` |")
|
|
279
|
+
lines.append("")
|
|
280
|
+
lines.append("## Tier counts")
|
|
281
|
+
lines.append("")
|
|
282
|
+
by_tier: dict[str, list[str]] = {}
|
|
283
|
+
for name, _, _, _, _ in rows:
|
|
284
|
+
t = CLASSIFICATION.get(name, {}).get("tier", "?")
|
|
285
|
+
by_tier.setdefault(t, []).append(name)
|
|
286
|
+
for t in ("safety-floor", "mechanical-already", "1", "2a", "2b", "3", "?"):
|
|
287
|
+
if t in by_tier:
|
|
288
|
+
lines.append(f"- **Tier `{t}`** — {len(by_tier[t])} rules")
|
|
289
|
+
lines.append("")
|
|
290
|
+
lines.append("## Matrix")
|
|
291
|
+
lines.append("")
|
|
292
|
+
lines.append("| Rule | type | raw | ext | trigger | obs | enforce | hook-cost | tier | dormant? | notes |")
|
|
293
|
+
lines.append("|---|---|---:|---:|---|---|---|---|---|---|---|")
|
|
294
|
+
for name, rtype, raw, ext, _ in rows:
|
|
295
|
+
c = CLASSIFICATION.get(name)
|
|
296
|
+
if c is None:
|
|
297
|
+
lines.append(f"| `{name}` | {rtype} | {raw} | {ext} | — | — | — | — | **?** | unknown | NOT CLASSIFIED |")
|
|
298
|
+
continue
|
|
299
|
+
lines.append(
|
|
300
|
+
f"| `{name}` | {rtype} | {raw} | {ext} | "
|
|
301
|
+
f"{c['trigger']} | {c['observability']} | {c['enforcement']} | "
|
|
302
|
+
f"{c['hook_cost']} | {c['tier']} | {c['dormant']} | {c['notes']} |"
|
|
303
|
+
)
|
|
304
|
+
lines.append("")
|
|
305
|
+
lines.append("## `load_context:` chains (CL Phase 1 inventory)")
|
|
306
|
+
lines.append("")
|
|
307
|
+
lines.append("Rules that load at least one context, with `rule → context → depth → chars`.")
|
|
308
|
+
lines.append("Chars are measured on the compressed context file (Model (b) literal).")
|
|
309
|
+
lines.append("")
|
|
310
|
+
lines.append("| Rule | Context | Depth | Chars |")
|
|
311
|
+
lines.append("|---|---|---:|---:|")
|
|
312
|
+
for name, _, _, _, chains in rows:
|
|
313
|
+
if not chains:
|
|
314
|
+
continue
|
|
315
|
+
for entry, size in chains:
|
|
316
|
+
depth = entry.count("/") - entry.count("contexts/") + 1 # heuristic
|
|
317
|
+
depth = 1 # all entries from this script are top-level (depth 1) since walk() returns flattened set
|
|
318
|
+
lines.append(f"| `{name}` | `{entry}` | {depth} | {size} |")
|
|
319
|
+
lines.append("")
|
|
320
|
+
lines.append("## Dormant-suspected (per RH Phase 1)")
|
|
321
|
+
lines.append("")
|
|
322
|
+
dormants = [n for n, c in CLASSIFICATION.items() if c["dormant"] == "suspected"]
|
|
323
|
+
for d in sorted(dormants):
|
|
324
|
+
lines.append(f"- `{d}` — {CLASSIFICATION[d]['notes']}")
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append("**Action:** absence of failures ≠ healthy trigger. Each suspected-dormant")
|
|
327
|
+
lines.append("rule needs a one-session live-fire test before its Tier classification is")
|
|
328
|
+
lines.append("locked. Tracked under RH Phase 1 follow-up.")
|
|
329
|
+
lines.append("")
|
|
330
|
+
lines.append("## Pilot candidates (RH Phase 3)")
|
|
331
|
+
lines.append("")
|
|
332
|
+
lines.append("Per the RH roadmap pilot-selection criteria (frequency ≥ 30 %, ≥ 2 observed")
|
|
333
|
+
lines.append("failures, binary-verifiable trigger, hook-cost = `low`):")
|
|
334
|
+
lines.append("")
|
|
335
|
+
lines.append("1. **`roadmap-progress-sync`** — file-edit hook on `agents/roadmaps/**`, low cost, deterministic.")
|
|
336
|
+
lines.append("2. **`onboarding-gate`** — first-turn settings check, 100 % frequency on un-onboarded projects.")
|
|
337
|
+
lines.append("3. **`context-hygiene`** — turn counter, medium cost (cross-platform persistence).")
|
|
338
|
+
lines.append("")
|
|
339
|
+
lines.append("Order locked in RH Phase 3: 1 → 2 → 3 (smallest hook first).")
|
|
340
|
+
lines.append("")
|
|
341
|
+
lines.append("## Cross-references")
|
|
342
|
+
lines.append("")
|
|
343
|
+
lines.append("- Budget contract: [`docs/contracts/load-context-budget-model.md`](../../docs/contracts/load-context-budget-model.md)")
|
|
344
|
+
lines.append("- Pattern precedent: `chat-history-cadence` (heartbeat hook + `scripts/chat_history.py`)")
|
|
345
|
+
lines.append("- Phase 2A finding: [`adr-always-rule-context-split-not-viable.md`](adr-always-rule-context-split-not-viable.md)")
|
|
346
|
+
lines.append("")
|
|
347
|
+
|
|
348
|
+
OUT.parent.mkdir(parents=True, exist_ok=True)
|
|
349
|
+
OUT.write_text("\n".join(lines), encoding="utf-8")
|
|
350
|
+
print(f"✅ Wrote {OUT.relative_to(REPO_ROOT)} ({len(rows)} rules, {len(lines)} lines)")
|
|
351
|
+
# Sanity: every rule classified
|
|
352
|
+
missing = [n for n, *_ in rows if n not in CLASSIFICATION]
|
|
353
|
+
if missing:
|
|
354
|
+
print(f"⚠️ {len(missing)} rule(s) not classified: {missing}", file=sys.stderr)
|
|
355
|
+
return 2
|
|
356
|
+
return 0
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
if __name__ == "__main__":
|
|
360
|
+
sys.exit(emit())
|