@event4u/agent-config 2.13.0 → 2.14.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/memory/learn-low-impact.md +143 -0
- package/.agent-src/rules/ask-when-uncertain.md +10 -6
- package/.agent-src/rules/copilot-routing.md +1 -1
- package/.agent-src/rules/devcontainer-routing.md +1 -1
- package/.agent-src/rules/external-reference-deep-dive.md +1 -1
- package/.agent-src/rules/fast-path-marker-visibility.md +38 -0
- package/.agent-src/rules/low-impact-corpus-privacy-floor.md +74 -0
- package/.agent-src/rules/symfony-routing.md +1 -1
- package/.agent-src/skills/ai-council/SKILL.md +208 -8
- package/.agent-src/templates/agents/agent-project-settings.example.yml +1 -1
- package/.claude-plugin/marketplace.json +2 -1
- package/CHANGELOG.md +299 -124
- package/README.md +6 -6
- package/config/gitignore-block.txt +6 -0
- package/docs/architecture.md +12 -12
- package/docs/archive/CHANGELOG-pre-2.11.0.md +141 -0
- package/docs/catalog.md +10 -7
- package/docs/contracts/adr-architectural-consensus-mechanism.md +4 -3
- package/docs/contracts/adr-level-6-productization.md +7 -9
- package/docs/contracts/ai-council-config.md +492 -20
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/command-surface-tiers.md +3 -2
- package/docs/contracts/cost-profile-defaults.md +5 -0
- package/docs/contracts/decision-engine-gates.md +5 -0
- package/docs/contracts/decision-trace-v1.md +2 -2
- package/docs/contracts/file-ownership-matrix.json +1735 -72
- package/docs/contracts/installed-tools-lockfile.md +2 -1
- package/docs/contracts/low-impact-corpus-format.md +95 -0
- package/docs/contracts/mcp-beta-criteria.md +6 -5
- package/docs/contracts/mcp-cloud-scope.md +5 -4
- package/docs/contracts/multi-tool-projection-fidelity.md +8 -2
- package/docs/contracts/release-trunk-sync.md +4 -3
- package/docs/contracts/tier-3-contrib-plugin.md +5 -6
- package/docs/getting-started.md +2 -2
- package/docs/guidelines/agent-infra/installed-tools-manifest.md +2 -1
- package/docs/installation.md +32 -0
- package/package.json +1 -1
- package/scripts/_cli/cmd_doctor.py +134 -0
- package/scripts/ai_council/airgap.py +165 -0
- package/scripts/ai_council/cli_hints.py +123 -0
- package/scripts/ai_council/clients.py +787 -5
- package/scripts/ai_council/compile_corpus.py +178 -0
- package/scripts/ai_council/confidence_gate.py +156 -0
- package/scripts/ai_council/config.py +1007 -11
- package/scripts/ai_council/consensus.py +41 -2
- package/scripts/ai_council/events_log.py +137 -0
- package/scripts/ai_council/learn_low_impact_preview.py +252 -0
- package/scripts/ai_council/low_impact.py +714 -0
- package/scripts/ai_council/low_impact_corpus.py +466 -0
- package/scripts/ai_council/low_impact_intake.py +163 -0
- package/scripts/ai_council/modes.py +6 -1
- package/scripts/ai_council/necessity.py +782 -0
- package/scripts/ai_council/orchestrator.py +252 -14
- package/scripts/ai_council/probation_gate.py +152 -0
- package/scripts/ai_council/redact_low_impact_entry.py +155 -0
- package/scripts/ai_council/replay.py +155 -0
- package/scripts/ai_council/session.py +19 -1
- package/scripts/ai_council/shadow_dispatch.py +235 -0
- package/scripts/ai_council/solo_dispatch.py +226 -0
- package/scripts/audit_cloud_compatibility.py +74 -0
- package/scripts/audit_command_surface.py +363 -0
- package/scripts/check_council_layout.py +11 -0
- package/scripts/council_cli.py +1046 -15
- package/scripts/install.sh +12 -0
|
@@ -207,6 +207,70 @@ def scan() -> list[dict]:
|
|
|
207
207
|
return rows
|
|
208
208
|
|
|
209
209
|
|
|
210
|
+
# step-9 P11 · U3 — Iron-Law bypass scan. Any Python module that loads
|
|
211
|
+
# `agents/.ai-council.yml` directly (yaml.safe_load / open + parse) instead
|
|
212
|
+
# of going through `scripts.ai_council.config.load_council_config` skips the
|
|
213
|
+
# `_reject_top_level_locked_dispatch` gate and is a potential Iron-Law
|
|
214
|
+
# bypass. The scan is intentionally over-broad — false positives are
|
|
215
|
+
# annotated with `# iron-law-ok: …` on the offending line.
|
|
216
|
+
_IRON_LAW_YAML_LOAD_RE = re.compile(
|
|
217
|
+
r"yaml\.(?:safe_load|load|full_load|unsafe_load)\s*\(",
|
|
218
|
+
)
|
|
219
|
+
_IRON_LAW_AI_COUNCIL_REF_RE = re.compile(
|
|
220
|
+
r"['\"]\.ai-council\.yml['\"]|ai-council\.yml",
|
|
221
|
+
)
|
|
222
|
+
_IRON_LAW_ALLOWLIST = (
|
|
223
|
+
# the canonical loader itself
|
|
224
|
+
"scripts/ai_council/config.py",
|
|
225
|
+
# tests are allowed to construct synthetic configs directly
|
|
226
|
+
"tests/",
|
|
227
|
+
# this audit script's own pattern definitions
|
|
228
|
+
"scripts/audit_cloud_compatibility.py",
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _iron_law_bypass_scan() -> list[dict]:
|
|
233
|
+
"""Scan ``scripts/`` for code that bypasses the Iron-Law validator.
|
|
234
|
+
|
|
235
|
+
A bypass is any module that calls a raw YAML loader on a value
|
|
236
|
+
associated with ``.ai-council.yml`` outside the canonical loader
|
|
237
|
+
in ``scripts/ai_council/config.py``. Proximity heuristic: the YAML
|
|
238
|
+
load line, or the 3 lines preceding it, must reference
|
|
239
|
+
``ai-council.yml``. Annotate intentional cases with
|
|
240
|
+
``# iron-law-ok: <reason>`` on the load line to suppress.
|
|
241
|
+
"""
|
|
242
|
+
findings: list[dict] = []
|
|
243
|
+
scripts_dir = ROOT / "scripts"
|
|
244
|
+
if not scripts_dir.is_dir():
|
|
245
|
+
return findings
|
|
246
|
+
for py in sorted(scripts_dir.rglob("*.py")):
|
|
247
|
+
rel = py.relative_to(ROOT).as_posix()
|
|
248
|
+
if any(rel.startswith(p) for p in _IRON_LAW_ALLOWLIST):
|
|
249
|
+
continue
|
|
250
|
+
try:
|
|
251
|
+
text = py.read_text(encoding="utf-8")
|
|
252
|
+
except OSError:
|
|
253
|
+
continue
|
|
254
|
+
lines = text.splitlines()
|
|
255
|
+
offending: list[int] = []
|
|
256
|
+
for i, line in enumerate(lines, start=1):
|
|
257
|
+
if not _IRON_LAW_YAML_LOAD_RE.search(line):
|
|
258
|
+
continue
|
|
259
|
+
if "iron-law-ok" in line:
|
|
260
|
+
continue
|
|
261
|
+
window = "\n".join(lines[max(0, i - 4):i])
|
|
262
|
+
if _IRON_LAW_AI_COUNCIL_REF_RE.search(window):
|
|
263
|
+
offending.append(i)
|
|
264
|
+
if offending:
|
|
265
|
+
findings.append({
|
|
266
|
+
"path": rel,
|
|
267
|
+
"lines": offending,
|
|
268
|
+
"reason": "raw YAML load on ai-council.yml — bypasses "
|
|
269
|
+
"_reject_top_level_locked_dispatch",
|
|
270
|
+
})
|
|
271
|
+
return findings
|
|
272
|
+
|
|
273
|
+
|
|
210
274
|
def summarize(rows: list[dict]) -> dict:
|
|
211
275
|
by_tier = Counter(r["tier"] for r in rows)
|
|
212
276
|
by_kind_tier: dict[str, Counter] = {}
|
|
@@ -251,8 +315,18 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
251
315
|
help="filter --details to one cloud-action category",
|
|
252
316
|
)
|
|
253
317
|
p.add_argument("--format", choices=["json", "md"], default="json")
|
|
318
|
+
p.add_argument(
|
|
319
|
+
"--iron-law",
|
|
320
|
+
action="store_true",
|
|
321
|
+
help="scan scripts/ for Iron-Law validator bypasses (step-9 P11 · U3)",
|
|
322
|
+
)
|
|
254
323
|
args = p.parse_args(argv)
|
|
255
324
|
|
|
325
|
+
if args.iron_law:
|
|
326
|
+
findings = _iron_law_bypass_scan()
|
|
327
|
+
print(json.dumps({"iron_law_bypass_findings": findings}, indent=2))
|
|
328
|
+
return 1 if findings else 0
|
|
329
|
+
|
|
256
330
|
rows = scan()
|
|
257
331
|
summary = summarize(rows)
|
|
258
332
|
|
|
@@ -0,0 +1,363 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Command-surface inventory + overlap detection + usage signal.
|
|
3
|
+
|
|
4
|
+
Walks ``.agent-src.uncompressed/commands/**/*.md``, collects metadata for
|
|
5
|
+
each command (path, description, aliases, line count, last-modified),
|
|
6
|
+
flags overlap pairs by keyword-cosine similarity, and adds a usage
|
|
7
|
+
signal from git history (commands not touched in 90+ days are
|
|
8
|
+
candidates for retirement).
|
|
9
|
+
|
|
10
|
+
Output:
|
|
11
|
+
- ``agents/reports/command-surface.json`` (machine-readable)
|
|
12
|
+
- ``agents/reports/command-surface.md`` (human-readable)
|
|
13
|
+
|
|
14
|
+
Context: ``agents/roadmaps/step-2-feedback-followup.md`` Phase 1 —
|
|
15
|
+
GPT's PR-#148 "108 commands" cognitive-load warning needs empirical
|
|
16
|
+
verification before any retirement decisions are made.
|
|
17
|
+
|
|
18
|
+
Usage:
|
|
19
|
+
python3 scripts/audit_command_surface.py
|
|
20
|
+
python3 scripts/audit_command_surface.py --root DIR
|
|
21
|
+
python3 scripts/audit_command_surface.py --quiet
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import math
|
|
29
|
+
import re
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
from collections import Counter
|
|
33
|
+
from dataclasses import asdict, dataclass, field
|
|
34
|
+
from datetime import datetime, timezone
|
|
35
|
+
from itertools import combinations
|
|
36
|
+
from pathlib import Path
|
|
37
|
+
from typing import List
|
|
38
|
+
|
|
39
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
40
|
+
DEFAULT_ROOT = REPO_ROOT / ".agent-src.uncompressed" / "commands"
|
|
41
|
+
REPORT_DIR = REPO_ROOT / "agents" / "reports"
|
|
42
|
+
OUT_JSON = REPORT_DIR / "command-surface.json"
|
|
43
|
+
OUT_MD = REPORT_DIR / "command-surface.md"
|
|
44
|
+
|
|
45
|
+
FRONTMATTER_RE = re.compile(r"^---\n(.*?)\n---", re.DOTALL)
|
|
46
|
+
DESCRIPTION_RE = re.compile(r'^description:\s*"?(.*?)"?\s*$', re.MULTILINE)
|
|
47
|
+
ALIASES_RE = re.compile(r"^aliases:\s*(.*)$", re.MULTILINE)
|
|
48
|
+
NAME_RE = re.compile(r"^name:\s*(.*)$", re.MULTILINE)
|
|
49
|
+
CLUSTER_RE = re.compile(r"^cluster:\s*(.*)$", re.MULTILINE)
|
|
50
|
+
TIER_RE = re.compile(r"^tier:\s*(\d+)", re.MULTILINE)
|
|
51
|
+
|
|
52
|
+
STOPWORDS = {
|
|
53
|
+
"the", "and", "for", "with", "when", "use", "or", "of", "to", "a", "an",
|
|
54
|
+
"is", "in", "on", "by", "be", "at", "as", "it", "if", "are", "this",
|
|
55
|
+
"that", "from", "but", "not", "can", "any", "all", "no", "after",
|
|
56
|
+
"before", "during", "user", "agent", "code", "project", "via", "into",
|
|
57
|
+
"onto", "even", "without", "naming", "run", "runs", "running", "each",
|
|
58
|
+
"every", "one", "two", "now", "then", "also", "based", "default",
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
OVERLAP_COSINE_THRESHOLD = 0.6
|
|
62
|
+
# Commands with few commits AND younger than this many days since first
|
|
63
|
+
# commit are flagged as low-signal — newer, less battle-tested entries.
|
|
64
|
+
LOW_SIGNAL_COMMIT_COUNT = 2
|
|
65
|
+
LOW_SIGNAL_AGE_DAYS = 30
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@dataclass
|
|
69
|
+
class Command:
|
|
70
|
+
name: str
|
|
71
|
+
path: str
|
|
72
|
+
relpath: str
|
|
73
|
+
directory: str
|
|
74
|
+
description: str
|
|
75
|
+
aliases: List[str] = field(default_factory=list)
|
|
76
|
+
tier: int | None = None
|
|
77
|
+
cluster: str = ""
|
|
78
|
+
line_count: int = 0
|
|
79
|
+
last_modified_iso: str = ""
|
|
80
|
+
days_since_modified: int | None = None
|
|
81
|
+
commit_count: int = 0
|
|
82
|
+
first_commit_iso: str = ""
|
|
83
|
+
days_since_first_commit: int | None = None
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def parse_frontmatter(text: str) -> dict:
|
|
87
|
+
m = FRONTMATTER_RE.search(text)
|
|
88
|
+
if not m:
|
|
89
|
+
return {}
|
|
90
|
+
block = m.group(1)
|
|
91
|
+
out: dict = {}
|
|
92
|
+
if d := DESCRIPTION_RE.search(block):
|
|
93
|
+
out["description"] = d.group(1).strip()
|
|
94
|
+
if n := NAME_RE.search(block):
|
|
95
|
+
out["name"] = n.group(1).strip().strip('"').strip("'")
|
|
96
|
+
if c := CLUSTER_RE.search(block):
|
|
97
|
+
out["cluster"] = c.group(1).strip().strip('"').strip("'")
|
|
98
|
+
if t := TIER_RE.search(block):
|
|
99
|
+
out["tier"] = int(t.group(1))
|
|
100
|
+
if a := ALIASES_RE.search(block):
|
|
101
|
+
raw = a.group(1).strip()
|
|
102
|
+
if raw.startswith("["):
|
|
103
|
+
inner = raw.strip("[]")
|
|
104
|
+
out["aliases"] = [x.strip().strip('"').strip("'") for x in inner.split(",") if x.strip()]
|
|
105
|
+
else:
|
|
106
|
+
out["aliases"] = [raw.strip('"').strip("'")] if raw else []
|
|
107
|
+
return out
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def keyword_vector(text: str) -> Counter[str]:
|
|
111
|
+
tokens = re.findall(r"[a-z][a-z0-9_-]{2,}", text.lower())
|
|
112
|
+
return Counter(t for t in tokens if t not in STOPWORDS)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def cosine(a: Counter[str], b: Counter[str]) -> float:
|
|
116
|
+
if not a or not b:
|
|
117
|
+
return 0.0
|
|
118
|
+
shared = set(a) & set(b)
|
|
119
|
+
if not shared:
|
|
120
|
+
return 0.0
|
|
121
|
+
num = sum(a[t] * b[t] for t in shared)
|
|
122
|
+
da = math.sqrt(sum(v * v for v in a.values()))
|
|
123
|
+
db = math.sqrt(sum(v * v for v in b.values()))
|
|
124
|
+
return num / (da * db) if da and db else 0.0
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def git_last_modified(path: Path) -> tuple[str, int | None]:
|
|
128
|
+
try:
|
|
129
|
+
out = subprocess.check_output(
|
|
130
|
+
["git", "log", "--follow", "-1", "--format=%cI", "--", str(path)],
|
|
131
|
+
cwd=REPO_ROOT, stderr=subprocess.DEVNULL, text=True,
|
|
132
|
+
).strip()
|
|
133
|
+
if not out:
|
|
134
|
+
return "", None
|
|
135
|
+
ts = datetime.fromisoformat(out)
|
|
136
|
+
days = (datetime.now(timezone.utc) - ts).days
|
|
137
|
+
return out, days
|
|
138
|
+
except (subprocess.CalledProcessError, ValueError):
|
|
139
|
+
return "", None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def git_history(path: Path) -> tuple[int, str, int | None]:
|
|
143
|
+
"""Return (commit_count, first_commit_iso, days_since_first_commit).
|
|
144
|
+
|
|
145
|
+
Uses ``--follow`` so renames (e.g. the ``.augment.uncompressed`` →
|
|
146
|
+
``.agent-src.uncompressed`` rename) don't reset the per-file history.
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
out = subprocess.check_output(
|
|
150
|
+
["git", "log", "--follow", "--format=%cI", "--", str(path)],
|
|
151
|
+
cwd=REPO_ROOT, stderr=subprocess.DEVNULL, text=True,
|
|
152
|
+
).strip().splitlines()
|
|
153
|
+
if not out:
|
|
154
|
+
return 0, "", None
|
|
155
|
+
first = out[-1]
|
|
156
|
+
ts = datetime.fromisoformat(first)
|
|
157
|
+
days = (datetime.now(timezone.utc) - ts).days
|
|
158
|
+
return len(out), first, days
|
|
159
|
+
except (subprocess.CalledProcessError, ValueError):
|
|
160
|
+
return 0, "", None
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def collect(root: Path) -> List[Command]:
|
|
164
|
+
commands: List[Command] = []
|
|
165
|
+
for md in sorted(root.rglob("*.md")):
|
|
166
|
+
if any(p == "_archive" for p in md.parts):
|
|
167
|
+
continue
|
|
168
|
+
text = md.read_text(encoding="utf-8")
|
|
169
|
+
fm = parse_frontmatter(text)
|
|
170
|
+
rel = md.relative_to(REPO_ROOT)
|
|
171
|
+
directory = str(md.parent.relative_to(root)) if md.parent != root else "."
|
|
172
|
+
last_iso, days = git_last_modified(md)
|
|
173
|
+
n_commits, first_iso, first_days = git_history(md)
|
|
174
|
+
commands.append(Command(
|
|
175
|
+
name=fm.get("name", md.stem),
|
|
176
|
+
path=str(md),
|
|
177
|
+
relpath=str(rel),
|
|
178
|
+
directory=directory,
|
|
179
|
+
description=fm.get("description", ""),
|
|
180
|
+
aliases=fm.get("aliases", []),
|
|
181
|
+
tier=fm.get("tier"),
|
|
182
|
+
cluster=fm.get("cluster", ""),
|
|
183
|
+
line_count=len(text.splitlines()),
|
|
184
|
+
last_modified_iso=last_iso,
|
|
185
|
+
days_since_modified=days,
|
|
186
|
+
commit_count=n_commits,
|
|
187
|
+
first_commit_iso=first_iso,
|
|
188
|
+
days_since_first_commit=first_days,
|
|
189
|
+
))
|
|
190
|
+
return commands
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def find_overlap_pairs(commands: List[Command]) -> list[dict]:
|
|
194
|
+
vectors = {c.relpath: keyword_vector(c.description) for c in commands}
|
|
195
|
+
pairs: list[dict] = []
|
|
196
|
+
for a, b in combinations(commands, 2):
|
|
197
|
+
if not a.description or not b.description:
|
|
198
|
+
continue
|
|
199
|
+
sim = cosine(vectors[a.relpath], vectors[b.relpath])
|
|
200
|
+
if sim < OVERLAP_COSINE_THRESHOLD:
|
|
201
|
+
continue
|
|
202
|
+
pairs.append({
|
|
203
|
+
"a": a.relpath,
|
|
204
|
+
"b": b.relpath,
|
|
205
|
+
"a_name": a.name,
|
|
206
|
+
"b_name": b.name,
|
|
207
|
+
"cosine": round(sim, 3),
|
|
208
|
+
"a_description": a.description,
|
|
209
|
+
"b_description": b.description,
|
|
210
|
+
})
|
|
211
|
+
return sorted(pairs, key=lambda p: -p["cosine"])
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def render_md(commands: List[Command], pairs: list[dict]) -> str:
|
|
215
|
+
by_dir: dict[str, list[Command]] = {}
|
|
216
|
+
for c in commands:
|
|
217
|
+
by_dir.setdefault(c.directory, []).append(c)
|
|
218
|
+
|
|
219
|
+
low_signal = [
|
|
220
|
+
c for c in commands
|
|
221
|
+
if c.commit_count and c.commit_count <= LOW_SIGNAL_COMMIT_COUNT
|
|
222
|
+
and (c.days_since_first_commit or 0) <= LOW_SIGNAL_AGE_DAYS
|
|
223
|
+
]
|
|
224
|
+
|
|
225
|
+
lines = [
|
|
226
|
+
"# Command-Surface Inventory",
|
|
227
|
+
"",
|
|
228
|
+
f"> Generated by `scripts/audit_command_surface.py`. "
|
|
229
|
+
f"Source: `.agent-src.uncompressed/commands/`.",
|
|
230
|
+
"",
|
|
231
|
+
"## Summary",
|
|
232
|
+
"",
|
|
233
|
+
f"- **Total commands:** {len(commands)}",
|
|
234
|
+
f"- **Top-level commands (directory `.`):** {len(by_dir.get('.', []))}",
|
|
235
|
+
f"- **Sub-cluster directories:** {len([d for d in by_dir if d != '.'])}",
|
|
236
|
+
f"- **Low-signal (≤{LOW_SIGNAL_COMMIT_COUNT} commits AND ≤{LOW_SIGNAL_AGE_DAYS}d old):** {len(low_signal)}",
|
|
237
|
+
f"- **Overlap pairs (cosine ≥ {OVERLAP_COSINE_THRESHOLD}):** {len(pairs)}",
|
|
238
|
+
"",
|
|
239
|
+
"## Per-directory counts",
|
|
240
|
+
"",
|
|
241
|
+
"| Directory | Count |",
|
|
242
|
+
"|---|---:|",
|
|
243
|
+
]
|
|
244
|
+
for d in sorted(by_dir):
|
|
245
|
+
lines.append(f"| `{d}` | {len(by_dir[d])} |")
|
|
246
|
+
lines.append("")
|
|
247
|
+
|
|
248
|
+
lines += ["## Likely-overlapping pairs", ""]
|
|
249
|
+
if not pairs:
|
|
250
|
+
lines.append("_No pairs above threshold._")
|
|
251
|
+
else:
|
|
252
|
+
lines += [
|
|
253
|
+
"| # | A | B | cosine | A description | B description |",
|
|
254
|
+
"|---|---|---|---:|---|---|",
|
|
255
|
+
]
|
|
256
|
+
for i, p in enumerate(pairs, 1):
|
|
257
|
+
lines.append(
|
|
258
|
+
f"| {i} | `{p['a_name']}` | `{p['b_name']}` | {p['cosine']:.2f} | "
|
|
259
|
+
f"{p['a_description']} | {p['b_description']} |"
|
|
260
|
+
)
|
|
261
|
+
lines.append("")
|
|
262
|
+
|
|
263
|
+
lines += [
|
|
264
|
+
"## Usage-signal note",
|
|
265
|
+
"",
|
|
266
|
+
"Per-command invocation telemetry is **not** available. Two surrogate signals "
|
|
267
|
+
"were considered:",
|
|
268
|
+
"",
|
|
269
|
+
"- **Filesystem mtime** — useless: `task sync` rewrites every file when the "
|
|
270
|
+
" compressed and uncompressed trees are regenerated.",
|
|
271
|
+
"- **Git history (`--follow`)** — uninformative here: the `.agent-src.uncompressed/` "
|
|
272
|
+
" directory is the result of a recent rename (`.augment.uncompressed/` → "
|
|
273
|
+
" `.agent-src.uncompressed/`), so almost every file shows a single recent commit "
|
|
274
|
+
f" on the current branch. {len(low_signal)} of {len(commands)} commands fall into the "
|
|
275
|
+
f" ≤{LOW_SIGNAL_COMMIT_COUNT}-commits / ≤{LOW_SIGNAL_AGE_DAYS}d-old bucket purely as a "
|
|
276
|
+
" rename artefact, not as a real cold-tail signal.",
|
|
277
|
+
"",
|
|
278
|
+
"**Implication for Phase 1 categorisation:** keep / merge / retire decisions must "
|
|
279
|
+
"be made on **intent** (description content, overlap with sibling commands, tier "
|
|
280
|
+
"placement, cluster fit) rather than usage data. The cosine-≥0.6 overlap "
|
|
281
|
+
"pairs above are the primary structural lever.",
|
|
282
|
+
"",
|
|
283
|
+
]
|
|
284
|
+
|
|
285
|
+
lines += [
|
|
286
|
+
"## Three-bucket categorisation (Phase 1 Step 4)",
|
|
287
|
+
"",
|
|
288
|
+
"The keep / merge / retire verdict lives in "
|
|
289
|
+
"[`command-surface-synthesis.md`](command-surface-synthesis.md) — hand-curated "
|
|
290
|
+
"and **not** regenerated by this script. Headline: 109 keep · 0 merge · 0 retire. "
|
|
291
|
+
"Every overlap pair and retire candidate surfaced by the council turned out to "
|
|
292
|
+
"be an intentional structural pattern (scope ladder, union dispatcher, thin "
|
|
293
|
+
"alias, tier-gated specialist), not redundancy.",
|
|
294
|
+
"",
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
lines += [
|
|
298
|
+
"## Full inventory",
|
|
299
|
+
"",
|
|
300
|
+
"Column `bucket` is left blank — the categorisation lives in "
|
|
301
|
+
"[`command-surface-synthesis.md`](command-surface-synthesis.md). Every command "
|
|
302
|
+
"in this table maps to `keep` unless named in that file's tables.",
|
|
303
|
+
"",
|
|
304
|
+
"| Name | Path | Tier | Cluster | Aliases | Lines | Commits | Age (d) | Bucket |",
|
|
305
|
+
"|---|---|---:|---|---|---:|---:|---:|---|",
|
|
306
|
+
]
|
|
307
|
+
for c in sorted(commands, key=lambda c: c.relpath):
|
|
308
|
+
aliases = ", ".join(c.aliases) if c.aliases else "—"
|
|
309
|
+
tier = "—" if c.tier is None else str(c.tier)
|
|
310
|
+
cluster = c.cluster or "—"
|
|
311
|
+
age = "—" if c.days_since_first_commit is None else str(c.days_since_first_commit)
|
|
312
|
+
lines.append(
|
|
313
|
+
f"| `{c.name}` | `{c.relpath}` | {tier} | {cluster} | {aliases} | "
|
|
314
|
+
f"{c.line_count} | {c.commit_count} | {age} | |"
|
|
315
|
+
)
|
|
316
|
+
lines.append("")
|
|
317
|
+
return "\n".join(lines)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def main() -> int:
|
|
321
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
322
|
+
parser.add_argument("--root", type=Path, default=DEFAULT_ROOT)
|
|
323
|
+
parser.add_argument("--quiet", action="store_true")
|
|
324
|
+
args = parser.parse_args()
|
|
325
|
+
if not args.root.exists():
|
|
326
|
+
print(f"error: {args.root} does not exist", file=sys.stderr)
|
|
327
|
+
return 2
|
|
328
|
+
|
|
329
|
+
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
|
330
|
+
commands = collect(args.root)
|
|
331
|
+
pairs = find_overlap_pairs(commands)
|
|
332
|
+
|
|
333
|
+
OUT_JSON.write_text(
|
|
334
|
+
json.dumps({
|
|
335
|
+
"total": len(commands),
|
|
336
|
+
"thresholds": {
|
|
337
|
+
"overlap_cosine": OVERLAP_COSINE_THRESHOLD,
|
|
338
|
+
"low_signal_commit_count": LOW_SIGNAL_COMMIT_COUNT,
|
|
339
|
+
"low_signal_age_days": LOW_SIGNAL_AGE_DAYS,
|
|
340
|
+
},
|
|
341
|
+
"commands": [asdict(c) for c in commands],
|
|
342
|
+
"overlap_pairs": pairs,
|
|
343
|
+
}, indent=2),
|
|
344
|
+
encoding="utf-8",
|
|
345
|
+
)
|
|
346
|
+
OUT_MD.write_text(render_md(commands, pairs), encoding="utf-8")
|
|
347
|
+
|
|
348
|
+
if not args.quiet:
|
|
349
|
+
print(f"✅ Audited {len(commands)} commands.")
|
|
350
|
+
print(f" JSON: {OUT_JSON.relative_to(REPO_ROOT)}")
|
|
351
|
+
print(f" MD: {OUT_MD.relative_to(REPO_ROOT)}")
|
|
352
|
+
print(f" Overlap pairs (cosine ≥ {OVERLAP_COSINE_THRESHOLD}): {len(pairs)}")
|
|
353
|
+
low_n = sum(
|
|
354
|
+
1 for c in commands
|
|
355
|
+
if c.commit_count and c.commit_count <= LOW_SIGNAL_COMMIT_COUNT
|
|
356
|
+
and (c.days_since_first_commit or 0) <= LOW_SIGNAL_AGE_DAYS
|
|
357
|
+
)
|
|
358
|
+
print(f" Low-signal (≤{LOW_SIGNAL_COMMIT_COUNT} commits, ≤{LOW_SIGNAL_AGE_DAYS}d): {low_n}")
|
|
359
|
+
return 0
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
if __name__ == "__main__":
|
|
363
|
+
sys.exit(main())
|
|
@@ -15,6 +15,11 @@ catches **misplacement**, not naming-conventions inside the dirs:
|
|
|
15
15
|
(e.g. agents/council-question-foo.md, agents/.council-foo.md).
|
|
16
16
|
- council-* files under any other subdirectory of agents/.
|
|
17
17
|
|
|
18
|
+
`agents/audit-*/` directories are exempt — historical audit bundles
|
|
19
|
+
are cohesive, checked-in narratives (the canonical council dirs are
|
|
20
|
+
gitignored) and may legitimately include council-* artefacts as part
|
|
21
|
+
of the audit's evidence trail.
|
|
22
|
+
|
|
18
23
|
Failure modes are enforced by `.agent-src.uncompressed/skills/ai-council/SKILL.md`
|
|
19
24
|
§ "Output path convention".
|
|
20
25
|
|
|
@@ -40,6 +45,10 @@ CANONICAL_DIRS = {
|
|
|
40
45
|
"council-responses": ".json",
|
|
41
46
|
"council-sessions": ".json",
|
|
42
47
|
}
|
|
48
|
+
# Subdirectory prefixes whose contents are exempt from the layout check.
|
|
49
|
+
# `audit-*/` covers historical audit bundles where council artefacts
|
|
50
|
+
# form part of the documented evidence trail.
|
|
51
|
+
EXEMPT_DIR_PREFIXES = ("audit-",)
|
|
43
52
|
# A council artefact is a file whose name starts with `council-` or
|
|
44
53
|
# `.council-`. This intentionally excludes roadmaps like
|
|
45
54
|
# `road-to-ai-council.md` whose stem only contains the word "council".
|
|
@@ -79,6 +88,8 @@ def find_violations(root: Path) -> list[str]:
|
|
|
79
88
|
continue # already handled above
|
|
80
89
|
if rel.parts[0] in CANONICAL_DIRS:
|
|
81
90
|
continue
|
|
91
|
+
if rel.parts[0].startswith(EXEMPT_DIR_PREFIXES):
|
|
92
|
+
continue
|
|
82
93
|
findings.append(
|
|
83
94
|
f"{path}: council artefact in non-canonical directory "
|
|
84
95
|
f"agents/{rel.parts[0]}/ — only council-questions/, "
|