@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.
- package/.agents/skills/setup-workflow/SKILL.md +26 -6
- package/.agents/skills/setup-workflow/workflow-advisories.md +97 -0
- package/.claude/hooks/_hook_utils.py +35 -0
- package/.claude/hooks/_safety_guard.py +24 -0
- package/.claude/hooks/baseline-capture-hint.py +25 -0
- package/.claude/hooks/block-bg-double-background.py +30 -0
- package/.claude/hooks/block-npm-install-in-pnpm.py +30 -0
- package/.claude/hooks/block-secrets.py +30 -0
- package/.claude/hooks/convention-drift-hint.py +27 -0
- package/.claude/hooks/grep-shim-guard.py +30 -0
- package/.claude/hooks/loc-offender-forewarn.py +56 -0
- package/.claude/hooks/migration-snapshot-reminder.py +24 -0
- package/.claude/hooks/pre-refactor-sweep.py +27 -0
- package/.claude/hooks/recon-size-hint.py +24 -0
- package/.claude/hooks/skill-drift-hint.py +9 -45
- package/.claude/hooks/typecheck-on-stop.py +27 -0
- package/.claude/hooks/typecheck-on-stop.sh +2 -0
- package/.claude/skills/setup-workflow/SKILL.md +26 -6
- package/.claude/skills/setup-workflow/workflow-advisories.md +97 -0
- package/README.md +27 -0
- package/agent-workflow-kit.package.json +94 -6
- package/package.json +1 -1
- package/scripts/loc_offender_gate.py +24 -0
- package/scripts/safety-guardrails/core.py +221 -0
- package/scripts/safety-guardrails/search.py +92 -0
- package/scripts/security/audit-gate.mjs +122 -0
- package/scripts/security/ensure-gitleaks.mjs +101 -0
- package/scripts/security/gitleaks-profile.json +14 -0
- package/scripts/security/install-git-hooks.mjs +52 -0
- package/scripts/workflow-advisories/capabilities.json +33 -0
- package/scripts/workflow-advisories/core.py +285 -0
- 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
|
|
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
|
|
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
|
|
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
|
|
290
|
-
this profile. Apply the
|
|
291
|
-
from [worktree-lifecycle.md](./worktree-lifecycle.md)
|
|
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
|
|
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
|
-
|
|
54
|
-
|
|
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())
|