@ikon85/agent-workflow-kit 0.23.0 → 0.25.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.
Files changed (32) hide show
  1. package/.agents/skills/setup-workflow/SKILL.md +26 -6
  2. package/.agents/skills/setup-workflow/workflow-advisories.md +97 -0
  3. package/.claude/hooks/_hook_utils.py +35 -0
  4. package/.claude/hooks/_safety_guard.py +24 -0
  5. package/.claude/hooks/baseline-capture-hint.py +25 -0
  6. package/.claude/hooks/block-bg-double-background.py +30 -0
  7. package/.claude/hooks/block-npm-install-in-pnpm.py +30 -0
  8. package/.claude/hooks/block-secrets.py +30 -0
  9. package/.claude/hooks/convention-drift-hint.py +27 -0
  10. package/.claude/hooks/grep-shim-guard.py +30 -0
  11. package/.claude/hooks/loc-offender-forewarn.py +56 -0
  12. package/.claude/hooks/migration-snapshot-reminder.py +24 -0
  13. package/.claude/hooks/pre-refactor-sweep.py +27 -0
  14. package/.claude/hooks/recon-size-hint.py +24 -0
  15. package/.claude/hooks/skill-drift-hint.py +9 -45
  16. package/.claude/hooks/typecheck-on-stop.py +27 -0
  17. package/.claude/hooks/typecheck-on-stop.sh +2 -0
  18. package/.claude/skills/setup-workflow/SKILL.md +26 -6
  19. package/.claude/skills/setup-workflow/workflow-advisories.md +97 -0
  20. package/README.md +27 -0
  21. package/agent-workflow-kit.package.json +94 -6
  22. package/package.json +1 -1
  23. package/scripts/loc_offender_gate.py +24 -0
  24. package/scripts/safety-guardrails/core.py +221 -0
  25. package/scripts/safety-guardrails/search.py +92 -0
  26. package/scripts/security/audit-gate.mjs +122 -0
  27. package/scripts/security/ensure-gitleaks.mjs +101 -0
  28. package/scripts/security/gitleaks-profile.json +14 -0
  29. package/scripts/security/install-git-hooks.mjs +52 -0
  30. package/scripts/workflow-advisories/capabilities.json +33 -0
  31. package/scripts/workflow-advisories/core.py +285 -0
  32. package/src/lib/bundle.mjs +12 -0
@@ -26,7 +26,7 @@ This is a prompt-driven skill, not a deterministic script. Explore, present what
26
26
  | `## Agent skills` + `## Prod` in CLAUDE.md **and** AGENTS.md | one-line pointers + deploy target (Sections C/G + Write) |
27
27
  | `.github/workflows/agent-workflow-kit-update.yml` | optional tested Kit update pull request for GitHub consumers (Section A2) |
28
28
  | `docs/agents/census.md` | optional-census choice, paths, state, and safe disable contract seeded from [census.md](./census.md) (Section A3) |
29
- | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle activation and one-time Memory Lifecycle policy seeds (Sections A4–A5) |
29
+ | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, and Workflow Advisories choices (Sections A4–A6) |
30
30
 
31
31
  ## Idempotency contract — read before writing anything
32
32
 
@@ -57,7 +57,7 @@ Read the current state; don't assume. For every target file, read its first line
57
57
  - `CONTEXT.md`, `CONTEXT-MAP.md`, `docs/adr/` — domain-doc layout.
58
58
  - `docs/agents/`, `docs/agents/skills/`, `docs/conventions/` — prior output of this skill.
59
59
  - `docs/agents/census.md`, `.census/profile.json`, `.census/active.json` — an existing census choice or consumer-owned census to adopt.
60
- - `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile and Worktree Lifecycle hook wiring to adopt.
60
+ - `docs/agents/workflow-capabilities.json`, `.claude/settings.json` — existing capability choices/profile plus Worktree Lifecycle and Workflow Advisories hook wiring to adopt.
61
61
  - `.memory/active/`, `.memory/archive/`, and `.memory/receipts/` — consumer-owned Memory Lifecycle policies or recovery evidence to adopt without normalization.
62
62
  - `gh auth status` (if GitHub) — is `gh` authenticated, and with which scopes?
63
63
 
@@ -199,6 +199,25 @@ activation, roots, retention, restore, or pruning grants. Template files have
199
199
  no kit-managed or `append-managed` region; a future bounded-region contract
200
200
  requires a separate decision.
201
201
 
202
+ ### 2e. Section A6 — Optional Workflow Advisories
203
+
204
+ > Workflow Advisories provides non-blocking, low-noise hints and
205
+ > affected-surface checks around large reads, first-impacting edits, refactors,
206
+ > and Stop. Paths, commands, thresholds, timeouts, and output budgets remain
207
+ > consumer-owned.
208
+
209
+ Read [workflow-advisories.md](./workflow-advisories.md) in full. Reconcile its
210
+ complete `yes / later / no / existing / disable` matrix. If no choice exists,
211
+ ask: *"Should setup activate the portable Workflow Advisories for this
212
+ repository?"* Offer exactly **Yes**, **Later**, and **No**.
213
+
214
+ - **Yes** — reconcile only `workflowAdvisories` in the shared capability
215
+ profile and only the exact kit-owned hook commands named by the seed.
216
+ - **Later / No** — record the choice without hook wiring.
217
+ - **Existing** — adopt profile and wiring byte-safely.
218
+ - **Disable** — remove exact kit-owned commands first, then set `enabled:
219
+ false`; preserve every profile value and unknown key.
220
+
202
221
  ### 3. Section B — Triage labels
203
222
 
204
223
  > When `triage` processes an incoming issue it applies labels (or your tracker's equivalent). It needs strings you've actually configured, or it creates duplicates.
@@ -284,11 +303,12 @@ pre-existing consumer-owned census file. `yes`, `later`, `no`, and an adopted
284
303
  existing census are terminal for ordinary setup reruns. Only an explicit user
285
304
  request changes a recorded choice.
286
305
 
287
- For `docs/agents/workflow-capabilities.json`, reconcile only the selected
306
+ For `docs/agents/workflow-capabilities.json`, reconcile only each selected
288
307
  capability section. Never replace the whole file: Project Release, Memory
289
- Lifecycle, Worktree Lifecycle, future sections, and unknown consumer keys share
290
- this profile. Apply the Worktree Lifecycle transition and hook ownership rules
291
- from [worktree-lifecycle.md](./worktree-lifecycle.md) transactionally.
308
+ Lifecycle, Worktree Lifecycle, Workflow Advisories, future sections, and
309
+ unknown consumer keys share this profile. Apply the transition and hook
310
+ ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md) and
311
+ [workflow-advisories.md](./workflow-advisories.md) transactionally.
292
312
 
293
313
  For Memory Lifecycle, use only the deterministic setup helper from Section A5.
294
314
  Do not copy templates with shell commands or edit the capability profile by
@@ -0,0 +1,97 @@
1
+ # Workflow Advisories setup contract
2
+
3
+ Workflow Advisories is one opt-in capability backed by the consumer-owned
4
+ `docs/agents/workflow-capabilities.json` profile. Enabling it activates seven
5
+ non-blocking rows: large-read, baseline, pre-refactor, affected-surface Stop,
6
+ convention freshness, migration-artifact reminder, and LoC forewarning. Every
7
+ threshold, glob, branch rule, source map, command, timeout, and output budget
8
+ stays consumer-owned.
9
+
10
+ ## Choice matrix
11
+
12
+ | State | Setup action |
13
+ |---|---|
14
+ | `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
15
+ | `yes` | Reconcile the enabled profile and exact kit-owned hook commands. |
16
+ | `later` | Record the retryable deferral; do not activate hooks. |
17
+ | `no` | Record the opt-out; do not activate hooks. |
18
+ | `existing` | Adopt the consumer profile byte-safely and preserve unknown keys. |
19
+ | `disable` | Remove only kit-owned hook wiring, then set `enabled: false`; retain profile values and unknown keys. |
20
+
21
+ ```json workflow-advisories-setup-effects
22
+ [
23
+ {"state":"missing","choice":null,"operations":[]},
24
+ {"state":"yes","choice":"yes","operations":["record-choice","reconcile-profile-enabled","reconcile-hook-wiring"]},
25
+ {"state":"later","choice":"later","operations":["record-choice"]},
26
+ {"state":"no","choice":"no","operations":["record-choice"]},
27
+ {"state":"existing","choice":"yes","operations":["adopt-existing"]},
28
+ {"state":"disable","choice":"yes","operations":["remove-hook-wiring","update-profile-disabled"]}
29
+ ]
30
+ ```
31
+
32
+ ## Profile shape
33
+
34
+ Reconcile only `workflowAdvisories`; preserve every sibling section and unknown
35
+ key. Consumer values are never normalized on adoption.
36
+
37
+ ```json
38
+ {
39
+ "workflowAdvisories": {
40
+ "choice": "yes",
41
+ "enabled": true,
42
+ "largeRead": {"tools": ["Read"], "lineThreshold": 500, "outputBudget": 500},
43
+ "baseline": {
44
+ "sourceGlobs": ["src/**"],
45
+ "branchRegex": "^(?:feat|fix)/",
46
+ "manifestPath": ".agent/baseline.json",
47
+ "stateDir": ".claude/logs/advisory-state",
48
+ "outputBudget": 500
49
+ },
50
+ "preRefactor": {
51
+ "promptMatchers": ["refactor"],
52
+ "surfaces": [],
53
+ "timeoutSeconds": 15,
54
+ "outputBudget": 1000
55
+ },
56
+ "stopChecks": {"surfaces": [], "timeoutSeconds": 30, "outputBudget": 1000},
57
+ "freshness": {"documents": [], "outputBudget": 1000},
58
+ "migration": {
59
+ "commandMatchers": [],
60
+ "artifact": "",
61
+ "refreshCommand": [],
62
+ "outputBudget": 500
63
+ },
64
+ "locForewarn": {
65
+ "branchRegex": "^(?:feat|fix)/(\\d+)-",
66
+ "issueCommand": ["gh", "issue", "view", "{issue}", "--json", "body", "-q", ".body"],
67
+ "timeoutSeconds": 5,
68
+ "outputBudget": 500
69
+ }
70
+ }
71
+ }
72
+ ```
73
+
74
+ Empty command surfaces are honest inactive defaults. Setup recommends concrete
75
+ project commands from the tools already present, then asks before activating.
76
+
77
+ ## Hook ownership
78
+
79
+ - PreToolUse on Read:
80
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/recon-size-hint.py"`
81
+ - PreToolUse on Edit/Write/MultiEdit:
82
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/baseline-capture-hint.py"`
83
+ - UserPromptSubmit:
84
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-refactor-sweep.py"`
85
+ - Stop:
86
+ `"$CLAUDE_PROJECT_DIR/.claude/hooks/typecheck-on-stop.sh"`
87
+ - SessionStart convention freshness:
88
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/convention-drift-hint.py"`
89
+ - PostToolUse on Bash migration commands:
90
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/migration-snapshot-reminder.py"`
91
+ - SessionStart LoC forewarning:
92
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/loc-offender-forewarn.py"`
93
+
94
+ All adapters are non-blocking. A failed or timed-out configured command remains
95
+ visibly failed in hook context; it is never reported as green. Issue lookup
96
+ failures silence only the LoC forewarning: the existing pre-push gate remains
97
+ the enforcing authority and stays fail-closed.
@@ -30,6 +30,8 @@ from pathlib import Path
30
30
  # (or from a linked worktree root), so this resolves to <root>/.claude/logs.
31
31
  LOG_DIR = Path(".claude/logs")
32
32
  _WORKTREE_CORE_MODULE = "_agent_workflow_kit_worktree_lifecycle"
33
+ _ADVISORY_CORE_MODULE = "_agent_workflow_kit_workflow_advisories"
34
+ _LOC_OFFENDER_GATE_MODULE = "_agent_workflow_kit_loc_offender_gate"
33
35
 
34
36
 
35
37
  def rotate_log_if_needed(log_path: Path, max_bytes: int = 100_000, generations: int = 3) -> None:
@@ -205,6 +207,39 @@ def load_worktree_lifecycle_core():
205
207
  return module
206
208
 
207
209
 
210
+ def load_workflow_advisories_core():
211
+ """Load the shipped Workflow Advisories core without requiring a Python package."""
212
+ existing = sys.modules.get(_ADVISORY_CORE_MODULE)
213
+ if existing is not None:
214
+ return existing
215
+ path = Path(__file__).resolve().parents[2] / "scripts" / "workflow-advisories" / "core.py"
216
+ spec = importlib.util.spec_from_file_location(_ADVISORY_CORE_MODULE, path)
217
+ if spec is None or spec.loader is None:
218
+ raise ImportError(f"cannot load Workflow Advisories core from {path}")
219
+ module = importlib.util.module_from_spec(spec)
220
+ sys.modules[_ADVISORY_CORE_MODULE] = module
221
+ spec.loader.exec_module(module)
222
+ return module
223
+
224
+
225
+ def load_loc_offender_gate():
226
+ """Load the shipped LoC gate so advisories consume its contracts directly."""
227
+ existing = sys.modules.get(_LOC_OFFENDER_GATE_MODULE)
228
+ if existing is not None:
229
+ return existing
230
+ path = Path(__file__).resolve().parents[2] / "scripts" / "loc_offender_gate.py"
231
+ module_dir = str(path.parent)
232
+ if module_dir not in sys.path:
233
+ sys.path.insert(0, module_dir)
234
+ spec = importlib.util.spec_from_file_location(_LOC_OFFENDER_GATE_MODULE, path)
235
+ if spec is None or spec.loader is None:
236
+ raise ImportError(f"cannot load LoC offender gate from {path}")
237
+ module = importlib.util.module_from_spec(spec)
238
+ sys.modules[_LOC_OFFENDER_GATE_MODULE] = module
239
+ spec.loader.exec_module(module)
240
+ return module
241
+
242
+
208
243
  def hook_event_output(event_name: str, context: str) -> dict:
209
244
  """Canonical non-blocking context payload for Claude hook events."""
210
245
  return {
@@ -0,0 +1,24 @@
1
+ """Load the shipped Safety Guardrails core for thin hook adapters."""
2
+
3
+ import importlib.util
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ _CORE_MODULE = "_agent_workflow_kit_safety_guardrails"
8
+
9
+
10
+ def load_core():
11
+ existing = sys.modules.get(_CORE_MODULE)
12
+ if existing is not None:
13
+ return existing
14
+ path = Path(__file__).resolve().parents[2] / "scripts" / "safety-guardrails" / "core.py"
15
+ module_dir = str(path.parent)
16
+ if module_dir not in sys.path:
17
+ sys.path.insert(0, module_dir)
18
+ spec = importlib.util.spec_from_file_location(_CORE_MODULE, path)
19
+ if spec is None or spec.loader is None:
20
+ raise ImportError(f"cannot load Safety Guardrails core from {path}")
21
+ module = importlib.util.module_from_spec(spec)
22
+ sys.modules[_CORE_MODULE] = module
23
+ spec.loader.exec_module(module)
24
+ return module
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse once-per-branch baseline advisory for impacting edits."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import current_branch, hook_event_output, load_workflow_advisories_core
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ core = load_workflow_advisories_core()
14
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
15
+ branch = payload.get("branch") or current_branch()
16
+ decision = core.baseline_decision(profile, payload, Path.cwd(), branch)
17
+ if decision.context:
18
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
19
+ except Exception:
20
+ pass
21
+ return 0
22
+
23
+
24
+ if __name__ == "__main__":
25
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """Thin PreToolUse adapter for profile-driven double-background prevention."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from _hook_utils import log
9
+ from _safety_guard import load_core
10
+
11
+ HOOK_NAME = "block-bg-double-background"
12
+
13
+
14
+ def main() -> int:
15
+ try:
16
+ payload = json.load(sys.stdin)
17
+ core = load_core()
18
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
19
+ decision = core.evaluate("double-background", profile, payload)
20
+ except Exception:
21
+ return 0
22
+ if decision.action == "block":
23
+ print(decision.message, file=sys.stderr)
24
+ log(HOOK_NAME, decision.log_message)
25
+ return 2
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """Thin PreToolUse adapter for profile-driven package-manager consistency."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from _hook_utils import log
9
+ from _safety_guard import load_core
10
+
11
+ HOOK_NAME = "block-package-manager"
12
+
13
+
14
+ def main() -> int:
15
+ try:
16
+ payload = json.load(sys.stdin)
17
+ core = load_core()
18
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
19
+ decision = core.evaluate("package-manager", profile, payload)
20
+ except Exception:
21
+ return 0
22
+ if decision.action == "block":
23
+ print(decision.message, file=sys.stderr)
24
+ log(HOOK_NAME, decision.log_message)
25
+ return 2
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """Thin PreToolUse adapter for the profile-driven secret guard."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from _hook_utils import log
9
+ from _safety_guard import load_core
10
+
11
+ HOOK_NAME = "block-secrets"
12
+
13
+
14
+ def main() -> int:
15
+ try:
16
+ payload = json.load(sys.stdin)
17
+ core = load_core()
18
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
19
+ decision = core.evaluate("secrets", profile, payload)
20
+ except Exception:
21
+ return 0
22
+ if decision.action == "block":
23
+ print(decision.message, file=sys.stderr)
24
+ log(HOOK_NAME, decision.log_message)
25
+ return 2
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart convention freshness advisory over profile-owned source maps."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ json.load(sys.stdin)
13
+ except Exception:
14
+ pass
15
+ try:
16
+ core = load_workflow_advisories_core()
17
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
18
+ decision = core.convention_freshness_decision(profile, Path.cwd())
19
+ if decision.context:
20
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
21
+ except Exception:
22
+ pass
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env python3
2
+ """Thin PreToolUse adapter for configured search-shim breakers."""
3
+
4
+ import json
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ from _hook_utils import log
9
+ from _safety_guard import load_core
10
+
11
+ HOOK_NAME = "grep-shim-guard"
12
+
13
+
14
+ def main() -> int:
15
+ try:
16
+ payload = json.load(sys.stdin)
17
+ core = load_core()
18
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
19
+ decision = core.evaluate("search-shim", profile, payload)
20
+ except Exception:
21
+ return 0
22
+ if decision.action == "block":
23
+ print(decision.message, file=sys.stderr)
24
+ log(HOOK_NAME, decision.log_message)
25
+ return 2
26
+ return 0
27
+
28
+
29
+ if __name__ == "__main__":
30
+ raise SystemExit(main())
@@ -0,0 +1,56 @@
1
+ #!/usr/bin/env python3
2
+ """SessionStart forewarning over the existing LoC gate and issue marker contracts."""
3
+ import json
4
+ import re
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ from _hook_utils import (
10
+ current_branch,
11
+ hook_event_output,
12
+ load_loc_offender_gate,
13
+ load_workflow_advisories_core,
14
+ )
15
+
16
+
17
+ def main() -> int:
18
+ try:
19
+ json.load(sys.stdin)
20
+ except Exception:
21
+ pass
22
+ try:
23
+ root = Path.cwd()
24
+ core = load_workflow_advisories_core()
25
+ profile = core.load_profile(root / "docs/agents/workflow-capabilities.json")
26
+ config = profile.get("locForewarn", {})
27
+ match = re.search(config.get("branchRegex", r"$^"), current_branch())
28
+ command = config.get("issueCommand", [])
29
+ if not match or not command:
30
+ return 0
31
+ issue = match.group(1)
32
+ argv = [str(part).replace("{issue}", issue) for part in command]
33
+ result = subprocess.run(
34
+ argv,
35
+ cwd=root,
36
+ capture_output=True,
37
+ text=True,
38
+ timeout=float(config.get("timeoutSeconds", 5)),
39
+ )
40
+ if result.returncode != 0:
41
+ return 0
42
+ gate = load_loc_offender_gate()
43
+ max_lines, offenders = gate.load_max_lines(root / "max-lines-allowlist.json")
44
+ context = gate.forewarning_context(result.stdout, max_lines, offenders)
45
+ if context:
46
+ budget = int(config.get("outputBudget", 500))
47
+ if budget > 0 and len(context) > budget:
48
+ context = context[: max(0, budget - 1)] + "…"
49
+ print(json.dumps(hook_event_output("SessionStart", context)))
50
+ except Exception:
51
+ pass
52
+ return 0
53
+
54
+
55
+ if __name__ == "__main__":
56
+ sys.exit(main())
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+ """PostToolUse migration artifact refresh reminder over consumer profile data."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ core = load_workflow_advisories_core()
14
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
15
+ decision = core.migration_reminder_decision(profile, payload)
16
+ if decision.context:
17
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
18
+ except Exception:
19
+ pass
20
+ return 0
21
+
22
+
23
+ if __name__ == "__main__":
24
+ sys.exit(main())
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """UserPromptSubmit non-blocking profile-driven pre-refactor command sweep."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core, run
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ if "changed_files" not in payload:
14
+ changed = run(["git", "diff", "--name-only", "HEAD"])
15
+ payload["changed_files"] = changed.splitlines() if changed else []
16
+ core = load_workflow_advisories_core()
17
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
18
+ decision = core.pre_refactor_decision(profile, payload, Path.cwd())
19
+ if decision.context:
20
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
21
+ except Exception:
22
+ pass
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,24 @@
1
+ #!/usr/bin/env python3
2
+ """PreToolUse non-blocking advisory for profile-defined large reads."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ core = load_workflow_advisories_core()
14
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
15
+ decision = core.large_read_decision(profile, payload)
16
+ if decision.context:
17
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
18
+ except Exception:
19
+ pass
20
+ return 0
21
+
22
+
23
+ if __name__ == "__main__":
24
+ sys.exit(main())
@@ -21,52 +21,21 @@ import json
21
21
  import sys
22
22
  from pathlib import Path
23
23
 
24
- from _hook_utils import log, run, repo_root
24
+ from _hook_utils import (
25
+ hook_event_output,
26
+ load_workflow_advisories_core,
27
+ log,
28
+ repo_root,
29
+ )
25
30
 
26
31
  HOOK_NAME = "skill-drift-hint"
27
32
  SKILLS_REL = ".claude/skills"
28
33
 
29
34
 
30
- def git_commit_time(root: str, rel_path: str) -> int | None:
31
- """Unix ctime of the last commit touching rel_path; None if untracked/unknown."""
32
- out = run(["git", "-C", root, "log", "-1", "--format=%ct", "--", rel_path])
33
- return int(out) if out.isdigit() else None
34
-
35
-
36
- def read_sources(sources_file: Path) -> list[str]:
37
- """Repo-relative source paths from a SOURCES.txt — skips blanks and `#` comments."""
38
- try:
39
- lines = sources_file.read_text(encoding="utf-8").splitlines()
40
- except Exception:
41
- return []
42
- out: list[str] = []
43
- for line in lines:
44
- s = line.strip()
45
- if not s or s.startswith("#"):
46
- continue
47
- out.append(s)
48
- return out
49
-
50
-
51
35
  def collect_stale(root: str) -> list[tuple[str, list[str]]]:
52
36
  """For each skill with a SOURCES.txt, list sources newer (in git) than its SKILL.md."""
53
- skills_dir = Path(root) / SKILLS_REL
54
- results: list[tuple[str, list[str]]] = []
55
- if not skills_dir.is_dir():
56
- return results
57
- for sources_file in sorted(skills_dir.glob("*/SOURCES.txt")):
58
- skill_name = sources_file.parent.name
59
- skill_ct = git_commit_time(root, f"{SKILLS_REL}/{skill_name}/SKILL.md")
60
- if skill_ct is None:
61
- continue # skill not committed yet → nothing to compare against
62
- stale = [
63
- src
64
- for src in read_sources(sources_file)
65
- if (src_ct := git_commit_time(root, src)) is not None and src_ct > skill_ct
66
- ]
67
- if stale:
68
- results.append((skill_name, stale))
69
- return results
37
+ core = load_workflow_advisories_core()
38
+ return core.collect_skill_stale(Path(root), SKILLS_REL)
70
39
 
71
40
 
72
41
  def build_context(root: str) -> str | None:
@@ -107,12 +76,7 @@ def main() -> int:
107
76
  if context is None:
108
77
  return 0
109
78
 
110
- payload = {
111
- "hookSpecificOutput": {
112
- "hookEventName": "SessionStart",
113
- "additionalContext": context,
114
- }
115
- }
79
+ payload = hook_event_output("SessionStart", context)
116
80
  print(json.dumps(payload))
117
81
  log(HOOK_NAME, f"emitted: {context.count(chr(0x26A0))} skill(s) flagged")
118
82
  return 0
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env python3
2
+ """Stop-event adapter for profile-defined affected-surface checks."""
3
+ import json
4
+ import sys
5
+ from pathlib import Path
6
+
7
+ from _hook_utils import hook_event_output, load_workflow_advisories_core, run
8
+
9
+
10
+ def main() -> int:
11
+ try:
12
+ payload = json.load(sys.stdin)
13
+ if "changed_files" not in payload:
14
+ changed = run(["git", "diff", "--name-only", "HEAD"])
15
+ payload["changed_files"] = changed.splitlines() if changed else []
16
+ core = load_workflow_advisories_core()
17
+ profile = core.load_profile(Path("docs/agents/workflow-capabilities.json"))
18
+ decision = core.stop_check_decision(profile, payload, Path.cwd())
19
+ if decision.context:
20
+ print(json.dumps(hook_event_output(decision.event_name, decision.context)))
21
+ except Exception:
22
+ pass
23
+ return 0
24
+
25
+
26
+ if __name__ == "__main__":
27
+ sys.exit(main())
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env sh
2
+ exec python3 "$(dirname "$0")/typecheck-on-stop.py"