@ikon85/agent-workflow-kit 0.24.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/workflow-advisories.md +28 -6
- package/.claude/hooks/_hook_utils.py +19 -0
- package/.claude/hooks/_safety_guard.py +24 -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/skill-drift-hint.py +9 -45
- package/.claude/skills/setup-workflow/workflow-advisories.md +28 -6
- package/README.md +13 -0
- package/agent-workflow-kit.package.json +35 -7
- 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 +100 -0
- package/src/lib/bundle.mjs +4 -0
|
@@ -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,
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
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
|
|
@@ -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,
|
|
6
|
-
|
|
7
|
-
|
|
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.
|
package/README.md
CHANGED
|
@@ -332,6 +332,19 @@ still reference. Flags: `--force` (overwrite pre-existing files on `init`),
|
|
|
332
332
|
|
|
333
333
|
## Release notes
|
|
334
334
|
|
|
335
|
+
### 0.25.0
|
|
336
|
+
|
|
337
|
+
- added: `.claude/hooks/convention-drift-hint.py`
|
|
338
|
+
- added: `.claude/hooks/loc-offender-forewarn.py`
|
|
339
|
+
- added: `.claude/hooks/migration-snapshot-reminder.py`
|
|
340
|
+
- added: `scripts/workflow-advisories/capabilities.json`
|
|
341
|
+
- changed: `.agents/skills/setup-workflow/workflow-advisories.md`
|
|
342
|
+
- changed: `.claude/hooks/_hook_utils.py`
|
|
343
|
+
- changed: `.claude/hooks/skill-drift-hint.py`
|
|
344
|
+
- changed: `.claude/skills/setup-workflow/workflow-advisories.md`
|
|
345
|
+
- changed: `scripts/loc_offender_gate.py`
|
|
346
|
+
- changed: `scripts/workflow-advisories/core.py`
|
|
347
|
+
|
|
335
348
|
### 0.24.0
|
|
336
349
|
|
|
337
350
|
- added: `.agents/skills/setup-workflow/workflow-advisories.md`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"kitVersion": "0.
|
|
2
|
+
"kitVersion": "0.25.0",
|
|
3
3
|
"files": [
|
|
4
4
|
{
|
|
5
5
|
"path": ".agents/skills/ask-matt/SKILL.md",
|
|
@@ -618,7 +618,7 @@
|
|
|
618
618
|
"kind": "skill",
|
|
619
619
|
"ownerSkill": "setup-workflow",
|
|
620
620
|
"surface": "codex",
|
|
621
|
-
"sha256": "
|
|
621
|
+
"sha256": "81ce0f5573d5f062adbc87149172e74ab322315bf348025c0ff2c27f8d8ec67b",
|
|
622
622
|
"mode": 420,
|
|
623
623
|
"origin": "kit"
|
|
624
624
|
},
|
|
@@ -886,7 +886,7 @@
|
|
|
886
886
|
{
|
|
887
887
|
"path": ".claude/hooks/_hook_utils.py",
|
|
888
888
|
"kind": "hook",
|
|
889
|
-
"sha256": "
|
|
889
|
+
"sha256": "9ad70fa80a57c7712df78718c578b58295f446c13a0f1863c69680f3889dd387",
|
|
890
890
|
"mode": 420,
|
|
891
891
|
"origin": "kit"
|
|
892
892
|
},
|
|
@@ -911,6 +911,13 @@
|
|
|
911
911
|
"mode": 493,
|
|
912
912
|
"origin": "kit"
|
|
913
913
|
},
|
|
914
|
+
{
|
|
915
|
+
"path": ".claude/hooks/convention-drift-hint.py",
|
|
916
|
+
"kind": "hook",
|
|
917
|
+
"sha256": "f89fc3f3f9d9b7508e3cdeeedd0e522af11ae10e638457da3910bfe4e43579f2",
|
|
918
|
+
"mode": 493,
|
|
919
|
+
"origin": "kit"
|
|
920
|
+
},
|
|
914
921
|
{
|
|
915
922
|
"path": ".claude/hooks/drift-guard.py",
|
|
916
923
|
"kind": "hook",
|
|
@@ -939,6 +946,20 @@
|
|
|
939
946
|
"mode": 493,
|
|
940
947
|
"origin": "kit"
|
|
941
948
|
},
|
|
949
|
+
{
|
|
950
|
+
"path": ".claude/hooks/loc-offender-forewarn.py",
|
|
951
|
+
"kind": "hook",
|
|
952
|
+
"sha256": "28ba5edb1898345fd852d1f420bb64c5a2e0dfd66aa8d2b752899fc28a6f80dd",
|
|
953
|
+
"mode": 493,
|
|
954
|
+
"origin": "kit"
|
|
955
|
+
},
|
|
956
|
+
{
|
|
957
|
+
"path": ".claude/hooks/migration-snapshot-reminder.py",
|
|
958
|
+
"kind": "hook",
|
|
959
|
+
"sha256": "693227fae1aa3e782ed5b61a9e8c3fc8c1880a617efd14cb86ca7f38d9a8f204",
|
|
960
|
+
"mode": 493,
|
|
961
|
+
"origin": "kit"
|
|
962
|
+
},
|
|
942
963
|
{
|
|
943
964
|
"path": ".claude/hooks/pre-refactor-sweep.py",
|
|
944
965
|
"kind": "hook",
|
|
@@ -956,7 +977,7 @@
|
|
|
956
977
|
{
|
|
957
978
|
"path": ".claude/hooks/skill-drift-hint.py",
|
|
958
979
|
"kind": "hook",
|
|
959
|
-
"sha256": "
|
|
980
|
+
"sha256": "2ce5c8cfc85dc76cf93abf1cd061440b8030109231a45a0920e36a5f1c06694a",
|
|
960
981
|
"mode": 493,
|
|
961
982
|
"origin": "kit"
|
|
962
983
|
},
|
|
@@ -1731,7 +1752,7 @@
|
|
|
1731
1752
|
"kind": "skill",
|
|
1732
1753
|
"ownerSkill": "setup-workflow",
|
|
1733
1754
|
"surface": "claude",
|
|
1734
|
-
"sha256": "
|
|
1755
|
+
"sha256": "81ce0f5573d5f062adbc87149172e74ab322315bf348025c0ff2c27f8d8ec67b",
|
|
1735
1756
|
"mode": 420,
|
|
1736
1757
|
"origin": "kit"
|
|
1737
1758
|
},
|
|
@@ -2159,7 +2180,7 @@
|
|
|
2159
2180
|
{
|
|
2160
2181
|
"path": "scripts/loc_offender_gate.py",
|
|
2161
2182
|
"kind": "script",
|
|
2162
|
-
"sha256": "
|
|
2183
|
+
"sha256": "a66650b3b194a00ecc3ba37acae4bbb1aeeb57762d27fb2dfd3bb1fc2734a8a5",
|
|
2163
2184
|
"mode": 493,
|
|
2164
2185
|
"origin": "kit"
|
|
2165
2186
|
},
|
|
@@ -2247,10 +2268,17 @@
|
|
|
2247
2268
|
"mode": 420,
|
|
2248
2269
|
"origin": "kit"
|
|
2249
2270
|
},
|
|
2271
|
+
{
|
|
2272
|
+
"path": "scripts/workflow-advisories/capabilities.json",
|
|
2273
|
+
"kind": "doc",
|
|
2274
|
+
"sha256": "04c731394fa458c141870fb89f2432092b3749955eccbda9b9b8db09636526c9",
|
|
2275
|
+
"mode": 420,
|
|
2276
|
+
"origin": "kit"
|
|
2277
|
+
},
|
|
2250
2278
|
{
|
|
2251
2279
|
"path": "scripts/workflow-advisories/core.py",
|
|
2252
2280
|
"kind": "script",
|
|
2253
|
-
"sha256": "
|
|
2281
|
+
"sha256": "dc47bc20ed1107d230f45ff7158e4fd81d6473be515e03cbd52274f3e87d840d",
|
|
2254
2282
|
"mode": 420,
|
|
2255
2283
|
"origin": "kit"
|
|
2256
2284
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikon85/agent-workflow-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.25.0",
|
|
4
4
|
"description": "Portable AI-agent workflow skills (plan → execute → land → learn) for Claude Code & Codex — grilling, TDD, diagnosis, two-axis code review, cross-model Codex review, design & domain-modeling, plus a skill router (ask-matt). npx init/update/diff/uninstall.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,6 +10,7 @@ from __future__ import annotations
|
|
|
10
10
|
|
|
11
11
|
import argparse
|
|
12
12
|
import json
|
|
13
|
+
import re
|
|
13
14
|
import subprocess
|
|
14
15
|
import sys
|
|
15
16
|
from pathlib import Path
|
|
@@ -25,6 +26,29 @@ class GateError(RuntimeError):
|
|
|
25
26
|
"""A condition the gate cannot evaluate → fail-closed (exit 1)."""
|
|
26
27
|
|
|
27
28
|
|
|
29
|
+
_ISSUE_MARKER_RE = re.compile(r"<!--\s*loc-offender:\s*([^>]+?)\s*-->")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def parse_issue_marker(body: str) -> list[str]:
|
|
33
|
+
"""Read the machine marker planted by board-sync at buildability transition."""
|
|
34
|
+
match = _ISSUE_MARKER_RE.search(body or "")
|
|
35
|
+
return [
|
|
36
|
+
path.strip() for path in match.group(1).split(",") if path.strip()
|
|
37
|
+
] if match else []
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def forewarning_context(body: str, max_lines: int, offenders: set[str]) -> str | None:
|
|
41
|
+
"""Render a non-blocking SessionStart warning from the gate-owned contracts."""
|
|
42
|
+
hits = sorted(set(parse_issue_marker(body)) & offenders)
|
|
43
|
+
if not hits:
|
|
44
|
+
return None
|
|
45
|
+
paths = ", ".join(hits)
|
|
46
|
+
return (
|
|
47
|
+
f"LoC offender advisory: {paths} is named by the issue marker and remains "
|
|
48
|
+
f"above the gate threshold ({max_lines} lines). The pre-push gate stays authoritative."
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
|
|
28
52
|
def _git(args, cwd=None) -> str:
|
|
29
53
|
r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
|
|
30
54
|
if r.returncode != 0:
|