@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
@@ -183,3 +183,103 @@ def stop_check_decision(profile: dict, payload: dict, root: Path) -> Decision:
183
183
  return _command_decision(
184
184
  commands, config, root, "Changed-surface stop checks:", "Stop",
185
185
  )
186
+
187
+
188
+ def git_commit_time(root: Path, relative_path: str) -> int | None:
189
+ try:
190
+ result = subprocess.run(
191
+ ["git", "-C", str(root), "log", "-1", "--format=%ct", "--", relative_path],
192
+ capture_output=True, text=True, timeout=5,
193
+ )
194
+ except (OSError, subprocess.TimeoutExpired):
195
+ return None
196
+ value = result.stdout.strip()
197
+ return int(value) if result.returncode == 0 and value.isdigit() else None
198
+
199
+
200
+ def read_source_list(path: Path) -> list[str]:
201
+ try:
202
+ lines = path.read_text(encoding="utf-8").splitlines()
203
+ except OSError:
204
+ return []
205
+ return [
206
+ line.strip() for line in lines
207
+ if line.strip() and not line.strip().startswith("#")
208
+ ]
209
+
210
+
211
+ def collect_stale_maps(
212
+ root: Path, maps: list[tuple[str, list[str]]],
213
+ ) -> list[tuple[str, list[str]]]:
214
+ stale_maps: list[tuple[str, list[str]]] = []
215
+ for document, sources in maps:
216
+ document_time = git_commit_time(root, document)
217
+ if document_time is None:
218
+ continue
219
+ stale = [
220
+ source for source in sources
221
+ if (source_time := git_commit_time(root, source)) is not None
222
+ and source_time > document_time
223
+ ]
224
+ if stale:
225
+ stale_maps.append((document, stale))
226
+ return stale_maps
227
+
228
+
229
+ def collect_skill_stale(root: Path, skills_relative: str) -> list[tuple[str, list[str]]]:
230
+ skills_dir = root / skills_relative
231
+ maps = [
232
+ (
233
+ f"{skills_relative}/{sources_file.parent.name}/SKILL.md",
234
+ read_source_list(sources_file),
235
+ )
236
+ for sources_file in sorted(skills_dir.glob("*/SOURCES.txt"))
237
+ ] if skills_dir.is_dir() else []
238
+ return [
239
+ (Path(document).parent.name, sources)
240
+ for document, sources in collect_stale_maps(root, maps)
241
+ ]
242
+
243
+
244
+ def convention_freshness_decision(profile: dict, root: Path) -> Decision:
245
+ config = profile.get("freshness", {})
246
+ maps = [
247
+ (entry.get("document", ""), entry.get("sources", []))
248
+ for entry in config.get("documents", [])
249
+ if entry.get("document")
250
+ ]
251
+ stale = collect_stale_maps(root, maps)
252
+ if not stale:
253
+ return Decision(None, "SessionStart")
254
+ lines = ["Convention freshness advisory:"]
255
+ for document, sources in stale:
256
+ lines.append(f"- {document} is older than:")
257
+ lines.extend(f" - {source}" for source in sources)
258
+ return Decision(
259
+ _bounded("\n".join(lines), int(config.get("outputBudget", 1000))),
260
+ "SessionStart",
261
+ )
262
+
263
+
264
+ def migration_reminder_decision(profile: dict, payload: dict) -> Decision:
265
+ config = profile.get("migration", {})
266
+ if payload.get("tool_name") != "Bash":
267
+ return Decision(None, "PostToolUse")
268
+ command = payload.get("tool_input", {}).get("command", "")
269
+ if not any(
270
+ re.search(pattern, command)
271
+ for pattern in config.get("commandMatchers", [])
272
+ ):
273
+ return Decision(None, "PostToolUse")
274
+ artifact = config.get("artifact")
275
+ refresh = config.get("refreshCommand", [])
276
+ if not artifact or not refresh:
277
+ return Decision(None, "PostToolUse")
278
+ message = (
279
+ f"Migration advisory: refresh {artifact} with `{' '.join(refresh)}` "
280
+ "before treating the migration result as complete."
281
+ )
282
+ return Decision(
283
+ _bounded(message, int(config.get("outputBudget", 500))),
284
+ "PostToolUse",
285
+ )
@@ -94,12 +94,30 @@ export const HELPER_FILES = [
94
94
  // Profile-driven non-blocking change-lifecycle advisories. The shell Stop
95
95
  // entry delegates to its sibling Python adapter; decisions stay in core.py.
96
96
  { path: 'scripts/workflow-advisories/core.py', kind: 'script', mode: 0o644 },
97
+ { path: 'scripts/workflow-advisories/capabilities.json', kind: 'doc', mode: 0o644 },
97
98
  { path: '.claude/hooks/recon-size-hint.py', kind: 'hook', mode: 0o755 },
98
99
  { path: '.claude/hooks/baseline-capture-hint.py', kind: 'hook', mode: 0o755 },
99
100
  { path: '.claude/hooks/pre-refactor-sweep.py', kind: 'hook', mode: 0o755 },
100
101
  { path: '.claude/hooks/typecheck-on-stop.py', kind: 'hook', mode: 0o755 },
101
102
  { path: '.claude/hooks/typecheck-on-stop.sh', kind: 'hook', mode: 0o755 },
103
+ { path: '.claude/hooks/convention-drift-hint.py', kind: 'hook', mode: 0o755 },
104
+ { path: '.claude/hooks/migration-snapshot-reminder.py', kind: 'hook', mode: 0o755 },
105
+ { path: '.claude/hooks/loc-offender-forewarn.py', kind: 'hook', mode: 0o755 },
102
106
  { path: '.claude/hooks/drift-guard.py', kind: 'hook', mode: 0o755 },
107
+ // Counted Safety Guardrails unit: shared policy/search core, one loader,
108
+ // four thin Agent adapters, and three portable repository-security
109
+ // primitives. Activation stays consumer-owned through setup-workflow.
110
+ { path: 'scripts/safety-guardrails/core.py', kind: 'script', mode: 0o644 },
111
+ { path: 'scripts/safety-guardrails/search.py', kind: 'script', mode: 0o644 },
112
+ { path: '.claude/hooks/_safety_guard.py', kind: 'hook', mode: 0o644 },
113
+ { path: '.claude/hooks/block-secrets.py', kind: 'hook', mode: 0o755 },
114
+ { path: '.claude/hooks/block-npm-install-in-pnpm.py', kind: 'hook', mode: 0o755 },
115
+ { path: '.claude/hooks/block-bg-double-background.py', kind: 'hook', mode: 0o755 },
116
+ { path: '.claude/hooks/grep-shim-guard.py', kind: 'hook', mode: 0o755 },
117
+ { path: 'scripts/security/install-git-hooks.mjs', kind: 'script', mode: 0o755 },
118
+ { path: 'scripts/security/ensure-gitleaks.mjs', kind: 'script', mode: 0o755 },
119
+ { path: 'scripts/security/gitleaks-profile.json', kind: 'doc', mode: 0o644 },
120
+ { path: 'scripts/security/audit-gate.mjs', kind: 'script', mode: 0o755 },
103
121
  // SessionStart skill-freshness drift-hint (audit-skills names it). For each
104
122
  // <skill>/SOURCES.txt it flags sources newer in git than the SKILL.md. Imports
105
123
  // _hook_utils (shipped above); stdlib-only otherwise. Executable hook → 0o755.