@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,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Lint rule frontmatter for the `tier:` key.
|
|
3
|
+
|
|
4
|
+
Hard-fails CI if any rule under .agent-src.uncompressed/rules/ lacks a
|
|
5
|
+
`tier:` declaration or uses an unknown tier value. The valid tier set is
|
|
6
|
+
locked by agents/contexts/hardening-pattern.md and the matrix in
|
|
7
|
+
agents/contexts/rule-trigger-matrix.md.
|
|
8
|
+
|
|
9
|
+
Hooked into `task ci` after `task lint-skills`.
|
|
10
|
+
|
|
11
|
+
Exit codes:
|
|
12
|
+
0 every rule declares a valid tier
|
|
13
|
+
1 one or more rules missing or using an invalid tier
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import sys
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
REPO = Path(__file__).resolve().parents[1]
|
|
21
|
+
RULES_DIR = REPO / ".agent-src.uncompressed" / "rules"
|
|
22
|
+
|
|
23
|
+
VALID_TIERS = frozenset({"1", "2a", "2b", "3", "safety-floor", "mechanical-already"})
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_tier(text: str) -> str | None:
|
|
27
|
+
if not text.startswith("---\n"):
|
|
28
|
+
return None
|
|
29
|
+
end = text.find("\n---\n", 4)
|
|
30
|
+
if end == -1:
|
|
31
|
+
return None
|
|
32
|
+
for line in text[4:end].splitlines():
|
|
33
|
+
if ":" not in line:
|
|
34
|
+
continue
|
|
35
|
+
k, _, v = line.partition(":")
|
|
36
|
+
if k.strip() == "tier":
|
|
37
|
+
return v.strip().strip('"').strip("'")
|
|
38
|
+
return None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def main() -> int:
|
|
42
|
+
rules = sorted(RULES_DIR.glob("*.md"))
|
|
43
|
+
if not rules:
|
|
44
|
+
print(f"lint_rule_tiers: no rules found under {RULES_DIR}", file=sys.stderr)
|
|
45
|
+
return 1
|
|
46
|
+
|
|
47
|
+
missing: list[str] = []
|
|
48
|
+
invalid: list[tuple[str, str]] = []
|
|
49
|
+
|
|
50
|
+
for rule in rules:
|
|
51
|
+
tier = parse_tier(rule.read_text(encoding="utf-8"))
|
|
52
|
+
if tier is None:
|
|
53
|
+
missing.append(rule.name)
|
|
54
|
+
elif tier not in VALID_TIERS:
|
|
55
|
+
invalid.append((rule.name, tier))
|
|
56
|
+
|
|
57
|
+
if missing or invalid:
|
|
58
|
+
print(
|
|
59
|
+
f"❌ lint_rule_tiers: {len(missing)} missing, "
|
|
60
|
+
f"{len(invalid)} invalid (of {len(rules)} rules)",
|
|
61
|
+
file=sys.stderr,
|
|
62
|
+
)
|
|
63
|
+
for name in missing:
|
|
64
|
+
print(f" missing tier: {name}", file=sys.stderr)
|
|
65
|
+
for name, tier in invalid:
|
|
66
|
+
print(f" invalid tier '{tier}': {name}", file=sys.stderr)
|
|
67
|
+
print(
|
|
68
|
+
f" valid tiers: {sorted(VALID_TIERS)}",
|
|
69
|
+
file=sys.stderr,
|
|
70
|
+
)
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
print(f"✅ lint_rule_tiers: {len(rules)} rules, all tier values valid")
|
|
74
|
+
return 0
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
if __name__ == "__main__":
|
|
78
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""lint_showcase_sessions.py — gate `docs/showcase.md` ↔ `docs/showcase/sessions/`.
|
|
3
|
+
|
|
4
|
+
Phase 1.6 deliverable for `road-to-feedback-consolidation.md`.
|
|
5
|
+
|
|
6
|
+
Rules:
|
|
7
|
+
1. Every `docs/showcase/sessions/<slug>.log` reference inside
|
|
8
|
+
`docs/showcase.md` must point to an existing file.
|
|
9
|
+
2. Every referenced session file must carry a YAML frontmatter block
|
|
10
|
+
containing at minimum `commit_sha` and a `metrics:` mapping (the
|
|
11
|
+
four outcome-baseline metrics; values may be null but the keys
|
|
12
|
+
must be present per `scripts/capture_showcase_session.py`).
|
|
13
|
+
3. Every file under `docs/showcase/sessions/` must be referenced
|
|
14
|
+
from `docs/showcase.md` — orphans are not allowed.
|
|
15
|
+
|
|
16
|
+
When `docs/showcase/sessions/` is empty AND `docs/showcase.md` carries
|
|
17
|
+
no session references, the linter exits 0 (clean — Phase 1.3-1.5 are
|
|
18
|
+
deferred to manual host-agent runs).
|
|
19
|
+
|
|
20
|
+
Exit codes:
|
|
21
|
+
0 clean
|
|
22
|
+
1 one or more violations
|
|
23
|
+
"""
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
ROOT = Path(__file__).resolve().parent.parent
|
|
31
|
+
SHOWCASE_MD = ROOT / "docs" / "showcase.md"
|
|
32
|
+
SESSIONS_DIR = ROOT / "docs" / "showcase" / "sessions"
|
|
33
|
+
|
|
34
|
+
REQUIRED_METRICS = {
|
|
35
|
+
"tool_call_count",
|
|
36
|
+
"reply_chars_mean",
|
|
37
|
+
"memory_hit_ratio",
|
|
38
|
+
"verify_pass_rate",
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
REF_RE = re.compile(r"docs/showcase/sessions/([A-Za-z0-9_\-]+)\.log")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_frontmatter(text: str) -> dict[str, str] | None:
|
|
45
|
+
if not text.startswith("---\n"):
|
|
46
|
+
return None
|
|
47
|
+
end = text.find("\n---\n", 4)
|
|
48
|
+
if end == -1:
|
|
49
|
+
return None
|
|
50
|
+
block: dict[str, str] = {}
|
|
51
|
+
current_key: str | None = None
|
|
52
|
+
nested: dict[str, str] = {}
|
|
53
|
+
for raw in text[4:end].splitlines():
|
|
54
|
+
if not raw.strip():
|
|
55
|
+
continue
|
|
56
|
+
if raw.startswith(" ") and current_key:
|
|
57
|
+
k, _, v = raw.strip().partition(":")
|
|
58
|
+
nested[k.strip()] = v.strip()
|
|
59
|
+
continue
|
|
60
|
+
if ":" not in raw:
|
|
61
|
+
continue
|
|
62
|
+
k, _, v = raw.partition(":")
|
|
63
|
+
v = v.strip()
|
|
64
|
+
if not v:
|
|
65
|
+
current_key = k.strip()
|
|
66
|
+
nested = {}
|
|
67
|
+
block[current_key] = ""
|
|
68
|
+
block[f"_{current_key}_nested"] = nested # type: ignore[assignment]
|
|
69
|
+
else:
|
|
70
|
+
block[k.strip()] = v
|
|
71
|
+
current_key = None
|
|
72
|
+
return block
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _validate_session(slug: str) -> list[str]:
|
|
76
|
+
errors: list[str] = []
|
|
77
|
+
path = SESSIONS_DIR / f"{slug}.log"
|
|
78
|
+
if not path.is_file():
|
|
79
|
+
errors.append(f"referenced session missing on disk: {path.relative_to(ROOT)}")
|
|
80
|
+
return errors
|
|
81
|
+
text = path.read_text(encoding="utf-8")
|
|
82
|
+
fm = _parse_frontmatter(text)
|
|
83
|
+
if fm is None:
|
|
84
|
+
errors.append(f"{slug}.log: no YAML frontmatter block")
|
|
85
|
+
return errors
|
|
86
|
+
if not fm.get("commit_sha"):
|
|
87
|
+
errors.append(f"{slug}.log: missing or empty `commit_sha`")
|
|
88
|
+
metrics = fm.get("_metrics_nested")
|
|
89
|
+
if not isinstance(metrics, dict):
|
|
90
|
+
errors.append(f"{slug}.log: missing `metrics:` mapping")
|
|
91
|
+
else:
|
|
92
|
+
missing = REQUIRED_METRICS - set(metrics.keys())
|
|
93
|
+
if missing:
|
|
94
|
+
errors.append(
|
|
95
|
+
f"{slug}.log: metrics block missing keys: {sorted(missing)}"
|
|
96
|
+
)
|
|
97
|
+
return errors
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
if not SHOWCASE_MD.is_file():
|
|
102
|
+
print(f"❌ {SHOWCASE_MD.relative_to(ROOT)} not found", file=sys.stderr)
|
|
103
|
+
return 1
|
|
104
|
+
|
|
105
|
+
text = SHOWCASE_MD.read_text(encoding="utf-8")
|
|
106
|
+
referenced = set(REF_RE.findall(text))
|
|
107
|
+
|
|
108
|
+
on_disk: set[str] = set()
|
|
109
|
+
if SESSIONS_DIR.is_dir():
|
|
110
|
+
on_disk = {p.stem for p in SESSIONS_DIR.glob("*.log")}
|
|
111
|
+
|
|
112
|
+
errors: list[str] = []
|
|
113
|
+
|
|
114
|
+
for slug in sorted(referenced):
|
|
115
|
+
errors.extend(_validate_session(slug))
|
|
116
|
+
|
|
117
|
+
orphans = on_disk - referenced
|
|
118
|
+
for slug in sorted(orphans):
|
|
119
|
+
errors.append(
|
|
120
|
+
f"orphan session: docs/showcase/sessions/{slug}.log "
|
|
121
|
+
f"is not referenced from docs/showcase.md"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
if errors:
|
|
125
|
+
print(
|
|
126
|
+
f"❌ lint_showcase_sessions: {len(errors)} violation(s) "
|
|
127
|
+
f"({len(referenced)} referenced, {len(on_disk)} on disk)",
|
|
128
|
+
file=sys.stderr,
|
|
129
|
+
)
|
|
130
|
+
for err in errors:
|
|
131
|
+
print(f" {err}", file=sys.stderr)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
if not referenced and not on_disk:
|
|
135
|
+
print(
|
|
136
|
+
"✅ lint_showcase_sessions: 0 sessions referenced, 0 on disk "
|
|
137
|
+
"(Phase 1.3-1.5 deferred to manual host-agent runs)"
|
|
138
|
+
)
|
|
139
|
+
else:
|
|
140
|
+
print(
|
|
141
|
+
f"✅ lint_showcase_sessions: {len(referenced)} session(s) "
|
|
142
|
+
f"valid ({len(on_disk)} on disk)"
|
|
143
|
+
)
|
|
144
|
+
return 0
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
if __name__ == "__main__":
|
|
148
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Platform-agnostic hook for the `minimal-safe-diff` rule.
|
|
3
|
+
|
|
4
|
+
Pre-edit gate: counts unique files touched in the current turn (or
|
|
5
|
+
session, when the platform lacks a turn-boundary signal) and warns
|
|
6
|
+
when the count exceeds the configured threshold. The hook never
|
|
7
|
+
blocks — it is observability infra. The rule body cites the resulting
|
|
8
|
+
state file when the agent prepares a diff for review.
|
|
9
|
+
|
|
10
|
+
Wired to multiple events via the manifest:
|
|
11
|
+
- session_start / user_prompt_submit → reset turn-scoped counters
|
|
12
|
+
- pre_tool_use → record the planned edit's path before execution
|
|
13
|
+
|
|
14
|
+
Output: `agents/state/minimal-safe-diff.json`
|
|
15
|
+
{
|
|
16
|
+
"schema_version": 1,
|
|
17
|
+
"session_id": "<str>",
|
|
18
|
+
"turn_started_at": "<iso8601|null>",
|
|
19
|
+
"files_touched_this_turn": ["a", "b", ...],
|
|
20
|
+
"count": <int>,
|
|
21
|
+
"threshold": <int>,
|
|
22
|
+
"warning": <bool>,
|
|
23
|
+
"checked_at": "<iso8601>"
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
Exit code is always 0.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import argparse
|
|
31
|
+
import datetime as _dt
|
|
32
|
+
import json
|
|
33
|
+
import re
|
|
34
|
+
import sys
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
|
|
37
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
38
|
+
from hooks.state_io import atomic_write_json # noqa: E402
|
|
39
|
+
|
|
40
|
+
STATE_FILE = Path("agents") / "state" / "minimal-safe-diff.json"
|
|
41
|
+
SETTINGS_FILE = ".agent-settings.yml"
|
|
42
|
+
DEFAULT_THRESHOLD = 5
|
|
43
|
+
MAX_TRACKED_PATHS = 200 # hard cap to keep the state file bounded
|
|
44
|
+
|
|
45
|
+
# Edit-tool names across platforms whose successful invocation results
|
|
46
|
+
# in a file being modified, created, or deleted. Keep explicit so an
|
|
47
|
+
# unknown tool doesn't trigger a false positive.
|
|
48
|
+
EDIT_TOOLS = frozenset({
|
|
49
|
+
"str-replace-editor", "str_replace_editor", # Augment
|
|
50
|
+
"save-file", "save_file", # Augment
|
|
51
|
+
"remove-files", "remove_files", # Augment
|
|
52
|
+
"Edit", "Write", "MultiEdit", # Claude Code
|
|
53
|
+
"edit_file", "edit-file", # Cursor
|
|
54
|
+
"create_file", "create-file", "delete_file", # variants
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _now() -> str:
|
|
59
|
+
return _dt.datetime.now(_dt.timezone.utc).isoformat(timespec="seconds")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _empty_state(threshold: int) -> dict:
|
|
63
|
+
return {
|
|
64
|
+
"schema_version": 1,
|
|
65
|
+
"session_id": "",
|
|
66
|
+
"turn_started_at": None,
|
|
67
|
+
"files_touched_this_turn": [],
|
|
68
|
+
"count": 0,
|
|
69
|
+
"threshold": threshold,
|
|
70
|
+
"warning": False,
|
|
71
|
+
"checked_at": _now(),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _read_threshold(consumer_root: Path) -> int:
|
|
76
|
+
"""Parse `hooks.minimal_safe_diff.threshold` from .agent-settings.yml.
|
|
77
|
+
|
|
78
|
+
Dependency-free YAML scan — we only need a single integer under a
|
|
79
|
+
nested block; pulling pyyaml in for this would be overkill.
|
|
80
|
+
"""
|
|
81
|
+
settings = consumer_root / SETTINGS_FILE
|
|
82
|
+
if not settings.is_file():
|
|
83
|
+
return DEFAULT_THRESHOLD
|
|
84
|
+
try:
|
|
85
|
+
text = settings.read_text(encoding="utf-8")
|
|
86
|
+
except OSError:
|
|
87
|
+
return DEFAULT_THRESHOLD
|
|
88
|
+
|
|
89
|
+
in_hooks = False
|
|
90
|
+
in_msd = False
|
|
91
|
+
for raw in text.splitlines():
|
|
92
|
+
line = raw.rstrip()
|
|
93
|
+
if not line or line.lstrip().startswith("#"):
|
|
94
|
+
continue
|
|
95
|
+
# top-level key resets nested context
|
|
96
|
+
if line and not line.startswith((" ", "\t")):
|
|
97
|
+
in_hooks = re.match(r"^hooks\s*:\s*$", line) is not None
|
|
98
|
+
in_msd = False
|
|
99
|
+
continue
|
|
100
|
+
if in_hooks:
|
|
101
|
+
m = re.match(r"^\s+minimal_safe_diff\s*:\s*$", line)
|
|
102
|
+
if m:
|
|
103
|
+
in_msd = True
|
|
104
|
+
continue
|
|
105
|
+
# leaving the minimal_safe_diff block when indent decreases
|
|
106
|
+
if in_msd and re.match(r"^\s{0,3}\S", line):
|
|
107
|
+
in_msd = False
|
|
108
|
+
if in_msd:
|
|
109
|
+
m = re.match(r"^\s+threshold\s*:\s*(\d+)\s*(?:#.*)?$", line)
|
|
110
|
+
if m:
|
|
111
|
+
try:
|
|
112
|
+
val = int(m.group(1))
|
|
113
|
+
return val if val > 0 else DEFAULT_THRESHOLD
|
|
114
|
+
except ValueError:
|
|
115
|
+
return DEFAULT_THRESHOLD
|
|
116
|
+
return DEFAULT_THRESHOLD
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _load_state(target: Path, threshold: int) -> dict:
|
|
120
|
+
if not target.is_file():
|
|
121
|
+
return _empty_state(threshold)
|
|
122
|
+
try:
|
|
123
|
+
decoded = json.loads(target.read_text(encoding="utf-8"))
|
|
124
|
+
if isinstance(decoded, dict):
|
|
125
|
+
base = _empty_state(threshold)
|
|
126
|
+
base.update(decoded)
|
|
127
|
+
base["threshold"] = threshold # always reflect current setting
|
|
128
|
+
return base
|
|
129
|
+
except (OSError, json.JSONDecodeError):
|
|
130
|
+
pass
|
|
131
|
+
return _empty_state(threshold)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def _candidate_paths(payload: dict) -> list[str]:
|
|
135
|
+
out: list[str] = []
|
|
136
|
+
fc = payload.get("file_changes")
|
|
137
|
+
if isinstance(fc, list):
|
|
138
|
+
for entry in fc:
|
|
139
|
+
if isinstance(entry, dict):
|
|
140
|
+
p = entry.get("path")
|
|
141
|
+
if isinstance(p, str) and p:
|
|
142
|
+
out.append(p)
|
|
143
|
+
ti = payload.get("tool_input")
|
|
144
|
+
if isinstance(ti, dict):
|
|
145
|
+
for key in ("path", "file_path", "target_file", "filename"):
|
|
146
|
+
v = ti.get(key)
|
|
147
|
+
if isinstance(v, str) and v:
|
|
148
|
+
out.append(v)
|
|
149
|
+
return out
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _normalize(path: str) -> str:
|
|
154
|
+
return path.lstrip("./").replace("\\", "/")
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _reset_turn(state: dict, session_id: str) -> dict:
|
|
158
|
+
state["session_id"] = session_id or state.get("session_id") or ""
|
|
159
|
+
state["turn_started_at"] = _now()
|
|
160
|
+
state["files_touched_this_turn"] = []
|
|
161
|
+
state["count"] = 0
|
|
162
|
+
state["warning"] = False
|
|
163
|
+
return state
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def _update(state: dict, event: str, envelope: dict, threshold: int) -> dict:
|
|
167
|
+
session_id = envelope.get("session_id") or state.get("session_id") or ""
|
|
168
|
+
if session_id and session_id != state.get("session_id"):
|
|
169
|
+
state = _reset_turn(state, session_id)
|
|
170
|
+
|
|
171
|
+
payload = envelope.get("payload") or {}
|
|
172
|
+
if not isinstance(payload, dict):
|
|
173
|
+
payload = {}
|
|
174
|
+
|
|
175
|
+
if event in ("session_start", "user_prompt_submit"):
|
|
176
|
+
state = _reset_turn(state, session_id)
|
|
177
|
+
elif event in ("pre_tool_use", "post_tool_use"):
|
|
178
|
+
tool = (payload.get("tool_name") or payload.get("toolName")
|
|
179
|
+
or payload.get("tool"))
|
|
180
|
+
if isinstance(tool, str) and tool in EDIT_TOOLS:
|
|
181
|
+
touched: list[str] = list(state.get("files_touched_this_turn") or [])
|
|
182
|
+
seen = set(touched)
|
|
183
|
+
for raw in _candidate_paths(payload):
|
|
184
|
+
norm = _normalize(raw)
|
|
185
|
+
if norm and norm not in seen:
|
|
186
|
+
seen.add(norm)
|
|
187
|
+
touched.append(norm)
|
|
188
|
+
if len(touched) > MAX_TRACKED_PATHS:
|
|
189
|
+
touched = touched[-MAX_TRACKED_PATHS:]
|
|
190
|
+
state["files_touched_this_turn"] = touched
|
|
191
|
+
state["count"] = len(touched)
|
|
192
|
+
state["warning"] = state["count"] > threshold
|
|
193
|
+
|
|
194
|
+
state["threshold"] = threshold
|
|
195
|
+
state["checked_at"] = _now()
|
|
196
|
+
return state
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def run(stdin_text: str, *, consumer_root: Path, verbose: bool = False) -> int:
|
|
200
|
+
envelope: dict = {}
|
|
201
|
+
if stdin_text.strip():
|
|
202
|
+
try:
|
|
203
|
+
decoded = json.loads(stdin_text)
|
|
204
|
+
if isinstance(decoded, dict):
|
|
205
|
+
envelope = decoded
|
|
206
|
+
except json.JSONDecodeError:
|
|
207
|
+
envelope = {}
|
|
208
|
+
|
|
209
|
+
event = envelope.get("event") or ""
|
|
210
|
+
threshold = _read_threshold(consumer_root)
|
|
211
|
+
target = consumer_root / STATE_FILE
|
|
212
|
+
state = _load_state(target, threshold)
|
|
213
|
+
state = _update(state, event, envelope, threshold)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
atomic_write_json(target, state)
|
|
217
|
+
except OSError:
|
|
218
|
+
if verbose:
|
|
219
|
+
print("minimal-safe-diff-hook: state write failed",
|
|
220
|
+
file=sys.stderr)
|
|
221
|
+
return 0
|
|
222
|
+
|
|
223
|
+
if verbose:
|
|
224
|
+
print(
|
|
225
|
+
f"minimal-safe-diff-hook: event={event} "
|
|
226
|
+
f"count={state.get('count')} threshold={threshold} "
|
|
227
|
+
f"warning={state.get('warning')}",
|
|
228
|
+
file=sys.stderr,
|
|
229
|
+
)
|
|
230
|
+
return 0
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def main(argv: list[str] | None = None) -> int:
|
|
234
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
235
|
+
parser.add_argument("--platform", default="generic",
|
|
236
|
+
help="informational platform tag")
|
|
237
|
+
parser.add_argument("--verbose", action="store_true",
|
|
238
|
+
help="emit one stderr line per invocation")
|
|
239
|
+
args = parser.parse_args(argv)
|
|
240
|
+
return run(sys.stdin.read(), consumer_root=Path.cwd(),
|
|
241
|
+
verbose=args.verbose)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
if __name__ == "__main__": # pragma: no cover
|
|
245
|
+
sys.exit(main())
|
|
@@ -26,11 +26,16 @@ from __future__ import annotations
|
|
|
26
26
|
|
|
27
27
|
import argparse
|
|
28
28
|
import datetime as _dt
|
|
29
|
-
import json
|
|
30
29
|
import re
|
|
31
30
|
import sys
|
|
32
31
|
from pathlib import Path
|
|
33
32
|
|
|
33
|
+
# Re-use the shared atomic-write helper so concerns honour the single
|
|
34
|
+
# `agents/state/.dispatcher.lock` discipline (hook-architecture-v1.md
|
|
35
|
+
# § Concurrency, Phase 7.4).
|
|
36
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
37
|
+
from hooks.state_io import atomic_write_json # noqa: E402
|
|
38
|
+
|
|
34
39
|
SETTINGS_FILE = ".agent-settings.yml"
|
|
35
40
|
STATE_DIR = Path("agents") / "state"
|
|
36
41
|
STATE_FILE = STATE_DIR / "onboarding-gate.json"
|
|
@@ -79,9 +84,12 @@ def _read_onboarded(settings_path: Path) -> tuple[bool, str]:
|
|
|
79
84
|
|
|
80
85
|
def _write_state(consumer_root: Path, required: bool, reason: str,
|
|
81
86
|
settings_present: bool) -> None:
|
|
82
|
-
"""Write `agents/state/onboarding-gate.json` atomically.
|
|
83
|
-
|
|
84
|
-
|
|
87
|
+
"""Write `agents/state/onboarding-gate.json` atomically.
|
|
88
|
+
|
|
89
|
+
Uses the shared `agents/state/.dispatcher.lock` so concurrent
|
|
90
|
+
dispatcher invocations across platforms cannot tear the file
|
|
91
|
+
(hook-architecture-v1.md § Concurrency, Phase 7.4).
|
|
92
|
+
"""
|
|
85
93
|
payload = {
|
|
86
94
|
"required": required,
|
|
87
95
|
"reason": reason,
|
|
@@ -89,10 +97,7 @@ def _write_state(consumer_root: Path, required: bool, reason: str,
|
|
|
89
97
|
timespec="seconds"),
|
|
90
98
|
"settings_present": settings_present,
|
|
91
99
|
}
|
|
92
|
-
|
|
93
|
-
tmp = target.with_suffix(".json.tmp")
|
|
94
|
-
tmp.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
|
|
95
|
-
tmp.replace(target)
|
|
100
|
+
atomic_write_json(consumer_root / STATE_FILE, payload)
|
|
96
101
|
|
|
97
102
|
|
|
98
103
|
def run(*, consumer_root: Path, verbose: bool = False) -> int:
|
package/scripts/readme_linter.py
CHANGED
|
@@ -193,15 +193,24 @@ def _detect_repo_type(root: Path, ctx: RepoContext) -> RepoType:
|
|
|
193
193
|
|
|
194
194
|
|
|
195
195
|
def _extract_taskfile_tasks(root: Path) -> list[str]:
|
|
196
|
+
tasks: list[str] = []
|
|
197
|
+
pattern = r"^\s{2}([\w:-]+):"
|
|
196
198
|
for name in ("Taskfile.yml", "Taskfile.yaml"):
|
|
197
199
|
path = root / name
|
|
198
200
|
if path.exists():
|
|
199
201
|
try:
|
|
200
|
-
|
|
201
|
-
return re.findall(r"^\s{2}([\w_-]+):", text, re.MULTILINE)
|
|
202
|
+
tasks.extend(re.findall(pattern, path.read_text(), re.MULTILINE))
|
|
202
203
|
except OSError:
|
|
203
204
|
pass
|
|
204
|
-
|
|
205
|
+
break
|
|
206
|
+
taskfiles_dir = root / "taskfiles"
|
|
207
|
+
if taskfiles_dir.is_dir():
|
|
208
|
+
for path in sorted(taskfiles_dir.glob("*.yml")):
|
|
209
|
+
try:
|
|
210
|
+
tasks.extend(re.findall(pattern, path.read_text(), re.MULTILINE))
|
|
211
|
+
except OSError:
|
|
212
|
+
pass
|
|
213
|
+
return tasks
|
|
205
214
|
|
|
206
215
|
|
|
207
216
|
def _extract_npm_scripts(root: Path) -> list[str]:
|
|
@@ -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())
|