@event4u/agent-config 1.19.0 → 1.21.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/agents.md +1 -1
- package/.agent-src/commands/bug-fix.md +1 -1
- package/.agent-src/commands/bug-investigate.md +2 -2
- package/.agent-src/commands/chat-history/import.md +166 -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/compress.md +12 -0
- package/.agent-src/commands/context/create.md +2 -2
- package/.agent-src/commands/context.md +1 -1
- package/.agent-src/commands/copilot-agents.md +1 -1
- package/.agent-src/commands/council/default.md +21 -12
- package/.agent-src/commands/council.md +1 -1
- package/.agent-src/commands/create-pr.md +28 -8
- package/.agent-src/commands/e2e-heal.md +1 -1
- package/.agent-src/commands/e2e-plan.md +1 -1
- package/.agent-src/commands/feature/dev.md +3 -3
- package/.agent-src/commands/feature.md +1 -1
- package/.agent-src/commands/fix/seeder.md +2 -2
- package/.agent-src/commands/fix.md +1 -1
- package/.agent-src/commands/jira-ticket.md +1 -1
- package/.agent-src/commands/judge.md +2 -2
- package/.agent-src/commands/memory.md +1 -1
- package/.agent-src/commands/mode.md +5 -5
- package/.agent-src/commands/module.md +1 -1
- package/.agent-src/commands/onboard.md +4 -4
- package/.agent-src/commands/optimize/augmentignore.md +1 -1
- package/.agent-src/commands/optimize-prompt.md +61 -0
- package/.agent-src/commands/optimize.md +1 -1
- package/.agent-src/commands/override.md +1 -1
- package/.agent-src/commands/review-changes.md +1 -1
- package/.agent-src/commands/review-routing.md +1 -1
- package/.agent-src/commands/roadmap.md +1 -1
- package/.agent-src/commands/set-cost-profile.md +3 -3
- package/.agent-src/commands/sync-agent-settings.md +2 -2
- package/.agent-src/commands/sync-gitignore.md +1 -1
- package/.agent-src/commands/tests/create.md +2 -2
- package/.agent-src/commands/tests.md +1 -1
- package/.agent-src/commands/threat-model.md +4 -4
- package/.agent-src/contexts/authority/commit-mechanics.md +14 -1
- package/.agent-src/contexts/authority/destructive-mechanics.md +14 -1
- package/.agent-src/contexts/authority/scope-mechanics.md +5 -0
- package/.agent-src/contexts/communication/rules-auto/guidelines-mechanics.md +76 -0
- 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 +4 -4
- package/.agent-src/contexts/communication/rules-auto/think-before-action-mechanics.md +98 -0
- package/.agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +93 -0
- package/.agent-src/contexts/communication/rules-auto/user-interaction-mechanics.md +125 -9
- package/.agent-src/contexts/execution/autonomy-mechanics.md +44 -0
- package/.agent-src/contexts/model-recommendations.md +2 -2
- package/.agent-src/contexts/override-system.md +1 -1
- package/.agent-src/personas/product-owner.md +2 -2
- package/.agent-src/personas/qa.md +1 -1
- package/.agent-src/rules/agent-authority.md +5 -6
- package/.agent-src/rules/agent-docs.md +11 -53
- package/.agent-src/rules/analysis-skill-routing.md +10 -40
- package/.agent-src/rules/architecture.md +6 -1
- package/.agent-src/rules/artifact-drafting-protocol.md +5 -0
- package/.agent-src/rules/artifact-engagement-recording.md +23 -59
- package/.agent-src/rules/ask-when-uncertain.md +24 -47
- package/.agent-src/rules/augment-portability.md +14 -62
- package/.agent-src/rules/augment-source-of-truth.md +10 -1
- package/.agent-src/rules/autonomous-execution.md +17 -98
- package/.agent-src/rules/capture-learnings.md +9 -80
- package/.agent-src/rules/cli-output-handling.md +12 -42
- package/.agent-src/rules/command-suggestion-policy.md +25 -73
- package/.agent-src/rules/commit-conventions.md +9 -58
- package/.agent-src/rules/commit-policy.md +16 -47
- package/.agent-src/rules/context-hygiene.md +5 -0
- package/.agent-src/rules/direct-answers.md +21 -42
- package/.agent-src/rules/docker-commands.md +11 -45
- package/.agent-src/rules/docs-sync.md +10 -56
- package/.agent-src/rules/downstream-changes.md +5 -0
- package/.agent-src/rules/e2e-testing.md +9 -44
- package/.agent-src/rules/guidelines.md +13 -75
- package/.agent-src/rules/improve-before-implement.md +10 -2
- package/.agent-src/rules/language-and-tone.md +35 -69
- package/.agent-src/rules/laravel-translations.md +11 -40
- package/.agent-src/rules/markdown-safe-codeblocks.md +4 -0
- package/.agent-src/rules/minimal-safe-diff.md +4 -0
- package/.agent-src/rules/missing-tool-handling.md +4 -0
- package/.agent-src/rules/model-recommendation.md +9 -61
- package/.agent-src/rules/no-attribution-footers.md +53 -0
- package/.agent-src/rules/no-cheap-questions.md +11 -27
- package/.agent-src/rules/no-council-references.md +76 -0
- package/.agent-src/rules/no-roadmap-references.md +8 -1
- package/.agent-src/rules/non-destructive-by-default.md +13 -43
- package/.agent-src/rules/onboarding-gate.md +9 -117
- package/.agent-src/rules/package-ci-checks.md +10 -37
- package/.agent-src/rules/php-coding.md +10 -55
- package/.agent-src/rules/preservation-guard.md +9 -0
- package/.agent-src/rules/review-routing-awareness.md +9 -97
- package/.agent-src/rules/reviewer-awareness.md +8 -83
- package/.agent-src/rules/roadmap-progress-sync.md +7 -170
- package/.agent-src/rules/role-mode-adherence.md +6 -2
- package/.agent-src/rules/rule-type-governance.md +8 -66
- package/.agent-src/rules/runtime-safety.md +5 -0
- package/.agent-src/rules/scope-control.md +17 -62
- package/.agent-src/rules/security-sensitive-stop.md +7 -1
- package/.agent-src/rules/size-enforcement.md +6 -1
- package/.agent-src/rules/skill-improvement-trigger.md +9 -49
- package/.agent-src/rules/skill-quality.md +7 -64
- package/.agent-src/rules/slash-command-routing-policy.md +11 -63
- package/.agent-src/rules/think-before-action.md +22 -87
- package/.agent-src/rules/token-efficiency.md +10 -74
- package/.agent-src/rules/token-optimizer-maintenance.md +68 -0
- package/.agent-src/rules/tool-safety.md +4 -0
- package/.agent-src/rules/ui-audit-gate.md +25 -61
- package/.agent-src/rules/upstream-proposal.md +9 -67
- package/.agent-src/rules/user-interaction.md +25 -95
- package/.agent-src/rules/verify-before-complete.md +1 -1
- package/.agent-src/skills/agent-docs-writing/SKILL.md +1 -1
- package/.agent-src/skills/ai-council/SKILL.md +69 -5
- package/.agent-src/skills/analysis-autonomous-mode/SKILL.md +1 -1
- package/.agent-src/skills/analysis-skill-router/SKILL.md +3 -3
- package/.agent-src/skills/artisan-commands/SKILL.md +2 -2
- package/.agent-src/skills/authz-review/SKILL.md +1 -1
- package/.agent-src/skills/aws-infrastructure/SKILL.md +5 -5
- package/.agent-src/skills/blast-radius-analyzer/SKILL.md +8 -8
- package/.agent-src/skills/bug-analyzer/SKILL.md +5 -5
- package/.agent-src/skills/code-refactoring/SKILL.md +4 -4
- package/.agent-src/skills/code-review/SKILL.md +2 -2
- package/.agent-src/skills/command-writing/SKILL.md +11 -0
- package/.agent-src/skills/composer-packages/SKILL.md +2 -2
- package/.agent-src/skills/context-authoring/SKILL.md +11 -0
- package/.agent-src/skills/context-document/SKILL.md +1 -1
- package/.agent-src/skills/copilot-agents-optimization/SKILL.md +23 -0
- package/.agent-src/skills/copilot-config/SKILL.md +1 -1
- package/.agent-src/skills/dcf-modeling/SKILL.md +89 -0
- package/.agent-src/skills/dependency-upgrade/SKILL.md +2 -2
- package/.agent-src/skills/devcontainer/SKILL.md +2 -2
- package/.agent-src/skills/developer-like-execution/SKILL.md +1 -1
- package/.agent-src/skills/docker/SKILL.md +1 -1
- package/.agent-src/skills/dto-creator/SKILL.md +1 -1
- package/.agent-src/skills/estimate-ticket/SKILL.md +2 -2
- package/.agent-src/skills/fe-design/SKILL.md +4 -4
- package/.agent-src/skills/feature-planning/SKILL.md +5 -5
- package/.agent-src/skills/funnel-analysis/SKILL.md +100 -0
- package/.agent-src/skills/laravel/SKILL.md +1 -1
- package/.agent-src/skills/laravel-notifications/SKILL.md +5 -5
- package/.agent-src/skills/laravel-pennant/SKILL.md +1 -1
- package/.agent-src/skills/laravel-pulse/SKILL.md +4 -4
- package/.agent-src/skills/laravel-reverb/SKILL.md +2 -2
- package/.agent-src/skills/laravel-scheduling/SKILL.md +1 -1
- package/.agent-src/skills/md-language-check/SKILL.md +1 -1
- package/.agent-src/skills/migration-creator/SKILL.md +7 -7
- package/.agent-src/skills/multi-tenancy/SKILL.md +8 -8
- package/.agent-src/skills/okr-tree-modeling/SKILL.md +93 -0
- package/.agent-src/skills/performance-analysis/SKILL.md +3 -3
- package/.agent-src/skills/pest-testing/SKILL.md +6 -6
- package/.agent-src/skills/php-service/SKILL.md +2 -2
- package/.agent-src/skills/project-analysis-hypothesis-driven/SKILL.md +3 -3
- package/.agent-src/skills/project-analysis-react/SKILL.md +1 -1
- package/.agent-src/skills/project-analysis-symfony/SKILL.md +1 -1
- package/.agent-src/skills/project-analysis-zend-laminas/SKILL.md +2 -2
- package/.agent-src/skills/project-analyzer/SKILL.md +4 -4
- package/.agent-src/skills/prompt-optimizer/SKILL.md +108 -0
- package/.agent-src/skills/readme-reviewer/SKILL.md +1 -1
- package/.agent-src/skills/rice-prioritization/SKILL.md +100 -0
- package/.agent-src/skills/rule-writing/SKILL.md +33 -0
- package/.agent-src/skills/sentry-integration/SKILL.md +1 -1
- package/.agent-src/skills/skill-writing/SKILL.md +14 -0
- package/.agent-src/skills/subagent-orchestration/SKILL.md +34 -2
- package/.agent-src/skills/terraform/SKILL.md +2 -2
- package/.agent-src/skills/terragrunt/SKILL.md +8 -8
- package/.agent-src/skills/test-performance/SKILL.md +5 -5
- package/.agent-src/skills/threat-modeling/SKILL.md +2 -2
- package/.agent-src/skills/token-optimizer/SKILL.md +110 -0
- package/.agent-src/skills/unit-economics-modeling/SKILL.md +104 -0
- package/.agent-src/skills/universal-project-analysis/SKILL.md +1 -1
- package/.agent-src/skills/using-git-worktrees/SKILL.md +1 -0
- package/.agent-src/templates/AGENTS.md +1 -1
- package/.agent-src/templates/agent-settings.md +25 -41
- package/.agent-src/templates/contexts/tenant-boundaries.md +2 -2
- package/.agent-src/templates/contexts.md +1 -1
- package/.agent-src/templates/copilot-instructions.md +21 -0
- package/.agent-src/templates/copilot-review-instructions.md +76 -0
- package/.agent-src/templates/features.md +1 -1
- package/.agent-src/templates/rule.md +127 -0
- package/.agent-src/templates/scripts/work_engine/hook_bootstrap.py +7 -5
- package/.agent-src/templates/scripts/work_engine/hooks/__init__.py +0 -4
- package/.agent-src/templates/scripts/work_engine/hooks/builtin/__init__.py +0 -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/memory_visibility.py +2 -3
- package/.agent-src/templates/skill.md +30 -1
- package/.claude-plugin/marketplace.json +11 -4
- package/AGENTS.md +71 -3
- package/CHANGELOG.md +180 -3
- package/README.md +24 -23
- package/config/agent-settings.template.yml +63 -23
- package/config/gitignore-block.txt +11 -4
- package/docs/architecture.md +84 -3
- package/docs/catalog.md +23 -11
- package/docs/contracts/adr-chat-history-split.md +10 -1
- package/docs/contracts/agent-memory-contract.md +1 -1
- package/docs/contracts/command-clusters.md +1 -1
- package/docs/contracts/context-paths.md +2 -1
- package/docs/contracts/cross-wing-handoff.md +133 -0
- package/docs/contracts/file-ownership-matrix.json +678 -609
- package/docs/contracts/hook-architecture-v1.md +8 -1
- package/docs/contracts/iron-law-overrides.txt +25 -0
- package/docs/contracts/kernel-membership.md +273 -0
- package/docs/contracts/load-context-schema.md +26 -11
- package/docs/contracts/memory-visibility-v1.md +8 -24
- package/docs/contracts/pilot/agent-authority.md +24 -0
- package/docs/contracts/pilot/direct-answers.md +70 -0
- package/docs/contracts/pilot/language-and-tone.md +63 -0
- package/docs/contracts/rule-classification.md +170 -0
- package/docs/contracts/rule-router.md +153 -0
- package/docs/customization.md +18 -7
- package/docs/decisions/ADR-001-kernel-swap-deferred.md +109 -0
- package/docs/decisions/ADR-002-kernel-bucket-overrides.md +124 -0
- package/docs/decisions/ADR-rule-kernel-and-router.md +122 -0
- package/docs/getting-started.md +19 -27
- package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +1 -1
- package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +176 -0
- package/docs/guidelines/agent-infra/rule-type-governance.md +73 -0
- package/docs/guidelines/agent-infra/size-and-scope.md +13 -2
- package/docs/guidelines/agent-infra/skill-quality-checklist.md +119 -0
- package/docs/guidelines/augment-portability-patterns.md +68 -0
- package/docs/guidelines/php/php-coding-patterns.md +62 -0
- 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/_p43_bodies.py +235 -0
- package/scripts/_p43_compress.py +118 -0
- package/scripts/_p4_migrate.py +199 -0
- package/scripts/_pilot_council_question.py +57 -0
- package/scripts/_pilot_measure.py +53 -0
- package/scripts/agent-config +1 -1
- package/scripts/ai_council/_default_prices.py +4 -4
- package/scripts/ai_council/clients.py +1 -1
- package/scripts/ai_council/modes.py +3 -4
- package/scripts/ai_council/pricing.py +10 -9
- package/scripts/ai_council/session.py +107 -5
- package/scripts/build_linear_digest.py +3 -5
- package/scripts/build_rule_trigger_matrix.py +1 -9
- package/scripts/chat_history.py +952 -596
- package/scripts/check_always_budget.py +39 -6
- package/scripts/check_compressed_paths.py +213 -0
- package/scripts/check_compression.py +15 -0
- package/scripts/check_context_paths.py +1 -0
- package/scripts/check_council_layout.py +105 -0
- package/scripts/check_council_references.py +145 -0
- package/scripts/check_portability.py +2 -0
- package/scripts/check_references.py +14 -2
- package/scripts/check_token_optimizer_freshness.py +131 -0
- package/scripts/compile_router.py +148 -0
- package/scripts/compress.py +219 -11
- package/scripts/council_cli.py +63 -9
- package/scripts/council_prune.py +81 -0
- package/scripts/count_token_optimizer_usage.sh +54 -0
- package/scripts/hook_manifest.yaml +33 -0
- package/scripts/hooks/augment-chat-history.sh +10 -0
- package/scripts/hooks/cowork-dispatcher.sh +98 -0
- package/scripts/hooks/dispatch_hook.py +35 -0
- package/scripts/hooks_status.py +12 -1
- package/scripts/install-hooks.sh +2 -2
- package/scripts/install.sh +81 -2
- package/scripts/iron_law_sha.py +98 -0
- package/scripts/lint_handoffs.py +214 -0
- package/scripts/lint_hook_manifest.py +2 -1
- package/scripts/lint_load_context.py +35 -5
- package/scripts/measure_rule_budget.py +314 -0
- package/scripts/prototype_lint_contradictions.py +150 -0
- package/scripts/redact_hook_capture.py +148 -0
- package/scripts/schemas/rule.schema.json +55 -6
- package/scripts/schemas/skill.schema.json +5 -0
- package/scripts/skill_linter.py +359 -7
- package/scripts/smoke_path_resolution.py +93 -0
- package/scripts/update_prices.py +3 -3
- package/scripts/validate_frontmatter.py +41 -1
- 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/contexts/communication/rules-auto/artifact-engagement-recording-mechanics.md +0 -72
- package/.agent-src/contexts/communication/rules-auto/augment-portability-mechanics.md +0 -79
- package/.agent-src/contexts/communication/rules-auto/cli-output-handling-mechanics.md +0 -87
- package/.agent-src/contexts/communication/rules-auto/command-suggestion-policy-mechanics.md +0 -62
- package/.agent-src/contexts/communication/rules-auto/docs-sync-mechanics.md +0 -78
- package/.agent-src/contexts/communication/rules-auto/package-ci-checks-mechanics.md +0 -85
- package/.agent-src/contexts/communication/rules-auto/review-routing-awareness-mechanics.md +0 -65
- package/.agent-src/contexts/communication/rules-auto/roadmap-progress-sync-mechanics.md +0 -78
- package/.agent-src/contexts/communication/rules-auto/ui-audit-gate-mechanics.md +0 -53
- package/.agent-src/rules/chat-history-cadence.md +0 -143
- package/.agent-src/rules/chat-history-ownership.md +0 -124
- package/.agent-src/rules/chat-history-visibility.md +0 -97
- 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
- /package/{docs → .agent-src/contexts}/contracts/artifact-engagement-flow.md +0 -0
- /package/{docs → .agent-src/contexts}/contracts/command-suggestion-flow.md +0 -0
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Measure rule-bucket char counts (kernel + auto) for the rule-kernel roadmap.
|
|
3
|
+
|
|
4
|
+
Source of truth: `.agent-src.uncompressed/rules/*.md`. Frontmatter (YAML
|
|
5
|
+
between two `---` lines at file start) is stripped before counting; only
|
|
6
|
+
the rule body counts toward the bucket.
|
|
7
|
+
|
|
8
|
+
Buckets follow the existing frontmatter `type:` field:
|
|
9
|
+
- `always` rules → always-bucket (today's kernel proxy).
|
|
10
|
+
- `auto` rules → auto-bucket.
|
|
11
|
+
|
|
12
|
+
Output:
|
|
13
|
+
- Default: stdout table (per-rule rows, top-5 oversize, totals).
|
|
14
|
+
- `--json`: deterministic JSON (sorted keys, sorted lists).
|
|
15
|
+
|
|
16
|
+
Acceptance per `road-to-kernel-and-router.md` P1.1: re-runnable,
|
|
17
|
+
deterministic, stdlib-only, no network.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import datetime as _dt
|
|
24
|
+
import json
|
|
25
|
+
import sys
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
REPO_ROOT = Path(__file__).resolve().parent.parent
|
|
29
|
+
RULES_DIR = REPO_ROOT / ".agent-src.uncompressed" / "rules"
|
|
30
|
+
OVERRIDES_FILE = REPO_ROOT / "docs" / "contracts" / "iron-law-overrides.txt"
|
|
31
|
+
TREND_FILE = REPO_ROOT / "agents" / ".rule-budget-history.jsonl"
|
|
32
|
+
|
|
33
|
+
# Council R2 amendments (2026-05-06) — see docs/contracts/kernel-membership.md § 5.1.
|
|
34
|
+
# Per-rule cap raised 1.5k → 2.5k; warning band raised 1.2k → 2.0k.
|
|
35
|
+
# ADR-002 (2026-05-06) — KERNEL_HARD raised 25k → 26k after empirical r_actual=0.795
|
|
36
|
+
# vs r_projected=0.712; see docs/decisions/ADR-002-kernel-bucket-overrides.md.
|
|
37
|
+
KERNEL_HARD = 26_000
|
|
38
|
+
KERNEL_TARGET = 20_000
|
|
39
|
+
PER_RULE_HARD = 2_500
|
|
40
|
+
PER_RULE_TARGET = 2_000
|
|
41
|
+
PER_RULE_OVERRIDE_CEILING = 4_000 # Iron-Law-override ADR ceiling.
|
|
42
|
+
|
|
43
|
+
# Locked kernel set — docs/contracts/kernel-membership.md § 4.
|
|
44
|
+
# This is the *kernel* (P1.3 lock), not "every always-rule". After P4 the
|
|
45
|
+
# `type:` frontmatter no longer maps 1:1 to kernel; the kernel is this set.
|
|
46
|
+
KERNEL_RULES: frozenset[str] = frozenset(
|
|
47
|
+
{
|
|
48
|
+
"agent-authority",
|
|
49
|
+
"ask-when-uncertain",
|
|
50
|
+
"commit-policy",
|
|
51
|
+
"direct-answers",
|
|
52
|
+
"language-and-tone",
|
|
53
|
+
"no-cheap-questions",
|
|
54
|
+
"non-destructive-by-default",
|
|
55
|
+
"scope-control",
|
|
56
|
+
"verify-before-complete",
|
|
57
|
+
}
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def strip_frontmatter(text: str) -> tuple[str, dict[str, str]]:
|
|
62
|
+
"""Strip leading YAML frontmatter and return (body, fields).
|
|
63
|
+
|
|
64
|
+
Minimal parser — handles `key: "value"` / `key: value` only. No nested
|
|
65
|
+
structures, no lists. Sufficient for the rule frontmatter contract.
|
|
66
|
+
"""
|
|
67
|
+
if not text.startswith("---\n"):
|
|
68
|
+
return text, {}
|
|
69
|
+
end = text.find("\n---\n", 4)
|
|
70
|
+
if end == -1:
|
|
71
|
+
return text, {}
|
|
72
|
+
raw = text[4:end]
|
|
73
|
+
body = text[end + 5 :]
|
|
74
|
+
fields: dict[str, str] = {}
|
|
75
|
+
for line in raw.splitlines():
|
|
76
|
+
if ":" not in line or line.startswith("#"):
|
|
77
|
+
continue
|
|
78
|
+
key, _, val = line.partition(":")
|
|
79
|
+
fields[key.strip()] = val.strip().strip('"').strip("'")
|
|
80
|
+
return body, fields
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def measure_rule(path: Path) -> dict[str, object]:
|
|
84
|
+
text = path.read_text(encoding="utf-8")
|
|
85
|
+
body, fields = strip_frontmatter(text)
|
|
86
|
+
return {
|
|
87
|
+
"id": path.stem,
|
|
88
|
+
"type": fields.get("type", "auto"),
|
|
89
|
+
"tier": fields.get("tier", ""),
|
|
90
|
+
"chars": len(body),
|
|
91
|
+
"lines": body.count("\n"),
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def collect() -> list[dict[str, object]]:
|
|
96
|
+
rules = [measure_rule(p) for p in sorted(RULES_DIR.glob("*.md"))]
|
|
97
|
+
return rules
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def load_overrides() -> set[str]:
|
|
101
|
+
"""Read iron-law-override allowlist (one rule-id per line, '#' comments)."""
|
|
102
|
+
if not OVERRIDES_FILE.exists():
|
|
103
|
+
return set()
|
|
104
|
+
out: set[str] = set()
|
|
105
|
+
for line in OVERRIDES_FILE.read_text(encoding="utf-8").splitlines():
|
|
106
|
+
s = line.split("#", 1)[0].strip()
|
|
107
|
+
if s:
|
|
108
|
+
out.add(s)
|
|
109
|
+
return out
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def aggregate(rules: list[dict[str, object]]) -> dict[str, object]:
|
|
113
|
+
always = [r for r in rules if r["type"] == "always"]
|
|
114
|
+
auto = [r for r in rules if r["type"] == "auto"]
|
|
115
|
+
kernel = [r for r in rules if r["id"] in KERNEL_RULES]
|
|
116
|
+
total_chars = sum(int(r["chars"]) for r in rules)
|
|
117
|
+
return {
|
|
118
|
+
"always_count": len(always),
|
|
119
|
+
"auto_count": len(auto),
|
|
120
|
+
"kernel_count": len(kernel),
|
|
121
|
+
"rule_count": len(rules),
|
|
122
|
+
"always_chars": sum(int(r["chars"]) for r in always),
|
|
123
|
+
"auto_chars": sum(int(r["chars"]) for r in auto),
|
|
124
|
+
"kernel_chars": sum(int(r["chars"]) for r in kernel),
|
|
125
|
+
"total_chars": total_chars,
|
|
126
|
+
"kernel_hard": KERNEL_HARD,
|
|
127
|
+
"kernel_target": KERNEL_TARGET,
|
|
128
|
+
"per_rule_hard": PER_RULE_HARD,
|
|
129
|
+
"per_rule_target": PER_RULE_TARGET,
|
|
130
|
+
"per_rule_override_ceiling": PER_RULE_OVERRIDE_CEILING,
|
|
131
|
+
"oversize_rules": sorted(
|
|
132
|
+
(r for r in rules if int(r["chars"]) > PER_RULE_HARD),
|
|
133
|
+
key=lambda r: (-int(r["chars"]), r["id"]),
|
|
134
|
+
),
|
|
135
|
+
"top5_largest": sorted(rules, key=lambda r: (-int(r["chars"]), r["id"]))[:5],
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def render_table(rules: list[dict[str, object]], agg: dict[str, object]) -> str:
|
|
140
|
+
lines: list[str] = []
|
|
141
|
+
lines.append("Rule budget — source: .agent-src.uncompressed/rules/")
|
|
142
|
+
lines.append("")
|
|
143
|
+
lines.append(f"{'id':<40} {'type':<7} {'tier':<5} {'chars':>7}")
|
|
144
|
+
lines.append("-" * 62)
|
|
145
|
+
for r in sorted(rules, key=lambda r: r["id"]):
|
|
146
|
+
flag = "!" if int(r["chars"]) > PER_RULE_HARD else (
|
|
147
|
+
"~" if int(r["chars"]) > PER_RULE_TARGET else " "
|
|
148
|
+
)
|
|
149
|
+
lines.append(
|
|
150
|
+
f"{r['id']:<40} {r['type']:<7} {str(r['tier']):<5} {r['chars']:>6}{flag}"
|
|
151
|
+
)
|
|
152
|
+
lines.append("")
|
|
153
|
+
lines.append(
|
|
154
|
+
f"kernel-bucket: {agg['kernel_chars']:>6} chars across {agg['kernel_count']} rules "
|
|
155
|
+
f"(target ≤ {KERNEL_TARGET}, hard ≤ {KERNEL_HARD})"
|
|
156
|
+
)
|
|
157
|
+
lines.append(
|
|
158
|
+
f"always-bucket: {agg['always_chars']:>6} chars across {agg['always_count']} rules "
|
|
159
|
+
f"(legacy frontmatter `type: always`)"
|
|
160
|
+
)
|
|
161
|
+
lines.append(
|
|
162
|
+
f" auto-bucket: {agg['auto_chars']:>6} chars across {agg['auto_count']} rules"
|
|
163
|
+
)
|
|
164
|
+
lines.append(f" total: {agg['total_chars']:>6} chars across {agg['rule_count']} rules")
|
|
165
|
+
lines.append("")
|
|
166
|
+
lines.append(f"top-5 largest:")
|
|
167
|
+
for r in agg["top5_largest"]: # type: ignore[index]
|
|
168
|
+
lines.append(f" {r['chars']:>5} {r['id']} ({r['type']})")
|
|
169
|
+
over = agg["oversize_rules"] # type: ignore[index]
|
|
170
|
+
if over:
|
|
171
|
+
lines.append("")
|
|
172
|
+
lines.append(f"OVER per-rule hard cap ({PER_RULE_HARD} chars): {len(over)} rule(s)")
|
|
173
|
+
return "\n".join(lines)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def kernel_budget_check(
|
|
177
|
+
rules: list[dict[str, object]], agg: dict[str, object], overrides: set[str]
|
|
178
|
+
) -> tuple[int, list[str]]:
|
|
179
|
+
"""Enforce kernel budget per Council R2 amendments.
|
|
180
|
+
|
|
181
|
+
Returns (exit_code, report_lines). Exit 0 = pass, 1 = breach.
|
|
182
|
+
|
|
183
|
+
Checks:
|
|
184
|
+
- Kernel-bucket sum ≤ KERNEL_HARD (25k).
|
|
185
|
+
- Each kernel rule ≤ PER_RULE_HARD (2.5k), unless listed in
|
|
186
|
+
`iron-law-overrides.txt` (then ≤ PER_RULE_OVERRIDE_CEILING = 4k).
|
|
187
|
+
- Missing kernel rules (rule-id in KERNEL_RULES but no file) → fail.
|
|
188
|
+
"""
|
|
189
|
+
out: list[str] = []
|
|
190
|
+
fails: list[str] = []
|
|
191
|
+
|
|
192
|
+
kernel_rules = [r for r in rules if r["id"] in KERNEL_RULES]
|
|
193
|
+
found_ids = {str(r["id"]) for r in kernel_rules}
|
|
194
|
+
missing = sorted(KERNEL_RULES - found_ids)
|
|
195
|
+
for mid in missing:
|
|
196
|
+
fails.append(f"missing kernel rule: {mid} (declared in KERNEL_RULES, no file found)")
|
|
197
|
+
|
|
198
|
+
bucket = int(agg["kernel_chars"])
|
|
199
|
+
out.append(
|
|
200
|
+
f"kernel-bucket: {bucket} / {KERNEL_HARD} chars "
|
|
201
|
+
f"({agg['kernel_count']} rules)"
|
|
202
|
+
)
|
|
203
|
+
if bucket > KERNEL_HARD:
|
|
204
|
+
fails.append(f"kernel-bucket {bucket} > hard cap {KERNEL_HARD}")
|
|
205
|
+
|
|
206
|
+
out.append(
|
|
207
|
+
f"per-rule cap: {PER_RULE_HARD} (override ceiling {PER_RULE_OVERRIDE_CEILING} "
|
|
208
|
+
f"with ADR; allowlist {OVERRIDES_FILE.relative_to(REPO_ROOT)})"
|
|
209
|
+
)
|
|
210
|
+
out.append("")
|
|
211
|
+
out.append(f"{'id':<28} {'chars':>6} {'cap':>6} {'status':<24}")
|
|
212
|
+
out.append("-" * 68)
|
|
213
|
+
for r in sorted(kernel_rules, key=lambda r: r["id"]):
|
|
214
|
+
rid = str(r["id"])
|
|
215
|
+
chars = int(r["chars"])
|
|
216
|
+
if rid in overrides:
|
|
217
|
+
cap = PER_RULE_OVERRIDE_CEILING
|
|
218
|
+
label = "OK (override)"
|
|
219
|
+
if chars > cap:
|
|
220
|
+
label = f"FAIL (>{cap} ceiling)"
|
|
221
|
+
fails.append(f"{rid} {chars} > override ceiling {cap}")
|
|
222
|
+
else:
|
|
223
|
+
cap = PER_RULE_HARD
|
|
224
|
+
if chars > cap:
|
|
225
|
+
label = "FAIL (needs override ADR)"
|
|
226
|
+
fails.append(f"{rid} {chars} > per-rule hard cap {cap} (no override)")
|
|
227
|
+
elif chars > PER_RULE_TARGET:
|
|
228
|
+
label = "warn (> target)"
|
|
229
|
+
else:
|
|
230
|
+
label = "OK"
|
|
231
|
+
out.append(f"{rid:<28} {chars:>6} {cap:>6} {label:<24}")
|
|
232
|
+
|
|
233
|
+
out.append("")
|
|
234
|
+
if fails:
|
|
235
|
+
out.append(f"❌ kernel budget check: {len(fails)} breach(es)")
|
|
236
|
+
for f in fails:
|
|
237
|
+
out.append(f" - {f}")
|
|
238
|
+
return 1, out
|
|
239
|
+
out.append(f"✅ kernel budget check: pass")
|
|
240
|
+
return 0, out
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def trend_append(agg: dict[str, object]) -> tuple[int, str]:
|
|
244
|
+
"""Append a daily snapshot to agents/.rule-budget-history.jsonl.
|
|
245
|
+
|
|
246
|
+
Idempotent per UTC day: if today's date already has a row, the file
|
|
247
|
+
is not modified. Snapshot fields: date, kernel_chars, auto_chars,
|
|
248
|
+
rule_count, total_chars. Read by `roadmap:progress` for the Kernel
|
|
249
|
+
track per `road-to-kernel-and-router.md` P5.3.
|
|
250
|
+
"""
|
|
251
|
+
today = _dt.datetime.now(_dt.timezone.utc).date().isoformat()
|
|
252
|
+
snapshot = {
|
|
253
|
+
"date": today,
|
|
254
|
+
"kernel_chars": int(agg["kernel_chars"]),
|
|
255
|
+
"auto_chars": int(agg["auto_chars"]),
|
|
256
|
+
"rule_count": int(agg["rule_count"]),
|
|
257
|
+
"total_chars": int(agg["total_chars"]),
|
|
258
|
+
}
|
|
259
|
+
TREND_FILE.parent.mkdir(parents=True, exist_ok=True)
|
|
260
|
+
if TREND_FILE.exists():
|
|
261
|
+
for line in TREND_FILE.read_text(encoding="utf-8").splitlines():
|
|
262
|
+
line = line.strip()
|
|
263
|
+
if not line:
|
|
264
|
+
continue
|
|
265
|
+
try:
|
|
266
|
+
row = json.loads(line)
|
|
267
|
+
except json.JSONDecodeError:
|
|
268
|
+
continue
|
|
269
|
+
if row.get("date") == today:
|
|
270
|
+
return 0, f"trend: {today} already recorded — no-op"
|
|
271
|
+
with TREND_FILE.open("a", encoding="utf-8") as fh:
|
|
272
|
+
fh.write(json.dumps(snapshot, sort_keys=True) + "\n")
|
|
273
|
+
return 0, f"trend: appended {today} → {TREND_FILE.relative_to(REPO_ROOT)}"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main(argv: list[str] | None = None) -> int:
|
|
277
|
+
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
|
|
278
|
+
parser.add_argument("--json", action="store_true", help="emit JSON instead of a table")
|
|
279
|
+
parser.add_argument(
|
|
280
|
+
"--kernel-budget-check",
|
|
281
|
+
action="store_true",
|
|
282
|
+
help="enforce Council R2 kernel-bucket + per-rule caps; exit 1 on breach",
|
|
283
|
+
)
|
|
284
|
+
parser.add_argument(
|
|
285
|
+
"--trend-append",
|
|
286
|
+
action="store_true",
|
|
287
|
+
help="append today's snapshot to agents/.rule-budget-history.jsonl (idempotent per UTC day)",
|
|
288
|
+
)
|
|
289
|
+
args = parser.parse_args(argv)
|
|
290
|
+
|
|
291
|
+
rules = collect()
|
|
292
|
+
agg = aggregate(rules)
|
|
293
|
+
|
|
294
|
+
if args.kernel_budget_check:
|
|
295
|
+
overrides = load_overrides()
|
|
296
|
+
code, report = kernel_budget_check(rules, agg, overrides)
|
|
297
|
+
print("\n".join(report))
|
|
298
|
+
return code
|
|
299
|
+
|
|
300
|
+
if args.trend_append:
|
|
301
|
+
code, msg = trend_append(agg)
|
|
302
|
+
print(msg)
|
|
303
|
+
return code
|
|
304
|
+
|
|
305
|
+
if args.json:
|
|
306
|
+
payload = {"rules": sorted(rules, key=lambda r: r["id"]), "summary": agg}
|
|
307
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
308
|
+
else:
|
|
309
|
+
print(render_table(rules, agg))
|
|
310
|
+
return 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
if __name__ == "__main__":
|
|
314
|
+
sys.exit(main())
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Prototype contradiction linter (P1.1 of road-to-package-optimization).
|
|
4
|
+
|
|
5
|
+
Hard acceptance: must flag >=3 real cross-artifact contradictions in this
|
|
6
|
+
repo within 5 s wall-clock and < $0.01 cost (deterministic, no LLM calls).
|
|
7
|
+
On failure, the roadmap closes with the null result documented; no
|
|
8
|
+
Phase 2 work begins.
|
|
9
|
+
|
|
10
|
+
Heuristic family — three deterministic checks across rules, skills,
|
|
11
|
+
commands, and contexts:
|
|
12
|
+
|
|
13
|
+
1. Routing mismatch: rule frontmatter `routes_to: [skill:foo]` but the
|
|
14
|
+
target artifact does not exist or has no matching trigger.
|
|
15
|
+
2. Trigger collision with imperative conflict: two artifacts share a
|
|
16
|
+
trigger keyword AND one body contains an `ALWAYS X` Iron Law while
|
|
17
|
+
the other contains `NEVER X` (or `MUST` vs `MUST NOT`) on the same
|
|
18
|
+
verb-object.
|
|
19
|
+
3. Catalog drift: a token-optimizer-style catalog row cites a path that
|
|
20
|
+
does not exist (subset of #1, broader scope than the freshness gate).
|
|
21
|
+
|
|
22
|
+
Stdlib only. JSON to stdout. Exit 0 = clean / Exit 1 = contradictions found.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import json
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
import time
|
|
31
|
+
from pathlib import Path
|
|
32
|
+
|
|
33
|
+
REPO = Path(__file__).resolve().parent.parent
|
|
34
|
+
SRC = REPO / ".agent-src.uncompressed"
|
|
35
|
+
|
|
36
|
+
ARTIFACT_DIRS = {
|
|
37
|
+
"rule": SRC / "rules",
|
|
38
|
+
"skill": SRC / "skills",
|
|
39
|
+
"command": SRC / "commands",
|
|
40
|
+
"context": SRC / "contexts",
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
FM_RE = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
|
|
44
|
+
ALWAYS_RE = re.compile(r"^\s*(ALWAYS|MUST)\s+([A-Z][^.\n]{2,80})", re.MULTILINE)
|
|
45
|
+
NEVER_RE = re.compile(r"^\s*(NEVER|MUST NOT|DO NOT)\s+([A-Z][^.\n]{2,80})", re.MULTILINE)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_artifact(path: Path, kind: str) -> dict:
|
|
49
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
50
|
+
fm: dict = {}
|
|
51
|
+
m = FM_RE.match(text)
|
|
52
|
+
body = text
|
|
53
|
+
if m:
|
|
54
|
+
body = text[m.end():]
|
|
55
|
+
for line in m.group(1).splitlines():
|
|
56
|
+
if ":" in line and not line.startswith(" "):
|
|
57
|
+
k, _, v = line.partition(":")
|
|
58
|
+
fm[k.strip()] = v.strip()
|
|
59
|
+
triggers = re.findall(r"`([a-z][a-z0-9_-]+)`", fm.get("description", ""))
|
|
60
|
+
routes = re.findall(r"(skill|rule|command):([a-z0-9_-]+)", fm.get("routes_to", ""))
|
|
61
|
+
always = [m.group(2).strip() for m in ALWAYS_RE.finditer(body)]
|
|
62
|
+
never = [m.group(2).strip() for m in NEVER_RE.finditer(body)]
|
|
63
|
+
return {
|
|
64
|
+
"kind": kind,
|
|
65
|
+
"path": str(path.relative_to(REPO)),
|
|
66
|
+
"id": path.stem if path.name != "SKILL.md" else path.parent.name,
|
|
67
|
+
"triggers": set(triggers),
|
|
68
|
+
"routes": routes,
|
|
69
|
+
"always": always,
|
|
70
|
+
"never": never,
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def collect() -> list[dict]:
|
|
75
|
+
out: list[dict] = []
|
|
76
|
+
for kind, root in ARTIFACT_DIRS.items():
|
|
77
|
+
if not root.exists():
|
|
78
|
+
continue
|
|
79
|
+
for p in root.rglob("*.md"):
|
|
80
|
+
if p.name in {"README.md", "INDEX.md"}:
|
|
81
|
+
continue
|
|
82
|
+
out.append(parse_artifact(p, kind))
|
|
83
|
+
return out
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def check_routing(arts: list[dict]) -> list[dict]:
|
|
87
|
+
by_id = {(a["kind"], a["id"]): a for a in arts}
|
|
88
|
+
flags: list[dict] = []
|
|
89
|
+
for a in arts:
|
|
90
|
+
for tgt_kind, tgt_id in a["routes"]:
|
|
91
|
+
if (tgt_kind, tgt_id) not in by_id:
|
|
92
|
+
flags.append({
|
|
93
|
+
"type": "routing_mismatch",
|
|
94
|
+
"artifact_a": a["path"],
|
|
95
|
+
"artifact_b": f"{tgt_kind}:{tgt_id} (missing)",
|
|
96
|
+
"evidence": f"{a['id']} routes_to {tgt_kind}:{tgt_id}, target not found",
|
|
97
|
+
})
|
|
98
|
+
return flags
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def normalize_verb(s: str) -> str:
|
|
102
|
+
return re.sub(r"[^a-z ]+", "", s.lower()).split(" ", 1)[0] if s else ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def check_imperative_conflict(arts: list[dict]) -> list[dict]:
|
|
106
|
+
flags: list[dict] = []
|
|
107
|
+
by_trigger: dict[str, list[dict]] = {}
|
|
108
|
+
for a in arts:
|
|
109
|
+
for t in a["triggers"]:
|
|
110
|
+
by_trigger.setdefault(t, []).append(a)
|
|
111
|
+
for trigger, group in by_trigger.items():
|
|
112
|
+
if len(group) < 2:
|
|
113
|
+
continue
|
|
114
|
+
for i, a in enumerate(group):
|
|
115
|
+
for b in group[i + 1:]:
|
|
116
|
+
a_verbs = {normalize_verb(s) for s in a["always"]}
|
|
117
|
+
b_verbs = {normalize_verb(s) for s in b["never"]}
|
|
118
|
+
conflict = a_verbs & b_verbs - {""}
|
|
119
|
+
if conflict:
|
|
120
|
+
flags.append({
|
|
121
|
+
"type": "imperative_conflict",
|
|
122
|
+
"artifact_a": a["path"],
|
|
123
|
+
"artifact_b": b["path"],
|
|
124
|
+
"evidence": f"shared trigger '{trigger}', a says ALWAYS {sorted(conflict)}, b says NEVER {sorted(conflict)}",
|
|
125
|
+
})
|
|
126
|
+
return flags
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def main() -> int:
|
|
130
|
+
t0 = time.monotonic()
|
|
131
|
+
arts = collect()
|
|
132
|
+
flags = check_routing(arts) + check_imperative_conflict(arts)
|
|
133
|
+
elapsed = time.monotonic() - t0
|
|
134
|
+
report = {
|
|
135
|
+
"artifacts_scanned": len(arts),
|
|
136
|
+
"elapsed_seconds": round(elapsed, 3),
|
|
137
|
+
"flags": flags,
|
|
138
|
+
"acceptance": {
|
|
139
|
+
"min_flags": 3,
|
|
140
|
+
"max_seconds": 5.0,
|
|
141
|
+
"passed": len(flags) >= 3 and elapsed < 5.0,
|
|
142
|
+
},
|
|
143
|
+
}
|
|
144
|
+
json.dump(report, sys.stdout, indent=2)
|
|
145
|
+
sys.stdout.write("\n")
|
|
146
|
+
return 0 if not flags else 1
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
if __name__ == "__main__":
|
|
150
|
+
sys.exit(main())
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Redact captured hook payloads for the verified-platforms roadmap.
|
|
3
|
+
|
|
4
|
+
Reads JSON capture files written by ``dispatch_hook.py`` (when
|
|
5
|
+
``AGENT_HOOK_CAPTURE_DIR`` is set) and produces a redacted version
|
|
6
|
+
suitable for pasting into
|
|
7
|
+
``agents/roadmaps/road-to-verified-chat-history-platforms.md``.
|
|
8
|
+
|
|
9
|
+
Redaction policy (per the roadmap's Capture-and-redact protocol):
|
|
10
|
+
|
|
11
|
+
- Replace string values at known user-content paths with
|
|
12
|
+
``<REDACTED>``. Default field allowlist mirrors the fallback list
|
|
13
|
+
in ``scripts/chat_history.py::_extract_hook_text`` plus Augment's
|
|
14
|
+
nested ``conversation.*`` shape.
|
|
15
|
+
- Preserve envelope keys (``hook_event_name``, ``session_id``,
|
|
16
|
+
``platform``, ``event``, ``cwd``, ``workspace_roots``,
|
|
17
|
+
``transcript_path``, ``model``, ``cursor_version``, …) so the
|
|
18
|
+
schema is reviewable.
|
|
19
|
+
- ``--strict`` redacts any string longer than ``--max-len`` (default
|
|
20
|
+
120) chars regardless of key, as a safety net for unknown fields.
|
|
21
|
+
|
|
22
|
+
Usage:
|
|
23
|
+
|
|
24
|
+
python3 scripts/redact_hook_capture.py <input> [--out <path>] [--strict]
|
|
25
|
+
|
|
26
|
+
Input may be a single JSON file or a directory; with a directory,
|
|
27
|
+
every ``*.json`` is redacted and written next to the original with
|
|
28
|
+
the suffix ``.redacted.json``.
|
|
29
|
+
"""
|
|
30
|
+
from __future__ import annotations
|
|
31
|
+
|
|
32
|
+
import argparse
|
|
33
|
+
import json
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
REDACTED = "<REDACTED>"
|
|
39
|
+
|
|
40
|
+
# Field names that carry user / agent content (from
|
|
41
|
+
# scripts/chat_history.py::_extract_hook_text fallback list +
|
|
42
|
+
# nested Augment shape). Matched case-insensitively against the
|
|
43
|
+
# leaf key.
|
|
44
|
+
_USER_CONTENT_KEYS = {
|
|
45
|
+
"prompt", "user_prompt", "userprompt", "first_user_msg",
|
|
46
|
+
"firstusermsg", "usermessage", "user_message", "text",
|
|
47
|
+
"response", "message", "content",
|
|
48
|
+
# Augment Code with includeConversationData
|
|
49
|
+
"agenttextresponse", "agent_text_response",
|
|
50
|
+
"agentcoderesponse", "agent_code_response",
|
|
51
|
+
# Cursor / generic
|
|
52
|
+
"submitted_prompt", "submittedprompt",
|
|
53
|
+
# Free-form transcript bodies (path stays — content is in another file)
|
|
54
|
+
"transcript", "transcript_text",
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
# Keys whose value is a structural / schema marker — keep as-is even
|
|
58
|
+
# when --strict would otherwise redact long values.
|
|
59
|
+
_ENVELOPE_KEYS_KEEP = {
|
|
60
|
+
"hook_event_name", "session_id", "transcript_path", "transcriptpath",
|
|
61
|
+
"platform", "event", "native_event", "captured_at", "cwd",
|
|
62
|
+
"workspace_roots", "model", "cursor_version", "user_email",
|
|
63
|
+
"conversation_id", "generation_id", "agent", "type",
|
|
64
|
+
"schema_version", "started_at", "completed_at", "_raw_text",
|
|
65
|
+
"path", "changetype", "change_type",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _redact_value(val: Any, *, key: str | None, strict: bool,
|
|
70
|
+
max_len: int) -> Any:
|
|
71
|
+
"""Recursively redact a value."""
|
|
72
|
+
norm_key = (key or "").lower().replace("-", "_")
|
|
73
|
+
if isinstance(val, dict):
|
|
74
|
+
return {k: _redact_value(v, key=k, strict=strict, max_len=max_len)
|
|
75
|
+
for k, v in val.items()}
|
|
76
|
+
if isinstance(val, list):
|
|
77
|
+
return [_redact_value(item, key=key, strict=strict, max_len=max_len)
|
|
78
|
+
for item in val]
|
|
79
|
+
if isinstance(val, str):
|
|
80
|
+
if norm_key in _ENVELOPE_KEYS_KEEP:
|
|
81
|
+
return val
|
|
82
|
+
if norm_key in _USER_CONTENT_KEYS:
|
|
83
|
+
return REDACTED
|
|
84
|
+
if strict and len(val) > max_len:
|
|
85
|
+
return REDACTED
|
|
86
|
+
return val
|
|
87
|
+
return val
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def redact(record: dict, *, strict: bool = False, max_len: int = 120) -> dict:
|
|
91
|
+
"""Redact a single capture record. Top-level envelope is preserved."""
|
|
92
|
+
out: dict = {}
|
|
93
|
+
for k, v in record.items():
|
|
94
|
+
if k == "raw_payload":
|
|
95
|
+
out[k] = _redact_value(v, key=None, strict=strict,
|
|
96
|
+
max_len=max_len)
|
|
97
|
+
else:
|
|
98
|
+
out[k] = _redact_value(v, key=k, strict=strict,
|
|
99
|
+
max_len=max_len)
|
|
100
|
+
return out
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _process_file(path: Path, *, out: Path | None, strict: bool,
|
|
104
|
+
max_len: int) -> Path:
|
|
105
|
+
record = json.loads(path.read_text(encoding="utf-8"))
|
|
106
|
+
redacted = redact(record, strict=strict, max_len=max_len)
|
|
107
|
+
target = out or path.with_suffix(".redacted.json")
|
|
108
|
+
target.write_text(json.dumps(redacted, indent=2) + "\n",
|
|
109
|
+
encoding="utf-8")
|
|
110
|
+
return target
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def main(argv: list[str] | None = None) -> int:
|
|
114
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
115
|
+
parser.add_argument("input", help="capture file or directory")
|
|
116
|
+
parser.add_argument("--out", default=None,
|
|
117
|
+
help="output path (single-file mode only)")
|
|
118
|
+
parser.add_argument("--strict", action="store_true",
|
|
119
|
+
help="redact any string longer than --max-len")
|
|
120
|
+
parser.add_argument("--max-len", type=int, default=120,
|
|
121
|
+
help="strict-mode length threshold (default 120)")
|
|
122
|
+
args = parser.parse_args(argv)
|
|
123
|
+
|
|
124
|
+
src = Path(args.input).expanduser()
|
|
125
|
+
if not src.exists():
|
|
126
|
+
sys.stderr.write(f"redact: input not found: {src}\n")
|
|
127
|
+
return 2
|
|
128
|
+
|
|
129
|
+
if src.is_dir():
|
|
130
|
+
if args.out:
|
|
131
|
+
sys.stderr.write("redact: --out is single-file only\n")
|
|
132
|
+
return 2
|
|
133
|
+
files = sorted(p for p in src.glob("*.json")
|
|
134
|
+
if not p.name.endswith(".redacted.json"))
|
|
135
|
+
for path in files:
|
|
136
|
+
target = _process_file(path, out=None, strict=args.strict,
|
|
137
|
+
max_len=args.max_len)
|
|
138
|
+
print(f"redacted: {target}")
|
|
139
|
+
return 0
|
|
140
|
+
|
|
141
|
+
target = _process_file(src, out=Path(args.out) if args.out else None,
|
|
142
|
+
strict=args.strict, max_len=args.max_len)
|
|
143
|
+
print(f"redacted: {target}")
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
raise SystemExit(main())
|