@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.
|
package/README.md
CHANGED
|
@@ -332,6 +332,33 @@ 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
|
+
|
|
348
|
+
### 0.24.0
|
|
349
|
+
|
|
350
|
+
- added: `.agents/skills/setup-workflow/workflow-advisories.md`
|
|
351
|
+
- added: `.claude/hooks/baseline-capture-hint.py`
|
|
352
|
+
- added: `.claude/hooks/pre-refactor-sweep.py`
|
|
353
|
+
- added: `.claude/hooks/recon-size-hint.py`
|
|
354
|
+
- added: `.claude/hooks/typecheck-on-stop.py`
|
|
355
|
+
- added: `.claude/hooks/typecheck-on-stop.sh`
|
|
356
|
+
- added: `.claude/skills/setup-workflow/workflow-advisories.md`
|
|
357
|
+
- added: `scripts/workflow-advisories/core.py`
|
|
358
|
+
- changed: `.agents/skills/setup-workflow/SKILL.md`
|
|
359
|
+
- changed: `.claude/hooks/_hook_utils.py`
|
|
360
|
+
- changed: `.claude/skills/setup-workflow/SKILL.md`
|
|
361
|
+
|
|
335
362
|
### 0.23.0
|
|
336
363
|
|
|
337
364
|
- added: `assets/memory-templates/meta_decision_layer_choice.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",
|
|
@@ -573,7 +573,7 @@
|
|
|
573
573
|
"kind": "skill",
|
|
574
574
|
"ownerSkill": "setup-workflow",
|
|
575
575
|
"surface": "codex",
|
|
576
|
-
"sha256": "
|
|
576
|
+
"sha256": "96c0c3ba9ed6b194030a8c45be618caf934e9c4ca8415ea19d726af33cd763cb",
|
|
577
577
|
"mode": 420,
|
|
578
578
|
"origin": "kit"
|
|
579
579
|
},
|
|
@@ -613,6 +613,15 @@
|
|
|
613
613
|
"mode": 420,
|
|
614
614
|
"origin": "kit"
|
|
615
615
|
},
|
|
616
|
+
{
|
|
617
|
+
"path": ".agents/skills/setup-workflow/workflow-advisories.md",
|
|
618
|
+
"kind": "skill",
|
|
619
|
+
"ownerSkill": "setup-workflow",
|
|
620
|
+
"surface": "codex",
|
|
621
|
+
"sha256": "81ce0f5573d5f062adbc87149172e74ab322315bf348025c0ff2c27f8d8ec67b",
|
|
622
|
+
"mode": 420,
|
|
623
|
+
"origin": "kit"
|
|
624
|
+
},
|
|
616
625
|
{
|
|
617
626
|
"path": ".agents/skills/setup-workflow/workflow-overview.md",
|
|
618
627
|
"kind": "skill",
|
|
@@ -877,10 +886,17 @@
|
|
|
877
886
|
{
|
|
878
887
|
"path": ".claude/hooks/_hook_utils.py",
|
|
879
888
|
"kind": "hook",
|
|
880
|
-
"sha256": "
|
|
889
|
+
"sha256": "9ad70fa80a57c7712df78718c578b58295f446c13a0f1863c69680f3889dd387",
|
|
881
890
|
"mode": 420,
|
|
882
891
|
"origin": "kit"
|
|
883
892
|
},
|
|
893
|
+
{
|
|
894
|
+
"path": ".claude/hooks/baseline-capture-hint.py",
|
|
895
|
+
"kind": "hook",
|
|
896
|
+
"sha256": "8f5cddfbb567afc677cf51f39d37e10804bf03e93d7d2fac7793cb5a42473ed3",
|
|
897
|
+
"mode": 493,
|
|
898
|
+
"origin": "kit"
|
|
899
|
+
},
|
|
884
900
|
{
|
|
885
901
|
"path": ".claude/hooks/branch-context.py",
|
|
886
902
|
"kind": "hook",
|
|
@@ -895,6 +911,13 @@
|
|
|
895
911
|
"mode": 493,
|
|
896
912
|
"origin": "kit"
|
|
897
913
|
},
|
|
914
|
+
{
|
|
915
|
+
"path": ".claude/hooks/convention-drift-hint.py",
|
|
916
|
+
"kind": "hook",
|
|
917
|
+
"sha256": "f89fc3f3f9d9b7508e3cdeeedd0e522af11ae10e638457da3910bfe4e43579f2",
|
|
918
|
+
"mode": 493,
|
|
919
|
+
"origin": "kit"
|
|
920
|
+
},
|
|
898
921
|
{
|
|
899
922
|
"path": ".claude/hooks/drift-guard.py",
|
|
900
923
|
"kind": "hook",
|
|
@@ -923,10 +946,38 @@
|
|
|
923
946
|
"mode": 493,
|
|
924
947
|
"origin": "kit"
|
|
925
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
|
+
},
|
|
963
|
+
{
|
|
964
|
+
"path": ".claude/hooks/pre-refactor-sweep.py",
|
|
965
|
+
"kind": "hook",
|
|
966
|
+
"sha256": "24e8554da18b00f2dbbcf6a3e186f9fb675a4b18471ce60e0ac63000f95a4906",
|
|
967
|
+
"mode": 493,
|
|
968
|
+
"origin": "kit"
|
|
969
|
+
},
|
|
970
|
+
{
|
|
971
|
+
"path": ".claude/hooks/recon-size-hint.py",
|
|
972
|
+
"kind": "hook",
|
|
973
|
+
"sha256": "3fd702d8875552ce3b79b5db1dda51403031bcacc80901a13553d2f60a2deb9b",
|
|
974
|
+
"mode": 493,
|
|
975
|
+
"origin": "kit"
|
|
976
|
+
},
|
|
926
977
|
{
|
|
927
978
|
"path": ".claude/hooks/skill-drift-hint.py",
|
|
928
979
|
"kind": "hook",
|
|
929
|
-
"sha256": "
|
|
980
|
+
"sha256": "2ce5c8cfc85dc76cf93abf1cd061440b8030109231a45a0920e36a5f1c06694a",
|
|
930
981
|
"mode": 493,
|
|
931
982
|
"origin": "kit"
|
|
932
983
|
},
|
|
@@ -944,6 +995,20 @@
|
|
|
944
995
|
"mode": 493,
|
|
945
996
|
"origin": "kit"
|
|
946
997
|
},
|
|
998
|
+
{
|
|
999
|
+
"path": ".claude/hooks/typecheck-on-stop.py",
|
|
1000
|
+
"kind": "hook",
|
|
1001
|
+
"sha256": "459e5f25f5fe97c62687eff1d3e7e76051ba8390e2b2908fc50c16ea53a95250",
|
|
1002
|
+
"mode": 493,
|
|
1003
|
+
"origin": "kit"
|
|
1004
|
+
},
|
|
1005
|
+
{
|
|
1006
|
+
"path": ".claude/hooks/typecheck-on-stop.sh",
|
|
1007
|
+
"kind": "hook",
|
|
1008
|
+
"sha256": "1bb57f53d53d18af92a8c7ce63f46be0658ae7d675a95d387646b4232001f1bc",
|
|
1009
|
+
"mode": 493,
|
|
1010
|
+
"origin": "kit"
|
|
1011
|
+
},
|
|
947
1012
|
{
|
|
948
1013
|
"path": ".claude/skills/ask-matt/SKILL.md",
|
|
949
1014
|
"kind": "skill",
|
|
@@ -1642,7 +1707,7 @@
|
|
|
1642
1707
|
"kind": "skill",
|
|
1643
1708
|
"ownerSkill": "setup-workflow",
|
|
1644
1709
|
"surface": "claude",
|
|
1645
|
-
"sha256": "
|
|
1710
|
+
"sha256": "96c0c3ba9ed6b194030a8c45be618caf934e9c4ca8415ea19d726af33cd763cb",
|
|
1646
1711
|
"mode": 420,
|
|
1647
1712
|
"origin": "kit"
|
|
1648
1713
|
},
|
|
@@ -1682,6 +1747,15 @@
|
|
|
1682
1747
|
"mode": 420,
|
|
1683
1748
|
"origin": "kit"
|
|
1684
1749
|
},
|
|
1750
|
+
{
|
|
1751
|
+
"path": ".claude/skills/setup-workflow/workflow-advisories.md",
|
|
1752
|
+
"kind": "skill",
|
|
1753
|
+
"ownerSkill": "setup-workflow",
|
|
1754
|
+
"surface": "claude",
|
|
1755
|
+
"sha256": "81ce0f5573d5f062adbc87149172e74ab322315bf348025c0ff2c27f8d8ec67b",
|
|
1756
|
+
"mode": 420,
|
|
1757
|
+
"origin": "kit"
|
|
1758
|
+
},
|
|
1685
1759
|
{
|
|
1686
1760
|
"path": ".claude/skills/setup-workflow/workflow-overview.md",
|
|
1687
1761
|
"kind": "skill",
|
|
@@ -2106,7 +2180,7 @@
|
|
|
2106
2180
|
{
|
|
2107
2181
|
"path": "scripts/loc_offender_gate.py",
|
|
2108
2182
|
"kind": "script",
|
|
2109
|
-
"sha256": "
|
|
2183
|
+
"sha256": "a66650b3b194a00ecc3ba37acae4bbb1aeeb57762d27fb2dfd3bb1fc2734a8a5",
|
|
2110
2184
|
"mode": 493,
|
|
2111
2185
|
"origin": "kit"
|
|
2112
2186
|
},
|
|
@@ -2194,6 +2268,20 @@
|
|
|
2194
2268
|
"mode": 420,
|
|
2195
2269
|
"origin": "kit"
|
|
2196
2270
|
},
|
|
2271
|
+
{
|
|
2272
|
+
"path": "scripts/workflow-advisories/capabilities.json",
|
|
2273
|
+
"kind": "doc",
|
|
2274
|
+
"sha256": "04c731394fa458c141870fb89f2432092b3749955eccbda9b9b8db09636526c9",
|
|
2275
|
+
"mode": 420,
|
|
2276
|
+
"origin": "kit"
|
|
2277
|
+
},
|
|
2278
|
+
{
|
|
2279
|
+
"path": "scripts/workflow-advisories/core.py",
|
|
2280
|
+
"kind": "script",
|
|
2281
|
+
"sha256": "dc47bc20ed1107d230f45ff7158e4fd81d6473be515e03cbd52274f3e87d840d",
|
|
2282
|
+
"mode": 420,
|
|
2283
|
+
"origin": "kit"
|
|
2284
|
+
},
|
|
2197
2285
|
{
|
|
2198
2286
|
"path": "scripts/worktree-lifecycle/capabilities.json",
|
|
2199
2287
|
"kind": "doc",
|
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:
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""Profile-driven decisions for independently activatable Safety Guardrails."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import shlex
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from search import scan as scan_search
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class GuardDecision:
|
|
17
|
+
action: str
|
|
18
|
+
message: str = ""
|
|
19
|
+
log_message: str = ""
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_profile(path: Path) -> dict[str, Any]:
|
|
23
|
+
document = json.loads(path.read_text(encoding="utf-8"))
|
|
24
|
+
section = document.get("safetyGuardrails")
|
|
25
|
+
if not isinstance(section, dict):
|
|
26
|
+
return {}
|
|
27
|
+
return section
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def inactive() -> GuardDecision:
|
|
31
|
+
return GuardDecision("allow")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _secret_name_is_sensitive(name: str, policy: dict[str, Any]) -> bool:
|
|
35
|
+
suffixes = tuple(policy.get("safeTemplateSuffixes") or ())
|
|
36
|
+
if name.startswith(".env"):
|
|
37
|
+
return not any(name.endswith(suffix) for suffix in suffixes)
|
|
38
|
+
return name in set(policy.get("sensitiveNames") or ())
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _secret_path_is_sensitive(path: str, policy: dict[str, Any]) -> bool:
|
|
42
|
+
candidate = Path(path)
|
|
43
|
+
if _secret_name_is_sensitive(candidate.name, policy):
|
|
44
|
+
return True
|
|
45
|
+
normalized = str(candidate).replace("\\", "/")
|
|
46
|
+
return any(fragment in normalized for fragment in policy.get("sensitivePathFragments") or ())
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def secret_decision(policy: dict[str, Any], payload: dict[str, Any]) -> GuardDecision:
|
|
50
|
+
if policy.get("enabled") is not True:
|
|
51
|
+
return inactive()
|
|
52
|
+
tool = payload.get("tool_name")
|
|
53
|
+
tool_input = payload.get("tool_input") or {}
|
|
54
|
+
if tool == "Bash":
|
|
55
|
+
return _secret_bash_decision(policy, str(tool_input.get("command") or ""))
|
|
56
|
+
path = str(tool_input.get("file_path") or "")
|
|
57
|
+
if not path or tool not in {"Read", "Edit", "Write", "MultiEdit"}:
|
|
58
|
+
return inactive()
|
|
59
|
+
if tool == "Write" and Path(path).name.startswith(".env"):
|
|
60
|
+
return inactive()
|
|
61
|
+
if not _secret_path_is_sensitive(path, policy):
|
|
62
|
+
return inactive()
|
|
63
|
+
return GuardDecision(
|
|
64
|
+
"block",
|
|
65
|
+
f"BLOCKED: access to sensitive path '{path}' is not allowed.",
|
|
66
|
+
f"blocked tool={tool} path={path!r}",
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
_DUMP_COMMANDS = {
|
|
71
|
+
"cat", "head", "tail", "less", "more", "strings", "nl", "xxd", "od",
|
|
72
|
+
"bat", "tac", "rev", "grep", "egrep", "fgrep", "rg", "ag", "awk", "sed", "cut",
|
|
73
|
+
}
|
|
74
|
+
_QUIET_GREP = {"grep", "egrep", "fgrep", "rg", "ag"}
|
|
75
|
+
_INSTALL_RE = re.compile(
|
|
76
|
+
r"\b(?P<manager>npm|pnpm|yarn|bun)\s+(?:install|i|ci)\b"
|
|
77
|
+
)
|
|
78
|
+
_BACKGROUND_RE = re.compile(r"(?<![<>&])&[ \t]*(?:;|$)", re.MULTILINE)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _secret_bash_decision(policy: dict[str, Any], command: str) -> GuardDecision:
|
|
82
|
+
if not command:
|
|
83
|
+
return inactive()
|
|
84
|
+
try:
|
|
85
|
+
segments = re.split(r"\n|;|&&|\|\||\|", command)
|
|
86
|
+
for segment in segments:
|
|
87
|
+
tokens = shlex.split(segment)
|
|
88
|
+
dumps = set(tokens) & _DUMP_COMMANDS
|
|
89
|
+
paths = [token for token in tokens if _secret_path_is_sensitive(token, policy)]
|
|
90
|
+
if not dumps or not paths:
|
|
91
|
+
continue
|
|
92
|
+
if dumps <= _QUIET_GREP and any(flag == "--quiet" or flag.startswith("-q") for flag in tokens):
|
|
93
|
+
continue
|
|
94
|
+
return GuardDecision(
|
|
95
|
+
"block",
|
|
96
|
+
f"BLOCKED: command would print sensitive path '{paths[0]}' to the transcript.",
|
|
97
|
+
f"blocked bash secret dump path={paths[0]!r}",
|
|
98
|
+
)
|
|
99
|
+
except ValueError:
|
|
100
|
+
return inactive()
|
|
101
|
+
return inactive()
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _effective_cwd(command: str, match_pos: int, fallback: Path) -> Path:
|
|
105
|
+
target = None
|
|
106
|
+
for match in re.finditer(r"(?:^|[;&|]\s*)cd\s+([^\s;&|]+)", command):
|
|
107
|
+
if match.end() <= match_pos:
|
|
108
|
+
target = match.group(1).strip("'\"")
|
|
109
|
+
if target is None:
|
|
110
|
+
return fallback
|
|
111
|
+
path = Path(target).expanduser()
|
|
112
|
+
return path if path.is_absolute() else fallback / path
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _detected_manager(policy: dict[str, Any], start: Path) -> tuple[str, Path] | None:
|
|
116
|
+
lockfiles = policy.get("lockfiles") or {}
|
|
117
|
+
try:
|
|
118
|
+
current = start.resolve()
|
|
119
|
+
except OSError:
|
|
120
|
+
return None
|
|
121
|
+
while True:
|
|
122
|
+
for filename, manager in lockfiles.items():
|
|
123
|
+
candidate = current / filename
|
|
124
|
+
if candidate.is_file():
|
|
125
|
+
return str(manager), candidate
|
|
126
|
+
if current.parent == current:
|
|
127
|
+
return None
|
|
128
|
+
current = current.parent
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def package_manager_decision(
|
|
132
|
+
policy: dict[str, Any],
|
|
133
|
+
payload: dict[str, Any],
|
|
134
|
+
cwd: Path,
|
|
135
|
+
) -> GuardDecision:
|
|
136
|
+
if policy.get("enabled") is not True or payload.get("tool_name") != "Bash":
|
|
137
|
+
return inactive()
|
|
138
|
+
command = str((payload.get("tool_input") or {}).get("command") or "")
|
|
139
|
+
match = _INSTALL_RE.search(command)
|
|
140
|
+
if match is None:
|
|
141
|
+
return inactive()
|
|
142
|
+
requested = match.group("manager")
|
|
143
|
+
detected = _detected_manager(policy, _effective_cwd(command, match.start(), cwd))
|
|
144
|
+
if detected is None or detected[0] == requested:
|
|
145
|
+
return inactive()
|
|
146
|
+
manager, lockfile = detected
|
|
147
|
+
return GuardDecision(
|
|
148
|
+
"block",
|
|
149
|
+
f"BLOCKED: `{match.group(0)}` conflicts with {lockfile.name}. "
|
|
150
|
+
f"Use `{manager} install` in this project.",
|
|
151
|
+
f"blocked package-manager requested={requested} expected={manager} lock={lockfile}",
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def double_background_decision(
|
|
156
|
+
policy: dict[str, Any],
|
|
157
|
+
payload: dict[str, Any],
|
|
158
|
+
) -> GuardDecision:
|
|
159
|
+
if policy.get("enabled") is not True or payload.get("tool_name") != "Bash":
|
|
160
|
+
return inactive()
|
|
161
|
+
surface = str(payload.get("surface") or "claude")
|
|
162
|
+
if surface not in set(policy.get("surfaces") or ()):
|
|
163
|
+
return inactive()
|
|
164
|
+
tool_input = payload.get("tool_input") or {}
|
|
165
|
+
is_background = tool_input.get("run_in_background") is True or (
|
|
166
|
+
str(tool_input.get("run_in_background") or "").lower() == "true"
|
|
167
|
+
)
|
|
168
|
+
command = str(tool_input.get("command") or "")
|
|
169
|
+
if not is_background or not _BACKGROUND_RE.search(command):
|
|
170
|
+
return inactive()
|
|
171
|
+
return GuardDecision(
|
|
172
|
+
"block",
|
|
173
|
+
"BLOCKED: run_in_background already detaches and captures this command; "
|
|
174
|
+
"remove the inner `&` background operator.",
|
|
175
|
+
"blocked double-background command",
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def search_shim_decision(
|
|
180
|
+
policy: dict[str, Any],
|
|
181
|
+
payload: dict[str, Any],
|
|
182
|
+
) -> GuardDecision:
|
|
183
|
+
if (
|
|
184
|
+
policy.get("enabled") is not True
|
|
185
|
+
or policy.get("detected") is not True
|
|
186
|
+
or payload.get("tool_name") != "Bash"
|
|
187
|
+
):
|
|
188
|
+
return inactive()
|
|
189
|
+
command = str((payload.get("tool_input") or {}).get("command") or "")
|
|
190
|
+
finding = scan_search(command, set(policy.get("commandNames") or ()))
|
|
191
|
+
if finding is None:
|
|
192
|
+
return inactive()
|
|
193
|
+
reason, pattern = finding
|
|
194
|
+
return GuardDecision(
|
|
195
|
+
"block",
|
|
196
|
+
f"BLOCKED: search shim cannot safely evaluate pattern `{pattern}` ({reason}). "
|
|
197
|
+
"Use `--fixed-strings` or `command grep`.",
|
|
198
|
+
f"blocked search-shim breaker reason={reason}",
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def evaluate(kind: str, profile: dict[str, Any], payload: dict[str, Any]) -> GuardDecision:
|
|
203
|
+
if kind == "secrets":
|
|
204
|
+
return secret_decision(profile.get("secrets") or {}, payload)
|
|
205
|
+
if kind == "package-manager":
|
|
206
|
+
return package_manager_decision(
|
|
207
|
+
profile.get("packageManager") or {},
|
|
208
|
+
payload,
|
|
209
|
+
Path.cwd(),
|
|
210
|
+
)
|
|
211
|
+
if kind == "double-background":
|
|
212
|
+
return double_background_decision(
|
|
213
|
+
profile.get("doubleBackground") or {},
|
|
214
|
+
payload,
|
|
215
|
+
)
|
|
216
|
+
if kind == "search-shim":
|
|
217
|
+
return search_shim_decision(
|
|
218
|
+
profile.get("searchShim") or {},
|
|
219
|
+
payload,
|
|
220
|
+
)
|
|
221
|
+
return inactive()
|