@ikon85/agent-workflow-kit 0.10.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 (273) hide show
  1. package/.agents/skills/ask-matt/SKILL.md +139 -0
  2. package/.agents/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  3. package/.agents/skills/audit-skills/SKILL.md +145 -0
  4. package/.agents/skills/board-to-waves/SKILL.md +135 -0
  5. package/.agents/skills/code-review/SKILL.md +64 -0
  6. package/.agents/skills/codebase-design/DEEPENING.md +37 -0
  7. package/.agents/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  8. package/.agents/skills/codebase-design/SKILL.md +114 -0
  9. package/.agents/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  10. package/.agents/skills/codex-adapter-sync/SKILL.md +151 -0
  11. package/.agents/skills/decision-gate/SKILL.md +67 -0
  12. package/.agents/skills/diagnose/SKILL.md +136 -0
  13. package/.agents/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  14. package/.agents/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  15. package/.agents/skills/domain-modeling/ADR-FORMAT.md +47 -0
  16. package/.agents/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  17. package/.agents/skills/domain-modeling/SKILL.md +76 -0
  18. package/.agents/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  19. package/.agents/skills/git-worktree-recover/SKILL.md +176 -0
  20. package/.agents/skills/grill-me/SKILL.md +54 -0
  21. package/.agents/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  22. package/.agents/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  23. package/.agents/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  24. package/.agents/skills/grill-with-docs/SKILL.md +137 -0
  25. package/.agents/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  26. package/.agents/skills/implement/SKILL.md +15 -0
  27. package/.agents/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  28. package/.agents/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  29. package/.agents/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  30. package/.agents/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  31. package/.agents/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  32. package/.agents/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  33. package/.agents/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  34. package/.agents/skills/improve-codebase-architecture/SKILL.md +88 -0
  35. package/.agents/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  36. package/.agents/skills/kit-release/SKILL.md +56 -0
  37. package/.agents/skills/local-ci/SKILL.md +94 -0
  38. package/.agents/skills/orchestrate-wave/SKILL.md +261 -0
  39. package/.agents/skills/orchestrate-wave/references/builder-contract.md +67 -0
  40. package/.agents/skills/prototype/LOGIC.md +79 -0
  41. package/.agents/skills/prototype/SKILL.md +30 -0
  42. package/.agents/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  43. package/.agents/skills/prototype/UI.md +112 -0
  44. package/.agents/skills/research/SKILL.md +12 -0
  45. package/.agents/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  46. package/.agents/skills/resolving-merge-conflicts/SKILL.md +14 -0
  47. package/.agents/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  48. package/.agents/skills/retro/SKILL.md +209 -0
  49. package/.agents/skills/scale-check/SKILL.md +141 -0
  50. package/.agents/skills/security-audit/SKILL.md +67 -0
  51. package/.agents/skills/setup-workflow/SKILL.md +187 -0
  52. package/.agents/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  53. package/.agents/skills/setup-workflow/board-sync.md +111 -0
  54. package/.agents/skills/setup-workflow/code-review.md +26 -0
  55. package/.agents/skills/setup-workflow/domain.md +51 -0
  56. package/.agents/skills/setup-workflow/issue-tracker-github.md +35 -0
  57. package/.agents/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  58. package/.agents/skills/setup-workflow/issue-tracker-local.md +30 -0
  59. package/.agents/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  60. package/.agents/skills/setup-workflow/spec-completeness-seed.md +44 -0
  61. package/.agents/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  62. package/.agents/skills/setup-workflow/triage-labels.md +18 -0
  63. package/.agents/skills/setup-workflow/workflow-overview.md +33 -0
  64. package/.agents/skills/spec-self-critique/SKILL.md +152 -0
  65. package/.agents/skills/spec-self-critique/scenarios.md +82 -0
  66. package/.agents/skills/tdd/SKILL.md +120 -0
  67. package/.agents/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  68. package/.agents/skills/tdd/deep-modules.md +33 -0
  69. package/.agents/skills/tdd/interface-design.md +31 -0
  70. package/.agents/skills/tdd/mocking.md +59 -0
  71. package/.agents/skills/tdd/refactoring.md +10 -0
  72. package/.agents/skills/tdd/tests.md +61 -0
  73. package/.agents/skills/to-issues/SKILL.md +325 -0
  74. package/.agents/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  75. package/.agents/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  76. package/.agents/skills/to-prd/SKILL.md +180 -0
  77. package/.agents/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  78. package/.agents/skills/to-waves/SKILL.md +260 -0
  79. package/.agents/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  80. package/.agents/skills/triage/AGENT-BRIEF.md +168 -0
  81. package/.agents/skills/triage/OUT-OF-SCOPE.md +101 -0
  82. package/.agents/skills/triage/SKILL.md +122 -0
  83. package/.agents/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  84. package/.agents/skills/verify-spike/SKILL.md +39 -0
  85. package/.agents/skills/wayfinder/SKILL.md +127 -0
  86. package/.agents/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  87. package/.agents/skills/wrapup/SKILL.md +71 -0
  88. package/.agents/skills/wrapup/SOURCES.txt +6 -0
  89. package/.agents/skills/zoom-out/SKILL.md +7 -0
  90. package/.agents/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  91. package/.claude/hooks/_hook_utils.py +184 -0
  92. package/.claude/hooks/drift-guard.py +176 -0
  93. package/.claude/hooks/skill-drift-hint.py +122 -0
  94. package/.claude/hooks/sync-board-status.py +191 -0
  95. package/.claude/skills/ask-matt/SKILL.md +139 -0
  96. package/.claude/skills/ask-matt/THIRD-PARTY-NOTICES.md +29 -0
  97. package/.claude/skills/audit-skills/SKILL.md +145 -0
  98. package/.claude/skills/board-to-waves/SKILL.md +148 -0
  99. package/.claude/skills/code-review/SKILL.md +64 -0
  100. package/.claude/skills/codebase-design/DEEPENING.md +37 -0
  101. package/.claude/skills/codebase-design/DESIGN-IT-TWICE.md +44 -0
  102. package/.claude/skills/codebase-design/SKILL.md +114 -0
  103. package/.claude/skills/codebase-design/THIRD-PARTY-NOTICES.md +29 -0
  104. package/.claude/skills/codex-build/SKILL.md +126 -0
  105. package/.claude/skills/codex-build/THIRD-PARTY-NOTICES.md +35 -0
  106. package/.claude/skills/codex-review/SKILL.md +145 -0
  107. package/.claude/skills/codex-review/THIRD-PARTY-NOTICES.md +31 -0
  108. package/.claude/skills/decision-gate/SKILL.md +67 -0
  109. package/.claude/skills/diagnose/SKILL.md +136 -0
  110. package/.claude/skills/diagnose/THIRD-PARTY-NOTICES.md +29 -0
  111. package/.claude/skills/diagnose/scripts/hitl-loop.template.sh +41 -0
  112. package/.claude/skills/domain-modeling/ADR-FORMAT.md +47 -0
  113. package/.claude/skills/domain-modeling/CONTEXT-FORMAT.md +60 -0
  114. package/.claude/skills/domain-modeling/SKILL.md +76 -0
  115. package/.claude/skills/domain-modeling/THIRD-PARTY-NOTICES.md +29 -0
  116. package/.claude/skills/git-guardrails-claude-code/SKILL.md +96 -0
  117. package/.claude/skills/git-guardrails-claude-code/THIRD-PARTY-NOTICES.md +29 -0
  118. package/.claude/skills/git-guardrails-claude-code/scripts/block-dangerous-git.sh +28 -0
  119. package/.claude/skills/git-worktree-recover/SKILL.md +182 -0
  120. package/.claude/skills/grill-me/SKILL.md +54 -0
  121. package/.claude/skills/grill-me/THIRD-PARTY-NOTICES.md +29 -0
  122. package/.claude/skills/grill-me-codex/SKILL.md +158 -0
  123. package/.claude/skills/grill-me-codex/THIRD-PARTY-NOTICES.md +32 -0
  124. package/.claude/skills/grill-with-docs/ADR-FORMAT.md +47 -0
  125. package/.claude/skills/grill-with-docs/CONTEXT-FORMAT.md +60 -0
  126. package/.claude/skills/grill-with-docs/SKILL.md +137 -0
  127. package/.claude/skills/grill-with-docs/THIRD-PARTY-NOTICES.md +29 -0
  128. package/.claude/skills/grill-with-docs-codex/ADR-FORMAT.md +47 -0
  129. package/.claude/skills/grill-with-docs-codex/CONTEXT-FORMAT.md +60 -0
  130. package/.claude/skills/grill-with-docs-codex/SKILL.md +233 -0
  131. package/.claude/skills/grill-with-docs-codex/THIRD-PARTY-NOTICES.md +33 -0
  132. package/.claude/skills/implement/SKILL.md +15 -0
  133. package/.claude/skills/implement/THIRD-PARTY-NOTICES.md +29 -0
  134. package/.claude/skills/improve-codebase-architecture/ADR-FORMAT.md +47 -0
  135. package/.claude/skills/improve-codebase-architecture/CONTEXT-FORMAT.md +60 -0
  136. package/.claude/skills/improve-codebase-architecture/DEEPENING.md +37 -0
  137. package/.claude/skills/improve-codebase-architecture/HTML-REPORT.md +123 -0
  138. package/.claude/skills/improve-codebase-architecture/INTERFACE-DESIGN.md +46 -0
  139. package/.claude/skills/improve-codebase-architecture/LANGUAGE.md +53 -0
  140. package/.claude/skills/improve-codebase-architecture/SKILL.md +88 -0
  141. package/.claude/skills/improve-codebase-architecture/THIRD-PARTY-NOTICES.md +29 -0
  142. package/.claude/skills/kit-release/SKILL.md +56 -0
  143. package/.claude/skills/local-ci/SKILL.md +94 -0
  144. package/.claude/skills/orchestrate-wave/SKILL.md +269 -0
  145. package/.claude/skills/orchestrate-wave/references/builder-contract.md +67 -0
  146. package/.claude/skills/prototype/LOGIC.md +79 -0
  147. package/.claude/skills/prototype/SKILL.md +30 -0
  148. package/.claude/skills/prototype/THIRD-PARTY-NOTICES.md +29 -0
  149. package/.claude/skills/prototype/UI.md +112 -0
  150. package/.claude/skills/research/SKILL.md +12 -0
  151. package/.claude/skills/research/THIRD-PARTY-NOTICES.md +29 -0
  152. package/.claude/skills/resolving-merge-conflicts/SKILL.md +14 -0
  153. package/.claude/skills/resolving-merge-conflicts/THIRD-PARTY-NOTICES.md +29 -0
  154. package/.claude/skills/retro/SKILL.md +209 -0
  155. package/.claude/skills/scale-check/SKILL.md +141 -0
  156. package/.claude/skills/security-audit/SKILL.md +67 -0
  157. package/.claude/skills/setup-pre-commit/SKILL.md +104 -0
  158. package/.claude/skills/setup-pre-commit/scripts/pre-commit.template.sh +35 -0
  159. package/.claude/skills/setup-workflow/SKILL.md +187 -0
  160. package/.claude/skills/setup-workflow/THIRD-PARTY-NOTICES.md +29 -0
  161. package/.claude/skills/setup-workflow/board-sync.md +111 -0
  162. package/.claude/skills/setup-workflow/code-review.md +26 -0
  163. package/.claude/skills/setup-workflow/domain.md +51 -0
  164. package/.claude/skills/setup-workflow/issue-tracker-github.md +35 -0
  165. package/.claude/skills/setup-workflow/issue-tracker-gitlab.md +34 -0
  166. package/.claude/skills/setup-workflow/issue-tracker-local.md +30 -0
  167. package/.claude/skills/setup-workflow/orchestrate-wave-seed.md +44 -0
  168. package/.claude/skills/setup-workflow/spec-completeness-seed.md +44 -0
  169. package/.claude/skills/setup-workflow/spec-self-critique-seed.md +43 -0
  170. package/.claude/skills/setup-workflow/triage-labels.md +18 -0
  171. package/.claude/skills/setup-workflow/workflow-overview.md +33 -0
  172. package/.claude/skills/skill-manifest.json +389 -0
  173. package/.claude/skills/spec-self-critique/SKILL.md +152 -0
  174. package/.claude/skills/spec-self-critique/scenarios.md +82 -0
  175. package/.claude/skills/tdd/SKILL.md +120 -0
  176. package/.claude/skills/tdd/THIRD-PARTY-NOTICES.md +29 -0
  177. package/.claude/skills/tdd/deep-modules.md +33 -0
  178. package/.claude/skills/tdd/interface-design.md +31 -0
  179. package/.claude/skills/tdd/mocking.md +59 -0
  180. package/.claude/skills/tdd/refactoring.md +10 -0
  181. package/.claude/skills/tdd/tests.md +61 -0
  182. package/.claude/skills/to-issues/SKILL.md +325 -0
  183. package/.claude/skills/to-issues/THIRD-PARTY-NOTICES.md +29 -0
  184. package/.claude/skills/to-prd/PROGRAM-PRD-FORMAT.md +162 -0
  185. package/.claude/skills/to-prd/SKILL.md +180 -0
  186. package/.claude/skills/to-prd/THIRD-PARTY-NOTICES.md +29 -0
  187. package/.claude/skills/to-waves/SKILL.md +260 -0
  188. package/.claude/skills/to-waves/SLICE-METADATA-FORMAT.md +74 -0
  189. package/.claude/skills/triage/AGENT-BRIEF.md +168 -0
  190. package/.claude/skills/triage/OUT-OF-SCOPE.md +101 -0
  191. package/.claude/skills/triage/SKILL.md +122 -0
  192. package/.claude/skills/triage/THIRD-PARTY-NOTICES.md +29 -0
  193. package/.claude/skills/verify-spike/SKILL.md +39 -0
  194. package/.claude/skills/wayfinder/SKILL.md +127 -0
  195. package/.claude/skills/wayfinder/THIRD-PARTY-NOTICES.md +29 -0
  196. package/.claude/skills/wrapup/SKILL.md +81 -0
  197. package/.claude/skills/wrapup/SOURCES.txt +6 -0
  198. package/.claude/skills/write-a-skill/GLOSSARY.md +201 -0
  199. package/.claude/skills/write-a-skill/SKILL.md +88 -0
  200. package/.claude/skills/write-a-skill/THIRD-PARTY-NOTICES.md +29 -0
  201. package/.claude/skills/zoom-out/SKILL.md +7 -0
  202. package/.claude/skills/zoom-out/THIRD-PARTY-NOTICES.md +29 -0
  203. package/LICENSE +28 -0
  204. package/PROVENANCE.md +39 -0
  205. package/README.md +619 -0
  206. package/agent-workflow-kit.package.json +1939 -0
  207. package/docs/agents/board-sync.md +71 -0
  208. package/docs/agents/code-review.md +14 -0
  209. package/docs/agents/domain.md +7 -0
  210. package/docs/agents/issue-tracker.md +12 -0
  211. package/docs/agents/security-audit-runbook-template.md +205 -0
  212. package/docs/agents/skills/orchestrate-wave.md +13 -0
  213. package/docs/agents/skills/spec-self-critique.md +18 -0
  214. package/docs/agents/triage-labels.md +10 -0
  215. package/docs/agents/wave-anchor-template.md +88 -0
  216. package/docs/conventions/spec-completeness.md +23 -0
  217. package/docs/index.html +56 -0
  218. package/docs/methodology.html +488 -0
  219. package/docs/methodology.svg +93 -0
  220. package/docs/workflow.png +0 -0
  221. package/package.json +40 -0
  222. package/scripts/anchor_table.py +343 -0
  223. package/scripts/board-sync.py +1232 -0
  224. package/scripts/board_config.py +147 -0
  225. package/scripts/board_fields.py +192 -0
  226. package/scripts/build-kit.mjs +116 -0
  227. package/scripts/build-kit.test.mjs +59 -0
  228. package/scripts/check-kit-staleness.mjs +45 -0
  229. package/scripts/check-kit-staleness.test.mjs +19 -0
  230. package/scripts/execute-ready-check.py +516 -0
  231. package/scripts/issue_deps.py +79 -0
  232. package/scripts/kit-release.mjs +100 -0
  233. package/scripts/kit-release.test.mjs +67 -0
  234. package/scripts/lib/audit-refs.mjs +146 -0
  235. package/scripts/lib/scrub.mjs +113 -0
  236. package/scripts/lib/scrub.test.mjs +187 -0
  237. package/scripts/loc_offender_core.py +196 -0
  238. package/scripts/loc_offender_gate.py +182 -0
  239. package/scripts/node_kind.py +68 -0
  240. package/scripts/portability_profile_scan.py +53 -0
  241. package/scripts/pr-body-check.py +263 -0
  242. package/scripts/program_graph.py +125 -0
  243. package/scripts/program_graph_parse.py +275 -0
  244. package/scripts/program_graph_validate.py +256 -0
  245. package/scripts/program_sync.py +160 -0
  246. package/scripts/release-delta-guard.mjs +96 -0
  247. package/scripts/release-delta-guard.test.mjs +53 -0
  248. package/scripts/test_board_sync_create_idempotency.py +104 -0
  249. package/scripts/test_dist_kit_smoke.py +199 -0
  250. package/scripts/test_kit_docs_language_census.py +178 -0
  251. package/scripts/test_skill_code_review_seed.py +89 -0
  252. package/scripts/test_skill_frontmatter_lint.py +226 -0
  253. package/scripts/test_skill_gh_lint.py +124 -0
  254. package/scripts/test_skill_language_census.py +254 -0
  255. package/scripts/test_skill_portability_lint.py +773 -0
  256. package/scripts/test_skill_publish_audit.py +300 -0
  257. package/scripts/test_skill_selfcontainment_lint.py +255 -0
  258. package/scripts/test_skill_setup_workflow_seeds.py +200 -0
  259. package/scripts/test_skill_stale_name_lint.py +126 -0
  260. package/scripts/test_skill_surface_refs.py +300 -0
  261. package/scripts/test_skill_trailing_artifact_lint.py +71 -0
  262. package/scripts/wrapup-land.py +573 -0
  263. package/src/cli.mjs +70 -0
  264. package/src/commands/diff.mjs +6 -0
  265. package/src/commands/init.mjs +59 -0
  266. package/src/commands/uninstall.mjs +43 -0
  267. package/src/commands/update.mjs +95 -0
  268. package/src/lib/atomicWrite.mjs +49 -0
  269. package/src/lib/bundle.mjs +133 -0
  270. package/src/lib/hash.mjs +13 -0
  271. package/src/lib/manifest.mjs +58 -0
  272. package/src/lib/sentinel.mjs +20 -0
  273. package/src/lib/settings.mjs +23 -0
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """LoC-offender drive gate — PURE decision core.
3
+
4
+ The decision logic + structured Loc-Defer parsing. No I/O. The git/subprocess
5
+ adapter + CLI live in loc_offender_gate.py, which imports this module. Splitting
6
+ keeps each file <= the 300-line size gate this very slice enforces.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+
12
+ # Band thresholds (changed = added + deleted LoC of the offender file in the slice).
13
+ EXEMPT_LOC = 5 # <= this: trivial touch, exempt
14
+ SUBSTANTIAL_LOC = 30 # >= this: substantial → shrink-or-defer owed; 6..29 = warn
15
+
16
+ DEFER_REASONS = {"grossfile-partial", "risky-split", "vendored-adjacent", "hotfix"}
17
+
18
+
19
+ @dataclass(frozen=True)
20
+ class FileChange:
21
+ """One file's change in the slice diff (base..head)."""
22
+
23
+ path: str # current (post-rename) repo-root POSIX path
24
+ old_path: str | None # pre-rename path, or None if not renamed
25
+ changed_loc: int # added + deleted
26
+ head_lines: int | None # current line count; None if the file was deleted
27
+ base_lines: int # line count at the slice base
28
+ binary: bool = False # numstat reported '-'/'-' (unparseable)
29
+
30
+
31
+ @dataclass(frozen=True)
32
+ class Defer:
33
+ """A parsed, well-formed Loc-Defer trailer."""
34
+
35
+ file: str
36
+ reason: str
37
+ followup: str | None # e.g. ''
38
+ reduced_net: int | None
39
+
40
+
41
+ @dataclass
42
+ class Verdict:
43
+ ok: bool = True
44
+ reds: list[str] = field(default_factory=list)
45
+ warnings: list[str] = field(default_factory=list)
46
+
47
+ @property
48
+ def exit_code(self) -> int:
49
+ return 0 if self.ok else 1
50
+
51
+
52
+ def normalize_defer_path(raw: str):
53
+ """Return (path, error). repo-root POSIX path, leading './' stripped.
54
+ Absolute paths and parent traversal ('..') are rejected (defer must name a
55
+ repo-relative offender, never escape the tree)."""
56
+ p = (raw or "").strip().replace("\\", "/")
57
+ if not p:
58
+ return None, "leerer file-Pfad"
59
+ if p.startswith("/"):
60
+ return None, f"absoluter Pfad nicht erlaubt: {p}"
61
+ while p.startswith("./"):
62
+ p = p[2:]
63
+ if p == ".." or p.startswith("../") or "/../" in p or p.endswith("/.."):
64
+ return None, f"Pfad-Traversal ('..') nicht erlaubt: {p}"
65
+ return p, None
66
+
67
+
68
+ def _parse_one(value: str):
69
+ """Parse one `file=…; reason=…; followup=…; reduced_net=…` trailer value.
70
+ Return (Defer, error). Syntactic validity only (enum, required-field rule,
71
+ path); the reduced_net-vs-reality check is semantic → done in evaluate()."""
72
+ fields: dict[str, str] = {}
73
+ for part in value.split(";"):
74
+ part = part.strip()
75
+ if not part:
76
+ continue
77
+ if "=" not in part:
78
+ return None, f"malformter Trailer-Teil (kein key=value): {part!r}"
79
+ k, _, val = part.partition("=")
80
+ fields[k.strip()] = val.strip()
81
+
82
+ raw_file = fields.get("file")
83
+ if not raw_file:
84
+ return None, f"Loc-Defer ohne file=: {value!r}"
85
+ path, perr = normalize_defer_path(raw_file)
86
+ if perr:
87
+ return None, perr
88
+
89
+ reason = fields.get("reason")
90
+ if reason not in DEFER_REASONS:
91
+ return None, f"unbekannter reason={reason!r} (erlaubt: {sorted(DEFER_REASONS)})"
92
+
93
+ followup = fields.get("followup") or None
94
+ reduced_raw = fields.get("reduced_net")
95
+ reduced_net = None
96
+ if reduced_raw is not None:
97
+ if not reduced_raw.lstrip("#").isdigit() or int(reduced_raw.lstrip("#")) <= 0:
98
+ return None, f"reduced_net muss positive Ganzzahl sein, war {reduced_raw!r}"
99
+ reduced_net = int(reduced_raw)
100
+
101
+ if followup is None and reduced_net is None:
102
+ return None, f"Loc-Defer für {path} braucht followup=#n ODER reduced_net>0"
103
+
104
+ return Defer(file=path, reason=reason, followup=followup, reduced_net=reduced_net), None
105
+
106
+
107
+ def parse_loc_defer_trailers(values):
108
+ """Parse all `Loc-Defer:` trailer values from the slice commits.
109
+ Return (defers, errors). A duplicate `file=` (same offender twice) is an
110
+ error — ambiguous intent → fail-closed."""
111
+ defers: list[Defer] = []
112
+ errors: list[str] = []
113
+ seen: set[str] = set()
114
+ for value in values:
115
+ defer, err = _parse_one(value)
116
+ if err:
117
+ errors.append(err)
118
+ continue
119
+ if defer.file in seen:
120
+ errors.append(f"doppelter Loc-Defer für {defer.file} — mehrdeutig.")
121
+ continue
122
+ seen.add(defer.file)
123
+ defers.append(defer)
124
+ return defers, errors
125
+
126
+
127
+ def _defer_covers(defer: Defer, change: FileChange) -> tuple[bool, str | None]:
128
+ """Semantic check: does this (already syntactically-valid) defer actually cover
129
+ the change? Returns (ok, error). reduced_net may not over-declare the real
130
+ reduction; grossfile-partial needs reduced_net >= changed OR a followup."""
131
+ actual_reduction = (change.base_lines - change.head_lines) if change.head_lines is not None else change.base_lines
132
+ if defer.reduced_net is not None and defer.reduced_net > actual_reduction:
133
+ return False, (
134
+ f"{defer.file}: reduced_net={defer.reduced_net} über-deklariert "
135
+ f"(reale Reduktion {actual_reduction}).")
136
+ if defer.reason == "grossfile-partial":
137
+ meets_reduced = defer.reduced_net is not None and defer.reduced_net >= change.changed_loc
138
+ if not meets_reduced and not defer.followup:
139
+ return False, (
140
+ f"{defer.file}: grossfile-partial verlangt reduced_net >= "
141
+ f"{change.changed_loc} (changed) ODER followup.")
142
+ return True, None
143
+
144
+
145
+ def offender_key(change: FileChange) -> str:
146
+ """The baseline path a change is matched against: the OLD path on a rename
147
+ (the offender was listed under its pre-rename path), else the current path."""
148
+ return change.old_path or change.path
149
+
150
+
151
+ def evaluate(changes, offenders, defers, max_lines, defer_errors=None) -> Verdict:
152
+ """Pure decision over the slice diff. Red if any substantial offender touch
153
+ is neither shrunk nor validly deferred. A non-empty `defer_errors` (malformed/
154
+ duplicate trailers from parse_loc_defer_trailers) is a global fail-closed red."""
155
+ v = Verdict()
156
+ for err in defer_errors or []:
157
+ v.ok = False
158
+ v.reds.append(f"malformter Loc-Defer-Trailer → fail-closed: {err}")
159
+ by_key = {d.file: d for d in defers}
160
+ for ch in changes:
161
+ key = offender_key(ch)
162
+ if key not in offenders:
163
+ continue
164
+ # Binary/unparseable numstat is checked FIRST: collect_changes reports
165
+ # changed_loc=0 for a binary file, so a band check would exempt it before
166
+ # this defensive fail-closed could fire. (Should never hit for .ts/.tsx.)
167
+ if ch.binary:
168
+ v.ok = False
169
+ v.reds.append(f"{key}: binäres/unparsebares numstat für einen Offender → fail-closed.")
170
+ continue
171
+ if ch.changed_loc <= EXEMPT_LOC:
172
+ continue
173
+ if ch.changed_loc < SUBSTANTIAL_LOC:
174
+ v.warnings.append(
175
+ f"{key}: {ch.changed_loc} LoC geändert (6–{SUBSTANTIAL_LOC - 1} = "
176
+ "Warnung) — beim nächsten substanziellen Touch schrumpfen.")
177
+ continue
178
+ # Substantial touch (>= SUBSTANTIAL_LOC).
179
+ if ch.head_lines is None: # deleted = best possible shrink
180
+ continue
181
+ if ch.head_lines <= max_lines or ch.head_lines < ch.base_lines:
182
+ continue # under the limit, or net-shrunk → green (prune enforced by)
183
+ # Still > max_lines, no net shrink → needs a valid defer.
184
+ defer = by_key.get(key)
185
+ if defer is not None:
186
+ covers, derr = _defer_covers(defer, ch)
187
+ if covers:
188
+ continue
189
+ v.ok = False
190
+ v.reds.append(derr)
191
+ continue
192
+ v.ok = False
193
+ v.reds.append(
194
+ f"{key} ({ch.head_lines}>{max_lines}) ≥{SUBSTANTIAL_LOC} LoC angefasst, "
195
+ "nicht geschrumpft, kein Loc-Defer.")
196
+ return v
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env python3
2
+ """LoC-offender drive gate — git/subprocess adapter + CLI.
3
+
4
+ Thin shell over the pure core in loc_offender_core.py. Invoked by .githooks/pre-push
5
+ as `loc_offender_gate.py --check --head <local_sha> [--base-ref <ref>]`, once per
6
+ non-delete pushed branch ref. Exit 1 = red (push blocked). Pure logic + its tests
7
+ live in loc_offender_core.py.
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import argparse
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ from loc_offender_core import (
18
+ FileChange,
19
+ Verdict,
20
+ evaluate,
21
+ parse_loc_defer_trailers,
22
+ )
23
+
24
+ class GateError(RuntimeError):
25
+ """A condition the gate cannot evaluate → fail-closed (exit 1)."""
26
+
27
+
28
+ def _git(args, cwd=None) -> str:
29
+ r = subprocess.run(["git", *args], cwd=cwd, capture_output=True, text=True)
30
+ if r.returncode != 0:
31
+ raise GateError(f"git {' '.join(args)}: {r.stderr.strip()}")
32
+ return r.stdout
33
+
34
+
35
+ def _ref_exists(ref: str, cwd=None) -> bool:
36
+ return subprocess.run(["git", "rev-parse", "--verify", "--quiet", ref],
37
+ cwd=cwd, capture_output=True).returncode == 0
38
+
39
+
40
+ def resolve_base_ref(explicit, cwd=None) -> str:
41
+ """origin/HEAD → origin/main → fail-closed. An explicit --base-ref wins if valid."""
42
+ if explicit:
43
+ if _ref_exists(explicit, cwd):
44
+ return explicit
45
+ raise GateError(f"--base-ref {explicit!r} nicht auflösbar.")
46
+ head = subprocess.run(["git", "rev-parse", "--abbrev-ref", "origin/HEAD"],
47
+ cwd=cwd, capture_output=True, text=True)
48
+ if head.returncode == 0 and head.stdout.strip():
49
+ return head.stdout.strip()
50
+ if _ref_exists("origin/main", cwd):
51
+ return "origin/main"
52
+ raise GateError("keine base-ref auflösbar (origin/HEAD, origin/main fehlen) → fail-closed.")
53
+
54
+
55
+ def load_max_lines(path=None):
56
+ """Read maxLines + offenders set from max-lines-allowlist.json (the single SSOT).
57
+
58
+ Missing file / malformed JSON / missing keys → GateError, not a bare
59
+ exception — run_gate's `except GateError` already turns that into a normal
60
+ red gate message (with the SKIP_CI_GUARDS escape hatch) instead of a raw
61
+ traceback escaping the gate.
62
+ """
63
+ p = Path(path) if path else _repo_root() / "max-lines-allowlist.json"
64
+ try:
65
+ raw = json.loads(p.read_text(encoding="utf-8"))
66
+ return int(raw["maxLines"]), set(raw["offenders"])
67
+ except GateError:
68
+ raise
69
+ except Exception as exc:
70
+ raise GateError(f"max-lines-allowlist.json unlesbar/ungültig ({p}): {exc}") from exc
71
+
72
+
73
+ def _repo_root() -> Path:
74
+ return Path(_git(["rev-parse", "--show-toplevel"]).strip())
75
+
76
+
77
+ def _count_lines_at(rev: str, path: str, cwd=None):
78
+ """wc -l style count of <rev>:<path>; None if the path does not exist there."""
79
+ r = subprocess.run(["git", "show", f"{rev}:{path}"], cwd=cwd, capture_output=True, text=True)
80
+ if r.returncode != 0:
81
+ return None
82
+ body = r.stdout
83
+ if body == "":
84
+ return 0
85
+ return len(body[:-1].split("\n")) if body.endswith("\n") else len(body.split("\n"))
86
+
87
+
88
+ def parse_numstat_z(raw: str):
89
+ """Parse `git diff --numstat -z --find-renames` into (added, deleted, old_path, path).
90
+ -z record: `A\\tD\\tpath\\0` normally; `A\\tD\\t\\0from\\0to\\0` for renames;
91
+ `-` for added/deleted on binary files."""
92
+ toks = raw.split("\0")
93
+ i, out = 0, []
94
+ while i < len(toks):
95
+ field = toks[i]
96
+ if field == "":
97
+ i += 1
98
+ continue
99
+ added, deleted, inline = (field.split("\t") + ["", "", ""])[:3]
100
+ if inline == "": # rename → next two tokens are from, to
101
+ old_path, path = toks[i + 1], toks[i + 2]
102
+ i += 3
103
+ else:
104
+ old_path, path = None, inline
105
+ i += 1
106
+ out.append((added, deleted, old_path, path))
107
+ return out
108
+
109
+
110
+ def collect_changes(base: str, head: str, offenders, cwd=None):
111
+ """Build FileChange entries for diff files that touch an offender (matched by
112
+ old path on rename). Non-offender files are skipped (no git line-count calls)."""
113
+ raw = _git(["diff", "--numstat", "-z", "--find-renames", base, head], cwd=cwd)
114
+ changes = []
115
+ for added, deleted, old_path, path in parse_numstat_z(raw):
116
+ key = old_path or path
117
+ if key not in offenders:
118
+ continue
119
+ binary = added == "-" or deleted == "-"
120
+ changed = 0 if binary else int(added) + int(deleted)
121
+ base_lines = _count_lines_at(base, key, cwd) or 0
122
+ head_lines = _count_lines_at(head, path, cwd)
123
+ changes.append(FileChange(path=path, old_path=old_path, changed_loc=changed,
124
+ head_lines=head_lines, base_lines=base_lines, binary=binary))
125
+ return changes
126
+
127
+
128
+ def collect_defer_values(base: str, head: str, cwd=None):
129
+ """All `Loc-Defer:` trailer values across the slice commits (real trailer block,
130
+ via git's own parser — not a %B body grep)."""
131
+ out = _git(["log", f"{base}..{head}",
132
+ "--format=%(trailers:key=Loc-Defer,valueonly,unfold)"], cwd=cwd)
133
+ return [ln.strip() for ln in out.splitlines() if ln.strip()]
134
+
135
+
136
+ DEFAULT_MAX_LINES = 300 # only the fallback when the allowlist itself is unreadable
137
+
138
+
139
+ def run_gate(head: str, base_ref=None, cwd=None):
140
+ """Resolve base, collect the slice diff + defers, evaluate. GateError → red.
141
+ Returns (Verdict, max_lines) so the caller's repair message needs no re-read."""
142
+ max_lines = DEFAULT_MAX_LINES
143
+ try:
144
+ ref = resolve_base_ref(base_ref, cwd)
145
+ base = _git(["merge-base", ref, head], cwd=cwd).strip()
146
+ max_lines, offenders = load_max_lines()
147
+ changes = collect_changes(base, head, offenders, cwd)
148
+ defers, errors = parse_loc_defer_trailers(collect_defer_values(base, head, cwd))
149
+ return evaluate(changes, offenders, defers, max_lines, defer_errors=errors), max_lines
150
+ except GateError as exc:
151
+ v = Verdict(ok=False)
152
+ v.reds.append(f"{exc} → fail-closed (Notausgang: SKIP_CI_GUARDS=1 git push).")
153
+ return v, max_lines
154
+
155
+
156
+ _REPAIR = (
157
+ "→ schrumpfen (≤{max}) ODER Aufschub:\n"
158
+ ' git commit --amend --trailer "Loc-Defer: file=<pfad>; reason=grossfile-partial; followup=#NNN"\n'
159
+ "→ Notausgang: SKIP_CI_GUARDS=1 git push")
160
+
161
+
162
+ def main(argv=None) -> int:
163
+ ap = argparse.ArgumentParser(prog="loc_offender_gate.py", description=__doc__.splitlines()[0])
164
+ ap.add_argument("--check", action="store_true", help="run the gate (exit 1 = red)")
165
+ ap.add_argument("--head", required=True, help="pushed commit SHA (the slice head)")
166
+ ap.add_argument("--base-ref", dest="base_ref", help="integration base ref (default: origin/HEAD→origin/main)")
167
+ args = ap.parse_args(argv)
168
+
169
+ v, max_lines = run_gate(args.head, args.base_ref)
170
+ for w in v.warnings:
171
+ print(f"⚠ LoC-Offender: {w}")
172
+ if v.ok:
173
+ return 0
174
+ print("🔴 LoC-Offender-Gate — Push blockiert:")
175
+ for r in v.reds:
176
+ print(f" {r}")
177
+ print(_REPAIR.format(max=max_lines))
178
+ return 1
179
+
180
+
181
+ if __name__ == "__main__":
182
+ sys.exit(main())
@@ -0,0 +1,68 @@
1
+ #!/usr/bin/env python3
2
+ """node_kind.py — pure node-kind classifier shared by execute-ready-check.py
3
+ .
4
+
5
+ execute-ready-check.py used to infer "is this an anchor?" structurally (no
6
+ native parent + has children). That heuristic breaks the moment an anchor gets
7
+ a NATIVE parent of its own — a Welle-Anker promoted under a Programm-PRD, or
8
+ the Programm-PRD itself: the checker would climb to the parent and audit the
9
+ anchor as if it were a plain leaf, forcing a bucket + rev-match on itself and
10
+ on its sibling anchors (which don't carry a bucket by design).
11
+
12
+ This module replaces that heuristic with an EXPLICIT classification, read off
13
+ the node's own labels/body — never off its position in the graph:
14
+
15
+ program — `<!-- prd: program -->` body marker. Highest precedence (a
16
+ distinct object, wins over any label a Programm-PRD might also
17
+ carry). Deliberately marker-only — the `type:program` label is a
18
+ LATER slice's addition; this module needs no profile key
19
+ for it and must keep working before that label exists.
20
+ anchor — `type:cluster` label. Wins over `wave-stub` when a node somehow
21
+ carries both (a drift/transition state) — mirrors the
22
+ to-prd/to-issues precedence ("type:cluster stops always; Wave
23
+ stops unless wave-stub").
24
+ wave_stub — `wave-stub` label (a board-to-waves candidate stub, OR an
25
+ un-promoted Stufe-1p program stub — same label, two
26
+ provenances, both structurally "not yet an anchor").
27
+ leaf — none of the above; an ordinary slice/child issue.
28
+
29
+ ROOT_KINDS = {program, anchor}: execute-ready-check.py evaluates these at
30
+ their OWN local root (their own children as siblings) and never lifts them to
31
+ a real parent, even when one exists. The multi-level, programme-wide graph
32
+ validation is validate-graph's job — this stays
33
+ rooted-local (target + at most one level of children).
34
+
35
+ Pure + I/O-free by design (mirrors anchor_table.py) — directly unit-tested
36
+ with plain dicts, no gh/board_config coupling.
37
+ """
38
+ from __future__ import annotations
39
+
40
+ import re
41
+
42
+ PROGRAM = "program"
43
+ ANCHOR = "anchor"
44
+ WAVE_STUB = "wave_stub"
45
+ LEAF = "leaf"
46
+
47
+ ROOT_KINDS = frozenset({PROGRAM, ANCHOR})
48
+
49
+ PROGRAM_MARKER_RE = re.compile(r"<!--\s*prd:\s*program\s*-->")
50
+
51
+
52
+ def classify_node(node: dict, *, cluster_type_label: str | None,
53
+ wave_stub_label: str | None) -> str:
54
+ """program | anchor | wave_stub | leaf for `node` ({body, labels, ...}).
55
+
56
+ `cluster_type_label`/`wave_stub_label` come from the board profile
57
+ (`_CFG["labels"].get(...)`) — passed in rather than read here so this
58
+ module stays board-profile-free and directly testable.
59
+ """
60
+ body = node.get("body") or ""
61
+ labels = node.get("labels") or []
62
+ if PROGRAM_MARKER_RE.search(body):
63
+ return PROGRAM
64
+ if cluster_type_label and cluster_type_label in labels:
65
+ return ANCHOR
66
+ if wave_stub_label and wave_stub_label in labels:
67
+ return WAVE_STUB
68
+ return LEAF
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """portability_profile_scan.py — profile-VALUE literal scan (#1878, Welle 49
3
+ Slice 7, extends the portability lint in test_skill_portability_lint.py).
4
+
5
+ A published (generic/vendored) skill body must reference the board profile
6
+ (`docs/agents/board-sync.md`, loaded by `board_config.py`) by KEY —
7
+ `prMarkers.retroValues`, `headings.vorBau` — never MANDATE the literal VALUE
8
+ a profile happens to configure (e.g. testreporter's `gefahren`/`übersprungen`,
9
+ `Vor Bau zu klären`). A consumer whose profile configures different words
10
+ silently fails a skill that hardcoded testreporter's wording as required —
11
+ the same drift class `test_skill_portability_lint.py`'s CONSTANT_PATTERNS
12
+ scan already closes for opaque board node/option ids; this closes it for the
13
+ human-facing convention words instead.
14
+
15
+ A line that also names the profile key is a reference, not a hardcode —
16
+ exempt (mirrors CONSTANT_PATTERNS' own exempt rule). A line carrying
17
+ `portability-lint: ok` is exempt too (deliberate doc example, e.g. a
18
+ copy-paste-ready marker/heading example where inlining the key would corrupt
19
+ the example).
20
+ """
21
+ from __future__ import annotations
22
+
23
+ import re
24
+
25
+ EXEMPT = "portability-lint: ok"
26
+
27
+ # (label, literal pattern, profile-key substring whose presence on the same
28
+ # line exempts the match — a reference, not a hardcode).
29
+ PROFILE_VALUE_PATTERNS = [
30
+ # prMarkers.retroValues — the two configured PR "**Retro:**"-line words.
31
+ # Backtick-quoted only: bare "gefahren"/"übersprungen" are ordinary German
32
+ # words (past participles of fahren/überspringen) outside this
33
+ # convention — unquoted would false-positive on normal prose (e.g. "Retro
34
+ # schon gefahren?").
35
+ ("prMarkers.retroValues literal", re.compile(r"`(?:gefahren|übersprungen)`"), "retroValues"),
36
+ # headings.vorBau — the configured HITL heading text. The phrase is
37
+ # specific enough that no quoting requirement is needed.
38
+ ("headings.vorBau literal", re.compile(r"Vor Bau zu klären"), "vorBau"),
39
+ ]
40
+
41
+
42
+ def find_profile_value_literals(text: str) -> list[tuple[int, str, str]]:
43
+ """Return (line_no, label, line) for every hardcoded MANDATORY profile-value
44
+ literal, minus lines that also reference the profile key (a profile-driven
45
+ reference, not a hardcode) or carry the exempt marker."""
46
+ out = []
47
+ for n, line in enumerate(text.splitlines(), 1):
48
+ if EXEMPT in line:
49
+ continue
50
+ for label, pattern, key in PROFILE_VALUE_PATTERNS:
51
+ if pattern.search(line) and key not in line:
52
+ out.append((n, label, line.strip()))
53
+ return out