@ikon85/agent-workflow-kit 0.24.0 → 0.26.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 (30) hide show
  1. package/.agents/skills/setup-workflow/SKILL.md +31 -5
  2. package/.agents/skills/setup-workflow/safety-guardrails.md +155 -0
  3. package/.agents/skills/setup-workflow/workflow-advisories.md +28 -6
  4. package/.claude/hooks/_hook_utils.py +19 -0
  5. package/.claude/hooks/_safety_guard.py +24 -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/skill-drift-hint.py +9 -45
  14. package/.claude/skills/setup-workflow/SKILL.md +31 -5
  15. package/.claude/skills/setup-workflow/safety-guardrails.md +155 -0
  16. package/.claude/skills/setup-workflow/workflow-advisories.md +28 -6
  17. package/README.md +31 -0
  18. package/agent-workflow-kit.package.json +132 -9
  19. package/package.json +1 -1
  20. package/scripts/loc_offender_gate.py +24 -0
  21. package/scripts/safety-guardrails/core.py +221 -0
  22. package/scripts/safety-guardrails/search.py +92 -0
  23. package/scripts/security/audit-gate.mjs +122 -0
  24. package/scripts/security/ensure-gitleaks.mjs +101 -0
  25. package/scripts/security/gitleaks-profile.json +14 -0
  26. package/scripts/security/install-git-hooks.mjs +52 -0
  27. package/scripts/test_skill_publish_audit.py +15 -0
  28. package/scripts/workflow-advisories/capabilities.json +33 -0
  29. package/scripts/workflow-advisories/core.py +100 -0
  30. package/src/lib/bundle.mjs +18 -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, Memory Lifecycle, and Workflow Advisories choices (Sections A4–A6) |
29
+ | `docs/agents/workflow-capabilities.json` + capability assets | consumer-owned Worktree Lifecycle, Memory Lifecycle, Workflow Advisories, and Safety Guardrails choices (Sections A4–A7) |
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 plus Worktree Lifecycle and Workflow Advisories hook wiring to adopt.
60
+ - `docs/agents/workflow-capabilities.json`, `.claude/settings.json`, `git config --get core.hooksPath` — existing capability choices/profile plus Worktree Lifecycle, Workflow Advisories, and Safety Guardrails 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
 
@@ -218,6 +218,31 @@ repository?"* Offer exactly **Yes**, **Later**, and **No**.
218
218
  - **Disable** — remove exact kit-owned commands first, then set `enabled:
219
219
  false`; preserve every profile value and unknown key.
220
220
 
221
+ ### 2f. Section A7 — Optional Safety Guardrails
222
+
223
+ > Safety Guardrails is a counted group of seven independently selectable
224
+ > protections: four agent-tool guards and three repository-security
225
+ > capabilities. Setup stages and validates the chosen profile plus wiring
226
+ > before activation; disabled rows remain exact no-ops.
227
+
228
+ Read [safety-guardrails.md](./safety-guardrails.md) in full. Reconcile its
229
+ complete `yes / later / no / existing / disable` matrix. If no choice exists,
230
+ ask: *"Should setup configure the portable Safety Guardrails for this
231
+ repository?"* Offer exactly **Yes**, **Later**, and **No**.
232
+
233
+ - **Yes** — ask which of the seven counted rows to enable, then reconcile only
234
+ `safetyGuardrails`, exact selected Agent-hook commands, and the selected
235
+ native Git-hook path. Report `N of 7 enabled`.
236
+ - **Later / No** — record the choice without Agent- or Git-hook wiring.
237
+ - **Existing** — adopt profile and wiring byte-safely; preserve unknown keys
238
+ and consumer-specific policies.
239
+ - **Disable** — remove only exact kit-owned Agent commands and Git wiring
240
+ first, then set `enabled: false`.
241
+
242
+ Testreporter uses the parity-on profile from the seed. Other consumers are
243
+ opt-in/manual: never copy its pnpm, path, shim, surface, or outage decisions
244
+ without repository evidence and user confirmation.
245
+
221
246
  ### 3. Section B — Triage labels
222
247
 
223
248
  > 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.
@@ -305,10 +330,11 @@ request changes a recorded choice.
305
330
 
306
331
  For `docs/agents/workflow-capabilities.json`, reconcile only each selected
307
332
  capability section. Never replace the whole file: Project Release, Memory
308
- Lifecycle, Worktree Lifecycle, Workflow Advisories, future sections, and
333
+ Lifecycle, Worktree Lifecycle, Workflow Advisories, Safety Guardrails, future sections, and
309
334
  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.
335
+ ownership rules from [worktree-lifecycle.md](./worktree-lifecycle.md),
336
+ [workflow-advisories.md](./workflow-advisories.md), and
337
+ [safety-guardrails.md](./safety-guardrails.md) transactionally.
312
338
 
313
339
  For Memory Lifecycle, use only the deterministic setup helper from Section A5.
314
340
  Do not copy templates with shell commands or edit the capability profile by
@@ -0,0 +1,155 @@
1
+ # Safety Guardrails setup contract
2
+
3
+ Safety Guardrails is a counted, opt-in capability group backed by the
4
+ consumer-owned `docs/agents/workflow-capabilities.json` profile. Its seven
5
+ rows are selected independently: four protect agent tool calls and three
6
+ bootstrap repository security. No repository name, package manager, hook
7
+ surface, threshold, path, or outage policy is inferred from the kit.
8
+
9
+ ## Choice matrix
10
+
11
+ | State | Setup action |
12
+ |---|---|
13
+ | `missing` | Ask `yes / later / no`; do not infer or write before the answer. |
14
+ | `yes` | Ask which rows to enable, stage profile plus selected wiring, validate, then activate. |
15
+ | `later` | Record the retryable deferral; do not activate any wiring. |
16
+ | `no` | Record the opt-out; do not activate any wiring. |
17
+ | `existing` | Adopt the consumer profile and existing wiring byte-safely. |
18
+ | `disable` | Remove exact kit-owned Agent and Git wiring first, then set `enabled: false`. |
19
+
20
+ ```json safety-guardrails-setup-effects
21
+ [
22
+ {"state":"missing","choice":null,"operations":[]},
23
+ {"state":"yes","choice":"yes","operations":["record-choice","stage-enabled-profile","stage-agent-hook-wiring","stage-git-hook-wiring","activate-staged-profile"]},
24
+ {"state":"later","choice":"later","operations":["record-choice"]},
25
+ {"state":"no","choice":"no","operations":["record-choice"]},
26
+ {"state":"existing","choice":"yes","operations":["adopt-existing"]},
27
+ {"state":"disable","choice":"yes","operations":["remove-agent-hook-wiring","remove-git-hook-wiring","update-profile-disabled"]}
28
+ ]
29
+ ```
30
+
31
+ ## Counted capability map
32
+
33
+ Every historical Safety row maps to one profile decision and one shipped
34
+ artifact. Setup reports the result as `N of 7 enabled`, never as one
35
+ all-or-nothing switch.
36
+
37
+ ```json safety-guardrails-capabilities
38
+ [
39
+ {"id":"secrets","profilePath":"safetyGuardrails.secrets.enabled","artifact":".claude/hooks/block-secrets.py"},
40
+ {"id":"packageManager","profilePath":"safetyGuardrails.packageManager.enabled","artifact":".claude/hooks/block-npm-install-in-pnpm.py"},
41
+ {"id":"doubleBackground","profilePath":"safetyGuardrails.doubleBackground.enabled","artifact":".claude/hooks/block-bg-double-background.py"},
42
+ {"id":"searchShim","profilePath":"safetyGuardrails.searchShim.enabled","artifact":".claude/hooks/grep-shim-guard.py"},
43
+ {"id":"gitHooks","profilePath":"safetyGuardrails.repositorySecurity.gitHooks.enabled","artifact":"scripts/security/install-git-hooks.mjs"},
44
+ {"id":"gitleaks","profilePath":"safetyGuardrails.repositorySecurity.gitleaks.enabled","artifact":"scripts/security/ensure-gitleaks.mjs"},
45
+ {"id":"dependencyAudit","profilePath":"safetyGuardrails.repositorySecurity.dependencyAudit.enabled","artifact":"scripts/security/audit-gate.mjs"}
46
+ ]
47
+ ```
48
+
49
+ ## Profile shape
50
+
51
+ Reconcile only `safetyGuardrails`; preserve every sibling section, unknown key,
52
+ and consumer-owned value. The following is the Testreporter parity-on example,
53
+ not a universal default. Other consumers choose each `enabled` value and fill
54
+ only facts proven in their repository.
55
+
56
+ ```json
57
+ {
58
+ "safetyGuardrails": {
59
+ "choice": "yes",
60
+ "enabled": true,
61
+ "secrets": {
62
+ "enabled": true,
63
+ "sensitiveNames": [".env", ".npmrc", "credentials.json"],
64
+ "sensitivePathFragments": ["secrets/", ".ssh/"],
65
+ "safeTemplateSuffixes": [".example", ".sample", ".template", ".dist"]
66
+ },
67
+ "packageManager": {
68
+ "enabled": true,
69
+ "lockfiles": {
70
+ "pnpm-lock.yaml": "pnpm",
71
+ "package-lock.json": "npm",
72
+ "yarn.lock": "yarn",
73
+ "bun.lockb": "bun"
74
+ }
75
+ },
76
+ "doubleBackground": {"enabled": true, "surfaces": ["claude"]},
77
+ "searchShim": {
78
+ "enabled": true,
79
+ "detected": true,
80
+ "commandNames": ["grep", "rg"]
81
+ },
82
+ "repositorySecurity": {
83
+ "gitHooks": {"enabled": true, "hooksPath": ".githooks"},
84
+ "gitleaks": {"enabled": true, "required": false},
85
+ "dependencyAudit": {
86
+ "enabled": true,
87
+ "packageManager": "pnpm",
88
+ "outagePolicy": "warn",
89
+ "blockSeverities": ["high", "critical"]
90
+ }
91
+ }
92
+ }
93
+ }
94
+ ```
95
+
96
+ An empty or disabled row is an honest no-op. `searchShim.detected` is a
97
+ consumer fact, not an installation request. `gitleaks.required: false` keeps
98
+ the verified optional provisioner non-blocking when the platform or network is
99
+ unsupported. Dependency-audit outage policy must be explicit.
100
+
101
+ ## Agent-hook ownership
102
+
103
+ Wire only enabled rows, using the exact kit-owned commands:
104
+
105
+ - `secrets` — `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-secrets.py"`
106
+ - `packageManager` —
107
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-npm-install-in-pnpm.py"`
108
+ - `doubleBackground` —
109
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/block-bg-double-background.py"`
110
+ - `searchShim` —
111
+ `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/grep-shim-guard.py"`
112
+
113
+ All four are PreToolUse adapters. Preserve unrelated matchers and commands in
114
+ `.claude/settings.json`. Disable removes only these exact commands.
115
+
116
+ ## Repository wiring
117
+
118
+ When `gitHooks.enabled` is true, run:
119
+
120
+ ```bash
121
+ node scripts/security/install-git-hooks.mjs <hooksPath>
122
+ ```
123
+
124
+ only after confirming the configured directory exists and contains the
125
+ consumer's intended hooks. Never create or replace consumer hook bodies.
126
+
127
+ When `gitleaks.enabled` is true, the pinned profile may be provisioned through
128
+ `scripts/security/ensure-gitleaks.mjs`; checksum mismatch, unsupported platform,
129
+ offline state, and unwritable destination remain distinct visible outcomes.
130
+ Never read a secret file to test the provisioner.
131
+
132
+ When `dependencyAudit.enabled` is true, use
133
+ `scripts/security/audit-gate.mjs` with the configured package manager and
134
+ outage policy. High/Critical findings block; Low/Moderate findings pass.
135
+
136
+ ## Transaction and failure contract
137
+
138
+ Prepare the next profile and `.claude/settings.json` in same-directory staging
139
+ files. Validate JSON, all selected artifact paths, the seven-row count, the
140
+ configured Git-hook directory, and package-manager facts before mutation.
141
+ Then:
142
+
143
+ 1. reconcile selected Agent commands in the staged settings;
144
+ 2. remember the prior `core.hooksPath` and apply the selected Git wiring;
145
+ 3. atomically rename staged settings and profile into place;
146
+ 4. on any failure, restore the prior Git config and leave active files
147
+ byte-for-byte unchanged.
148
+
149
+ Disable reverses the safety order: remove exact Agent commands and restore or
150
+ unset only the kit-owned Git wiring before committing `enabled: false`.
151
+ Repeated activation with the same choices is byte-idempotent.
152
+
153
+ Setup and verification may inspect paths, profile metadata, command exit
154
+ status, advisory counts, and hashes. They must never read secret contents or
155
+ print environment/config values.
@@ -1,10 +1,11 @@
1
1
  # Workflow Advisories setup contract
2
2
 
3
3
  Workflow Advisories is one opt-in capability backed by the consumer-owned
4
- `docs/agents/workflow-capabilities.json` profile. Enabling it activates
5
- non-blocking large-read, baseline, pre-refactor, and affected-surface Stop
6
- adapters. Every threshold, glob, branch rule, command, timeout, and output
7
- budget stays 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.
8
9
 
9
10
  ## Choice matrix
10
11
 
@@ -52,7 +53,20 @@ key. Consumer values are never normalized on adoption.
52
53
  "timeoutSeconds": 15,
53
54
  "outputBudget": 1000
54
55
  },
55
- "stopChecks": {"surfaces": [], "timeoutSeconds": 30, "outputBudget": 1000}
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
+ }
56
70
  }
57
71
  }
58
72
  ```
@@ -70,6 +84,14 @@ project commands from the tools already present, then asks before activating.
70
84
  `python3 "$CLAUDE_PROJECT_DIR/.claude/hooks/pre-refactor-sweep.py"`
71
85
  - Stop:
72
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"`
73
93
 
74
94
  All adapters are non-blocking. A failed or timed-out configured command remains
75
- visibly failed in hook context; it is never reported as green.
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.
@@ -31,6 +31,7 @@ from pathlib import Path
31
31
  LOG_DIR = Path(".claude/logs")
32
32
  _WORKTREE_CORE_MODULE = "_agent_workflow_kit_worktree_lifecycle"
33
33
  _ADVISORY_CORE_MODULE = "_agent_workflow_kit_workflow_advisories"
34
+ _LOC_OFFENDER_GATE_MODULE = "_agent_workflow_kit_loc_offender_gate"
34
35
 
35
36
 
36
37
  def rotate_log_if_needed(log_path: Path, max_bytes: int = 100_000, generations: int = 3) -> None:
@@ -221,6 +222,24 @@ def load_workflow_advisories_core():
221
222
  return module
222
223
 
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
+
224
243
  def hook_event_output(event_name: str, context: str) -> dict:
225
244
  """Canonical non-blocking context payload for Claude hook events."""
226
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,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())
@@ -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