@event4u/agent-config 1.18.0 → 1.20.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/agent-handoff.md +14 -10
- package/.agent-src/commands/chat-history/import.md +170 -0
- package/.agent-src/commands/chat-history/learn.md +178 -0
- package/.agent-src/commands/chat-history/show.md +17 -18
- package/.agent-src/commands/chat-history.md +26 -25
- package/.agent-src/commands/council/default.md +77 -82
- package/.agent-src/commands/create-pr.md +28 -8
- 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/commands/sync-gitignore.md +1 -1
- package/.agent-src/contexts/communication/rules-auto/skill-quality-mechanics.md +76 -0
- package/.agent-src/contexts/communication/rules-auto/slash-command-routing-policy-mechanics.md +3 -3
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +5 -12
- 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/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 +22 -0
- package/.agent-src/rules/direct-answers.md +11 -2
- 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 +38 -6
- 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-attribution-footers.md +48 -0
- package/.agent-src/rules/no-cheap-questions.md +1 -0
- package/.agent-src/rules/no-roadmap-references.md +2 -1
- package/.agent-src/rules/non-destructive-by-default.md +1 -0
- package/.agent-src/rules/onboarding-gate.md +26 -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 +22 -0
- package/.agent-src/rules/role-mode-adherence.md +2 -2
- package/.agent-src/rules/rule-type-governance.md +1 -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 +50 -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 +22 -5
- package/.agent-src/rules/verify-before-complete.md +1 -0
- package/.agent-src/skills/ai-council/SKILL.md +4 -5
- package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
- package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
- package/.agent-src/skills/md-language-check/SKILL.md +1 -1
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
- package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
- package/.agent-src/skills/roadmap-management/SKILL.md +29 -4
- package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/skills/verify-completion-evidence/SKILL.md +8 -1
- package/.agent-src/templates/agent-settings.md +21 -26
- package/.agent-src/templates/roadmaps.md +8 -3
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +16 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +4 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/_chat_history_base.py +7 -51
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_append.py +1 -2
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_halt_append.py +1 -2
- 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 +110 -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/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +8 -4
- package/AGENTS.md +44 -3
- package/CHANGELOG.md +173 -0
- package/README.md +22 -22
- package/config/agent-settings.template.yml +42 -13
- package/config/gitignore-block.txt +4 -4
- package/docs/architecture.md +3 -3
- package/docs/catalog.md +18 -13
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/adr-settings-sync-engine.md +127 -0
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/decision-trace-v1.md +146 -0
- package/docs/contracts/file-ownership-matrix.json +348 -126
- package/docs/contracts/hook-architecture-v1.md +220 -0
- package/docs/contracts/memory-visibility-v1.md +122 -0
- package/docs/contracts/one-off-script-lifecycle.md +109 -0
- package/docs/contracts/rule-interactions.yml +22 -0
- package/docs/customization.md +2 -1
- package/docs/development.md +4 -1
- package/docs/getting-started.md +21 -29
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- package/docs/guidelines/agent-infra/layered-settings.md +32 -13
- package/docs/hook-payload-capture.md +221 -0
- package/docs/migrations/commands-1.15.0.md +17 -12
- package/docs/skills-catalog.md +5 -4
- package/llms.txt +4 -3
- package/package.json +1 -1
- package/scripts/agent-config +45 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/bundler.py +3 -3
- package/scripts/ai_council/clients.py +25 -9
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/one_off_archive/2026-05/README.md +22 -0
- package/scripts/ai_council/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/pricing.py +10 -9
- package/scripts/ai_council/session.py +92 -0
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/capture_showcase_session.py +361 -0
- package/scripts/chat_history.py +963 -597
- package/scripts/check_always_budget.py +7 -2
- package/scripts/check_references.py +12 -2
- package/scripts/context_hygiene_hook.py +14 -6
- package/scripts/council_cli.py +407 -0
- package/scripts/hook_manifest.yaml +217 -0
- package/scripts/hooks/__init__.py +1 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/augment-dispatcher.sh +72 -0
- package/scripts/hooks/cline-dispatcher.sh +86 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/cursor-dispatcher.sh +76 -0
- package/scripts/hooks/dispatch_hook.py +383 -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 +157 -0
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.py +725 -87
- package/scripts/install.sh +38 -1
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +217 -0
- package/scripts/lint_one_off_age.py +184 -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 +13 -8
- package/scripts/readme_linter.py +12 -3
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/roadmap_progress_hook.py +5 -0
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +163 -1
- package/scripts/sync_agent_settings.py +32 -129
- package/scripts/sync_yaml_rt.py +734 -0
- package/scripts/update_prices.py +3 -3
- package/scripts/verify_before_complete_hook.py +216 -0
- package/.agent-src/commands/chat-history/checkpoint.md +0 -126
- package/.agent-src/commands/chat-history/clear.md +0 -103
- package/.agent-src/commands/chat-history/resume.md +0 -183
- package/.agent-src/rules/chat-history-cadence.md +0 -109
- package/.agent-src/rules/chat-history-ownership.md +0 -123
- package/.agent-src/rules/chat-history-visibility.md +0 -96
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_heartbeat.py +0 -50
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/chat_history_turn_check.py +0 -49
- package/scripts/check_phase_coupling.py +0 -148
|
@@ -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())
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
"""Runtime pricing layer for the AI Council.
|
|
2
2
|
|
|
3
|
-
Reads
|
|
4
|
-
and the Markdown table, and exposes:
|
|
3
|
+
Reads `agents/.agent-prices.md` from the repo root, parses YAML
|
|
4
|
+
frontmatter and the Markdown table, and exposes:
|
|
5
5
|
|
|
6
|
-
- `load_prices()` — parse
|
|
6
|
+
- `load_prices()` — parse `agents/.agent-prices.md` (bootstraps if missing)
|
|
7
7
|
- `estimate_input_tokens()` — chars / 4 heuristic
|
|
8
8
|
- `estimate_cost()` — input + output USD for a single member
|
|
9
9
|
- `is_stale()` — True if `last_updated` is older than the
|
|
10
10
|
most recent UTC Monday 00:00
|
|
11
|
-
- `bootstrap_from_defaults()` — write a fresh
|
|
12
|
-
`_default_prices.DEFAULT_PRICES`
|
|
11
|
+
- `bootstrap_from_defaults()` — write a fresh `agents/.agent-prices.md`
|
|
12
|
+
from `_default_prices.DEFAULT_PRICES`
|
|
13
13
|
|
|
14
14
|
The orchestrator never reads `_default_prices` directly. It always
|
|
15
|
-
goes through `load_prices()` so user edits to
|
|
15
|
+
goes through `load_prices()` so user edits to
|
|
16
|
+
`agents/.agent-prices.md` win.
|
|
16
17
|
"""
|
|
17
18
|
|
|
18
19
|
from __future__ import annotations
|
|
@@ -24,7 +25,7 @@ from pathlib import Path
|
|
|
24
25
|
from scripts.ai_council._default_prices import DEFAULT_PRICES, LAST_UPDATED, as_rows
|
|
25
26
|
|
|
26
27
|
REPO_ROOT = Path(__file__).resolve().parents[2]
|
|
27
|
-
PRICES_FILE = REPO_ROOT / ".agent-prices.md"
|
|
28
|
+
PRICES_FILE = REPO_ROOT / "agents" / ".agent-prices.md"
|
|
28
29
|
|
|
29
30
|
# Heuristic: 1 token ≈ 4 characters of English text. OpenAI's tiktoken
|
|
30
31
|
# is more accurate but pulls in a heavy dep we explicitly avoid.
|
|
@@ -115,14 +116,14 @@ def is_stale(table: PriceTable, now: _dt.datetime | None = None) -> bool:
|
|
|
115
116
|
|
|
116
117
|
|
|
117
118
|
def load_prices(path: Path = PRICES_FILE) -> PriceTable:
|
|
118
|
-
"""Parse
|
|
119
|
+
"""Parse `agents/.agent-prices.md`; bootstrap from defaults if missing."""
|
|
119
120
|
if not path.exists():
|
|
120
121
|
bootstrap_from_defaults(path)
|
|
121
122
|
return _parse(path.read_text(encoding="utf-8"))
|
|
122
123
|
|
|
123
124
|
|
|
124
125
|
def bootstrap_from_defaults(path: Path = PRICES_FILE) -> None:
|
|
125
|
-
"""Write a fresh
|
|
126
|
+
"""Write a fresh `agents/.agent-prices.md` from `_default_prices.py`."""
|
|
126
127
|
rows = as_rows()
|
|
127
128
|
body = _render_markdown(LAST_UPDATED, "shipped-default", rows)
|
|
128
129
|
path.write_text(body, encoding="utf-8")
|
|
@@ -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
|
|
@@ -71,14 +71,6 @@ add("no-cheap-questions.md", "pre-send Q&A check", "agent-only", "output",
|
|
|
71
71
|
"NA-soft", "3", notes="Pre-send self-check, no platform surface")
|
|
72
72
|
|
|
73
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
74
|
add("onboarding-gate.md", "first turn (settings.onboarded == false)", "settings",
|
|
83
75
|
"state", "low", "1",
|
|
84
76
|
notes="Pilot candidate — frequency 100% on un-onboarded projects, binary verifiable")
|
|
@@ -341,7 +333,7 @@ def emit():
|
|
|
341
333
|
lines.append("## Cross-references")
|
|
342
334
|
lines.append("")
|
|
343
335
|
lines.append("- Budget contract: [`docs/contracts/load-context-budget-model.md`](../../docs/contracts/load-context-budget-model.md)")
|
|
344
|
-
lines.append("- Pattern precedent: `
|
|
336
|
+
lines.append("- Pattern precedent: `roadmap-progress-sync` (PostToolUse path-filter hook)")
|
|
345
337
|
lines.append("- Phase 2A finding: [`adr-always-rule-context-split-not-viable.md`](adr-always-rule-context-split-not-viable.md)")
|
|
346
338
|
lines.append("")
|
|
347
339
|
|