@renoxar/koolie 1.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.
Files changed (693) hide show
  1. package/.gitattributes +16 -0
  2. package/.gitignore +51 -0
  3. package/.koolie/QUELLREPOSITORIUM.md +18 -0
  4. package/.koolie/core/CHANGELOG.md +11832 -0
  5. package/.koolie/core/LICENSE +674 -0
  6. package/.koolie/core/LICENSE-HINWEIS.md +98 -0
  7. package/.koolie/core/OWNERS.md +36 -0
  8. package/.koolie/core/VERSION +1 -0
  9. package/.koolie/core/banner.py +397 -0
  10. package/.koolie/core/build/README.md +34 -0
  11. package/.koolie/core/build/assemble.py +354 -0
  12. package/.koolie/core/build/build-docx.py +194 -0
  13. package/.koolie/core/build/doc/00-kopf.md +18 -0
  14. package/.koolie/core/build/doc/01-executive-summary.md +24 -0
  15. package/.koolie/core/build/doc/02-zielbild.md +13 -0
  16. package/.koolie/core/build/doc/03-ziele-nichtziele.md +28 -0
  17. package/.koolie/core/build/doc/04-geltungsbereich.md +25 -0
  18. package/.koolie/core/build/doc/05-glossar.md +47 -0
  19. package/.koolie/core/build/doc/06-leitprinzipien.md +5 -0
  20. package/.koolie/core/build/doc/07-architektur.md +60 -0
  21. package/.koolie/core/build/doc/07a-abbildungsschicht.md +66 -0
  22. package/.koolie/core/build/doc/08-trennung.md +24 -0
  23. package/.koolie/core/build/doc/09-betriebsmodi.md +18 -0
  24. package/.koolie/core/build/doc/10-arbeitsablauf.md +5 -0
  25. package/.koolie/core/build/doc/11-datenschutz.md +5 -0
  26. package/.koolie/core/build/doc/12-sicherheit.md +5 -0
  27. package/.koolie/core/build/doc/13-risiko.md +5 -0
  28. package/.koolie/core/build/doc/14-rollen.md +9 -0
  29. package/.koolie/core/build/doc/15-referenzstruktur.md +124 -0
  30. package/.koolie/core/build/doc/16-agentenanweisung.md +8 -0
  31. package/.koolie/core/build/doc/17-overlay.md +29 -0
  32. package/.koolie/core/build/doc/18-skill-standard.md +5 -0
  33. package/.koolie/core/build/doc/19-skill-template.md +5 -0
  34. package/.koolie/core/build/doc/20-referenz-skills.md +70 -0
  35. package/.koolie/core/build/doc/21-prompts.md +53 -0
  36. package/.koolie/core/build/doc/22-checklisten.md +49 -0
  37. package/.koolie/core/build/doc/23-entscheidungsbaeume.md +29 -0
  38. package/.koolie/core/build/doc/24-onboarding.md +33 -0
  39. package/.koolie/core/build/doc/25-governance.md +35 -0
  40. package/.koolie/core/build/doc/26-qs-test.md +9 -0
  41. package/.koolie/core/build/doc/27-pilot.md +13 -0
  42. package/.koolie/core/build/doc/28-uebernahme.md +5 -0
  43. package/.koolie/core/build/doc/29-grenzen.md +87 -0
  44. package/.koolie/core/build/doc/30-roadmap.md +5 -0
  45. package/.koolie/core/build/doc/31-anhaenge.md +151 -0
  46. package/.koolie/core/build/doc/32-abschluss.md +92 -0
  47. package/.koolie/core/build/ref-a4.docx +0 -0
  48. package/.koolie/core/checklists/01-preflight.md +54 -0
  49. package/.koolie/core/checklists/02-privacy-context.md +51 -0
  50. package/.koolie/core/checklists/03-before-code-change.md +51 -0
  51. package/.koolie/core/checklists/04-review-ai-code.md +58 -0
  52. package/.koolie/core/checklists/05-testing.md +48 -0
  53. package/.koolie/core/checklists/06-security.md +50 -0
  54. package/.koolie/core/checklists/07-new-dependency.md +57 -0
  55. package/.koolie/core/checklists/08-merge-request.md +47 -0
  56. package/.koolie/core/checklists/09-onboarding.md +52 -0
  57. package/.koolie/core/checklists/10-project-adoption.md +74 -0
  58. package/.koolie/core/checklists/11-framework-release.md +71 -0
  59. package/.koolie/core/checklists/README.md +28 -0
  60. package/.koolie/core/clientmap.py +1358 -0
  61. package/.koolie/core/clients/README.md +113 -0
  62. package/.koolie/core/clients/_template/CLIENT_PACK.md +197 -0
  63. package/.koolie/core/clients/claude-code/CLIENT_PACK.md +383 -0
  64. package/.koolie/core/clients/claude-code/manifest.json +232 -0
  65. package/.koolie/core/clients/claude-code/root-template/.claude/README.md +127 -0
  66. package/.koolie/core/clients/cursor/CLIENT_PACK.md +229 -0
  67. package/.koolie/core/clients/cursor/manifest.json +330 -0
  68. package/.koolie/core/clients/cursor/root-template/.cursor/README.md +36 -0
  69. package/.koolie/core/clients/devin-desktop/CLIENT_PACK.md +251 -0
  70. package/.koolie/core/clients/devin-desktop/manifest.json +163 -0
  71. package/.koolie/core/clients/devin-desktop/root-template/.devin/README.md +79 -0
  72. package/.koolie/core/clients/kiro/CLIENT_PACK.md +223 -0
  73. package/.koolie/core/clients/kiro/manifest.json +300 -0
  74. package/.koolie/core/clients/kiro/root-template/.kiro/README.md +37 -0
  75. package/.koolie/core/clients/openai-codex/CLIENT_PACK.md +257 -0
  76. package/.koolie/core/clients/openai-codex/manifest.json +208 -0
  77. package/.koolie/core/clients/openai-codex/root-template/.codex/README.md +35 -0
  78. package/.koolie/core/decision-trees/01-context-allowed.md +46 -0
  79. package/.koolie/core/decision-trees/02-may-ai-do-task.md +47 -0
  80. package/.koolie/core/decision-trees/03-analyze-or-modify.md +50 -0
  81. package/.koolie/core/decision-trees/04-required-review.md +49 -0
  82. package/.koolie/core/decision-trees/05-stop-or-escalate.md +47 -0
  83. package/.koolie/core/decision-trees/06-rule-placement.md +58 -0
  84. package/.koolie/core/decision-trees/README.md +14 -0
  85. package/.koolie/core/docs/ADOPTION_GUIDE.md +582 -0
  86. package/.koolie/core/docs/DOCUMENTATION_STANDARD.md +91 -0
  87. package/.koolie/core/docs/PLACEHOLDER_REGISTRY.md +79 -0
  88. package/.koolie/core/docs/ROADMAP.md +848 -0
  89. package/.koolie/core/docs/RUNTIME_GLOSSARY.md +113 -0
  90. package/.koolie/core/examples/README.md +11 -0
  91. package/.koolie/core/examples/example-ergebnisbericht.md +31 -0
  92. package/.koolie/core/examples/example-mr-description.md +31 -0
  93. package/.koolie/core/examples/example-overlay-runtime.md +48 -0
  94. package/.koolie/core/framework/core/00-principles.md +93 -0
  95. package/.koolie/core/framework/core/01-governance.md +58 -0
  96. package/.koolie/core/framework/core/02-privacy.md +141 -0
  97. package/.koolie/core/framework/core/03-security.md +86 -0
  98. package/.koolie/core/framework/core/04-quality.md +48 -0
  99. package/.koolie/core/framework/core/05-working-model.md +199 -0
  100. package/.koolie/core/framework/core/06-prompting-rules.md +51 -0
  101. package/.koolie/core/framework/core/07-review-rules.md +52 -0
  102. package/.koolie/core/framework/core/08-skill-conventions.md +115 -0
  103. package/.koolie/core/framework/core/09-risk-model.md +87 -0
  104. package/.koolie/core/framework/core/10-error-escalation.md +53 -0
  105. package/.koolie/core/framework/org-policies/MAPPING_CLASSIFICATION.md +16 -0
  106. package/.koolie/core/framework/org-policies/README.md +29 -0
  107. package/.koolie/core/framework/overlay-patterns/general/documents/branching-strategy/muster-general.md +43 -0
  108. package/.koolie/core/framework/overlay-patterns/general/documents/coding-guidelines/muster-general.md +82 -0
  109. package/.koolie/core/framework/overlay-patterns/general/documents/definition-of-done/muster-general.md +52 -0
  110. package/.koolie/core/framework/overlay-patterns/general/documents/definition-of-ready/muster-general.md +54 -0
  111. package/.koolie/core/framework/overlay-patterns/general/documents/quality/muster-general.md +59 -0
  112. package/.koolie/core/framework/overlay-patterns/general/documents/security/muster-general.md +52 -0
  113. package/.koolie/core/framework/overlay-patterns/general.md +150 -0
  114. package/.koolie/core/framework/role-packs/README.md +63 -0
  115. package/.koolie/core/framework/role-packs/_template/ROLE_PACK.md +58 -0
  116. package/.koolie/core/framework/role-packs/requirements-engineering/ROLE_PACK.md +158 -0
  117. package/.koolie/core/framework/role-packs/requirements-engineering/runtime/30-role-requirements-engineering.md +88 -0
  118. package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/CHANGELOG.md +10 -0
  119. package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/EXAMPLES.md +148 -0
  120. package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/SKILL.md +195 -0
  121. package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/TESTS.md +22 -0
  122. package/.koolie/core/framework/role-packs/software-development/ROLE_PACK.md +95 -0
  123. package/.koolie/core/framework/role-packs/software-development/runtime/30-role-software-development.md +26 -0
  124. package/.koolie/core/framework/runtime/agents/fw-reviewer.md +34 -0
  125. package/.koolie/core/framework/runtime/client-settings.json +3 -0
  126. package/.koolie/core/framework/runtime/hooks.json +20 -0
  127. package/.koolie/core/framework/runtime/mcp-config.example.json +4 -0
  128. package/.koolie/core/framework/runtime/permissions.json +106 -0
  129. package/.koolie/core/framework/runtime/root-instruction-local.example.md +19 -0
  130. package/.koolie/core/framework/runtime/root-instruction.md +116 -0
  131. package/.koolie/core/framework/runtime/rules/00-framework-core.md +49 -0
  132. package/.koolie/core/framework/runtime/rules/10-privacy-security.md +49 -0
  133. package/.koolie/core/framework/runtime/rules/15-development-rules.md +41 -0
  134. package/.koolie/core/framework/runtime/rules/16-plan-spezifikation.md +32 -0
  135. package/.koolie/core/framework/runtime/rules/20-project-overlay.md +60 -0
  136. package/.koolie/core/framework/skills/fw-bugfix-prepare/CHANGELOG.md +14 -0
  137. package/.koolie/core/framework/skills/fw-bugfix-prepare/EXAMPLES.md +57 -0
  138. package/.koolie/core/framework/skills/fw-bugfix-prepare/SKILL.md +177 -0
  139. package/.koolie/core/framework/skills/fw-bugfix-prepare/TESTS.md +15 -0
  140. package/.koolie/core/framework/skills/fw-change-analyze/CHANGELOG.md +12 -0
  141. package/.koolie/core/framework/skills/fw-change-analyze/EXAMPLES.md +66 -0
  142. package/.koolie/core/framework/skills/fw-change-analyze/SKILL.md +167 -0
  143. package/.koolie/core/framework/skills/fw-change-analyze/TESTS.md +16 -0
  144. package/.koolie/core/framework/skills/fw-change-small/CHANGELOG.md +13 -0
  145. package/.koolie/core/framework/skills/fw-change-small/EXAMPLES.md +68 -0
  146. package/.koolie/core/framework/skills/fw-change-small/SKILL.md +175 -0
  147. package/.koolie/core/framework/skills/fw-change-small/TESTS.md +13 -0
  148. package/.koolie/core/framework/skills/fw-code-explain/CHANGELOG.md +11 -0
  149. package/.koolie/core/framework/skills/fw-code-explain/EXAMPLES.md +61 -0
  150. package/.koolie/core/framework/skills/fw-code-explain/SKILL.md +163 -0
  151. package/.koolie/core/framework/skills/fw-code-explain/TESTS.md +11 -0
  152. package/.koolie/core/framework/skills/fw-docs-update/CHANGELOG.md +10 -0
  153. package/.koolie/core/framework/skills/fw-docs-update/EXAMPLES.md +59 -0
  154. package/.koolie/core/framework/skills/fw-docs-update/SKILL.md +160 -0
  155. package/.koolie/core/framework/skills/fw-docs-update/TESTS.md +12 -0
  156. package/.koolie/core/framework/skills/fw-error-analyze/CHANGELOG.md +10 -0
  157. package/.koolie/core/framework/skills/fw-error-analyze/EXAMPLES.md +67 -0
  158. package/.koolie/core/framework/skills/fw-error-analyze/SKILL.md +160 -0
  159. package/.koolie/core/framework/skills/fw-error-analyze/TESTS.md +12 -0
  160. package/.koolie/core/framework/skills/fw-mr-description/CHANGELOG.md +12 -0
  161. package/.koolie/core/framework/skills/fw-mr-description/EXAMPLES.md +69 -0
  162. package/.koolie/core/framework/skills/fw-mr-description/SKILL.md +170 -0
  163. package/.koolie/core/framework/skills/fw-mr-description/TESTS.md +12 -0
  164. package/.koolie/core/framework/skills/fw-overlay-pflege/CHANGELOG.md +7 -0
  165. package/.koolie/core/framework/skills/fw-overlay-pflege/EXAMPLES.md +48 -0
  166. package/.koolie/core/framework/skills/fw-overlay-pflege/SKILL.md +147 -0
  167. package/.koolie/core/framework/skills/fw-overlay-pflege/TESTS.md +13 -0
  168. package/.koolie/core/framework/skills/fw-plan/CHANGELOG.md +14 -0
  169. package/.koolie/core/framework/skills/fw-plan/EXAMPLES.md +69 -0
  170. package/.koolie/core/framework/skills/fw-plan/SKILL.md +168 -0
  171. package/.koolie/core/framework/skills/fw-plan/TESTS.md +14 -0
  172. package/.koolie/core/framework/skills/fw-refactor/CHANGELOG.md +10 -0
  173. package/.koolie/core/framework/skills/fw-refactor/EXAMPLES.md +70 -0
  174. package/.koolie/core/framework/skills/fw-refactor/SKILL.md +173 -0
  175. package/.koolie/core/framework/skills/fw-refactor/TESTS.md +14 -0
  176. package/.koolie/core/framework/skills/fw-repo-analyze/CHANGELOG.md +10 -0
  177. package/.koolie/core/framework/skills/fw-repo-analyze/EXAMPLES.md +52 -0
  178. package/.koolie/core/framework/skills/fw-repo-analyze/SKILL.md +149 -0
  179. package/.koolie/core/framework/skills/fw-repo-analyze/TESTS.md +11 -0
  180. package/.koolie/core/framework/skills/fw-review-support/CHANGELOG.md +12 -0
  181. package/.koolie/core/framework/skills/fw-review-support/EXAMPLES.md +69 -0
  182. package/.koolie/core/framework/skills/fw-review-support/SKILL.md +179 -0
  183. package/.koolie/core/framework/skills/fw-review-support/TESTS.md +13 -0
  184. package/.koolie/core/framework/skills/fw-tests/CHANGELOG.md +9 -0
  185. package/.koolie/core/framework/skills/fw-tests/EXAMPLES.md +65 -0
  186. package/.koolie/core/framework/skills/fw-tests/SKILL.md +169 -0
  187. package/.koolie/core/framework/skills/fw-tests/TESTS.md +12 -0
  188. package/.koolie/core/framework/tech-packs/README.md +15 -0
  189. package/.koolie/core/framework/tech-packs/_template/TECH_PACK.md +61 -0
  190. package/.koolie/core/governance/ADOPTION_REGISTRY.md +89 -0
  191. package/.koolie/core/governance/CHANGE_REQUEST_TEMPLATE.md +60 -0
  192. package/.koolie/core/governance/DECISION_LOG.md +779 -0
  193. package/.koolie/core/governance/EXCEPTION_PROCESS.md +31 -0
  194. package/.koolie/core/governance/FEEDBACK_PROCESS.md +34 -0
  195. package/.koolie/core/governance/FRAMEWORK_DEV_PROFILE.md +135 -0
  196. package/.koolie/core/governance/INCIDENT_HANDLING.md +47 -0
  197. package/.koolie/core/governance/PRIORITY_HIERARCHY.md +61 -0
  198. package/.koolie/core/governance/RACI.md +37 -0
  199. package/.koolie/core/governance/RELEASE_PROCESS.md +183 -0
  200. package/.koolie/core/governance/change-requests/CR-2026-001-definition-release-1-0-0.md +67 -0
  201. package/.koolie/core/governance/change-requests/CR-2026-002-client-packs.md +63 -0
  202. package/.koolie/core/governance/change-requests/CR-2026-003-install-client-schalter.md +70 -0
  203. package/.koolie/core/governance/change-requests/CR-2026-004-client-pack-claude-code.md +82 -0
  204. package/.koolie/core/governance/change-requests/CR-2026-005-kern-neutralisierung.md +75 -0
  205. package/.koolie/core/governance/change-requests/CR-2026-006-gemeinsame-skill-quelle.md +77 -0
  206. package/.koolie/core/governance/change-requests/CR-2026-007-geteilte-kernbestandteile.md +80 -0
  207. package/.koolie/core/governance/change-requests/CR-2026-008-berechtigungen-und-hooks.md +85 -0
  208. package/.koolie/core/governance/change-requests/CR-2026-009-umbenennung-leitwerk.md +77 -0
  209. package/.koolie/core/governance/change-requests/CR-2026-010-restduplikation.md +73 -0
  210. package/.koolie/core/governance/change-requests/CR-2026-011-hauptdokument.md +72 -0
  211. package/.koolie/core/governance/change-requests/CR-2026-012-schreibschutz-kernverzeichnis.md +106 -0
  212. package/.koolie/core/governance/change-requests/CR-2026-013-validator-blindstellen.md +128 -0
  213. package/.koolie/core/governance/change-requests/CR-2026-014-kurzform-langform.md +125 -0
  214. package/.koolie/core/governance/change-requests/CR-2026-015-versionskette.md +150 -0
  215. package/.koolie/core/governance/change-requests/CR-2026-016-ap2-claude-code.md +127 -0
  216. package/.koolie/core/governance/change-requests/CR-2026-017-ladebedingungen-claude-code.md +173 -0
  217. package/.koolie/core/governance/change-requests/CR-2026-018-quellenliste-claude-code.md +121 -0
  218. package/.koolie/core/governance/change-requests/CR-2026-019-strukturentscheidungen-fortschreiben.md +143 -0
  219. package/.koolie/core/governance/change-requests/CR-2026-020-akteursbezeichnung.md +118 -0
  220. package/.koolie/core/governance/change-requests/CR-2026-021-hook-interpreter.md +113 -0
  221. package/.koolie/core/governance/change-requests/CR-2026-022-artefaktversionen.md +73 -0
  222. package/.koolie/core/governance/change-requests/CR-2026-023-shell-lesesperre.md +128 -0
  223. package/.koolie/core/governance/change-requests/CR-2026-024-clientbindungen.md +78 -0
  224. package/.koolie/core/governance/change-requests/CR-2026-025-betriebsmodi.md +110 -0
  225. package/.koolie/core/governance/change-requests/CR-2026-026-fail-closed.md +134 -0
  226. package/.koolie/core/governance/change-requests/CR-2026-027-zeichenlimit-einstufung.md +105 -0
  227. package/.koolie/core/governance/change-requests/CR-2026-028-berechtigungsmodi.md +130 -0
  228. package/.koolie/core/governance/change-requests/CR-2026-029-hook-ablageort.md +115 -0
  229. package/.koolie/core/governance/change-requests/CR-2026-030-lesende-werkzeuge.md +143 -0
  230. package/.koolie/core/governance/change-requests/CR-2026-031-regelquellen-ausserhalb.md +182 -0
  231. package/.koolie/core/governance/change-requests/CR-2026-032-fremde-skill-ablage.md +151 -0
  232. package/.koolie/core/governance/change-requests/CR-2026-033-modusabhaengige-durchsetzung.md +131 -0
  233. package/.koolie/core/governance/change-requests/CR-2026-034-nachweisbedingungen.md +116 -0
  234. package/.koolie/core/governance/change-requests/CR-2026-035-readme-in-der-regelablage.md +115 -0
  235. package/.koolie/core/governance/change-requests/CR-2026-036-ortsangaben-hook-konfiguration.md +108 -0
  236. package/.koolie/core/governance/change-requests/CR-2026-037-meldender-hook-clientgebunden.md +133 -0
  237. package/.koolie/core/governance/change-requests/CR-2026-038-importsteuerung.md +159 -0
  238. package/.koolie/core/governance/change-requests/CR-2026-039-normative-saetze-in-kommentaren.md +124 -0
  239. package/.koolie/core/governance/change-requests/CR-2026-040-erh01-ist-clientgebunden.md +115 -0
  240. package/.koolie/core/governance/change-requests/CR-2026-041-s5-nicht-abbildbar.md +128 -0
  241. package/.koolie/core/governance/change-requests/CR-2026-042-abwesenheitsnachweis-suchwerkzeug.md +102 -0
  242. package/.koolie/core/governance/change-requests/CR-2026-043-validator-gibt-trefferwerte-aus.md +135 -0
  243. package/.koolie/core/governance/change-requests/CR-2026-044-aktivierungspruefung-clientgebunden.md +112 -0
  244. package/.koolie/core/governance/change-requests/CR-2026-045-update-verliert-die-clientwahl.md +116 -0
  245. package/.koolie/core/governance/change-requests/CR-2026-046-erstinstallation-ueberschreibt-projektdatei.md +125 -0
  246. package/.koolie/core/governance/change-requests/CR-2026-047-zusagen-je-zugriffskanal.md +133 -0
  247. package/.koolie/core/governance/change-requests/CR-2026-048-modusgrenze-ohne-durchsetzung.md +137 -0
  248. package/.koolie/core/governance/change-requests/CR-2026-049-sondenlauf-kodierungsabhaengig.md +126 -0
  249. package/.koolie/core/governance/change-requests/CR-2026-050-allowed-tools-und-verworfene-permissions.md +110 -0
  250. package/.koolie/core/governance/change-requests/CR-2026-051-quellenkarte-und-ueberholter-stand.md +110 -0
  251. package/.koolie/core/governance/change-requests/CR-2026-052-drei-regelkonflikte.md +166 -0
  252. package/.koolie/core/governance/change-requests/CR-2026-053-lesesperre-und-entwicklungsprofil.md +144 -0
  253. package/.koolie/core/governance/change-requests/CR-2026-054-aktivierung-verlangt-aktivitaet.md +124 -0
  254. package/.koolie/core/governance/change-requests/CR-2026-055-domain-ausnahme-ohne-mechanismus.md +173 -0
  255. package/.koolie/core/governance/change-requests/CR-2026-056-eingabeschema-und-pfadidentitaet.md +245 -0
  256. package/.koolie/core/governance/change-requests/CR-2026-057-disallowed-tools-und-s3.md +148 -0
  257. package/.koolie/core/governance/change-requests/CR-2026-058-unteragent-reichweite.md +203 -0
  258. package/.koolie/core/governance/change-requests/CR-2026-059-unteragent-tiefe-und-widerspruch.md +125 -0
  259. package/.koolie/core/governance/change-requests/CR-2026-060-stumme-brueche.md +170 -0
  260. package/.koolie/core/governance/change-requests/CR-2026-061-berechtigungsdatei-ohne-pruefung.md +169 -0
  261. package/.koolie/core/governance/change-requests/CR-2026-062-werkzeugabbildung-des-frontmatters.md +178 -0
  262. package/.koolie/core/governance/change-requests/CR-2026-063-skillaufruf-ohne-freigabe.md +207 -0
  263. package/.koolie/core/governance/change-requests/CR-2026-064-pruefregister-nicht-nachgezaehlt.md +141 -0
  264. package/.koolie/core/governance/change-requests/CR-2026-065-devin-suchwerkzeuge-erhoben.md +170 -0
  265. package/.koolie/core/governance/change-requests/CR-2026-066-schlitzdeckung-ohne-abgleich.md +153 -0
  266. package/.koolie/core/governance/change-requests/CR-2026-067-hookblock-und-praeparationsregister.md +140 -0
  267. package/.koolie/core/governance/change-requests/CR-2026-068-sondenlauf-nebenlaeufig.md +106 -0
  268. package/.koolie/core/governance/change-requests/CR-2026-069-bytecode-des-kerns.md +108 -0
  269. package/.koolie/core/governance/change-requests/CR-2026-070-d11-zaehler.md +114 -0
  270. package/.koolie/core/governance/change-requests/CR-2026-071-strukturentscheidungen-bestaetigen.md +322 -0
  271. package/.koolie/core/governance/change-requests/CR-2026-072-modulstatus-heben.md +315 -0
  272. package/.koolie/core/governance/change-requests/CR-2026-073-nicht-skill-traeger-heben.md +246 -0
  273. package/.koolie/core/governance/change-requests/CR-2026-074-restliche-nicht-skill-traeger-heben.md +271 -0
  274. package/.koolie/core/governance/change-requests/CR-2026-075-ap2-zielversion-devin-desktop.md +335 -0
  275. package/.koolie/core/governance/change-requests/CR-2026-076-erster-sitzungstest.md +455 -0
  276. package/.koolie/core/governance/change-requests/CR-2026-077-sitzungstest-schranken.md +425 -0
  277. package/.koolie/core/governance/change-requests/CR-2026-078-planung-nach-1-0-0.md +294 -0
  278. package/.koolie/core/governance/change-requests/CR-2026-079-umbenennung-vorziehen.md +233 -0
  279. package/.koolie/core/governance/change-requests/CR-2026-080-clientbindung-des-kerns.md +321 -0
  280. package/.koolie/core/governance/change-requests/CR-2026-081-produktnamen-im-kern.md +297 -0
  281. package/.koolie/core/governance/change-requests/CR-2026-082-sitzungstest-pi-ds-2.md +145 -0
  282. package/.koolie/core/governance/change-requests/CR-2026-083-sitzungstest-ne-sc.md +300 -0
  283. package/.koolie/core/governance/change-requests/CR-2026-084-trockenlauf-gegen-den-committeten-stand.md +117 -0
  284. package/.koolie/core/governance/change-requests/CR-2026-085-vorbedingungen-sitzungstest-5.md +182 -0
  285. package/.koolie/core/governance/change-requests/CR-2026-086-grenzfaelle-gegen-die-fassungen.md +164 -0
  286. package/.koolie/core/governance/change-requests/CR-2026-087-produktbeobachtung-quellenliste.md +206 -0
  287. package/.koolie/core/governance/change-requests/CR-2026-088-vorbedingungen-testblaetter.md +127 -0
  288. package/.koolie/core/governance/change-requests/CR-2026-089-herrichtung-uebungsrepositorium.md +226 -0
  289. package/.koolie/core/governance/change-requests/CR-2026-090-vorbedingungen-sitzungstest-5-zweiter-durchgang.md +255 -0
  290. package/.koolie/core/governance/change-requests/CR-2026-091-sitzungstest-5.md +163 -0
  291. package/.koolie/core/governance/change-requests/CR-2026-092-pruefmittelwort-und-buendelschnitt.md +226 -0
  292. package/.koolie/core/governance/change-requests/CR-2026-093-sechzehnte-praeparation.md +151 -0
  293. package/.koolie/core/governance/change-requests/CR-2026-094-testblaetter-buendel-1.md +113 -0
  294. package/.koolie/core/governance/change-requests/CR-2026-095-pruefapparat-filter.md +65 -0
  295. package/.koolie/core/governance/change-requests/CR-2026-096-vorbedingungen-buendel-2.md +225 -0
  296. package/.koolie/core/governance/change-requests/CR-2026-097-testblaetter-buendel-2.md +162 -0
  297. package/.koolie/core/governance/change-requests/CR-2026-098-anforderungen-auslieferung.md +93 -0
  298. package/.koolie/core/governance/change-requests/CR-2026-099-k74-und-vorbedingungen-buendel-3.md +119 -0
  299. package/.koolie/core/governance/change-requests/CR-2026-100-testblaetter-buendel-3.md +120 -0
  300. package/.koolie/core/governance/change-requests/CR-2026-101-k77-eigener-posten.md +165 -0
  301. package/.koolie/core/governance/change-requests/CR-2026-102-k77-entschieden.md +118 -0
  302. package/.koolie/core/governance/change-requests/CR-2026-103-vorbedingungen-buendel-4.md +76 -0
  303. package/.koolie/core/governance/change-requests/CR-2026-104-herrichtung-buendel-4.md +82 -0
  304. package/.koolie/core/governance/change-requests/CR-2026-105-messapparat-buendel-4.md +227 -0
  305. package/.koolie/core/governance/change-requests/CR-2026-106-uebergabe-einchecken.md +91 -0
  306. package/.koolie/core/governance/change-requests/CR-2026-107-uebergabe-im-release-commit.md +111 -0
  307. package/.koolie/core/governance/change-requests/CR-2026-108-testblaetter-buendel-4.md +118 -0
  308. package/.koolie/core/governance/change-requests/CR-2026-109-berichtsweg-des-aufraeumers.md +108 -0
  309. package/.koolie/core/governance/change-requests/CR-2026-110-vorbedingungen-des-nachlaufs.md +184 -0
  310. package/.koolie/core/governance/change-requests/CR-2026-111-der-apparat-auf-dem-weg-der-wiederaufnahme.md +127 -0
  311. package/.koolie/core/governance/change-requests/CR-2026-112-der-arbeitsplatz-im-kern.md +103 -0
  312. package/.koolie/core/governance/change-requests/CR-2026-113-nachlauf-buendel-4.md +103 -0
  313. package/.koolie/core/governance/change-requests/CR-2026-114-vorbedingungen-buendel-5.md +317 -0
  314. package/.koolie/core/governance/change-requests/CR-2026-115-herrichtung-buendel-5.md +244 -0
  315. package/.koolie/core/governance/change-requests/CR-2026-116-messtag-buendel-5.md +178 -0
  316. package/.koolie/core/governance/change-requests/CR-2026-117-sammelzellen-zentraler-katalog.md +251 -0
  317. package/.koolie/core/governance/change-requests/CR-2026-118-quellenzuordnung-matrixzeilen.md +115 -0
  318. package/.koolie/core/governance/change-requests/CR-2026-119-umbenennung-koolie.md +204 -0
  319. package/.koolie/core/governance/change-requests/CR-2026-120-ap2-rest-devin-desktop.md +96 -0
  320. package/.koolie/core/governance/change-requests/CR-2026-121-verify-marker-abschaffen.md +185 -0
  321. package/.koolie/core/governance/change-requests/CR-2026-122-umbenennung-koolie-lauf.md +131 -0
  322. package/.koolie/core/governance/change-requests/CR-2026-123-vortrag-und-namensabsatz.md +170 -0
  323. package/.koolie/core/governance/change-requests/CR-2026-124-hauptdokument-ap11.md +237 -0
  324. package/.koolie/core/governance/change-requests/CR-2026-125-word-fassung-und-blinder-fleck.md +204 -0
  325. package/.koolie/core/governance/change-requests/CR-2026-126-lizenz-gpl3.md +112 -0
  326. package/.koolie/core/governance/change-requests/CR-2026-127-gegenzeichnung-rollenfrage.md +96 -0
  327. package/.koolie/core/governance/change-requests/CR-2026-128-freigabelauf-1.0.0.md +278 -0
  328. package/.koolie/core/governance/change-requests/CR-2026-129-archiv-zeilenenden.md +90 -0
  329. package/.koolie/core/governance/change-requests/CR-2026-130-reihenfolge-des-hebens.md +204 -0
  330. package/.koolie/core/governance/change-requests/CR-2026-131-chronik-und-vorlage.md +194 -0
  331. package/.koolie/core/governance/change-requests/CR-2026-132-erhebung-openai-codex.md +185 -0
  332. package/.koolie/core/governance/change-requests/CR-2026-133-bau-openai-codex.md +97 -0
  333. package/.koolie/core/governance/change-requests/CR-2026-134-uebergabe-lokal.md +117 -0
  334. package/.koolie/core/governance/change-requests/CR-2026-135-pfadlisten-nul-getrennt.md +139 -0
  335. package/.koolie/core/governance/change-requests/CR-2026-136-overlay-werte-einordnung.md +156 -0
  336. package/.koolie/core/governance/change-requests/CR-2026-137-kopierweg-kern.md +144 -0
  337. package/.koolie/core/governance/change-requests/CR-2026-138-overlay-muster-general.md +118 -0
  338. package/.koolie/core/governance/change-requests/CR-2026-139-overlay-muster-dokumente.md +105 -0
  339. package/.koolie/core/governance/change-requests/CR-2026-140-installer-je-zielsystem.md +120 -0
  340. package/.koolie/core/governance/change-requests/CR-2026-141-lieferumfang.md +123 -0
  341. package/.koolie/core/governance/change-requests/CR-2026-142-dokumentationsstandard.md +90 -0
  342. package/.koolie/core/governance/change-requests/CR-2026-143-klasse-b-core-laufzeit.md +89 -0
  343. package/.koolie/core/governance/change-requests/CR-2026-144-klasse-b-governance.md +90 -0
  344. package/.koolie/core/governance/change-requests/CR-2026-145-klasse-b-skills.md +81 -0
  345. package/.koolie/core/governance/change-requests/CR-2026-146-code-pack-posten.md +84 -0
  346. package/.koolie/core/governance/change-requests/CR-2026-147-regel-register-posten.md +78 -0
  347. package/.koolie/core/governance/change-requests/CR-2026-148-mehrprojekt-tokenlast.md +70 -0
  348. package/.koolie/core/governance/change-requests/CR-2026-149-regelablage-pfadtoken.md +71 -0
  349. package/.koolie/core/governance/change-requests/CR-2026-150-client-pack-kiro.md +76 -0
  350. package/.koolie/core/governance/change-requests/CR-2026-151-skill-anweisungen.md +71 -0
  351. package/.koolie/core/governance/change-requests/CR-2026-152-testblaetter-modellwechsel.md +78 -0
  352. package/.koolie/core/governance/change-requests/CR-2026-153-attributionszeile.md +88 -0
  353. package/.koolie/core/governance/change-requests/CR-2026-154-auffindbarkeit.md +102 -0
  354. package/.koolie/core/governance/change-requests/CR-2026-155-client-pack-cursor.md +80 -0
  355. package/.koolie/core/governance/change-requests/CR-2026-156-mandat-und-reibung.md +100 -0
  356. package/.koolie/core/governance/change-requests/CR-2026-157-mcp-anbindung.md +93 -0
  357. package/.koolie/core/governance/change-requests/CR-2026-158-registerpflege.md +69 -0
  358. package/.koolie/core/governance/change-requests/CR-2026-159-powershell.md +49 -0
  359. package/.koolie/core/governance/change-requests/CR-2026-160-messapparat.md +59 -0
  360. package/.koolie/core/governance/change-requests/CR-2026-161-pruefwerkzeuge.md +52 -0
  361. package/.koolie/core/governance/change-requests/CR-2026-162-schutzschicht.md +65 -0
  362. package/.koolie/core/governance/change-requests/CR-2026-163-pfadsemantik.md +61 -0
  363. package/.koolie/core/governance/change-requests/CR-2026-164-modi-ausnahmen.md +65 -0
  364. package/.koolie/core/governance/change-requests/CR-2026-165-installer-banner.md +406 -0
  365. package/.koolie/core/governance/change-requests/CR-2026-166-nachlauf-aufzeichnungen.md +59 -0
  366. package/.koolie/core/governance/change-requests/CR-2026-167-einsatzarchitektur.md +60 -0
  367. package/.koolie/core/governance/change-requests/CR-2026-168-paketquellen.md +64 -0
  368. package/.koolie/core/governance/change-requests/CR-2026-169-modusbindung-messfragen.md +58 -0
  369. package/.koolie/core/governance/change-requests/CR-2026-170-erste-veroeffentlichung.md +62 -0
  370. package/.koolie/core/governance/change-requests/CR-2026-171-befehl-im-projektverzeichnis.md +57 -0
  371. package/.koolie/core/governance/change-requests/CR-2026-172-auftritt-npm-suche.md +58 -0
  372. package/.koolie/core/install.py +2402 -0
  373. package/.koolie/core/install_dialog.py +227 -0
  374. package/.koolie/core/koexistenz.py +112 -0
  375. package/.koolie/core/mandat.py +605 -0
  376. package/.koolie/core/onboarding/COMPLETION_CRITERIA.md +37 -0
  377. package/.koolie/core/onboarding/GUIDE.md +103 -0
  378. package/.koolie/core/onboarding/KNOWLEDGE_CHECK.md +48 -0
  379. package/.koolie/core/onboarding/MENTOR_CHECKLIST.md +40 -0
  380. package/.koolie/core/onboarding/QUICKSTART.md +50 -0
  381. package/.koolie/core/onboarding/REFERENCE.md +69 -0
  382. package/.koolie/core/onboarding/exercises/EXERCISES.md +78 -0
  383. package/.koolie/core/onboarding/exercises/README.md +120 -0
  384. package/.koolie/core/pilot/METRICS.md +59 -0
  385. package/.koolie/core/pilot/PILOT_CONCEPT.md +42 -0
  386. package/.koolie/core/prompts/01-understand-codebase.md +99 -0
  387. package/.koolie/core/prompts/02-impact-analysis.md +104 -0
  388. package/.koolie/core/prompts/03-implementation-planning.md +102 -0
  389. package/.koolie/core/prompts/04-code-generation.md +100 -0
  390. package/.koolie/core/prompts/05-test-generation.md +103 -0
  391. package/.koolie/core/prompts/06-refactoring.md +102 -0
  392. package/.koolie/core/prompts/07-debugging.md +97 -0
  393. package/.koolie/core/prompts/08-security-review.md +92 -0
  394. package/.koolie/core/prompts/09-performance-analysis.md +89 -0
  395. package/.koolie/core/prompts/10-documentation.md +91 -0
  396. package/.koolie/core/prompts/11-merge-request-review.md +94 -0
  397. package/.koolie/core/prompts/12-developer-training.md +90 -0
  398. package/.koolie/core/prompts/README.md +88 -0
  399. package/.koolie/core/templates/MR_AI_DISCLOSURE.md +37 -0
  400. package/.koolie/core/templates/PLAN_TEMPLATE.md +57 -0
  401. package/.koolie/core/templates/SKILL_TEMPLATE.md +159 -0
  402. package/.koolie/core/templates/project-overlay/OVERLAY.md +315 -0
  403. package/.koolie/core/templates/project-overlay/documents/README.md +43 -0
  404. package/.koolie/core/templates/project-overlay/documents/ai-governance/.gitkeep.md +3 -0
  405. package/.koolie/core/templates/project-overlay/documents/ai-process-model/.gitkeep.md +3 -0
  406. package/.koolie/core/templates/project-overlay/documents/architecture/.gitkeep.md +3 -0
  407. package/.koolie/core/templates/project-overlay/documents/architecture/decisions/README.md +3 -0
  408. package/.koolie/core/templates/project-overlay/documents/branching-strategy/.gitkeep.md +3 -0
  409. package/.koolie/core/templates/project-overlay/documents/coding-guidelines/.gitkeep.md +3 -0
  410. package/.koolie/core/templates/project-overlay/documents/definition-of-done/.gitkeep.md +3 -0
  411. package/.koolie/core/templates/project-overlay/documents/definition-of-ready/.gitkeep.md +3 -0
  412. package/.koolie/core/templates/project-overlay/documents/deployment/.gitkeep.md +3 -0
  413. package/.koolie/core/templates/project-overlay/documents/glossary/.gitkeep.md +3 -0
  414. package/.koolie/core/templates/project-overlay/documents/quality/.gitkeep.md +3 -0
  415. package/.koolie/core/templates/project-overlay/documents/roadmap/.gitkeep.md +3 -0
  416. package/.koolie/core/templates/project-overlay/documents/roles/.gitkeep.md +3 -0
  417. package/.koolie/core/templates/project-overlay/documents/security/.gitkeep.md +3 -0
  418. package/.koolie/core/templates/project-overlay/exceptions/EXCEPTIONS.md +9 -0
  419. package/.koolie/core/templates/project-overlay/forbidden-terms.txt +8 -0
  420. package/.koolie/core/templates/project-overlay/overlay-manifest.yaml +73 -0
  421. package/.koolie/core/templates/rules/21-overlay-TEMPLATE.md.template +28 -0
  422. package/.koolie/core/templates/rules/40-tech-TEMPLATE.md.template +33 -0
  423. package/.koolie/core/tests/EDGE_CASES.md +67 -0
  424. package/.koolie/core/tests/TEST_CATALOG.md +170 -0
  425. package/.koolie/core/tests/erhebungen/README.md +326 -0
  426. package/.koolie/core/tests/erhebungen/ablage.py +357 -0
  427. package/.koolie/core/tests/erhebungen/apparat/__init__.py +22 -0
  428. package/.koolie/core/tests/erhebungen/apparat/baum.py +214 -0
  429. package/.koolie/core/tests/erhebungen/apparat/belege.py +91 -0
  430. package/.koolie/core/tests/erhebungen/apparat/clients.py +321 -0
  431. package/.koolie/core/tests/erhebungen/apparat/freigabe.py +74 -0
  432. package/.koolie/core/tests/erhebungen/apparat/kontingent.py +64 -0
  433. package/.koolie/core/tests/erhebungen/apparat/laeufer.py +155 -0
  434. package/.koolie/core/tests/erhebungen/apparat/reihe.py +202 -0
  435. package/.koolie/core/tests/erhebungen/apparat/selbsttest.py +280 -0
  436. package/.koolie/core/tests/erhebungen/apparat/stand.py +30 -0
  437. package/.koolie/core/tests/erhebungen/apparat/vorpruefung.py +70 -0
  438. package/.koolie/core/tests/erhebungen/auswerten-b4.py +468 -0
  439. package/.koolie/core/tests/erhebungen/auswerten-b5.py +517 -0
  440. package/.koolie/core/tests/erhebungen/auswerten-dd.py +172 -0
  441. package/.koolie/core/tests/erhebungen/baeume-b4.py +334 -0
  442. package/.koolie/core/tests/erhebungen/baeume-b5.py +339 -0
  443. package/.koolie/core/tests/erhebungen/baeume_loeschen.py +138 -0
  444. package/.koolie/core/tests/erhebungen/cc-overlay-fuellen.py +277 -0
  445. package/.koolie/core/tests/erhebungen/dossier-b4.py +178 -0
  446. package/.koolie/core/tests/erhebungen/dossier-b5.py +202 -0
  447. package/.koolie/core/tests/erhebungen/historie-bauen-b4.py +642 -0
  448. package/.koolie/core/tests/erhebungen/k-bauen-b3.py +841 -0
  449. package/.koolie/core/tests/erhebungen/lauf-dd.py +146 -0
  450. package/.koolie/core/tests/erhebungen/lauf.py +114 -0
  451. package/.koolie/core/tests/erhebungen/mcp-waechter.py +278 -0
  452. package/.koolie/core/tests/erhebungen/messbaum-schnitt.py +256 -0
  453. package/.koolie/core/tests/erhebungen/messen.py +72 -0
  454. package/.koolie/core/tests/erhebungen/node-waechter.py +74 -0
  455. package/.koolie/core/tests/erhebungen/packaktivierung.py +403 -0
  456. package/.koolie/core/tests/erhebungen/prompts-schreiben-b4.py +210 -0
  457. package/.koolie/core/tests/erhebungen/prompts-schreiben-b5.py +251 -0
  458. package/.koolie/core/tests/erhebungen/reihe-b4.py +171 -0
  459. package/.koolie/core/tests/erhebungen/reihe-b5.py +172 -0
  460. package/.koolie/core/tests/erhebungen/stand-b4.py +135 -0
  461. package/.koolie/core/tests/erhebungen/stand-b5.py +137 -0
  462. package/.koolie/core/tests/erhebungen/trust-b4.py +56 -0
  463. package/.koolie/core/tests/erhebungen/trust-b5.py +56 -0
  464. package/.koolie/core/tests/erhebungen/turn2-schreiben-b4.py +105 -0
  465. package/.koolie/core/tests/erhebungen/umgebungen-bauen-b4.py +377 -0
  466. package/.koolie/core/tests/erhebungen/umgebungen-bauen-b5.py +363 -0
  467. package/.koolie/core/tests/erhebungen/zaehlen46.py +74 -0
  468. package/.koolie/core/tests/erhebungen/zustand-b4.py +82 -0
  469. package/.koolie/core/tests/erhebungen/zustand-b5.py +82 -0
  470. package/.koolie/core/tests/protocols/2026-09-10-AP2-claude-code-wirkungsnachweise.md +194 -0
  471. package/.koolie/core/tests/protocols/2026-09-10-AP2-claude-code.md +468 -0
  472. package/.koolie/core/tests/protocols/2026-09-10-CR-2026-020-akteursbezeichnung.md +99 -0
  473. package/.koolie/core/tests/protocols/2026-09-10-CR-2026-021-hook-interpreter.md +138 -0
  474. package/.koolie/core/tests/protocols/2026-09-10-CR-2026-022-artefaktversionen.md +87 -0
  475. package/.koolie/core/tests/protocols/2026-09-10-CR-2026-023-shell-lesesperre.md +135 -0
  476. package/.koolie/core/tests/protocols/2026-09-10-CR-2026-024-clientbindungen.md +98 -0
  477. package/.koolie/core/tests/protocols/2026-09-10-FW-DS-03.md +90 -0
  478. package/.koolie/core/tests/protocols/2026-09-10-FW-KO-01.md +105 -0
  479. package/.koolie/core/tests/protocols/2026-09-10-FW-KO-02.md +123 -0
  480. package/.koolie/core/tests/protocols/2026-09-10-FW-KO-04.md +73 -0
  481. package/.koolie/core/tests/protocols/2026-09-10-FW-RE-02.md +99 -0
  482. package/.koolie/core/tests/protocols/2026-09-10-FW-VN-01-wiederholung.md +108 -0
  483. package/.koolie/core/tests/protocols/2026-09-10-FW-VN-01.md +249 -0
  484. package/.koolie/core/tests/protocols/2026-09-10-FW-ZA-05.md +99 -0
  485. package/.koolie/core/tests/protocols/2026-09-11-AP2-devin-desktop.md +257 -0
  486. package/.koolie/core/tests/protocols/2026-09-11-CR-2026-026-fail-closed.md +113 -0
  487. package/.koolie/core/tests/protocols/2026-09-11-erhebungen-K21-K26.md +243 -0
  488. package/.koolie/core/tests/protocols/2026-09-11-wirkungsnachweise-0.26.0.md +101 -0
  489. package/.koolie/core/tests/protocols/2026-09-12-B01-allowed-tools.md +152 -0
  490. package/.koolie/core/tests/protocols/2026-09-12-B04-B05-gegenpruefung.md +204 -0
  491. package/.koolie/core/tests/protocols/2026-09-12-erhebungen-K28-S5-B9-bypass.md +268 -0
  492. package/.koolie/core/tests/protocols/2026-09-12-mehrprojekt-arbeitsbereich.md +154 -0
  493. package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.26.1.md +126 -0
  494. package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.27.0.md +114 -0
  495. package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.28.0.md +129 -0
  496. package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.29.0.md +121 -0
  497. package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.30.0.md +112 -0
  498. package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.31.0.md +107 -0
  499. package/.koolie/core/tests/protocols/2026-09-13-B06-gegenpruefung.md +370 -0
  500. package/.koolie/core/tests/protocols/2026-09-13-B07-B09-gegenpruefung.md +211 -0
  501. package/.koolie/core/tests/protocols/2026-09-13-B08-B11-gegenpruefung.md +179 -0
  502. package/.koolie/core/tests/protocols/2026-09-13-erhebung-disallowed-tools.md +147 -0
  503. package/.koolie/core/tests/protocols/2026-09-13-erhebung-unteragent-tiefe.md +139 -0
  504. package/.koolie/core/tests/protocols/2026-09-13-erhebung-unteragent.md +214 -0
  505. package/.koolie/core/tests/protocols/2026-09-13-gegenpruefung-berechtigungsdatei.md +253 -0
  506. package/.koolie/core/tests/protocols/2026-09-13-gegenpruefung-stumme-brueche.md +202 -0
  507. package/.koolie/core/tests/protocols/2026-09-13-gegenpruefung-werkzeugabbildung.md +219 -0
  508. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.32.0.md +133 -0
  509. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.33.0.md +169 -0
  510. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.34.0.md +187 -0
  511. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.35.0.md +187 -0
  512. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.36.0.md +137 -0
  513. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.37.0.md +112 -0
  514. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.38.0.md +154 -0
  515. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.39.0.md +188 -0
  516. package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.40.0.md +214 -0
  517. package/.koolie/core/tests/protocols/2026-09-14-erhebung-devin-werkzeuge.md +267 -0
  518. package/.koolie/core/tests/protocols/2026-09-14-erhebung-skillaufruf.md +238 -0
  519. package/.koolie/core/tests/protocols/2026-09-14-gegenpruefung-pruefregister.md +156 -0
  520. package/.koolie/core/tests/protocols/2026-09-14-gegenpruefung-schlitzdeckung.md +206 -0
  521. package/.koolie/core/tests/protocols/2026-09-14-gegenpruefung-skillwahl.md +127 -0
  522. package/.koolie/core/tests/protocols/2026-09-14-migrationslauf-pilot.md +134 -0
  523. package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.41.0.md +175 -0
  524. package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.42.0.md +180 -0
  525. package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.43.0.md +165 -0
  526. package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.44.0.md +130 -0
  527. package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-d11-zaehlregeln.md +311 -0
  528. package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-modulstatus.md +524 -0
  529. package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-nicht-skill-traeger.md +225 -0
  530. package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-restliche-nicht-skill-traeger.md +345 -0
  531. package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-strukturentscheidungen.md +392 -0
  532. package/.koolie/core/tests/protocols/2026-09-15-herrichtung-uebungsrepositorium.md +213 -0
  533. package/.koolie/core/tests/protocols/2026-09-15-migrationslauf-pilot-0.46.0.md +143 -0
  534. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.45.0.md +127 -0
  535. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.46.0.md +193 -0
  536. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.47.0.md +149 -0
  537. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.49.0.md +202 -0
  538. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.50.0.md +183 -0
  539. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.51.0.md +202 -0
  540. package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.52.0.md +168 -0
  541. package/.koolie/core/tests/protocols/2026-09-16-AP2-zielversion-devin-desktop.md +231 -0
  542. package/.koolie/core/tests/protocols/2026-09-16-wirkungsnachweise-0.53.0.md +350 -0
  543. package/.koolie/core/tests/protocols/2026-09-17-nachtrag-migrationshinweis-0.54.1.md +121 -0
  544. package/.koolie/core/tests/protocols/2026-09-17-sitzungstest-pi-ds.md +523 -0
  545. package/.koolie/core/tests/protocols/2026-09-17-sitzungstest-schranken.md +433 -0
  546. package/.koolie/core/tests/protocols/2026-09-17-wirkungsnachweise-0.54.0.md +159 -0
  547. package/.koolie/core/tests/protocols/2026-09-18-FW-AK-01.md +442 -0
  548. package/.koolie/core/tests/protocols/2026-09-18-FW-KO-05.md +231 -0
  549. package/.koolie/core/tests/protocols/2026-09-18-herrichtung-uebungsrepositorium.md +273 -0
  550. package/.koolie/core/tests/protocols/2026-09-18-nachtrag-preis-umbenennung-0.56.1.md +102 -0
  551. package/.koolie/core/tests/protocols/2026-09-18-sitzungstest-5.md +349 -0
  552. package/.koolie/core/tests/protocols/2026-09-18-sitzungstest-ne-sc.md +430 -0
  553. package/.koolie/core/tests/protocols/2026-09-18-sitzungstest-pi-ds-2.md +499 -0
  554. package/.koolie/core/tests/protocols/2026-09-18-vorbedingungen-sitzungstest-5-zweiter-durchgang.md +170 -0
  555. package/.koolie/core/tests/protocols/2026-09-18-vorbedingungen-testblaetter.md +184 -0
  556. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.55.0.md +202 -0
  557. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.56.0.md +135 -0
  558. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.56.2.md +154 -0
  559. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.57.0.md +228 -0
  560. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.57.1.md +262 -0
  561. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.58.0.md +135 -0
  562. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.59.0.md +150 -0
  563. package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.60.0.md +340 -0
  564. package/.koolie/core/tests/protocols/2026-09-19-herrichtung-buendel-4.md +235 -0
  565. package/.koolie/core/tests/protocols/2026-09-19-k77-zurechenbarkeit.md +236 -0
  566. package/.koolie/core/tests/protocols/2026-09-19-pruefapparat-filter.md +50 -0
  567. package/.koolie/core/tests/protocols/2026-09-19-pruefmittelwort-und-buendelschnitt.md +239 -0
  568. package/.koolie/core/tests/protocols/2026-09-19-sechzehnte-praeparation.md +135 -0
  569. package/.koolie/core/tests/protocols/2026-09-19-testblaetter-buendel-1.md +250 -0
  570. package/.koolie/core/tests/protocols/2026-09-19-testblaetter-buendel-2.md +229 -0
  571. package/.koolie/core/tests/protocols/2026-09-19-testblaetter-buendel-3.md +293 -0
  572. package/.koolie/core/tests/protocols/2026-09-19-vorbedingungen-buendel-2.md +236 -0
  573. package/.koolie/core/tests/protocols/2026-09-19-vorbedingungen-buendel-3.md +253 -0
  574. package/.koolie/core/tests/protocols/2026-09-19-vorbedingungen-buendel-4.md +138 -0
  575. package/.koolie/core/tests/protocols/2026-09-20-messapparat-buendel-4.md +433 -0
  576. package/.koolie/core/tests/protocols/2026-09-20-testblaetter-buendel-4.md +321 -0
  577. package/.koolie/core/tests/protocols/2026-09-20-uebergabe-im-release-commit.md +146 -0
  578. package/.koolie/core/tests/protocols/2026-09-20-vorbedingungen-nachlauf-b4.md +259 -0
  579. package/.koolie/core/tests/protocols/2026-09-21-arbeitsplatz-im-kern.md +127 -0
  580. package/.koolie/core/tests/protocols/2026-09-21-nachlauf-buendel-4.md +157 -0
  581. package/.koolie/core/tests/protocols/2026-09-21-vorbedingungen-buendel-5.md +247 -0
  582. package/.koolie/core/tests/protocols/2026-09-21-wiederaufnahme-nachlauf-b4.md +190 -0
  583. package/.koolie/core/tests/protocols/2026-09-22-ap2-rest-devin-desktop.md +614 -0
  584. package/.koolie/core/tests/protocols/2026-09-22-hauptdokument-ap11.md +443 -0
  585. package/.koolie/core/tests/protocols/2026-09-22-herrichtung-buendel-5.md +224 -0
  586. package/.koolie/core/tests/protocols/2026-09-22-messtag-buendel-5.md +327 -0
  587. package/.koolie/core/tests/protocols/2026-09-22-quellenzuordnung-matrixzeilen.md +230 -0
  588. package/.koolie/core/tests/protocols/2026-09-22-sammelzellen-zentraler-katalog.md +354 -0
  589. package/.koolie/core/tests/protocols/2026-09-22-umbenennung-koolie.md +296 -0
  590. package/.koolie/core/tests/protocols/2026-09-22-verify-marker-abschaffen.md +283 -0
  591. package/.koolie/core/tests/protocols/2026-09-22-vortrag-und-name.md +261 -0
  592. package/.koolie/core/tests/protocols/2026-09-22-wirkungsnachweise-0.86.0.md +158 -0
  593. package/.koolie/core/tests/protocols/2026-09-23-archiv-zeilenenden.md +113 -0
  594. package/.koolie/core/tests/protocols/2026-09-23-bau-openai-codex.md +168 -0
  595. package/.koolie/core/tests/protocols/2026-09-23-chronik-und-vorlage.md +224 -0
  596. package/.koolie/core/tests/protocols/2026-09-23-erhebung-openai-codex.md +309 -0
  597. package/.koolie/core/tests/protocols/2026-09-23-freigabelauf-1.0.0-vorbereitung.md +119 -0
  598. package/.koolie/core/tests/protocols/2026-09-23-freigabelauf-1.0.0.md +150 -0
  599. package/.koolie/core/tests/protocols/2026-09-23-gegenzeichnung-rollenfrage.md +198 -0
  600. package/.koolie/core/tests/protocols/2026-09-23-reihenfolge-des-hebens.md +220 -0
  601. package/.koolie/core/tests/protocols/2026-09-23-word-fassung-und-blinder-fleck.md +307 -0
  602. package/.koolie/core/tests/protocols/2026-09-24-kopierweg-kern.md +98 -0
  603. package/.koolie/core/tests/protocols/2026-09-24-overlay-werte-einordnung.md +111 -0
  604. package/.koolie/core/tests/protocols/2026-09-24-pfadlisten-nul-getrennt.md +128 -0
  605. package/.koolie/core/tests/protocols/2026-09-24-uebergabe-lokal.md +85 -0
  606. package/.koolie/core/tests/protocols/2026-09-25-code-pack-posten.md +86 -0
  607. package/.koolie/core/tests/protocols/2026-09-25-dokumentationsstandard.md +101 -0
  608. package/.koolie/core/tests/protocols/2026-09-25-installer-je-zielsystem.md +134 -0
  609. package/.koolie/core/tests/protocols/2026-09-25-klasse-b-core-laufzeit.md +126 -0
  610. package/.koolie/core/tests/protocols/2026-09-25-klasse-b-governance.md +133 -0
  611. package/.koolie/core/tests/protocols/2026-09-25-klasse-b-skills.md +91 -0
  612. package/.koolie/core/tests/protocols/2026-09-25-lieferumfang.md +76 -0
  613. package/.koolie/core/tests/protocols/2026-09-25-overlay-muster-dokumente.md +107 -0
  614. package/.koolie/core/tests/protocols/2026-09-25-overlay-muster-general.md +100 -0
  615. package/.koolie/core/tests/protocols/2026-09-25-regel-register-posten.md +87 -0
  616. package/.koolie/core/tests/protocols/2026-09-26-attributionszeile.md +83 -0
  617. package/.koolie/core/tests/protocols/2026-09-26-auffindbarkeit.md +85 -0
  618. package/.koolie/core/tests/protocols/2026-09-26-bau-cursor.md +68 -0
  619. package/.koolie/core/tests/protocols/2026-09-26-bau-kiro.md +75 -0
  620. package/.koolie/core/tests/protocols/2026-09-26-mehrprojekt-tokenlast.md +174 -0
  621. package/.koolie/core/tests/protocols/2026-09-26-regelablage-pfadtoken.md +133 -0
  622. package/.koolie/core/tests/protocols/2026-09-26-skill-anweisungen.md +111 -0
  623. package/.koolie/core/tests/protocols/2026-09-26-testblaetter-modellwechsel.md +106 -0
  624. package/.koolie/core/tests/protocols/2026-09-27-mandat-und-reibung.md +58 -0
  625. package/.koolie/core/tests/protocols/2026-09-28-mcp-anbindung.md +72 -0
  626. package/.koolie/core/tests/protocols/2026-09-29-messapparat.md +75 -0
  627. package/.koolie/core/tests/protocols/2026-09-29-powershell.md +58 -0
  628. package/.koolie/core/tests/protocols/2026-09-29-pruefwerkzeuge.md +64 -0
  629. package/.koolie/core/tests/protocols/2026-09-29-registerpflege.md +228 -0
  630. package/.koolie/core/tests/protocols/2026-09-29-schutzschicht.md +95 -0
  631. package/.koolie/core/tests/protocols/2026-09-30-einsatzarchitektur.md +104 -0
  632. package/.koolie/core/tests/protocols/2026-09-30-modi-ausnahmen.md +77 -0
  633. package/.koolie/core/tests/protocols/2026-09-30-nachlauf-banner.md +85 -0
  634. package/.koolie/core/tests/protocols/2026-09-30-paketquellen.md +65 -0
  635. package/.koolie/core/tests/protocols/2026-09-30-pfadsemantik.md +79 -0
  636. package/.koolie/core/tests/protocols/2026-10-01-auftritt-und-suche.md +58 -0
  637. package/.koolie/core/tests/protocols/2026-10-01-befehl-im-projektverzeichnis.md +47 -0
  638. package/.koolie/core/tests/protocols/2026-10-01-erste-veroeffentlichung.md +60 -0
  639. package/.koolie/core/tests/protocols/2026-10-01-modusbindung-messfragen.md +107 -0
  640. package/.koolie/core/tests/protocols/README.md +14 -0
  641. package/.koolie/core/tests/scripts/hook-check-secrets.py +1307 -0
  642. package/.koolie/core/tests/scripts/hook-overlay-status.py +120 -0
  643. package/.koolie/core/tests/scripts/mermaid_renderer.py +67 -0
  644. package/.koolie/core/tests/scripts/overlay_status.py +130 -0
  645. package/.koolie/core/tests/scripts/probe-pruefungen.py +110 -0
  646. package/.koolie/core/tests/scripts/pruefungen/__init__.py +4 -0
  647. package/.koolie/core/tests/scripts/pruefungen/berechtigungen.py +1559 -0
  648. package/.koolie/core/tests/scripts/pruefungen/bestand.py +1433 -0
  649. package/.koolie/core/tests/scripts/pruefungen/dokumente.py +1262 -0
  650. package/.koolie/core/tests/scripts/pruefungen/gemeinsam.py +784 -0
  651. package/.koolie/core/tests/scripts/pruefungen/hooks.py +1188 -0
  652. package/.koolie/core/tests/scripts/pruefungen/overlay.py +1251 -0
  653. package/.koolie/core/tests/scripts/pruefungen/packs.py +1314 -0
  654. package/.koolie/core/tests/scripts/pruefungen/register.py +950 -0
  655. package/.koolie/core/tests/scripts/pruefungen/testkatalog.py +1202 -0
  656. package/.koolie/core/tests/scripts/pruefungen/werkzeuge.py +957 -0
  657. package/.koolie/core/tests/scripts/sonden/__init__.py +4 -0
  658. package/.koolie/core/tests/scripts/sonden/apparat.py +653 -0
  659. package/.koolie/core/tests/scripts/sonden/teil01_grundbestand_und_installation.py +999 -0
  660. package/.koolie/core/tests/scripts/sonden/teil02_packs_mandat_mcp.py +942 -0
  661. package/.koolie/core/tests/scripts/sonden/teil03_pruefungen_26_bis_36.py +1184 -0
  662. package/.koolie/core/tests/scripts/sonden/teil04_pruefungen_37_bis_45.py +1491 -0
  663. package/.koolie/core/tests/scripts/sonden/teil05_pruefungen_46_bis_55.py +1447 -0
  664. package/.koolie/core/tests/scripts/sonden/teil06_pruefungen_57_bis_65.py +739 -0
  665. package/.koolie/core/tests/scripts/sonden/teil07_overlay_und_lieferung.py +1014 -0
  666. package/.koolie/core/tests/scripts/sonden/teil08_pruefungen_66_bis_80.py +1325 -0
  667. package/.koolie/core/tests/scripts/sonden/teil09_pruefmittel_81_und_82.py +505 -0
  668. package/.koolie/core/tests/scripts/sonden/teil10_pruefungen_83_bis_95.py +1218 -0
  669. package/.koolie/core/tests/scripts/sonden/teil11_pruefungen_104_und_105.py +149 -0
  670. package/.koolie/core/tests/scripts/sonden/teil12_pruefungen_106_und_107.py +136 -0
  671. package/.koolie/core/tests/scripts/sonden/teil13_pfad_und_mustersemantik.py +150 -0
  672. package/.koolie/core/tests/scripts/sonden/teil14_modi_ausnahmen_skills.py +204 -0
  673. package/.koolie/core/tests/scripts/sonden/teil15_banner_und_nachlauf.py +193 -0
  674. package/.koolie/core/tests/scripts/sonden/teil16_koexistenz.py +114 -0
  675. package/.koolie/core/tests/scripts/sonden/teil17_paketquellen.py +115 -0
  676. package/.koolie/core/tests/scripts/sonden/teil18_modusbindung_m3_m5.py +113 -0
  677. package/.koolie/core/tests/scripts/validate-framework.py +1104 -0
  678. package/.koolie/core/tests/scripts/validate-output.py +296 -0
  679. package/.koolie/core/wirksamkeit.py +430 -0
  680. package/CONTRIBUTING.md +119 -0
  681. package/LICENSE +674 -0
  682. package/QUICKSTART.en.md +135 -0
  683. package/QUICKSTART.md +129 -0
  684. package/README.en.md +108 -0
  685. package/README.md +107 -0
  686. package/install.cmd +44 -0
  687. package/install.command +46 -0
  688. package/package.json +21 -0
  689. package/paketquellen/README.md +41 -0
  690. package/paketquellen/bauen.py +411 -0
  691. package/paketquellen/koolie.cmd +23 -0
  692. package/paketquellen/koolie_befehl.py +82 -0
  693. package/paketquellen/npm/koolie.js +26 -0
@@ -0,0 +1,2402 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ install.py - Legt die Wurzeldateien des Koolie-Frameworks in einem Projekt an.
4
+
5
+ Hintergrund: Zwei Dinge muessen im Wurzelverzeichnis des Projekts liegen, weil der
6
+ KI-Client sie nur dort findet - die Wurzel-Anweisungsdatei und die Laufzeitschicht mit
7
+ Regeln, Skills, Agentenprofilen, Berechtigungen und Hooks. Alles Uebrige des Frameworks
8
+ liegt gebuendelt im Kernverzeichnis und wird nur kopiert, nicht installiert.
9
+
10
+ Die wenigsten dieser Dateien liegen noch je Client Pack: Regeltexte, Skills, Overlay,
11
+ Berechtigungen und Hooks liegen einmal im Kern und werden bei der Installation in die
12
+ Form des gewaehlten Clients gebracht. Wie, steht im Manifest des Packs; die
13
+ Semantikabbildung der Berechtigungen und Hooks in clientmap.py.
14
+
15
+ Dieses Skript loest das auf: Es kopiert die Wurzelbestandteile aus dem
16
+ root-template/ des gewaehlten Client Packs an ihren Platz und unterscheidet dabei
17
+ sauber zwischen Core (wird bei einem Update ueberschrieben) und Projektbestandteilen
18
+ (werden nie ueberschrieben).
19
+
20
+ Welcher KI-Client verwendet wird, entscheidet --client. Die Wurzelartefakte jedes
21
+ Clients liegen unter clients/<client>/root-template/; welche Zusagen
22
+ des Frameworks ein Client technisch durchsetzt, steht in seiner CLIENT_PACK.md
23
+ (clients/README.md).
24
+
25
+ Aufruf (aus dem Wurzelverzeichnis des Projekts):
26
+
27
+ python .koolie/core/install.py # Erstinstallation, Standard-Client
28
+ python .koolie/core/install.py --client <name> # anderer Client
29
+ python .koolie/core/install.py --list-clients # verfuegbare Client Packs
30
+ python .koolie/core/install.py --overlay general # Erstinstallation mit Overlay-Muster
31
+ python .koolie/core/install.py --overlay # verfuegbare Overlay-Muster
32
+ python .koolie/core/install.py --update # Core aktualisieren
33
+ python .koolie/core/install.py --check # nur pruefen, nichts schreiben
34
+ python .koolie/core/install.py --probe # wirkt die Schutzschicht? (K-195)
35
+ python .koolie/core/install.py --dry-run # zeigen, was passieren wuerde
36
+
37
+ Aufruf aus dem Framework (Klon oder entpacktes Release-Archiv) in ein anderes Projekt -
38
+ kopiert nur den Kern und installiert danach dort (D-362):
39
+
40
+ python .koolie/core/install.py --target <projekt> [--client <name>] [--overlay <name>]
41
+ python .koolie/core/install.py --target <projekt> --update # Projekt heben
42
+ python .koolie/core/install.py --target <projekt> --lieferumfang nutzung
43
+ # ohne Nachweisschicht (D-367); gilt beim Heben weiter
44
+
45
+ Die Starter install.cmd (Windows) und install.command (macOS) in der Wurzel des
46
+ Frameworks fragen diese Angaben ab (install_dialog.py) und rufen genau das auf.
47
+
48
+ Exit-Code 0 = in Ordnung, 1 = Abweichungen gefunden (bei --check), eine fehlende
49
+ Muss-Kontrolle (bei --probe) oder Fehler.
50
+ """
51
+ from __future__ import annotations
52
+
53
+ import argparse
54
+ import filecmp
55
+ import json
56
+ import re
57
+ import hashlib
58
+ import os
59
+ import shutil
60
+ import subprocess
61
+ import sys
62
+ import textwrap
63
+
64
+ HERE = os.path.dirname(os.path.abspath(__file__))
65
+ sys.path.insert(0, HERE)
66
+ import clientmap # noqa: E402 (liegt neben dieser Datei)
67
+ import wirksamkeit # noqa: E402 (liegt neben dieser Datei; K-195, D-488)
68
+ import koexistenz # noqa: E402 (liegt neben dieser Datei; K-31, D-514)
69
+
70
+ CLIENT_PACKS = os.path.join(HERE, "clients")
71
+ # Der Standard bleibt der Client, fuer den das Framework urspruenglich gebaut wurde.
72
+ # Ein Projekt waehlt bei der Erstinstallation, danach steht die Wahl im Overlay.
73
+ DEFAULT_CLIENT = "devin-desktop"
74
+
75
+
76
+ def available_clients() -> list[str]:
77
+ """Client Packs mit Wurzelartefakten. Ein Pack ohne root-template/ ist nicht installierbar."""
78
+ if not os.path.isdir(CLIENT_PACKS):
79
+ return []
80
+ return sorted(
81
+ name for name in os.listdir(CLIENT_PACKS)
82
+ if not name.startswith("_")
83
+ and os.path.isdir(os.path.join(CLIENT_PACKS, name, "root-template"))
84
+ and os.path.exists(os.path.join(CLIENT_PACKS, name, "manifest.json"))
85
+ )
86
+
87
+
88
+ def client_template(client: str) -> str:
89
+ return os.path.join(CLIENT_PACKS, client, "root-template")
90
+
91
+ # ---------------------------------------------------------------------------
92
+ # Welche Datei Core ist und welche dem Projekt gehoert, steht im Manifest des
93
+ # Client Packs (clients/<client>/manifest.json). Frueher standen diese Listen hier
94
+ # fest verdrahtet - das ging nur, solange es genau einen Client gab.
95
+ #
96
+ # Core = gehoert zum Framework, wird bei --update ueberschrieben.
97
+ # Aenderungswuensche laufen als Aenderungsantrag an den Framework Owner
98
+ # (.koolie/core/governance/FEEDBACK_PROCESS.md).
99
+ # Saat (seed) = wird nur bei der Erstinstallation angelegt. Danach gehoert es dem
100
+ # Projekt und wird nie ueberschrieben - hier stehen die Projektwerte.
101
+ #
102
+ # Laufzeitfassungen von Packs stehen absichtlich in keiner der beiden Listen: Ein Pack
103
+ # wird im Overlay aktiviert (framework/role-packs/README.md Punkt 4), nicht durch die
104
+ # Installation. Wer ein Pack aktiviert hat, bekommt seine Bestandteile ueber
105
+ # activated_pack_relpaths() aktualisiert.
106
+ # ---------------------------------------------------------------------------
107
+ MANIFEST_PFLICHTFELDER = ("client", "skills_dir", "pack_runtime_dir",
108
+ "core_skill_prefix", "core_paths", "seed_paths")
109
+
110
+
111
+ def nachschritte(schritte: list, ab: int) -> None:
112
+ """Gibt die packeigenen Schritte nach Installation oder Hebung numeriert aus.
113
+
114
+ Sie stehen im Manifest (post_install_steps, post_update_steps), weil sie nur ein Pack
115
+ betreffen - bei openai-codex die Vertrauensbedingung, ohne die Konfiguration und
116
+ Schutz-Hook nicht laden (K-123, D-395). Lange Saetze werden umbrochen.
117
+ """
118
+ for i, schritt in enumerate(schritte, ab):
119
+ zeilen = textwrap.wrap(schritt, width=80)
120
+ for j, zeile in enumerate(zeilen):
121
+ print(f" {i}. {zeile}" if j == 0 else f" {zeile}")
122
+
123
+
124
+ def load_manifest(client: str) -> dict:
125
+ path = os.path.join(CLIENT_PACKS, client, "manifest.json")
126
+ with open(path, encoding="utf-8") as fh:
127
+ m = json.load(fh)
128
+ fehlend = [f for f in MANIFEST_PFLICHTFELDER if f not in m]
129
+ if fehlend:
130
+ raise ValueError(f"{client}/manifest.json: Pflichtfelder fehlen: {', '.join(fehlend)}")
131
+ # <CORE_DIR> steht in keinem Manifest: Die Lage des Kernverzeichnisses ist keine
132
+ # Eigenschaft eines Clients, sondern dieser Installation. Sie wird hier gesetzt,
133
+ # damit eine spaetere Umbenennung (Roadmap P3) nur eine Stelle beruehrt.
134
+ #
135
+ # 0.88.0: Bis dahin stand hier os.path.basename(HERE) - und genau diese Zeile,
136
+ # die eine Umbenennung billig machen sollte, waere an ihr zerbrochen: Der Kern
137
+ # liegt seit 0.88.0 unter `.koolie/core`, und basename() liefert davon "core".
138
+ # Die Lage steht deshalb in clientmap.CORE_REL (D-299).
139
+ m.setdefault("runtime_placeholders", {})["<CORE_DIR>"] = clientmap.CORE_REL
140
+ return m
141
+
142
+
143
+ class Report:
144
+ def __init__(self) -> None:
145
+ self.created: list[str] = []
146
+ self.updated: list[str] = []
147
+ self.unchanged: list[str] = []
148
+ self.kept: list[str] = []
149
+ self.drifted: list[str] = []
150
+ self.missing: list[str] = []
151
+
152
+
153
+ def sha(path: str) -> str:
154
+ h = hashlib.sha256()
155
+ with open(path, "rb") as fh:
156
+ for block in iter(lambda: fh.read(65536), b""):
157
+ h.update(block)
158
+ return h.hexdigest()
159
+
160
+
161
+ def iter_files(base: str, rel: str):
162
+ """Liefert alle Dateien unter rel (Datei oder Verzeichnis), relativ zu base."""
163
+ full = os.path.join(base, rel)
164
+ if os.path.isfile(full):
165
+ yield rel
166
+ elif os.path.isdir(full):
167
+ for dirpath, dirnames, filenames in os.walk(full):
168
+ dirnames[:] = [d for d in dirnames if d != "__pycache__"]
169
+ for fn in sorted(filenames):
170
+ yield os.path.relpath(os.path.join(dirpath, fn), base).replace(os.sep, "/")
171
+
172
+
173
+ def core_relpaths(template: str, man: dict) -> list[str]:
174
+ """Alle Core-Dateien des Templates, inklusive der Framework-Skills."""
175
+ out: list[str] = []
176
+ for rel in man["core_paths"]:
177
+ out.extend(iter_files(template, rel))
178
+ # Die Framework-Skills stehen seit 0.5.0 nicht mehr im Template, sondern einmal
179
+ # unter framework/skills/; sie werden ueber framework_skill_files() gerendert.
180
+ return out
181
+
182
+
183
+ def activated_pack_relpaths(root: str, man: dict) -> list[str]:
184
+ """Bestandteile von Packs, die dieses Projekt aktiviert hat.
185
+
186
+ Ein Pack wird aktiviert, indem seine Skills nach .devin/skills/ und seine Laufzeitfassung
187
+ nach .devin/rules/ kopiert werden. Ob ein Pack aktiv ist, entscheidet das Projekt
188
+ (Overlay Abschnitt 1) - was in einem aktivierten Pack-Skill steht, ist dagegen
189
+ Framework-Inhalt und gehoert damit zum Core-Aktualisierungsumfang. Ohne diese Funktion
190
+ behielte ein Projekt seine Kopie aus dem Release der Aktivierung, stillschweigend und
191
+ ohne Hinweis.
192
+
193
+ Nur was in einer Pack-Quellablage dieses Kerns eine Entsprechung hat, wird erfasst.
194
+ Projekteigene Packs (etwa unter .koolie/project-overlay/tech-packs/) bleiben unberuehrt.
195
+ """
196
+ out: list[str] = []
197
+ for kind in ("role-packs", "tech-packs"):
198
+ base = os.path.join(HERE, "framework", kind)
199
+ if not os.path.isdir(base):
200
+ continue
201
+ for pack in sorted(os.listdir(base)):
202
+ src_skills = os.path.join(base, pack, "skills")
203
+ if os.path.isdir(src_skills):
204
+ for skill in sorted(os.listdir(src_skills)):
205
+ if not os.path.isdir(os.path.join(src_skills, skill)):
206
+ continue
207
+ # Nur wenn im Ziel aktiviert
208
+ if not os.path.isdir(os.path.join(root, *man["skills_dir"].split("/"), skill)):
209
+ continue
210
+ for dirpath, dirnames, filenames in os.walk(os.path.join(src_skills, skill)):
211
+ dirnames[:] = [d for d in dirnames if d != "__pycache__"]
212
+ for fn in sorted(filenames):
213
+ src = os.path.join(dirpath, fn)
214
+ innen = os.path.relpath(src, os.path.join(src_skills, skill))
215
+ out.append((os.path.relpath(src, HERE).replace(os.sep, "/"),
216
+ f"{man['skills_dir']}/{skill}/{innen}".replace(os.sep, "/")))
217
+ src_runtime = os.path.join(base, pack, "runtime")
218
+ if os.path.isdir(src_runtime):
219
+ for fn in sorted(os.listdir(src_runtime)):
220
+ if not fn.endswith(".md"):
221
+ continue
222
+ # Der Name in der Ablage traegt die Endung des Packs (bei cursor
223
+ # .mdc, CR-2026-155) - die Quelle bleibt *.md.
224
+ ziel = clientmap.regeldatei(man, fn)
225
+ # Nur wenn im Ziel aktiviert
226
+ if not os.path.exists(os.path.join(root, *man["pack_runtime_dir"].split("/"), ziel)):
227
+ continue
228
+ src = os.path.join(src_runtime, fn)
229
+ out.append((os.path.relpath(src, HERE).replace(os.sep, "/"),
230
+ f"{man['pack_runtime_dir']}/{ziel}"))
231
+ return out
232
+
233
+
234
+ def seed_relpaths(template: str, man: dict) -> list[str]:
235
+ out: list[str] = []
236
+ for rel in man["seed_paths"]:
237
+ out.extend(iter_files(template, rel))
238
+ return out
239
+
240
+
241
+ # Die Platzhalteraufloesung liegt in clientmap, weil der Validator sie ebenfalls braucht.
242
+ resolve_placeholders = clientmap.resolve_placeholders
243
+
244
+
245
+ # Frontmatter-Felder eines Skills, die eine Zusage tragen. Sie duerfen beim Rendern
246
+ # nicht einfach entfallen: Wer sie verwirft, verwirft eine Schutzaussage, und genau
247
+ # dieser Befundtyp zieht sich durch dieses Projekt - AP2-CC-01 fuer 'triggers', B01 fuer
248
+ # 'permissions'. Ein Pack darf ein solches Feld nur dann in drop_fields fuehren, wenn es
249
+ # den Ersatz benennt: entweder als Abbildung auf ein eigenes Feld oder als
250
+ # ausdruecklichen Begleitsatz, dass es keinen gibt und was stattdessen traegt.
251
+ #
252
+ # 'triggers' hat seit AP2-CC-01 eine Abbildung (model_invocation_field). 'permissions'
253
+ # hatte keine. Gemessen am 2026-09-12: 'allowed-tools' ist bei claude-code eine
254
+ # Vorabfreigabe und keine Beschraenkung (B01,
255
+ # tests/protocols/2026-09-12-B01-allowed-tools.md) - und das Feld 'permissions', das die
256
+ # Beschraenkung wirklich trug, stand in drop_fields. Die Zusage fiel damit doppelt aus,
257
+ # und beides war fuer sich genommen dokumentiert. Der Ersatz ist die globale
258
+ # Berechtigungsschicht samt Schutz-Hook; das ist weniger als eine Beschraenkung je Skill,
259
+ # und es gehoert hingeschrieben statt verschwiegen (CR-2026-050, D-50).
260
+ #
261
+ # Die Bauform ist dieselbe wie bei hook_tools_absent aus 0.30.0: Eine Abwesenheit wird
262
+ # deklariert, nie erraten.
263
+ ZUSAGENTRAGENDE_SKILLFELDER = {
264
+ "permissions": "skill_permissions_ersatz",
265
+ "triggers": "model_invocation_field",
266
+ }
267
+
268
+
269
+ # Welchen Eimer in hook_tools ein Verb der Quelle trifft, sagt clientmap.VERB_BRUECKE.
270
+ # Die Werkzeugnamen kommen aus hook_tools und NICHT aus skill_frontmatter.tool_names:
271
+ # Das fuehrt fuer 'edit' nur Edit und Write, hook_tools.write fuehrt zusaetzlich
272
+ # NotebookEdit. Fuer eine Vorabfreigabe ist der Unterschied harmlos, fuer eine SPERRE
273
+ # ist er eine Luecke - und eine Luecke in der Liste ist ausnutzbar (CR-2026-057 E2,
274
+ # D-28). Seit 0.40.0 steht die Bruecke an EINER Stelle: Bis dahin fuehrte sie hier
275
+ # 'edit', 'write', 'exec', 'read' und 'search' - drei Verben des Vokabulars der Quelle
276
+ # und zwei der Durchsetzungsschicht - und kannte 'grep' und 'glob' NICHT, obwohl jede
277
+ # der vierzehn Quellen sie im Nachbarfeld desselben Frontmatters fuehrt. Ein
278
+ # 'deny: glob' erzeugte deshalb lautlos keine Sperre (D-78, D-79).
279
+
280
+
281
+ def _deny_der_quelle(fm: str) -> list[str]:
282
+ """Die Eintraege unter permissions.deny, in der Reihenfolge der Quelle.
283
+
284
+ Gelesen wird nur der deny-Block unterhalb von permissions. Der allow-Block bleibt
285
+ ungelesen und wird nicht abgebildet: 'allowed-tools' ist bei den Clients dieses
286
+ Projekts eine Vorabfreigabe und keine Zusage (B01, CR-2026-057 E4).
287
+ """
288
+ m = re.search(r"^permissions:[ \t]*\n((?:[ \t]+\S.*\n)+)", fm, re.M)
289
+ if not m:
290
+ return []
291
+ block = m.group(1)
292
+ m2 = re.search(r"^([ \t]+)deny:[ \t]*\n((?:[ \t]+-[ \t]+.*\n)+)", block, re.M)
293
+ if not m2:
294
+ return []
295
+ return [z.strip().lstrip("-").strip() for z in m2.group(2).splitlines() if z.strip()]
296
+
297
+
298
+ def deny_abbilden(fm: str, man: dict) -> list[str]:
299
+ """Die Werkzeugnamen, die dieser Client fuer permissions.deny sperren kann.
300
+
301
+ Zwei Bauformen stehen in der Quelle, und nur eine ist abbildbar:
302
+
303
+ - GROBE VERBEN ('edit', 'exec') werden ueber hook_tools auf Werkzeugnamen
304
+ abgebildet. Sieben der zwoelf Quellskills tragen ausschliesslich diese Form.
305
+ - BEFEHLSGENAUE EINTRAEGE ('Exec(git push)') werden NICHT abgebildet. Gemessen am
306
+ 2026-09-13: Ein disallowed-tools-Eintrag mit Klammer laesst den Befehl LAUTLOS
307
+ durchlaufen - keine Verweigerung, keine Fehlermeldung. Wer ihn schreibt, hat gar
308
+ keine Schranke, nicht bloss eine groebere. Das ganze Werkzeug zu sperren waere
309
+ strenger als gemeint und machte die allow-Eintraege derselben Skills wirkungslos.
310
+ Die Nicht-Abbildung ist im Manifest deklariert, nicht verschwiegen
311
+ (skill_deny_unmapped, CR-2026-057 E3; Bauart wie hook_tools_absent, D-47).
312
+
313
+ Ein Verb AUSSERHALB des Vokabulars bricht seit 0.40.0 ab, statt lautlos
314
+ auszufallen (D-79). Bis dahin kannte die Bruecke 'grep' und 'glob' nicht - die
315
+ beiden meistgenannten Verben des Vokabulars, vierzehn Fundstellen je im
316
+ Nachbarfeld desselben Frontmatters -, und ein 'deny: glob' erzeugte gar keine
317
+ Sperre: keine Verweigerung, keine Meldung, der Validator 0 Fehler. Gemessen am
318
+ 2026-09-13 (tests/protocols/2026-09-13-gegenpruefung-werkzeugabbildung.md, M6).
319
+ """
320
+ eimer = man.get("hook_tools") or {}
321
+ ziel: list[str] = []
322
+ for eintrag in _deny_der_quelle(fm):
323
+ if "(" in eintrag:
324
+ continue # befehlsgenau - nicht ausdrueckbar, siehe Docstring
325
+ verb = eintrag.strip().lower()
326
+ if verb not in clientmap.VERB_BRUECKE:
327
+ raise clientmap.AbbildungsFehler(
328
+ f"{man.get('client', '?')}: permissions.deny nennt das Verb "
329
+ f"'{verb}', das Vokabular des Frontmatters kennt nur "
330
+ f"{list(clientmap.FRONTMATTER_VERBEN)}. Bis 0.39.0 fiel ein solcher "
331
+ f"Eintrag LAUTLOS aus: Der Skill trug eine Sperre im Quelltext und "
332
+ f"keine in der installierten Fassung (D-79)")
333
+ name = clientmap.VERB_BRUECKE[verb]
334
+ for w in eimer.get(name, []):
335
+ if w not in ziel:
336
+ ziel.append(w)
337
+ return ziel
338
+
339
+
340
+ def _zusagenfelder_pruefen(fm: str, fmt: dict, man: dict) -> None:
341
+ """Bricht ab, wenn ein zusagentragendes Feld ohne benannten Ersatz entfiele.
342
+
343
+ Geprueft wird nur, was die Quelle wirklich fuehrt: Ein Pack, dessen Skills das Feld
344
+ gar nicht tragen, wird nicht zur Erklaerung von etwas gezwungen, das es nicht gibt.
345
+ """
346
+ entfallend = set(fmt.get("drop_fields", []))
347
+ if fmt.get("tools_format") == "csv":
348
+ entfallend.add("allowed-tools")
349
+ for feld, ersatzfeld in sorted(ZUSAGENTRAGENDE_SKILLFELDER.items()):
350
+ if feld not in entfallend:
351
+ continue
352
+ if not re.search(rf"^{feld}:", fm, re.M):
353
+ continue
354
+ if str(fmt.get(ersatzfeld) or "").strip():
355
+ continue
356
+ raise clientmap.AbbildungsFehler(
357
+ f"{man.get('client', '?')}: Das Skill-Frontmatter-Feld '{feld}' traegt eine "
358
+ f"Zusage und steht in drop_fields - es entfiele ersatzlos. Das Pack MUSS in "
359
+ f"skill_frontmatter.{ersatzfeld} benennen, was an seine Stelle tritt, oder "
360
+ f"ausdruecklich festhalten, dass es keinen Ersatz gibt und was stattdessen "
361
+ f"traegt (D-18, D-50). Ein folgenloses Verwerfen ist der Befund AP2-CC-01"
362
+ )
363
+
364
+
365
+ def render_skill_frontmatter(text: str, man: dict) -> str:
366
+ """Bringt das Frontmatter eines Skills in die Form, die dieser Client erwartet.
367
+
368
+ Quellformat ist die reichere Listenform: allowed-tools als Liste, dazu permissions
369
+ und triggers. Ein Client, der nur eine kommagetrennte Werkzeugliste kennt, bekommt
370
+ sie umgeformt; Felder, die er nicht kennt, entfallen (K-18: Frontmatter nur mit
371
+ dokumentierten Feldern).
372
+
373
+ `triggers` faellt dabei nicht ersatzlos: Nennt das Manifest ein
374
+ `model_invocation_field`, wird die Aussage "dieser Skill ist nicht modellgetriggert"
375
+ in das Feld dieses Clients uebersetzt. Ohne diese Abbildung verfiel die Zusage S4 beim
376
+ Rendern - der Validator erzwang `triggers: [user]` in der Quelle, und die installierte
377
+ Fassung trug nichts davon (AP2-CC-01, D-26).
378
+
379
+ Seit 0.31.0 gilt das fuer **jedes** zusagentragende Feld: Steht es in drop_fields und
380
+ fuehrt die Quelle es, muss das Pack den Ersatz benennen - sonst scheitert die
381
+ Installation. Das Feld `permissions` fiel bis dahin genau so weg, wie `triggers` es
382
+ vor AP2-CC-01 tat: im Manifest deklariert, in der Wirkung unbemerkt (B01, D-50).
383
+ """
384
+ fmt = man.get("skill_frontmatter", {})
385
+ if not text.startswith("---\n") or "\n---\n" not in text:
386
+ return text
387
+ kopf, rumpf = text.split("\n---\n", 1)
388
+ fm = kopf[4:].rstrip("\n") + "\n"
389
+
390
+ # Vor jeder Umformung: Was entfiele hier, ohne dass ein Ersatz benannt ist?
391
+ _zusagenfelder_pruefen(fm, fmt, man)
392
+
393
+ # Vor dem Verwerfen lesen: die Quelle nennt triggers als Liste.
394
+ # Dasselbe gilt seit 0.35.0 fuer permissions.deny - auch das ist ein zusagentragendes
395
+ # Feld, und auch es stand bis dahin in drop_fields ohne Abbildung (CR-2026-057, D-65).
396
+ deny_feld = fmt.get("skill_deny_field")
397
+ verbotene = deny_abbilden(fm, man) if deny_feld else []
398
+ feld = fmt.get("model_invocation_field")
399
+ nur_nutzer = False
400
+ if feld:
401
+ m = re.search(r"^triggers:[ \t]*\n((?:[ \t]+-[ \t]+\S+[ \t]*\n)+)", fm, re.M)
402
+ ausloeser = [x.strip("- \t") for x in m.group(1).strip().split("\n")] if m else []
403
+ nur_nutzer = bool(ausloeser) and "model" not in ausloeser
404
+
405
+ m = re.search(r"^allowed-tools:[ \t]*\n((?:[ \t]+-[ \t]+\S+[ \t]*\n)+)", fm, re.M)
406
+ werkzeuge = [x.strip("- \t") for x in m.group(1).strip().split("\n")] if m else []
407
+ # Die Verben werden in BEIDEN Zweigen geprueft UND in beiden umgeschrieben. Bis
408
+ # 0.42.0 liess der Listen-Zweig das Feld unberuehrt, waehrend der Agenten-Renderer
409
+ # daneben immer umschrieb - dieselbe Abbildung desselben Manifests mit zwei
410
+ # Ergebnissen. Aufgefallen ist es am 2026-09-14, als devin-desktop seine gemessene
411
+ # Abbildung bekam: Das Agentenprofil trug danach find_file_by_name, die Skilldatei
412
+ # weiter glob - also weiter einen Namen, den dieser Client nicht fuehrt
413
+ # (CR-2026-065, D-87). Ein Schreibfehler im Frontmatter faellt weiterhin beim
414
+ # Installieren auf und nicht erst im uebernehmenden Projekt (D-78).
415
+ ziel: list[str] = []
416
+ for w in werkzeuge:
417
+ for y in clientmap.frontmatter_werkzeuge(man, "skill_frontmatter", w):
418
+ if y not in ziel:
419
+ ziel.append(y)
420
+
421
+ if fmt.get("tools_format") == "csv":
422
+ for f in ["allowed-tools"] + list(fmt.get("drop_fields", [])):
423
+ fm = re.sub(rf"^{f}:.*\n(?:[ \t]+\S.*\n)*", "", fm, flags=re.M)
424
+ if ziel:
425
+ fm = fm.rstrip("\n") + f"\nallowed-tools: {', '.join(ziel)}\n"
426
+ else:
427
+ for f in fmt.get("drop_fields", []):
428
+ fm = re.sub(rf"^{f}:.*\n(?:[ \t]+\S.*\n)*", "", fm, flags=re.M)
429
+ if ziel:
430
+ # An Ort und Stelle ersetzen, nicht ans Ende haengen: Bei einer
431
+ # Abbildung, die die Namen nicht aendert, bleibt die Datei dann
432
+ # zeichengleich, und der Diff eines Releases zeigt nur, was sich
433
+ # wirklich geaendert hat.
434
+ eintraege = "allowed-tools:\n" + "".join(f" - {y}\n" for y in ziel)
435
+ fm = re.sub(r"^allowed-tools:[ \t]*\n(?:[ \t]+-[ \t]+\S+[ \t]*\n)+",
436
+ lambda _m: eintraege, fm, count=1, flags=re.M)
437
+
438
+ if nur_nutzer:
439
+ fm = fm.rstrip("\n") + f"\n{feld}: true\n"
440
+
441
+ if verbotene:
442
+ fm = fm.rstrip("\n") + f"\n{deny_feld}: {', '.join(verbotene)}\n"
443
+
444
+ return "---\n" + fm + "---\n" + rumpf
445
+
446
+
447
+ def _globs_lesen(fm: str) -> list[str]:
448
+ """Dateimuster der Quelle - als Liste oder als einzelne Zeichenkette notiert."""
449
+ m = re.search(r"^globs:[ \t]*\n((?:[ \t]+-[ \t]+.*\n)+)", fm, re.M)
450
+ if m:
451
+ return [z.strip().lstrip("-").strip().strip("\"'") for z in m.group(1).splitlines() if z.strip()]
452
+ m = re.search(r"^globs:[ \t]*(\S.*?)[ \t]*$", fm, re.M)
453
+ return [m.group(1).strip().strip("\"'")] if m else []
454
+
455
+
456
+ def ist_regelquelle(src_rel: str) -> bool:
457
+ """Wahr fuer jede Quelle, die als Regeldatei in der Regelablage landet.
458
+
459
+ Drei Herkuenfte mit demselben Quellfrontmatter: die Core-Regeltexte, die beiden
460
+ Regelvorlagen und die Laufzeitfassungen aktivierter Role- und Technology Packs.
461
+ Alle drei muessen dieselbe Abbildung durchlaufen - eine Pack-Regel, deren
462
+ Ladebedingung beim Rendern verfaellt, waere derselbe Befund wie AP2-CC-03, nur
463
+ eine Ebene tiefer. Bis 0.14.0 lief nur die erste Herkunft durch render_rule.
464
+ """
465
+ if src_rel.startswith(("framework/runtime/rules/", "templates/rules/")):
466
+ return True
467
+ return bool(re.match(r"framework/(role|tech)-packs/[^/]+/runtime/[^/]+\.md$", src_rel))
468
+
469
+
470
+ def _regel_kommentar(desc: str, hinweis: str, herkunft: str) -> str:
471
+ return ("<!-- Laufzeitregel. Inhaltlich identisch zur Fassung anderer Client Packs;\n"
472
+ " abweichend ist nur die Ladebedingung.\n"
473
+ f" Zweck: {desc}\n"
474
+ f" Ladeverhalten: {hinweis}\n"
475
+ f" Entsprechung in der Kernquelle: {herkunft}. -->\n\n")
476
+
477
+
478
+ def render_rule(text: str, man: dict) -> str:
479
+ """Bringt einen Regeltext in die Form, die dieser Client erwartet.
480
+
481
+ Quellformat ist das YAML-Frontmatter mit `description`, `trigger` und - bei
482
+ `trigger: glob` - `globs`; das ist die reichere Form. Drei Bauarten:
483
+
484
+ 1. Der Client kennt dieselben Ladetrigger (`devin-desktop`): Quellform ist
485
+ Zielform, es ist nichts zu tun.
486
+ 2. Der Client kennt eine **eigene** Bedingungssprache (`rule_triggers` im
487
+ Manifest, bei `claude-code` das Feld `paths` mit Glob-Mustern): Jeder
488
+ Ladetrigger der Quelle wird darauf abgebildet. Ein Trigger ohne Eintrag in
489
+ der Abbildung laesst die Installation scheitern - ersatzloses Verwerfen waere
490
+ ein Verlust der Zusage (D-26, D-27). Felder, die der Client fuer Regeldateien
491
+ nicht dokumentiert, entfallen im Frontmatter (K-18) und stehen im Kommentar.
492
+ 3. Der Client kennt keine Ladebedingung (`rule_frontmatter: "comment"`): Aus dem
493
+ Frontmatter wird ein Kommentar, die Datei wirkt erst durch eine Einbindung.
494
+ Kein Pack nutzt diese Bauart derzeit.
495
+
496
+ Der Regeltext selbst bleibt in allen drei Faellen identisch.
497
+ """
498
+ bauart = man.get("rule_frontmatter", "yaml")
499
+ trigger_map = man.get("rule_triggers")
500
+ if bauart == "yaml" and not trigger_map:
501
+ return text
502
+ if not text.startswith("---\n") or "\n---\n" not in text:
503
+ return text
504
+ kopf, rumpf = text.split("\n---\n", 1)
505
+ fm = kopf[4:]
506
+ m = re.search(r"^description:\s*(.+)$", fm, re.M)
507
+ desc = m.group(1).strip() if m else ""
508
+ m = re.search(r"^trigger:\s*(\S+)\s*$", fm, re.M)
509
+ trig = m.group(1).strip() if m else ""
510
+
511
+ if bauart == "comment":
512
+ # Ohne Ladebedingung wirkt eine Regel nur ueber die Einbindung - und dann
513
+ # ausnahmslos. Fuer eine Regel, die auch in der Quelle always_on ist, aendert
514
+ # das nichts; fuer jede andere ist es eine Verschaerfung, die hier benannt wird.
515
+ hinweis = "Wird über den Import in der Wurzel-Anweisungsdatei immer geladen."
516
+ if trig != "always_on":
517
+ hinweis += (" Bei Clients mit Ladebedingungen lädt diese Datei nur bei Relevanz;"
518
+ " immer zu laden ist eine Verschärfung, keine Lockerung.")
519
+ return _regel_kommentar(desc, hinweis, f"trigger: {trig or 'unbekannt'}") + rumpf.lstrip("\n")
520
+
521
+ # Bauart 2: eigene Bedingungssprache des Clients.
522
+ abbildung = trigger_map.get("map", {})
523
+ if trig not in abbildung:
524
+ raise clientmap.AbbildungsFehler(
525
+ f"{man.get('client', '?')}: Ladetrigger '{trig or '(fehlt)'}' hat keinen Eintrag in "
526
+ f"rule_triggers.map - die Regel liesse sich nur durch Weglassen der Ladebedingung "
527
+ f"abbilden, und das waere ein Verlust der Zusage (D-27). Bekannt: "
528
+ f"{', '.join(sorted(abbildung)) or '(keine)'}")
529
+ eintrag = abbildung[trig] or {}
530
+ feld = eintrag.get("condition")
531
+ herkunft = f"trigger: {trig}"
532
+ kopfzeilen = ""
533
+ # Feste Frontmatter-Felder je Ladetrigger (seit 1.13.0, D-414): Der Client kiro
534
+ # verlangt neben dem Musterfeld den Lademodus als eigenes Feld - ohne
535
+ # "inclusion: fileMatch" laedt eine Datei mit fileMatchPattern IMMER.
536
+ fest = "".join(f"{k}: {v}\n" for k, v in (eintrag.get("fixed") or {}).items())
537
+ if feld:
538
+ quellfeld = eintrag.get("from", "globs")
539
+ if quellfeld != "globs":
540
+ raise clientmap.AbbildungsFehler(
541
+ f"{man.get('client', '?')}/manifest.json: rule_triggers.map['{trig}'].from nennt "
542
+ f"'{quellfeld}'; die Kernquelle fuehrt Dateimuster nur unter 'globs'")
543
+ muster = _globs_lesen(fm)
544
+ if not muster:
545
+ raise clientmap.AbbildungsFehler(
546
+ f"{man.get('client', '?')}: Ladetrigger '{trig}' bildet auf '{feld}' ab, die Quelle "
547
+ f"nennt aber keine Dateimuster - die Ladebedingung waere leer")
548
+ for wert in muster:
549
+ if '"' in wert:
550
+ raise clientmap.AbbildungsFehler(
551
+ f"{man.get('client', '?')}: Dateimuster '{wert}' enthaelt ein "
552
+ f"Anfuehrungszeichen und laesst sich nicht als {feld}-Eintrag notieren")
553
+ kopfzeilen = ("---\n" + fest + f"{feld}:\n" + "".join(f' - "{w}"\n' for w in muster)
554
+ + "---\n\n")
555
+ hinweis = (f"Lädt, sobald der Client eine Datei liest, die auf eines der Muster in "
556
+ f"`{feld}` passt.")
557
+ herkunft += f", globs: {', '.join(muster)}"
558
+ elif trig == "always_on":
559
+ hinweis = (f"Wird bei jedem Sitzungsstart geladen – eine Regeldatei ohne `"
560
+ f"{trigger_map.get('condition_field', 'Bedingungsfeld')}`-Feld lädt unbedingt "
561
+ f"und braucht keine Einbindung.")
562
+ else:
563
+ hinweis = (f"Wird bei jedem Sitzungsstart geladen. Dieser Client kennt für Regeldateien "
564
+ f"nur die Bedingung über Dateimuster; gegenüber `{trig}` ist unbedingtes Laden "
565
+ f"eine Verschärfung, keine Lockerung.")
566
+ if fest and not feld:
567
+ kopfzeilen = "---\n" + fest + "---\n\n"
568
+ # Ein Pack, dessen Client mehr Lademodi kennt, als es nutzt, sagt den Grund selbst
569
+ # (rule_triggers.map[...].hint) - der Standardsatz oben behauptete sonst, der Client
570
+ # kenne nur Dateimuster (D-414).
571
+ hinweis = eintrag.get("hint") or hinweis
572
+ return kopfzeilen + _regel_kommentar(desc, hinweis, herkunft) + rumpf.lstrip("\n")
573
+
574
+
575
+ def render_root_instruction(text: str, man: dict) -> str:
576
+ """Setzt an der Marke RUNTIME_IMPORTS die Einbindungen dieses Clients ein.
577
+
578
+ Ein Client, der Regeldateien nicht von sich aus laedt, braucht eine Einbindung in
579
+ der Wurzel-Anweisung; welche das sind, steht im Manifest. Beide ausgelieferten Packs
580
+ laden ihre Regelablage seit 0.15.0 selbst und lassen die Liste leer - dann faellt die
581
+ Marke ersatzlos weg. Die Mechanik bleibt fuer ein kuenftiges Pack erhalten und ist
582
+ damit von keinem Pack mehr erprobt (ROADMAP, "Bewusst offen gelassen").
583
+ """
584
+ marke = "<!-- RUNTIME_IMPORTS -->"
585
+ if marke not in text:
586
+ return text
587
+ imports = man.get("root_instruction_imports", []) or []
588
+ if not imports:
589
+ # Marke samt umgebender Leerzeile entfernen, damit die Datei genauso aussieht
590
+ # wie ohne Marke - sonst meldet --check bei jedem Projekt eine Scheindrift.
591
+ return text.replace(marke + "\n\n", "")
592
+ block = (man.get("root_instruction_imports_intro",
593
+ "Die folgenden Regeldateien sind Bestandteil dieser Anweisung "
594
+ "und werden mit ihr geladen:")
595
+ + "\n\n" + "\n".join(imports))
596
+ return text.replace(marke, block)
597
+
598
+
599
+ def render_agent(text: str, man: dict) -> str:
600
+ """Formt das Frontmatter eines Agentenprofils um (Feldname und Werkzeugformat)."""
601
+ fmt = man.get("agent_frontmatter")
602
+ if not fmt or not text.startswith("---\n") or "\n---\n" not in text:
603
+ return text
604
+ kopf, rumpf = text.split("\n---\n", 1)
605
+ fm = kopf[4:].rstrip("\n") + "\n"
606
+ m = re.search(r"^allowed-tools:[ \t]*\n((?:[ \t]+-[ \t]+\S+[ \t]*\n)+)", fm, re.M)
607
+ werkzeuge = [x.strip("- \t") for x in m.group(1).strip().split("\n")] if m else []
608
+ ziel: list[str] = []
609
+ for w in werkzeuge:
610
+ for y in clientmap.frontmatter_werkzeuge(man, "agent_frontmatter", w):
611
+ if y not in ziel:
612
+ ziel.append(y)
613
+ fm = re.sub(r"^allowed-tools:.*\n(?:[ \t]+\S.*\n)*", "", fm, flags=re.M)
614
+ # Ein Client ohne Werkzeugfeld, aber mit einem Schalter fuer ein nur lesendes Profil
615
+ # (cursor: readonly, CR-2026-155). Die Liste entfaellt; der Schalter steht genau
616
+ # dann, wenn sie weder edit noch exec nennt - ein Profil mit einem dieser Verben
617
+ # bekommt ihn nicht, und darin liegt keine Lockerung, weil ohne Schalter der Client
618
+ # sein Standardverhalten behaelt.
619
+ nur_lesend = fmt.get("readonly_field")
620
+ if nur_lesend:
621
+ if werkzeuge and not ({"edit", "exec"} & set(werkzeuge)):
622
+ fm = fm.rstrip("\n") + f"\n{nur_lesend}: true\n"
623
+ return "---\n" + fm + "---\n" + rumpf
624
+ feld = fmt.get("tools_field", "allowed-tools")
625
+ if ziel:
626
+ if fmt.get("tools_format") == "csv":
627
+ fm = fm.rstrip("\n") + f"\n{feld}: {', '.join(ziel)}\n"
628
+ else:
629
+ fm = fm.rstrip("\n") + f"\n{feld}:\n" + "".join(f" - {t}\n" for t in ziel)
630
+ return "---\n" + fm + "---\n" + rumpf
631
+
632
+
633
+ TESTBLATT_KOPF = "| Test-ID |"
634
+ TESTBLATT_ERGEBNIS = "Ergebnisstatus"
635
+ _TABELLENSTRICH = re.compile(r"(?<!\\)\|")
636
+
637
+
638
+ def ist_testblatt(src_rel: str) -> bool:
639
+ """Ein dezentrales Testblatt eines Skills - Kernskill oder Skill eines Packs."""
640
+ return os.path.basename(src_rel) == "TESTS.md" and "/skills/" in src_rel
641
+
642
+
643
+ def render_testblatt(text: str, src_rel: str) -> str:
644
+ """Die Ergebniszellen bleiben im Kern; die Laufzeitschicht bekommt nur die Testfaelle.
645
+
646
+ EIN TESTBLATT IST REGELQUELLE UND AUFZEICHNUNG ZUGLEICH (CR-2026-160, D-471, K-56).
647
+ Die Spalte Ergebnisstatus haelt fest, was im Quellrepositorium gemessen wurde - mit
648
+ Laufkennungen, Befunden und Protokollverweisen. In der Skillablage eines Projekts
649
+ liest der Client sie als Teil des Skills. Jede Zelle wird deshalb durch `offen` mit
650
+ Verweis auf die Kernfassung ersetzt: fuer das Projekt ist der Testfall ungemessen.
651
+ Die Kopie des Kerns im Projekt bleibt unberuehrt.
652
+
653
+ Eine Tabellenzeile darf ueber mehrere Textzeilen laufen; sie endet, wenn sie so viele
654
+ unmaskierte Striche traegt wie ihr Kopf. Stimmt die Zaehlung nicht, bricht die
655
+ Abbildung ab - eine halb ersetzte Zeile gaebe fremde Ergebnisse weiter.
656
+ """
657
+ kern = src_rel.replace("\\", "/")
658
+ ersatz = f" `offen` im Projekt – gemessen wird im Quellrepositorium; Ergebnis dort: `<CORE_DIR>/{kern}` (D-471) |"
659
+ aus: list[str] = []
660
+ striche = 0
661
+ offen: list[str] = []
662
+ for zeile in text.splitlines(keepends=True):
663
+ if offen:
664
+ offen.append(zeile)
665
+ elif striche and zeile.startswith("|") and not zeile.startswith("|---"):
666
+ offen = [zeile]
667
+ else:
668
+ if zeile.startswith(TESTBLATT_KOPF):
669
+ kopf = [z.strip() for z in _TABELLENSTRICH.split(zeile.strip())[1:-1]]
670
+ striche = len(_TABELLENSTRICH.findall(zeile)) if kopf and kopf[-1] == TESTBLATT_ERGEBNIS else 0
671
+ elif striche and not zeile.startswith("|"):
672
+ striche = 0
673
+ aus.append(zeile)
674
+ continue
675
+ zeilen = "".join(offen)
676
+ n = len(_TABELLENSTRICH.findall(zeilen))
677
+ if n < striche:
678
+ continue
679
+ if n > striche:
680
+ raise ValueError(f"{src_rel}: Tabellenzeile mit {n} statt {striche} Strichen - "
681
+ "Ergebnisspalte nicht sicher kuerzbar (D-471)")
682
+ positionen = [m.start() for m in _TABELLENSTRICH.finditer(zeilen)]
683
+ ende = "\n" if zeilen.endswith("\n") else ""
684
+ aus.append(zeilen[:positionen[-2] + 1] + ersatz + ende)
685
+ offen = []
686
+ if offen:
687
+ raise ValueError(f"{src_rel}: Tabellenzeile ohne Abschluss (D-471)")
688
+ return "".join(aus)
689
+
690
+
691
+ def render_for_client(text: str, man: dict, src_rel: str, dst_rel: str = "") -> str:
692
+ """Waehlt die Transformation anhand der Quelle und loest danach die Platzhalter auf.
693
+
694
+ DAS ZIEL ENTSCHEIDET MIT, SEIT ES EINE QUELLE MIT ZWEI ZIELEN GIBT (CR-2026-133,
695
+ D-346). Bei einem Client, dessen Berechtigungsschicht in zwei Traeger zerfaellt -
696
+ Pfade in der Konfigurationsdatei, Befehle in einer eigenen Regeldatei -, wird
697
+ dieselbe Kernquelle zweimal gerendert. Die Quelle allein traegt die Auskunft
698
+ darueber nicht mehr.
699
+ """
700
+ if src_rel == "framework/runtime/permissions.json":
701
+ policy = clientmap.exec_policy_file(man)
702
+ if policy and dst_rel == resolve_placeholders(policy, man):
703
+ return clientmap.render_exec_policy(text, man)
704
+ if clientmap.permissions_format(man) == "toml":
705
+ return clientmap.render_permissions_toml(text, man)
706
+ if clientmap.permissions_format(man) == "kiro-agent":
707
+ return clientmap.render_permissions_kiro(text, man)
708
+ if man.get("ignore_file") and dst_rel == man["ignore_file"]:
709
+ # Dieselbe Kernquelle, ein zweites Ziel (CR-2026-155, D-443): die
710
+ # Ausschlussdatei aus den Leseverboten.
711
+ return clientmap.render_ignore_cursor(text, man)
712
+ if clientmap.permissions_format(man) == "cursor-json":
713
+ return clientmap.render_permissions_cursor(text, man)
714
+ # Kennt der Client keine eigene Hook-Datei, wandern die Hooks hier mit hinein.
715
+ hooks = (clientmap.load_source(HERE, "hooks.json")
716
+ if clientmap.hooks_in_permissions(man) else None)
717
+ return clientmap.render_permissions(text, man, hooks)
718
+ if src_rel == "framework/runtime/hooks.json":
719
+ return clientmap.render_hooks(text, man)
720
+ if src_rel == "framework/runtime/client-settings.json":
721
+ # Die Quelle erklaert nur; den Inhalt traegt das Manifest (D-414).
722
+ return clientmap.render_client_settings(man)
723
+ if os.path.basename(src_rel) == "SKILL.md":
724
+ text = render_skill_frontmatter(text, man)
725
+ elif ist_testblatt(src_rel):
726
+ text = render_testblatt(text, src_rel)
727
+ elif ist_regelquelle(src_rel):
728
+ text = render_rule(text, man)
729
+ elif src_rel == "framework/runtime/root-instruction.md":
730
+ text = render_root_instruction(text, man)
731
+ elif src_rel.startswith("framework/runtime/agents/"):
732
+ text = render_agent(text, man)
733
+ return resolve_placeholders(text, man)
734
+
735
+
736
+ def shared_files(man: dict, schluessel: str) -> list[tuple[str, str]]:
737
+ """(Quelle relativ zu HERE, Ziel relativ zum Projekt) fuer geteilte Kernbestandteile.
738
+
739
+ Geteilt heisst: Der Inhalt gehoert dem Framework, nur der Zielpfad - und mitunter die
740
+ Form - haengt vom Client ab. Frueher lagen diese Dateien in jedem root-template
741
+ erneut; identische Dateien mussten doppelt gepflegt werden.
742
+
743
+ Das Manifest fuehrt sie unter shared_core (wird bei --update ueberschrieben) und
744
+ shared_seed (nur bei der Erstinstallation angelegt). Ein Eintrag ist
745
+ {"src": <Pfad relativ zu HERE>, "dst": <Zielpfad, darf Laufzeit-Platzhalter enthalten>};
746
+ ist src ein Verzeichnis, wird es rekursiv abgebildet.
747
+ """
748
+ out: list[tuple[str, str]] = []
749
+ for eintrag in man.get(schluessel, []) or []:
750
+ src_rel = eintrag["src"]
751
+ dst_rel = resolve_placeholders(eintrag["dst"], man)
752
+ voll = os.path.join(HERE, *src_rel.split("/"))
753
+ if os.path.isfile(voll):
754
+ out.append((src_rel, dst_rel))
755
+ elif os.path.isdir(voll):
756
+ for dirpath, dirnames, filenames in os.walk(voll):
757
+ dirnames[:] = [d for d in dirnames if d != "__pycache__"]
758
+ for fn in sorted(filenames):
759
+ src = os.path.join(dirpath, fn)
760
+ innen = os.path.relpath(src, voll).replace(os.sep, "/")
761
+ out.append((f"{src_rel}/{innen}", f"{dst_rel}/{innen}"))
762
+ return sorted(out, key=lambda x: x[1])
763
+
764
+
765
+ def framework_skill_files(man: dict) -> list[tuple[str, str]]:
766
+ """(Quelle relativ zu HERE, Ziel relativ zum Projekt) fuer alle Framework-Skills.
767
+
768
+ Die Skills liegen seit 0.5.0 einmal unter framework/skills/ und werden bei der
769
+ Installation in die Form des gewaehlten Clients gebracht. Vorher lagen sie je
770
+ Client Pack im root-template - identischer Rumpf, abweichendes Frontmatter,
771
+ doppelte Pflege.
772
+ """
773
+ quelle = os.path.join(HERE, "framework", "skills")
774
+ if not os.path.isdir(quelle):
775
+ return []
776
+ out: list[tuple[str, str]] = []
777
+ for skill in sorted(os.listdir(quelle)):
778
+ if not os.path.isdir(os.path.join(quelle, skill)):
779
+ continue
780
+ if not skill.startswith(man["core_skill_prefix"]):
781
+ continue
782
+ for dirpath, dirnames, filenames in os.walk(os.path.join(quelle, skill)):
783
+ dirnames[:] = [d for d in dirnames if d != "__pycache__"]
784
+ for fn in sorted(filenames):
785
+ src = os.path.join(dirpath, fn)
786
+ innen = os.path.relpath(src, os.path.join(quelle, skill)).replace(os.sep, "/")
787
+ out.append((os.path.relpath(src, HERE).replace(os.sep, "/"),
788
+ f"{man['skills_dir']}/{skill}/{innen}"))
789
+ return out
790
+
791
+
792
+ def write_rendered(src: str, dst: str, man: dict, dry: bool, dst_rel: str = "") -> None:
793
+ if dry:
794
+ return
795
+ text = render_for_client(read_text(src), man, quell_kennung(src), dst_rel)
796
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
797
+ with open(dst, "w", encoding="utf-8", newline="\n") as fh:
798
+ fh.write(text)
799
+
800
+
801
+ def read_text(path: str) -> str:
802
+ with open(path, encoding="utf-8") as fh:
803
+ return fh.read()
804
+
805
+
806
+ def quell_kennung(src: str) -> str:
807
+ """Pfad der Quelle relativ zum Kern - er entscheidet ueber die Formtransformation."""
808
+ return os.path.relpath(src, HERE).replace(os.sep, "/")
809
+
810
+
811
+ def rendered_matches(src: str, dst: str, man: dict, dst_rel: str = "") -> bool:
812
+ if not os.path.exists(dst):
813
+ return False
814
+ soll = render_for_client(read_text(src), man, quell_kennung(src), dst_rel)
815
+ return read_text(dst) == soll
816
+
817
+
818
+ def copy_file(src: str, dst: str, dry: bool) -> None:
819
+ if dry:
820
+ return
821
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
822
+ shutil.copy2(src, dst)
823
+
824
+
825
+ def pruefe_abbildung(root: str, man: dict, muster: dict | None = None) -> None:
826
+ """Rendert jede abzubildende Quelle einmal, bevor die erste Datei geschrieben wird.
827
+
828
+ D-26 verlangt, dass die Installation scheitert, wenn eine Aussage der Quelle sich
829
+ nicht abbilden laesst. Ohne diesen Vorlauf scheiterte sie mitten im Schreiben und
830
+ hinterliess ein halb angelegtes Projekt - gescheitert ist das eine, halb gelungen
831
+ das andere. Der Vorlauf kostet einen zweiten Rendervorgang und keine Schreiboperation.
832
+
833
+ Mit einem Overlay-Muster laeuft der Fuellschritt hier mit: Ein Muster, dessen Anker
834
+ in einer der drei Saatquellen fehlt, bricht vor dem ersten Schreibvorgang ab
835
+ (MusterFehler) und nicht nach der Haelfte.
836
+ """
837
+ quellen = list(activated_pack_relpaths(root, man))
838
+ quellen += framework_skill_files(man)
839
+ quellen += shared_files(man, "shared_core")
840
+ for src_rel, dst_rel in quellen:
841
+ src = os.path.join(HERE, *src_rel.split("/"))
842
+ render_for_client(read_text(src), man, quell_kennung(src), dst_rel)
843
+ for src_rel, dst_rel in shared_files(man, "shared_seed"):
844
+ render_mit_muster(os.path.join(HERE, *src_rel.split("/")), man, dst_rel, muster)
845
+
846
+
847
+ def ignorierte_kerndateien(root: str) -> list[str]:
848
+ """Die geschriebenen Kerndateien, die das aufnehmende Projekt ignoriert.
849
+
850
+ ANLASS, UND ER IST GEMESSEN (`K-117`, CR-2026-133, D-349). Beim Heben auf `1.3.0`
851
+ lagen in einem der beiden aufnehmenden Projekte 525 Kerndateien im Arbeitsbaum und
852
+ 483 im Versionierten: Die projekteigene `.gitignore`-Zeile `build/` trifft auch
853
+ `<CORE_DIR>/build/`, und damit die gesamte Quelle des Hauptdokuments.
854
+ ➡️ Ein Kern, der ausgeliefert, aber nicht versioniert wird, ist beim naechsten
855
+ Klonen dieses Projekts unvollstaendig.
856
+ Der Validator sieht es nicht, weil er den ARBEITSBAUM misst; Pruefung 81 sieht es
857
+ nicht, weil sie die Zeilenendeform der VERFOLGTEN Traeger misst - also gerade
858
+ derer, die noch da sind.
859
+
860
+ DIES IST EINE AUSKUNFT UND KEINE SCHRANKE, und das ist die Entscheidung
861
+ (D-349, Bauform von D-34): Das `.gitignore` gehoert dem Projekt. Das Framework
862
+ sagt, was es beobachtet, und ueberlaesst dem Projekt, was daraus folgt; der
863
+ Uebernahmeleitfaden nennt die Negativregel.
864
+
865
+ Ohne Git - oder ausserhalb eines Repositoriums - gibt die Funktion eine leere
866
+ Liste zurueck. Eine Auskunft, die nicht erhoben werden kann, wird nicht behauptet.
867
+
868
+ DIE PFADE GEHEN ALS BYTES MIT NUL-TRENNUNG HIN UND ZURUECK (`-z`, CR-2026-135,
869
+ D-352). Bis 1.4.1 gingen sie mit `text=True` als Zeilen hin: Unter Windows wird
870
+ daraus `\\r\\n`, git bekam jeden Pfad mit angehaengtem `\\r`, verglich ihn so gegen
871
+ die Muster und gab ihn gequotet zurueck. Ein Verzeichnismuster (`build/`) traf
872
+ trotzdem, weil es am Elternverzeichnis greift; ein DATEImuster (`*.md`) traf nie -
873
+ gemessen: git meldet zwei ignorierte Dateien, die Funktion null. Und ohne `-z`
874
+ quotet git jeden Pfad mit Nicht-ASCII-Zeichen (`core.quotePath`); die Ausgabe ist
875
+ dann kein Pfad mehr. `-z` schaltet beides ab.
876
+ """
877
+ kern = os.path.join(root, *clientmap.CORE_REL.split("/"))
878
+ if not os.path.isdir(kern):
879
+ return []
880
+ dateien: list[str] = []
881
+ for dirpath, dirnames, filenames in os.walk(kern):
882
+ dirnames[:] = [d for d in dirnames if d != "__pycache__"]
883
+ for fn in filenames:
884
+ rel = os.path.relpath(os.path.join(dirpath, fn), root).replace(os.sep, "/")
885
+ dateien.append(rel)
886
+ if not dateien:
887
+ return []
888
+ try:
889
+ proc = subprocess.run(["git", "-C", root, "check-ignore", "-z", "--stdin"],
890
+ input=b"\0".join(d.encode("utf-8") for d in dateien) + b"\0",
891
+ capture_output=True, timeout=60)
892
+ except (OSError, subprocess.SubprocessError):
893
+ return []
894
+ # Exit 0 = mindestens einer ignoriert, 1 = keiner, alles andere ist kein Ergebnis.
895
+ if proc.returncode not in (0, 1):
896
+ return []
897
+ return sorted(z.decode("utf-8", "replace").replace("\\", "/")
898
+ for z in proc.stdout.split(b"\0") if z)
899
+
900
+
901
+ # Das Kennzeichen des Framework-Repositoriums (D-351). Der Validator liest dieselbe
902
+ # Datei unter demselben Pfad (QUELLREPO_KENNZEICHEN); Sonde D354 haelt beide Stellen
903
+ # an einer echten Installation gegeneinander.
904
+ QUELLREPO_KENNZEICHEN = ".koolie/QUELLREPOSITORIUM.md"
905
+
906
+
907
+ _FEHLT = object()
908
+
909
+
910
+ def _json_punktpfad(pfad: str, schluessel: str) -> object:
911
+ """Der Wert unter einem Punktpfad einer JSON-Datei - oder _FEHLT."""
912
+ try:
913
+ with open(pfad, encoding="utf-8-sig") as fh:
914
+ wert = json.load(fh)
915
+ except (OSError, ValueError):
916
+ return _FEHLT
917
+ for teil in filter(None, schluessel.split(".")):
918
+ if not isinstance(wert, dict) or teil not in wert:
919
+ return _FEHLT
920
+ wert = wert[teil]
921
+ return wert
922
+
923
+
924
+ def belegte_importkanaele(root: str, man: dict) -> list[tuple[str, str]]:
925
+ """Die Importkanaele aus dem Manifest, die auf diesem Arbeitsplatz belegt sind.
926
+
927
+ ANLASS, UND ER IST GEMESSEN (K-156, D-411). Bei devin-desktop schaltet die
928
+ Importsteuerung, die fremde Windsurf-Quellen fernhaelt, zugleich die eigene
929
+ Regelablage ab. Das Pack laesst sie deshalb seit 1.12.1 zu - und damit auch eine
930
+ Regel aus dem Benutzerprofil, die in jeder Sitzung laedt (D-290). Das Manifest nennt
931
+ diese Kanaele (import_channels_report); dieses Werkzeug meldet, welche davon belegt
932
+ sind: eine nicht leere Datei, eine vorhandene Datei, ein vorhandenes Verzeichnis.
933
+
934
+ DIES IST EINE AUSKUNFT UND KEINE SCHRANKE: Was im Kanal steht, beurteilt der Mensch,
935
+ und die Benutzerkonfiguration des Clients hat ohnehin Vorrang (K-27).
936
+ """
937
+ belegt: list[tuple[str, str]] = []
938
+ for kanal in man.get("import_channels_report") or []:
939
+ rel = kanal.get("path", "")
940
+ if kanal.get("scope") == "user":
941
+ pfad = os.path.expanduser(rel)
942
+ else:
943
+ pfad = os.path.join(root, *rel.split("/"))
944
+ wann = kanal.get("when", "exists")
945
+ if wann == "nonempty":
946
+ treffer = os.path.isfile(pfad) and os.path.getsize(pfad) > 0
947
+ elif wann == "entries":
948
+ # Ein Verzeichnis mit mindestens einem Eintrag - ein leeres legt der
949
+ # Client selbst an und traegt nichts (D-414).
950
+ treffer = os.path.isdir(pfad) and bool(os.listdir(pfad))
951
+ elif wann in ("json_key", "json_not"):
952
+ # Seit 1.16.0 (CR-2026-155): Bei cursor zaehlt nicht, DASS eine Datei da ist,
953
+ # sondern was darin steht - Hooks in ~/.claude/settings.json laufen in jeder
954
+ # Sitzung dieses Clients mit (gemessen), und die Commit-Attribution steht nur global.
955
+ # json_key: der Schluessel (Punktpfad) ist belegt. json_not: sein Wert ist
956
+ # nicht der genannte - auch wenn er fehlt, denn dann gilt der Standard des
957
+ # Clients.
958
+ wert = _json_punktpfad(pfad, kanal.get("key", ""))
959
+ if wann == "json_key":
960
+ treffer = bool(wert) and wert is not _FEHLT
961
+ else:
962
+ treffer = os.path.isfile(pfad) and wert != kanal.get("value")
963
+ else:
964
+ treffer = os.path.exists(pfad)
965
+ if treffer:
966
+ belegt.append((rel, kanal.get("what", "")))
967
+ return belegt
968
+
969
+
970
+ def fehlende_zusatzschluessel(root: str, man: dict) -> list[tuple[str, str, object]]:
971
+ """Deklarierte Zusatzschluessel, die in der vorhandenen Berechtigungsdatei fehlen
972
+ oder einen anderen Wert tragen - als (Feld, Schluessel, deklarierter Wert).
973
+
974
+ ANLASS, UND ER IST ZWEIMAL EINGETRETEN (D-154, D-434). --update fasst die
975
+ Berechtigungsdatei nie an, weil sie Projektwerte traegt. Ein Schluessel, den ein
976
+ Release in settings_extra oder permissions_extra neu deklariert, erreicht deshalb
977
+ jede Erstinstallation und kein bestehendes Projekt - so bei autoMemoryEnabled
978
+ (1.0-Reihe, D-154) und bei attribution (1.14.2, D-433). Beide Male stand der
979
+ Nachtrag nur im Migrationshinweis.
980
+
981
+ DIES IST EINE AUSKUNFT UND KEINE SCHRANKE: Das Werkzeug schreibt nichts. Eine
982
+ Abweichung kann gewollt sein; ihr Ort ist dann die nutzerlokale Datei des Clients,
983
+ und das entscheidet der Mensch. Pruefung 54 misst dieselbe Deklaration an der
984
+ ERZEUGTEN Datei, nicht an der eines Projekts.
985
+ """
986
+ if clientmap.permissions_format(man) != "json":
987
+ return []
988
+ pfad = os.path.join(root, *man.get("permissions_file", "").split("/"))
989
+ try:
990
+ cfg = json.loads(open(pfad, encoding="utf-8").read())
991
+ except (OSError, ValueError):
992
+ return [] # fehlt oder kaputt: das melden der Validator und Pruefung 3
993
+ if not isinstance(cfg, dict):
994
+ return []
995
+ abweichend: list[tuple[str, str, object]] = []
996
+ for feld in ("settings_extra", "permissions_extra"):
997
+ ebene = cfg if feld == "settings_extra" else cfg.get("permissions", {})
998
+ if not isinstance(ebene, dict):
999
+ ebene = {}
1000
+ for schluessel, wert in (man.get(feld) or {}).items():
1001
+ if ebene.get(schluessel, object()) != wert:
1002
+ abweichend.append((feld, schluessel, wert))
1003
+ return abweichend
1004
+
1005
+
1006
+ def veraltete_overlaysperre(root: str, man: dict) -> list[str]:
1007
+ """Zeilen der Berechtigungsdatei, die das Overlay noch sperren (CR-2026-156, D-448).
1008
+
1009
+ Bis 1.16.0 erzeugte die Kernquelle eine Schreibsperre auf .koolie/project-overlay/**.
1010
+ Seit 1.17.0 sperrt das Overlay allein der Schutz-Hook, und ein Mandat des Menschen
1011
+ hebt die Sperre fuer ein gedecktes Ziel auf (Modus M6). Eine statische Sperre in der
1012
+ Berechtigungsdatei hebt kein Mandat auf - steht die alte Regel noch da, bleibt M6
1013
+ wirkungslos. --update schreibt die Datei nicht (sie traegt Projektwerte); es nennt die
1014
+ Zeilen. Eine Auskunft, keine Schranke - das Entfernen ist eine Lockerung und damit
1015
+ Sache des Menschen.
1016
+ """
1017
+ pfad = os.path.join(root, *man.get("permissions_file", "").split("/"))
1018
+ try:
1019
+ zeilen = open(pfad, encoding="utf-8").read().splitlines()
1020
+ except OSError:
1021
+ return []
1022
+ return [z.strip() for z in zeilen
1023
+ if "project-overlay/**" in z and "deny_must_contain" not in z]
1024
+
1025
+
1026
+ def koexistenz_auskunft(root: str, man: dict) -> list[str]:
1027
+ """Die Auskunft zu fremden Agenten-Rahmenwerken im Projekt (1.21.0, K-31, D-514).
1028
+
1029
+ GEMESSEN am 2026-09-30: OpenSpec und GitHub Spec Kit aendern keine Datei von Koolie und
1030
+ Koolie keine ihrer Dateien - in beiden Reihenfolgen. Die Reibung ist der geteilte
1031
+ Namensraum der Skills (Pruefung 111) und, bei einem Generator, der in die Wurzel-Anweisung
1032
+ schreibt, das Budget der stets geladenen Texte (Pruefung 4, K-185) - diesen Fall faengt
1033
+ schon der Abbruch vor der Aktualisierung (D-515). Eine Auskunft und keine Schranke:
1034
+ install.py schreibt in keiner fremden Datei.
1035
+ """
1036
+ befunde = koexistenz.erkennen(root, man["skills_dir"])
1037
+ if not befunde:
1038
+ return []
1039
+ deklariert = koexistenz.deklariert(root)
1040
+ aus = [f"HINWEIS ({len(befunde)}): In diesem Projekt liegt ein fremdes Agenten-Rahmenwerk "
1041
+ f"neben Koolie (K-31):"]
1042
+ offen = []
1043
+ for b in befunde:
1044
+ teile = []
1045
+ if b["skills"]:
1046
+ teile.append(f"{len(b['skills'])} Skill(s) in {man['skills_dir']}/")
1047
+ teile += b["spuren"]
1048
+ aus.append(f" {b['name']}: {', '.join(teile)}")
1049
+ if b["praefix"] and not any(koexistenz.ist_deklariert(s, deklariert) for s in b["skills"]):
1050
+ if b["skills"]:
1051
+ offen.append(b["praefix"])
1052
+ aus.append("Koolie traegt Ebene 1 (Sicherheit, Berechtigungen, Schutz-Hook); das fremde")
1053
+ aus.append("Rahmenwerk seine Prozessartefakte in seiner eigenen Ablage. Keines aendert die")
1054
+ aus.append("Dateien des anderen (gemessen fuer OpenSpec und Spec Kit, 2026-09-30).")
1055
+ if offen:
1056
+ aus.append("Seine Skills folgen nicht den Regeln fuer Koolie-Skills. Im Overlay-Manifest")
1057
+ aus.append(f"deklarieren, damit der Validator sie nicht als solche prueft (Pruefung 111):")
1058
+ aus.append(f" fremde_skills: {', '.join(offen)}")
1059
+ if any(b["skills"] for b in befunde):
1060
+ aus.append(f"Jeden fremden Skill in {man['permissions_file']} einem Korb zuordnen (allow")
1061
+ aus.append("oder ask), sonst faellt sein Aufruf in die Rueckfrage (Pruefung 72, D-238).")
1062
+ aus.append("Uebernahmeleitfaden Abschnitt 8. Dies ist eine Auskunft und keine Schranke.")
1063
+ return aus
1064
+
1065
+
1066
+ def traegt_quellrepo_kennzeichen(root: str) -> bool:
1067
+ """Liegt im Ziel das Kennzeichen des Framework-Repositoriums?
1068
+
1069
+ ANLASS, UND ER IST GEMESSEN (CR-2026-137, D-354). Das Kennzeichen liegt NEBEN dem
1070
+ Kern, und wer ganz `.koolie/` statt nur `.koolie/core/` kopiert, nimmt es mit - aus
1071
+ dem Arbeitsbaum des Frameworks ebenso wie aus dem Release-Archiv, denn es ist
1072
+ versioniert. Danach haelt der Validator das Projekt fuer das Quellrepositorium:
1073
+ Pruefung 79 verlangt die Lizenz in der Projektwurzel, und sobald das Projekt die
1074
+ Datei committet, misst Pruefung 81 die Zeilenenden jedes Projekttraegers. Keine der
1075
+ Meldungen nennt die Ursache. Aus dem Arbeitsbaum kommt zugleich das Overlay des
1076
+ Frameworks mit und ersetzt das des Projekts - still, weil install.py das Overlay
1077
+ nie anfasst und es deshalb als unberuehrt meldet.
1078
+
1079
+ DIES IST EINE AUSKUNFT UND KEINE SCHRANKE (D-354, Bauform von D-349). Ein Abbruch
1080
+ traefe das Framework-Repositorium selbst, das seine Wurzeldateien mit demselben
1081
+ Aufruf erzeugt; kein Merkmal trennt beide Faelle sicher - auch nicht, ob git die
1082
+ Datei verfolgt, denn ein Projekt, das die Kopie committet hat, sieht dann genauso
1083
+ aus.
1084
+ """
1085
+ return os.path.isfile(os.path.join(root, *QUELLREPO_KENNZEICHEN.split("/")))
1086
+
1087
+
1088
+ # ---------------------------------------------------------------------------
1089
+ # Overlay-Muster (--overlay <name>, D-126) und der Fuellschritt (D-353, D-355)
1090
+ # ---------------------------------------------------------------------------
1091
+ # ANLASS, UND ER IST GEMESSEN (CR-2026-136, 2.3). Ein Muster, das nur OVERLAY.md
1092
+ # vorbefuellt, erreicht die Schicht nicht, die sperrt: An einer Wegwerf-Installation
1093
+ # stand Read(<EXCLUDED_PATHS>) danach woertlich im deny-Korb, und Pruefung 59 enthielt
1094
+ # sich. Der Fuellschritt schreibt deshalb DREI Traeger aus EINER Quelle - der
1095
+ # Wertetabelle des Musters -, und zwar genau einmal, bei der Erstinstallation.
1096
+ #
1097
+ # ABGRENZUNG ZU D-76. Gelesen wird ein Traeger des KERNS, nicht des Projekts, und
1098
+ # zugesagt ist der ANFANGSZUSTAND, kein Kanal. --update liest das Muster nie.
1099
+ #
1100
+ # DIE GRENZE STEHT IM WERKZEUG, NICHT NUR IM MUSTER (CR-2026-138 E1). Ein Muster darf
1101
+ # ausschliesslich Platzhalter fuellen, deren Wert in einem deny-Eintrag steht - es
1102
+ # SPERRT, es gibt nichts frei. Fuehrt eine Wertedatei einen anderen Platzhalter, bricht
1103
+ # die Installation ab, bevor die erste Datei geschrieben ist.
1104
+ MUSTER_ABLAGE = os.path.join(HERE, "framework", "overlay-patterns")
1105
+ MUSTER_PLATZHALTER = ("<CI_CONFIG_PATHS>", "<QUALITY_GATE_CONFIG_PATHS>", "<EXCLUDED_PATHS>")
1106
+ MUSTER_QUELLE_RECHTE = "framework/runtime/permissions.json"
1107
+ MUSTER_QUELLE_LAUFZEIT = "framework/runtime/rules/20-project-overlay.md"
1108
+ MUSTER_QUELLE_OVERLAY = "templates/project-overlay/OVERLAY.md"
1109
+ MUSTER_WERTEABSCHNITT = "Die Werte"
1110
+ # DER ZWEITE GEGENSTAND: DOKUMENTE (CR-2026-139, D-359, D-360). Ein Muster darf neben
1111
+ # den Werten Dokumente mitliefern - allgemeine Praktiken, die auf jedes Projekt passen.
1112
+ # Sie liegen unter <muster>/documents/<typ>/, werden bei der Erstinstallation nach
1113
+ # .koolie/project-overlay/documents/<typ>/ geschrieben und im Manifest registriert:
1114
+ # K1, Status entwurf, Laden on-demand. Sie geben nichts frei - die Liste der
1115
+ # freigegebenen K1-Dokumente in der Laufzeitfassung bleibt ein Ausfuellschlitz, bis
1116
+ # der Overlay Owner sie fuellt.
1117
+ #
1118
+ # EIN REGISTER, NICHT ZWEI. Welche Dokumente es gibt, sagt die Ablage; die Tabelle im
1119
+ # Abschnitt "Die Dokumente" der Musterdatei beschreibt sie, und install.py haelt beide
1120
+ # gegeneinander. Ein Typ ist gueltig, wenn die Dokumentablage der Overlay-Vorlage ein
1121
+ # Verzeichnis dieses Namens fuehrt - dieselbe Ablage, in die geschrieben wird.
1122
+ MUSTER_DOKUMENTABSCHNITT = "Die Dokumente"
1123
+ MUSTER_DOKUMENTVERMERK = "Muster – vom Overlay Owner zu prüfen und anzupassen."
1124
+ MUSTER_QUELLE_MANIFEST = "templates/project-overlay/overlay-manifest.yaml"
1125
+ MUSTER_DOKUMENTZIEL = ".koolie/project-overlay/documents"
1126
+ MUSTER_TYPABLAGE = os.path.join(HERE, "templates", "project-overlay", "documents")
1127
+
1128
+
1129
+ class MusterFehler(Exception):
1130
+ """Ein Muster laesst sich nicht vollstaendig anwenden - die Installation bricht ab."""
1131
+
1132
+
1133
+ def verfuegbare_muster() -> list[str]:
1134
+ """Die Namen der mitgelieferten Overlay-Muster (Dateiname ohne .md)."""
1135
+ if not os.path.isdir(MUSTER_ABLAGE):
1136
+ return []
1137
+ return sorted(fn[:-3] for fn in os.listdir(MUSTER_ABLAGE)
1138
+ if fn.endswith(".md") and fn != "README.md")
1139
+
1140
+
1141
+ def muster_laden(name: str) -> dict:
1142
+ """Liest Name, Version und Wertetabelle eines Musters und prueft seine Grenze.
1143
+
1144
+ Die Werte stehen je in einer Codespanne der Spalte "Werte"; die Zeile wird ueber
1145
+ die Platzhalterzelle gefunden, nie ueber eine Spaltennummer - dieselbe Lesart wie
1146
+ Pruefung 42 und 59.
1147
+ """
1148
+ if name not in verfuegbare_muster():
1149
+ raise MusterFehler(f"Unbekanntes Overlay-Muster '{name}'. Verfuegbar: "
1150
+ f"{', '.join(verfuegbare_muster()) or 'keines'}")
1151
+ pfad = os.path.join(MUSTER_ABLAGE, name + ".md")
1152
+ version = None
1153
+ werte: dict[str, list[str]] = {}
1154
+ abschnitt = ""
1155
+ for zeile in read_text(pfad).split("\n"):
1156
+ if zeile.startswith("## "):
1157
+ abschnitt = zeile[3:].strip()
1158
+ continue
1159
+ zellen = [z.strip() for z in zeile.strip().strip("|").split("|")]
1160
+ if zellen and zellen[0] == "Version" and version is None and len(zellen) >= 2:
1161
+ version = zellen[1].strip("`")
1162
+ continue
1163
+ # Nur die Wertetabelle zaehlt. Eine andere Tabelle derselben Datei darf einen
1164
+ # Platzhalter in der ersten Spalte nennen - etwa den Traeger <PERMISSIONS_FILE> -,
1165
+ # ohne dass er zum Musterwert wird (gefunden beim ersten Lauf, CR-2026-138).
1166
+ if abschnitt != MUSTER_WERTEABSCHNITT:
1167
+ continue
1168
+ if len(zellen) < 2:
1169
+ continue
1170
+ m = re.fullmatch(r"`(<[A-Z_]+>)`", zellen[0])
1171
+ if not m:
1172
+ continue
1173
+ platzhalter = m.group(1)
1174
+ if platzhalter not in MUSTER_PLATZHALTER:
1175
+ raise MusterFehler(
1176
+ f"Das Muster '{name}' fuellt {platzhalter}. Ein Muster darf nur sperren: "
1177
+ f"zulaessig sind {', '.join(MUSTER_PLATZHALTER)} (CR-2026-138 E1)")
1178
+ liste = re.findall(r"`([^`]+)`", zellen[1])
1179
+ if not liste or any("<" in w or ">" in w for w in liste):
1180
+ raise MusterFehler(f"Das Muster '{name}' fuehrt fuer {platzhalter} keinen "
1181
+ f"gueltigen Wert: {zellen[1]}")
1182
+ werte[platzhalter] = liste
1183
+ if not werte or not version:
1184
+ raise MusterFehler(f"Das Muster '{name}' traegt keine Version oder keine Werte "
1185
+ f"(Abschnitt '## {MUSTER_WERTEABSCHNITT}')")
1186
+ return {"name": name, "version": version, "werte": werte,
1187
+ "dokumente": muster_dokumente(name)}
1188
+
1189
+
1190
+ def muster_dokumente(name: str) -> list[dict]:
1191
+ """Die Dokumente eines Musters, gegen die Beschreibung in seiner Musterdatei gehalten.
1192
+
1193
+ Jedes Dokument liegt unter <muster>/documents/<typ>/<datei>.md, traegt als erste
1194
+ Zeile eine Ueberschrift der Ebene 1 (sie wird der Titel im Manifest) und den
1195
+ Vorschlagsvermerk. Die Tabelle im Abschnitt "Die Dokumente" nennt in ihrer ersten
1196
+ Spalte genau die Typen, fuer die ein Dokument da ist - nicht mehr und nicht weniger.
1197
+ Jede Abweichung bricht ab, bevor die erste Datei geschrieben ist.
1198
+ """
1199
+ ablage = os.path.join(MUSTER_ABLAGE, name, "documents")
1200
+ beschrieben: set[str] = set()
1201
+ abschnitt = ""
1202
+ for zeile in read_text(os.path.join(MUSTER_ABLAGE, name + ".md")).split("\n"):
1203
+ # Jede Ueberschrift beendet den Abschnitt, auch eine der Ebene 3: Darunter steht
1204
+ # eine Feldtabelle (`status`, `load`), deren erste Spalte sonst als Typ gelaese.
1205
+ if zeile.startswith("#"):
1206
+ abschnitt = zeile.lstrip("#").strip()
1207
+ continue
1208
+ if abschnitt != MUSTER_DOKUMENTABSCHNITT:
1209
+ continue
1210
+ zellen = [z.strip() for z in zeile.strip().strip("|").split("|")]
1211
+ m = re.fullmatch(r"`([a-z][a-z-]*)`", zellen[0]) if zellen else None
1212
+ if m:
1213
+ beschrieben.add(m.group(1))
1214
+ dokumente: list[dict] = []
1215
+ if os.path.isdir(ablage):
1216
+ for typ in sorted(os.listdir(ablage)):
1217
+ verz = os.path.join(ablage, typ)
1218
+ if not os.path.isdir(verz):
1219
+ continue
1220
+ if not os.path.isdir(os.path.join(MUSTER_TYPABLAGE, typ)):
1221
+ raise MusterFehler(f"Das Muster '{name}' liefert ein Dokument vom Typ "
1222
+ f"'{typ}', den die Dokumentablage der Overlay-Vorlage "
1223
+ f"nicht kennt")
1224
+ for fn in sorted(os.listdir(verz)):
1225
+ if not fn.endswith(".md"):
1226
+ continue
1227
+ text = read_text(os.path.join(verz, fn))
1228
+ kopf = text.split("\n", 1)[0].strip()
1229
+ if not kopf.startswith("# ") or MUSTER_DOKUMENTVERMERK not in text:
1230
+ raise MusterFehler(f"Das Muster '{name}': {typ}/{fn} traegt keine "
1231
+ f"Ueberschrift der Ebene 1 in der ersten Zeile oder "
1232
+ f"nicht den Vermerk '{MUSTER_DOKUMENTVERMERK}'")
1233
+ dokumente.append({"typ": typ, "datei": fn, "titel": kopf[2:].strip(),
1234
+ "quelle": os.path.join(verz, fn)})
1235
+ geliefert = {d["typ"] for d in dokumente}
1236
+ if geliefert != beschrieben:
1237
+ raise MusterFehler(
1238
+ f"Das Muster '{name}': Ablage und Beschreibung stimmen nicht ueberein - "
1239
+ f"nur abgelegt: {', '.join(sorted(geliefert - beschrieben)) or 'nichts'}; "
1240
+ f"nur beschrieben: {', '.join(sorted(beschrieben - geliefert)) or 'nichts'} "
1241
+ f"(Abschnitt '## {MUSTER_DOKUMENTABSCHNITT}')")
1242
+ return dokumente
1243
+
1244
+
1245
+ def muster_manifest(text: str, muster: dict) -> str:
1246
+ """Ersetzt die Beispieleintraege der Manifestvorlage durch die Dokumente des Musters.
1247
+
1248
+ Die Vorlage fuehrt unter `documents:` drei Beispiele mit Ausfuellschlitzen. Neben
1249
+ echten Eintraegen waeren sie ein Register mit Eintraegen, die auf nichts zeigen;
1250
+ sie werden deshalb ersetzt, nicht ergaenzt (D-360).
1251
+ """
1252
+ if not muster["dokumente"]:
1253
+ return text
1254
+ anker = "\ndocuments:\n"
1255
+ if text.count(anker) != 1:
1256
+ raise MusterFehler("overlay-manifest.yaml: der Schluessel 'documents:' steht nicht "
1257
+ "genau einmal am Zeilenanfang - die Eintraege des Musters haetten "
1258
+ "keinen Ort")
1259
+ eintraege = []
1260
+ for nr, dok in enumerate(muster["dokumente"], start=1):
1261
+ titel = dok["titel"].replace('"', "'")
1262
+ eintraege.append(
1263
+ f' - id: "DOC-{nr:03d}"\n'
1264
+ f' type: "{dok["typ"]}"\n'
1265
+ f' title: "{titel}"\n'
1266
+ f' path: "{MUSTER_DOKUMENTZIEL}/{dok["typ"]}/{dok["datei"]}"\n'
1267
+ f' context_class: "K1"\n'
1268
+ f' status: "entwurf"\n'
1269
+ f' load: "on-demand"\n'
1270
+ f' approved_by: "<APPROVAL_ROLE>"\n'
1271
+ f' approved_on: "<TBD: Datum der Freigabe durch den Overlay Owner>"\n'
1272
+ f' sanitization: "nicht erforderlich"\n'
1273
+ f' notes: "Aus dem Overlay-Muster {muster["name"]} {muster["version"]} - vom '
1274
+ f'Overlay Owner zu pruefen und anzupassen; erst nach Freigabe in der '
1275
+ f'Laufzeitfassung als K1-Dokument fuehren"\n')
1276
+ return text[:text.index(anker)] + anker + "\n".join(eintraege)
1277
+
1278
+
1279
+ def muster_berechtigungen(quelltext: str, muster: dict) -> str:
1280
+ """Die Kernquelle der Berechtigungen, jeder Schlitz des Musters je Wert entfaltet.
1281
+
1282
+ Entfaltet wird VOR dem Rendern, also in der neutralen Regelmenge - damit gilt fuer
1283
+ jeden Wert dieselbe Abbildung auf das Client Pack wie fuer jede andere Regel, und
1284
+ ein Pack, das eine Musterform nicht abbilden kann, sagt es auf demselben Weg.
1285
+ """
1286
+ quelle = json.loads(quelltext)
1287
+ gefunden: set[str] = set()
1288
+ for korb in ("deny", "ask", "allow"):
1289
+ neu = []
1290
+ for regel in quelle.get(korb, []):
1291
+ platzhalter = regel.get("pattern")
1292
+ if platzhalter in muster["werte"]:
1293
+ if korb != "deny":
1294
+ raise MusterFehler(f"permissions.json fuehrt {platzhalter} im {korb}-Korb;"
1295
+ f" ein Musterwert dort waere eine Freigabe")
1296
+ gefunden.add(platzhalter)
1297
+ for wert in muster["werte"][platzhalter]:
1298
+ neu.append({k: (wert if k == "pattern" else v) for k, v in regel.items()})
1299
+ else:
1300
+ neu.append(regel)
1301
+ quelle[korb] = neu
1302
+ fehlt = [p for p in muster["werte"] if p not in gefunden]
1303
+ if fehlt:
1304
+ raise MusterFehler(f"permissions.json fuehrt keinen Schlitz fuer {', '.join(fehlt)}")
1305
+ return json.dumps(quelle, indent=2, ensure_ascii=False) + "\n"
1306
+
1307
+
1308
+ def _werte_spannen(werte: list[str]) -> str:
1309
+ return ", ".join(f"`{w}`" for w in werte)
1310
+
1311
+
1312
+ def muster_laufzeitfassung(text: str, muster: dict) -> str:
1313
+ """Setzt den Wert hinter jeden gebundenen Platzhalter der Laufzeitfassung.
1314
+
1315
+ Die Bindung bleibt stehen (D-160): ersetzt wird der Ausfuellschlitz HINTER
1316
+ die Klammer mit dem Platzhalter in Codespanne, nicht der Platzhalter selbst. Die Laufzeitfassung fuehrt von den
1317
+ drei Platzhaltern nur <EXCLUDED_PATHS>; die beiden anderen stehen nur in OVERLAY.md
1318
+ und der Berechtigungsdatei.
1319
+ """
1320
+ for platzhalter, werte in muster["werte"].items():
1321
+ anker = f"(`{platzhalter}`): "
1322
+ if anker not in text:
1323
+ if platzhalter == "<EXCLUDED_PATHS>":
1324
+ raise MusterFehler(f"20-project-overlay.md bindet {platzhalter} nicht mehr "
1325
+ f"in der Form {anker.strip()} - der Wert kaeme nicht an")
1326
+ continue
1327
+ text, n = re.subn(re.escape(anker) + r"`<TBD[^`]*>`",
1328
+ lambda _m: anker + _werte_spannen(werte), text, count=1)
1329
+ if n != 1:
1330
+ raise MusterFehler(f"20-project-overlay.md: hinter {anker.strip()} steht kein "
1331
+ f"Ausfuellschlitz")
1332
+ return text
1333
+
1334
+
1335
+ def muster_overlay(text: str, muster: dict) -> str:
1336
+ """Fuellt die Spalte "Wert" der drei Zeilen in Abschnitt 4 und vermerkt das Muster."""
1337
+ zeilen = text.split("\n")
1338
+ offen = set(muster["werte"])
1339
+ for i, zeile in enumerate(zeilen):
1340
+ zellen = zeile.split("|")
1341
+ for j, zelle in enumerate(zellen[:-1]):
1342
+ platzhalter = zelle.strip().strip("`")
1343
+ if platzhalter in offen and zelle.strip() == f"`{platzhalter}`" \
1344
+ and zellen[j + 1].strip().startswith("`<TBD"):
1345
+ zellen[j + 1] = f" {_werte_spannen(muster['werte'][platzhalter])} "
1346
+ zeilen[i] = "|".join(zellen)
1347
+ offen.discard(platzhalter)
1348
+ break
1349
+ if offen:
1350
+ raise MusterFehler(f"OVERLAY.md: keine Wertzeile mit Ausfuellschlitz fuer "
1351
+ f"{', '.join(sorted(offen))}")
1352
+ text = "\n".join(zeilen)
1353
+ alt = "| Overlay angelegt |"
1354
+ if alt not in text:
1355
+ raise MusterFehler("OVERLAY.md: die Zeile 'Overlay angelegt' im Aenderungsverlauf "
1356
+ "fehlt - der Vermerk des Musters haette keinen Ort")
1357
+ dokumente = ""
1358
+ if muster["dokumente"]:
1359
+ dokumente = (f"; {len(muster['dokumente'])} Musterdokumente unter `documents/` im "
1360
+ f"Manifest registriert (Status `entwurf`, erst nach Prüfung und "
1361
+ f"Freigabe durch den Overlay Owner verbindlich)")
1362
+ return text.replace(alt, f"| Overlay angelegt aus dem Muster `{muster['name']}` "
1363
+ f"`{muster['version']}` – vorbefüllt: "
1364
+ f"{', '.join(f'`{p}`' for p in muster['werte'])}{dokumente} |", 1)
1365
+
1366
+
1367
+ def render_mit_muster(src: str, man: dict, dst_rel: str, muster: dict | None) -> str:
1368
+ """render_for_client, dazu der Fuellschritt fuer die drei Saattraeger."""
1369
+ kennung = quell_kennung(src)
1370
+ quelle = read_text(src)
1371
+ if muster and kennung == MUSTER_QUELLE_RECHTE:
1372
+ quelle = muster_berechtigungen(quelle, muster)
1373
+ text = render_for_client(quelle, man, kennung, dst_rel)
1374
+ if muster and kennung == MUSTER_QUELLE_LAUFZEIT:
1375
+ text = muster_laufzeitfassung(text, muster)
1376
+ if muster and kennung == MUSTER_QUELLE_OVERLAY:
1377
+ text = muster_overlay(text, muster)
1378
+ if muster and kennung == MUSTER_QUELLE_MANIFEST:
1379
+ text = muster_manifest(text, muster)
1380
+ return text
1381
+
1382
+
1383
+ def muster_dokumentziele(muster: dict | None) -> list[tuple[str, str]]:
1384
+ """(Quelle absolut, Ziel relativ zum Projekt) fuer die Dokumente eines Musters."""
1385
+ if not muster:
1386
+ return []
1387
+ return [(d["quelle"], f"{MUSTER_DOKUMENTZIEL}/{d['typ']}/{d['datei']}")
1388
+ for d in muster["dokumente"]]
1389
+
1390
+
1391
+ def run(root: str, template: str, man: dict, mode: str, dry: bool,
1392
+ muster: dict | None = None) -> Report:
1393
+ rep = Report()
1394
+ pruefe_abbildung(root, man, muster)
1395
+
1396
+ for rel in core_relpaths(template, man):
1397
+ src = os.path.join(template, rel)
1398
+ dst = os.path.join(root, rel)
1399
+ if not os.path.exists(dst):
1400
+ if mode == "check":
1401
+ rep.missing.append(rel)
1402
+ else:
1403
+ copy_file(src, dst, dry)
1404
+ rep.created.append(rel)
1405
+ continue
1406
+ if filecmp.cmp(src, dst, shallow=False):
1407
+ rep.unchanged.append(rel)
1408
+ elif mode == "check":
1409
+ rep.drifted.append(rel)
1410
+ elif mode in ("install", "update"):
1411
+ copy_file(src, dst, dry)
1412
+ rep.updated.append(rel)
1413
+
1414
+ # Aktivierte Pack-Bestandteile: Quelle liegt im Pack, Ziel in der Laufzeitschicht.
1415
+ # Werden nie neu angelegt - die Aktivierung bleibt Projektentscheidung -, aber
1416
+ # aktualisiert, sobald sie vorhanden sind.
1417
+ for src_rel, dst_rel in activated_pack_relpaths(root, man):
1418
+ src = os.path.join(HERE, src_rel)
1419
+ dst = os.path.join(root, dst_rel)
1420
+ if not os.path.exists(dst):
1421
+ continue
1422
+ if rendered_matches(src, dst, man, dst_rel):
1423
+ rep.unchanged.append(dst_rel)
1424
+ elif mode == "check":
1425
+ rep.drifted.append(f"{dst_rel} (Pack-Quelle: {src_rel})")
1426
+ else:
1427
+ write_rendered(src, dst, man, dry, dst_rel)
1428
+ rep.updated.append(f"{dst_rel} (aus dem Pack aktualisiert)")
1429
+
1430
+ # Framework-Skills: eine Quelle, je Client gerendert.
1431
+ for src_rel, dst_rel in framework_skill_files(man):
1432
+ src = os.path.join(HERE, src_rel)
1433
+ dst = os.path.join(root, dst_rel)
1434
+ if not os.path.exists(dst):
1435
+ if mode == "check":
1436
+ rep.missing.append(dst_rel)
1437
+ else:
1438
+ write_rendered(src, dst, man, dry, dst_rel)
1439
+ rep.created.append(dst_rel)
1440
+ elif rendered_matches(src, dst, man, dst_rel):
1441
+ rep.unchanged.append(dst_rel)
1442
+ elif mode == "check":
1443
+ rep.drifted.append(f"{dst_rel} (Quelle: {src_rel})")
1444
+ else:
1445
+ write_rendered(src, dst, man, dry, dst_rel)
1446
+ rep.updated.append(dst_rel)
1447
+
1448
+ # Geteilte Core-Bestandteile aus dem Kern (Vorlagen, spaeter weitere).
1449
+ for src_rel, dst_rel in shared_files(man, "shared_core"):
1450
+ src = os.path.join(HERE, *src_rel.split("/"))
1451
+ dst = os.path.join(root, *dst_rel.split("/"))
1452
+ if not os.path.exists(dst):
1453
+ if mode == "check":
1454
+ rep.missing.append(dst_rel)
1455
+ else:
1456
+ write_rendered(src, dst, man, dry, dst_rel)
1457
+ rep.created.append(dst_rel)
1458
+ elif rendered_matches(src, dst, man, dst_rel):
1459
+ rep.unchanged.append(dst_rel)
1460
+ elif mode == "check":
1461
+ rep.drifted.append(f"{dst_rel} (Quelle: {src_rel})")
1462
+ else:
1463
+ write_rendered(src, dst, man, dry, dst_rel)
1464
+ rep.updated.append(dst_rel)
1465
+
1466
+ for rel in seed_relpaths(template, man):
1467
+ src = os.path.join(template, rel)
1468
+ dst = os.path.join(root, rel)
1469
+ if os.path.exists(dst):
1470
+ rep.kept.append(rel)
1471
+ elif mode == "check":
1472
+ rep.missing.append(rel)
1473
+ else:
1474
+ copy_file(src, dst, dry)
1475
+ rep.created.append(rel)
1476
+
1477
+ # Geteilte Saat aus dem Kern: einmal angelegt, danach Eigentum des Projekts. Mit
1478
+ # einem Overlay-Muster fuellt dieser Schritt die Schlitze des Musters (D-355).
1479
+ for src_rel, dst_rel in shared_files(man, "shared_seed"):
1480
+ src = os.path.join(HERE, *src_rel.split("/"))
1481
+ dst = os.path.join(root, *dst_rel.split("/"))
1482
+ if os.path.exists(dst):
1483
+ rep.kept.append(dst_rel)
1484
+ elif mode == "check":
1485
+ rep.missing.append(dst_rel)
1486
+ else:
1487
+ if not dry:
1488
+ text = render_mit_muster(src, man, dst_rel, muster)
1489
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
1490
+ with open(dst, "w", encoding="utf-8", newline="\n") as fh:
1491
+ fh.write(text)
1492
+ rep.created.append(dst_rel)
1493
+
1494
+ # Die Dokumente eines Musters (D-359): nur bei der Erstinstallation, nie ueber
1495
+ # ein vorhandenes Dokument, danach Eigentum des Projekts wie jede Saat.
1496
+ if mode == "install":
1497
+ for src, dst_rel in muster_dokumentziele(muster):
1498
+ dst = os.path.join(root, *dst_rel.split("/"))
1499
+ if os.path.exists(dst):
1500
+ rep.kept.append(dst_rel)
1501
+ continue
1502
+ if not dry:
1503
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
1504
+ with open(dst, "w", encoding="utf-8", newline="\n") as fh:
1505
+ fh.write(read_text(src))
1506
+ rep.created.append(dst_rel)
1507
+
1508
+ return rep
1509
+
1510
+
1511
+ def skill_herkunft() -> dict[str, str]:
1512
+ """Welcher Teil des Kerns liefert welchen Skill.
1513
+
1514
+ Grundlage von --list-skills. Der Kern liefert unter framework/skills/, die Rollen- und
1515
+ Technologiepacks unter framework/<art>/<pack>/skills/. Was in einer Installation liegt
1516
+ und in keiner dieser Quellen vorkommt, stammt aus dem Projekt.
1517
+ """
1518
+ herkunft: dict[str, str] = {}
1519
+ kern = os.path.join(HERE, "framework", "skills")
1520
+ if os.path.isdir(kern):
1521
+ for name in sorted(os.listdir(kern)):
1522
+ if os.path.isdir(os.path.join(kern, name)):
1523
+ herkunft[name] = "Kern"
1524
+ for art in ("role-packs", "tech-packs"):
1525
+ basis = os.path.join(HERE, "framework", art)
1526
+ if not os.path.isdir(basis):
1527
+ continue
1528
+ for pack in sorted(os.listdir(basis)):
1529
+ quelle = os.path.join(basis, pack, "skills")
1530
+ if not os.path.isdir(quelle):
1531
+ continue
1532
+ for name in sorted(os.listdir(quelle)):
1533
+ if os.path.isdir(os.path.join(quelle, name)):
1534
+ herkunft.setdefault(name, f"{art[:-1]} {pack}")
1535
+ return herkunft
1536
+
1537
+
1538
+ def list_skills(root: str, man: dict, client: str) -> int:
1539
+ """Zaehlt die Skills dieser Installation auf: Name, Herkunft, Aufrufbarkeit, Pfad.
1540
+
1541
+ Warum das Framework eine Auskunft gibt, die eigentlich der Client geben sollte: Zusage
1542
+ S5 sagt zu, dass die geladenen Skills vollstaendig aufzaehlbar sind, samt Herkunft und
1543
+ Aufrufbarkeit. Bei 'claude-code' ist sie am 2026-09-12 als [NICHT ABBILDBAR] gemessen
1544
+ worden - es gibt kein Aufzaehlungskommando, und die Sitzung erhaelt Skills als Name und
1545
+ Kurzbeschreibung ohne Pfad; auf die Frage nach der Herkunft antwortete sie "Herkunft
1546
+ unbekannt - fuer alle 84".
1547
+
1548
+ Ein Ausfall, der nur eingetragen und nicht ersetzt wird, ist eine stillschweigende
1549
+ Verschlechterung. Aufzaehlbarkeit ist das einzige Mittel, das einen unbemerkten Skill
1550
+ ueberhaupt bemerkt (AP2-DD-16, K-24) - deshalb Ersatz statt Abbuchung (CR-2026-041 E3,
1551
+ D-42).
1552
+
1553
+ GRENZE, und sie steht auch in der Ausgabe: Diese Funktion sieht die Installation, nicht
1554
+ die Sitzung. Skills aus Ablagen ausserhalb des Projektverzeichnisses fuehrt sie nicht -
1555
+ genau die also, die niemand bemerkt. Eine Teilauskunft, die sich fuer eine vollstaendige
1556
+ ausgibt, waere der Befundtyp, gegen den dieses Projekt seine Sonden baut.
1557
+ """
1558
+ ablage = man["skills_dir"]
1559
+ basis = os.path.join(root, *ablage.split("/"))
1560
+ print(f"Skills in {root}")
1561
+ print(f"Client Pack: {client}; Ablage: {ablage}")
1562
+ print()
1563
+ if not os.path.isdir(basis):
1564
+ print(f"Keine Skill-Ablage unter {ablage}.")
1565
+ print("Ist in dieses Wurzelverzeichnis installiert worden?")
1566
+ return 1
1567
+
1568
+ herkunft = skill_herkunft()
1569
+ feld = man.get("skill_frontmatter", {}).get("model_invocation_field")
1570
+ zeilen: list[tuple[str, str, str, str]] = []
1571
+ for name in sorted(os.listdir(basis)):
1572
+ pfad = os.path.join(basis, name, "SKILL.md")
1573
+ if not os.path.isfile(pfad):
1574
+ continue
1575
+ fm = ""
1576
+ try:
1577
+ text = read_text(pfad).replace("\r\n", "\n")
1578
+ if text.startswith("---\n") and "\n---\n" in text:
1579
+ fm = text.split("\n---\n", 1)[0][4:]
1580
+ except OSError:
1581
+ pass
1582
+ if feld and re.search(rf"^{re.escape(feld)}:[ \t]*true\b", fm, re.M):
1583
+ aufruf = "nur Nutzer"
1584
+ elif feld:
1585
+ aufruf = "Nutzer+Modell"
1586
+ else:
1587
+ # Ein Client ohne eigenes Feld traegt die Aussage weiter als triggers-Liste
1588
+ # der Quelle. Dann ist sie dort zu lesen und nicht "unbekannt" (K-18).
1589
+ m = re.search(r"^triggers:[ \t]*\n((?:[ \t]+-[ \t]+\S+[ \t]*\n)+)",
1590
+ fm + "\n", re.M)
1591
+ ausloeser = [x.strip("- \t") for x in m.group(1).strip().split("\n")] if m else []
1592
+ aufruf = ("Nutzer+Modell" if "model" in ausloeser
1593
+ else "nur Nutzer" if ausloeser else "unbekannt")
1594
+ zeilen.append((name, herkunft.get(name, "Projekt"), aufruf,
1595
+ f"{ablage}/{name}/SKILL.md"))
1596
+
1597
+ if not zeilen:
1598
+ print("Kein Skill gefunden.")
1599
+ else:
1600
+ b1 = max([len("Name")] + [len(z[0]) for z in zeilen])
1601
+ b2 = max([len("Herkunft")] + [len(z[1]) for z in zeilen])
1602
+ b3 = max([len("Aufrufbar")] + [len(z[2]) for z in zeilen])
1603
+ print(f"{'Name':<{b1}} {'Herkunft':<{b2}} {'Aufrufbar':<{b3}} Pfad")
1604
+ for z in zeilen:
1605
+ print(f"{z[0]:<{b1}} {z[1]:<{b2}} {z[2]:<{b3}} {z[3]}")
1606
+ print()
1607
+ print(f"{len(zeilen)} Skills in dieser Installation.")
1608
+ print()
1609
+ print("GRENZE DIESER AUSKUNFT: Sie fuehrt die Skills dieser Installation. Skills aus")
1610
+ print("Ablagen ausserhalb des Projektverzeichnisses - Benutzerprofil, andere Werkzeuge -")
1611
+ print("sieht auch das Framework nicht; ob der Client sie in die Sitzung mitfuehrt, steht")
1612
+ print("im Client Pack unter S5. Diese Liste ist Ersatz fuer eine fehlende Clientauskunft,")
1613
+ print("kein vollstaendiger Ersatz.")
1614
+ return 0
1615
+
1616
+
1617
+ def installierte_clients(root: str) -> list[str]:
1618
+ """Welche Client Packs in diesem Projekt installiert sind - an ihrer Laufzeitschicht.
1619
+
1620
+ Dieselbe Regel wie im Validator: Das Pack, dessen runtime_dir im Zielverzeichnis
1621
+ tatsaechlich liegt. Eine Aktualisierung richtet sich an etwas Vorhandenes; was
1622
+ vorhanden ist, ist ablesbar, und ein Vorgabewert ist dort eine Vermutung, die niemand
1623
+ braucht.
1624
+
1625
+ Bis 0.27.0 gab es diese Erkennung hier nicht. `--update` ohne `--client` fiel auf die
1626
+ Vorgabe zurueck und legte in einer Installation des anderen Packs eine **zweite**
1627
+ Laufzeitschicht an - gemessen am 2026-09-12: 60 Dateien, und dabei "0 aktualisiert".
1628
+ Der Aufruf tat nicht zu viel, er tat das Falsche (B10, D-45).
1629
+
1630
+ Liegen bereits zwei Schichten da, meldet die Funktion beide. Der Schaden ist dann
1631
+ schon eingetreten; die Aufgabe der Erkennung ist es, ihn zu zeigen statt ihn
1632
+ fortzuschreiben.
1633
+ """
1634
+ treffer = []
1635
+ for name in available_clients():
1636
+ try:
1637
+ man = load_manifest(name)
1638
+ except (OSError, ValueError, json.JSONDecodeError):
1639
+ continue
1640
+ if os.path.isdir(os.path.join(root, *man["runtime_dir"].split("/"))):
1641
+ treffer.append(name)
1642
+ return treffer
1643
+
1644
+
1645
+ def kollisionen(root: str, template: str, man: dict) -> list[str]:
1646
+ """Vorhandene Dateien, die das Framework beansprucht und die von seiner Fassung abweichen.
1647
+
1648
+ Bei einer **Erstinstallation** ist jede davon eine Datei des Projekts: Das Framework hat
1649
+ in dieses Verzeichnis noch nie geschrieben. Bis 0.28.0 wurden sie kommentarlos
1650
+ ueberschrieben - in der Zusammenfassung ausgewiesen als "1 aktualisiert", ein Wort, das
1651
+ nach Pflege klingt und hier Verlust bedeutet.
1652
+
1653
+ Der wahrscheinliche Fall ist die Wurzel-Anweisungsdatei. Ihr Name ist keine Erfindung
1654
+ des Frameworks, sondern die Konvention des Clients - deshalb belegt ein Projekt ihn
1655
+ oft schon. Gemessen am 2026-09-12 im Trockenlauf gegen ein reales Projekt: Der erste
1656
+ Befehl des Uebernahmeleitfadens haette eine versionierte Datei von 34 Kilobyte ersetzt
1657
+ (CR-2026-046, D-46).
1658
+
1659
+ Unterschieden wird am **Modus**, nicht an der Herkunft der Datei: Eine Erkennung ueber
1660
+ den Kopfkommentar waere feiner und wuerde bei .example- und JSON-Artefakten versagen,
1661
+ die keinen tragen. Fuer `--update` bleibt das Ueberschreiben richtig - dort ist das
1662
+ Pack bereits installiert, und genau dafuer ist der Modus da.
1663
+ """
1664
+ out: list[str] = []
1665
+ for rel in core_relpaths(template, man):
1666
+ dst = os.path.join(root, rel)
1667
+ if os.path.exists(dst) and not filecmp.cmp(os.path.join(template, rel), dst,
1668
+ shallow=False):
1669
+ out.append(rel.replace(os.sep, "/"))
1670
+ for src_rel, dst_rel in framework_skill_files(man):
1671
+ dst = os.path.join(root, dst_rel)
1672
+ if os.path.exists(dst) and not rendered_matches(os.path.join(HERE, src_rel), dst, man):
1673
+ out.append(dst_rel)
1674
+ for src_rel, dst_rel in shared_files(man, "shared_core"):
1675
+ dst = os.path.join(root, *dst_rel.split("/"))
1676
+ if os.path.exists(dst) and not rendered_matches(
1677
+ os.path.join(HERE, *src_rel.split("/")), dst, man):
1678
+ out.append(dst_rel)
1679
+ return sorted(set(out))
1680
+
1681
+
1682
+ # --- Der Kopierweg in ein Projekt (D-362, CR-2026-140) ------------------------------
1683
+ #
1684
+ # Bis 1.6.0 stand der Kopierweg nur im Leitfaden: den Kern von Hand ins Projekt
1685
+ # kopieren, dann install.py dort aufrufen. An genau diesem Handgriff sind zwei Befunde
1686
+ # entstanden - wer ganz .koolie/ kopiert, nimmt das Kennzeichen und aus dem Arbeitsbaum
1687
+ # das Overlay des Frameworks mit (D-354), und wer mit `git archive HEAD` hebt, liefert
1688
+ # den Stand von vorhin (D-333). --target geht denselben Weg als Werkzeug: Es kopiert NUR
1689
+ # den Kern, aus einem Klon NUR das Verfolgte, und ruft danach die Installation IM
1690
+ # Projekt auf - mit dem kopierten install.py, denn die Hook-Kommandos zeigen auf den
1691
+ # Kern im Zielprojekt.
1692
+ #
1693
+ # WAS ES NICHT TUT: Es zieht keinen Overlay-Wert nach und es committet nicht. Das
1694
+ # bleiben Handgriffe des Projekts.
1695
+ #
1696
+ # DER LIEFERUMFANG (D-367, seit 1.8.0): --lieferumfang nutzung laesst die
1697
+ # Nachweisschicht weg (clientmap.NACHWEIS_ABLAGEN - dort steht, warum sie aufgezaehlt
1698
+ # ist und nicht das Noetige). Die Wahl steht danach in LIEFERUMFANG neben VERSION und
1699
+ # gilt beim naechsten Heben weiter: Das Heben ersetzt das Verzeichnis, und ohne diese
1700
+ # Datei wuechse ein reduziertes Projekt beim ersten --update auf den ganzen Kern. Ein
1701
+ # Wechsel geschieht nur, wenn --lieferumfang ihn ausdruecklich nennt.
1702
+
1703
+ # Die Mindestversion ist GEMESSEN, nicht gesetzt (D-363): Installation, Validator und
1704
+ # Hooks laufen unter 3.8.20 zeilengleich zu 3.14, alle Python-Traeger des Repositoriums
1705
+ # kompilieren dort. 3.7 war auf dem Messplatz nicht beschaffbar. Die beiden Starter
1706
+ # pruefen dieselbe Zahl - eine Sonde haelt die drei Stellen gegeneinander.
1707
+ PYTHON_MINDEST = (3, 8)
1708
+
1709
+ ZIEL_NEU = "core.koolie-neu"
1710
+ ZIEL_ALT = "core.koolie-alt"
1711
+
1712
+ # Windows begrenzt ohne Langpfad-Freigabe einen Dateipfad auf 259 und einen
1713
+ # Verzeichnispfad beim Anlegen auf 247 Zeichen (MAX_PATH). Gemessen am 2026-09-25
1714
+ # (D-368): --target in ein Projekt unter dem Ablagebereich einer Sitzung brach beim
1715
+ # Kopieren ab. Der Rueckbau hielt, aber die Meldung fragte, ob eine Datei geoeffnet sei
1716
+ # - den Grund nannte sie nicht. Geprueft wird deshalb VOR der ersten Kopie, gegen den
1717
+ # laengsten Pfad unter dem Zwischenverzeichnis, das laenger heisst als der Kern.
1718
+ # Auch mit Freigabe bleibt die Grenze: git und die Clients halten sich nicht alle daran.
1719
+ PFAD_GRENZE_DATEI = 259
1720
+ PFAD_GRENZE_VERZEICHNIS = 247
1721
+
1722
+
1723
+ def zu_langer_pfad(basis: str, dateien: list[str]) -> tuple[str, int]:
1724
+ """Der erste Pfad unter basis, der unter Windows seine Grenze reisst, und diese
1725
+ Grenze - sonst ('', 0)."""
1726
+ if os.name != "nt":
1727
+ return "", 0
1728
+ for rel in dateien:
1729
+ datei = os.path.join(basis, *rel.split("/"))
1730
+ if len(datei) > PFAD_GRENZE_DATEI:
1731
+ return datei, PFAD_GRENZE_DATEI
1732
+ if len(os.path.dirname(datei)) > PFAD_GRENZE_VERZEICHNIS:
1733
+ return os.path.dirname(datei), PFAD_GRENZE_VERZEICHNIS
1734
+ return "", 0
1735
+
1736
+
1737
+ def quellwurzel() -> str:
1738
+ """Das Verzeichnis, in dem der Kern dieses install.py liegt: zwei Ebenen hoeher."""
1739
+ return os.path.dirname(os.path.dirname(HERE))
1740
+
1741
+
1742
+ def _gleicher_pfad(a: str, b: str) -> bool:
1743
+ return os.path.normcase(os.path.abspath(a)) == os.path.normcase(os.path.abspath(b))
1744
+
1745
+
1746
+ def _git(*argv: str):
1747
+ try:
1748
+ return subprocess.run(["git", *argv], capture_output=True)
1749
+ except OSError:
1750
+ return None
1751
+
1752
+
1753
+ def quelle_kerndateien() -> tuple[list[str], str]:
1754
+ """Die Dateien des Kerns, relativ zu HERE, und woher die Liste stammt.
1755
+
1756
+ Aus einem Klon des Frameworks nur das Verfolgte, gelesen aus dem Arbeitsbaum (D-333)
1757
+ - sonst kaemen Bytecode, build/out/ und liegengebliebene Entwuerfe mit. Ohne Klon
1758
+ (entpacktes Release-Archiv) alles ausser den Erzeugnissen, die ein Lauf dort
1759
+ hinterlaesst. 🔴 Der Klon wird nur anerkannt, wenn seine Wurzel GENAU die Quellwurzel
1760
+ ist: Ein Archiv, das in einem fremden Repositorium entpackt wurde, ist dort
1761
+ unverfolgt, und `git ls-files` lieferte eine leere Liste.
1762
+ """
1763
+ wurzel = quellwurzel()
1764
+ top = _git("-C", wurzel, "rev-parse", "--show-toplevel")
1765
+ if top is not None and top.returncode == 0 and _gleicher_pfad(
1766
+ top.stdout.decode("utf-8", "replace").strip(), wurzel):
1767
+ p = _git("-C", wurzel, "ls-files", "-z", "--", clientmap.CORE_REL)
1768
+ if p is not None and p.returncode == 0:
1769
+ praefix = clientmap.CORE_REL + "/"
1770
+ rel = sorted(n[len(praefix):] for n in p.stdout.decode("utf-8").split("\0")
1771
+ if n.startswith(praefix))
1772
+ if "install.py" in rel:
1773
+ return rel, "Klon - nur Verfolgtes, aus dem Arbeitsbaum"
1774
+ rel = []
1775
+ ausgabe = os.path.join(HERE, "build", "out")
1776
+ for dirpath, dirnames, filenames in os.walk(HERE):
1777
+ dirnames[:] = sorted(d for d in dirnames
1778
+ if d not in ("__pycache__", ".git")
1779
+ and not _gleicher_pfad(os.path.join(dirpath, d), ausgabe))
1780
+ for fn in sorted(filenames):
1781
+ if fn.endswith((".pyc", ".pyo")):
1782
+ continue
1783
+ rel.append(os.path.relpath(os.path.join(dirpath, fn), HERE).replace(os.sep, "/"))
1784
+ return rel, "Verzeichnis - ohne __pycache__ und build/out"
1785
+
1786
+
1787
+ def _loesche_baum(pfad: str) -> None:
1788
+ """Einen Baum loeschen, auch mit schreibgeschuetzten Dateien.
1789
+
1790
+ Ohne onerror/onexc von shutil.rmtree: Die beiden Namen haben sich zwischen den
1791
+ Python-Fassungen abgeloest, und dieses Werkzeug laeuft ab 3.8 (D-363).
1792
+ """
1793
+ for dirpath, dirnames, filenames in os.walk(pfad, topdown=False):
1794
+ for fn in filenames:
1795
+ f = os.path.join(dirpath, fn)
1796
+ try:
1797
+ os.chmod(f, 0o666)
1798
+ except OSError:
1799
+ pass
1800
+ os.remove(f)
1801
+ for d in dirnames:
1802
+ os.rmdir(os.path.join(dirpath, d))
1803
+ os.rmdir(pfad)
1804
+
1805
+
1806
+ def kern_version(kern: str) -> str:
1807
+ try:
1808
+ with open(os.path.join(kern, "VERSION"), encoding="utf-8") as fh:
1809
+ return fh.read().strip() or "unbekannt"
1810
+ except OSError:
1811
+ return "unbekannt"
1812
+
1813
+
1814
+ def in_projekt_installieren(args) -> int:
1815
+ """--target: den Kern ins Projekt kopieren, dann dort install.py aufrufen."""
1816
+ ziel = os.path.abspath(args.target)
1817
+ if args.check or args.list_skills:
1818
+ print("FEHLER: --target kopiert und installiert. --check und --list-skills "
1819
+ "gehoeren an das install.py im Projekt.", file=sys.stderr)
1820
+ return 1
1821
+ if args.root is not None:
1822
+ print("FEHLER: --target und --root schliessen einander aus - das Ziel ist das "
1823
+ "Projekt, das --target nennt.", file=sys.stderr)
1824
+ return 1
1825
+ if not os.path.isdir(ziel):
1826
+ print(f"FEHLER: Das Projektverzeichnis gibt es nicht: {ziel}", file=sys.stderr)
1827
+ return 1
1828
+ wurzel = quellwurzel()
1829
+ if _gleicher_pfad(ziel, wurzel):
1830
+ print("FEHLER: Quelle und Ziel sind dasselbe Verzeichnis. --target nennt das "
1831
+ "Projekt, in das installiert wird - nicht das, aus dem kopiert wird.",
1832
+ file=sys.stderr)
1833
+ return 1
1834
+ if os.path.normcase(ziel + os.sep).startswith(os.path.normcase(HERE + os.sep)):
1835
+ print("FEHLER: Das Ziel liegt im Kern selbst.", file=sys.stderr)
1836
+ return 1
1837
+
1838
+ zielkern = os.path.join(ziel, *clientmap.CORE_REL.split("/"))
1839
+ ablage = os.path.dirname(zielkern)
1840
+ vorhanden = os.path.isdir(zielkern)
1841
+ if vorhanden and not args.update:
1842
+ print(f"FEHLER: In {ziel} liegt bereits ein Kern (Stand "
1843
+ f"{kern_version(zielkern)}). Eine Erstinstallation wuerde ihn ersetzen.\n"
1844
+ f"Heben auf diesen Stand: --update.", file=sys.stderr)
1845
+ return 1
1846
+ if args.update and not vorhanden:
1847
+ print(f"FEHLER: --update hebt eine vorhandene Installation - in {ziel} liegt "
1848
+ f"kein {clientmap.CORE_REL}/. Fuer eine Erstinstallation --update "
1849
+ f"weglassen.", file=sys.stderr)
1850
+ return 1
1851
+ if vorhanden and not os.path.isfile(os.path.join(zielkern, "install.py")):
1852
+ # Nur ein Verzeichnis, das wie ein Kern aussieht, wird ersetzt - nie ein fremdes.
1853
+ print(f"FEHLER: {zielkern} traegt kein install.py und ist deshalb kein Kern "
1854
+ f"dieses Frameworks. Es wird nicht ersetzt.", file=sys.stderr)
1855
+ return 1
1856
+
1857
+ # Der Lieferumfang (D-367): ausdruecklich genannt, sonst der bisherige des Projekts,
1858
+ # sonst voll. Eine reduzierte Quelle kann keinen vollen Kern liefern.
1859
+ quellumfang = clientmap.lieferumfang(HERE)
1860
+ bisher = clientmap.lieferumfang(zielkern) if vorhanden else ""
1861
+ for wert, wo in ((quellumfang, f"der Quelle ({HERE})"),
1862
+ (bisher or "voll", f"des Projekts ({zielkern})")):
1863
+ if wert not in clientmap.LIEFERUMFAENGE:
1864
+ print(f"FEHLER: {clientmap.LIEFERUMFANG_DATEI} {wo} traegt '{wert}' - bekannt "
1865
+ f"sind {', '.join(clientmap.LIEFERUMFAENGE)}. Kopiert wird nichts.",
1866
+ file=sys.stderr)
1867
+ return 1
1868
+ umfang = args.lieferumfang or bisher or "voll"
1869
+ if umfang == "voll" and quellumfang != "voll":
1870
+ print(f"FEHLER: Die Quelle ist selbst eine Installation mit Lieferumfang "
1871
+ f"'{quellumfang}' - ihr fehlt die Nachweisschicht, und einen vollen Kern kann "
1872
+ f"sie nicht liefern. Quelle ist ein Klon oder das Release-Archiv.",
1873
+ file=sys.stderr)
1874
+ return 1
1875
+
1876
+ dateien, herkunft = quelle_kerndateien()
1877
+ # Die Datei der Wahl wird nie mitkopiert, sondern fuer das Ziel geschrieben.
1878
+ dateien = [rel for rel in dateien if rel != clientmap.LIEFERUMFANG_DATEI]
1879
+ fehlen = [rel for rel in dateien if not os.path.isfile(os.path.join(HERE, *rel.split("/")))]
1880
+ if fehlen:
1881
+ print(f"FEHLER: {len(fehlen)} verfolgte Datei(en) des Kerns fehlen im Arbeitsbaum "
1882
+ f"der Quelle, etwa {fehlen[0]}. Kopiert wird nichts.", file=sys.stderr)
1883
+ return 1
1884
+ weg = [rel for rel in dateien if clientmap.ist_nachweis(rel)] if umfang == "nutzung" else []
1885
+ if weg:
1886
+ dateien = [rel for rel in dateien if not clientmap.ist_nachweis(rel)]
1887
+
1888
+ quellstand = kern_version(HERE)
1889
+ print(f"Quelle: {wurzel} (Stand {quellstand})")
1890
+ print(f"Ziel: {ziel}")
1891
+ if vorhanden:
1892
+ print(f"Modus: Heben von {kern_version(zielkern)} auf {quellstand}")
1893
+ else:
1894
+ print("Modus: Erstinstallation")
1895
+ print(f"Kern: {len(dateien)} Dateien ({herkunft}); nur {clientmap.CORE_REL}/, "
1896
+ f"nie ganz .koolie/")
1897
+ wechsel = f" (bisher {bisher})" if bisher and bisher != umfang else ""
1898
+ if umfang == "nutzung":
1899
+ print(f"Umfang: nutzung{wechsel} - ohne die Nachweisschicht, {len(weg)} Dateien "
1900
+ f"weniger: {', '.join(clientmap.NACHWEIS_ABLAGEN)}")
1901
+ else:
1902
+ print(f"Umfang: voll{wechsel} - der ganze Kern")
1903
+
1904
+ lang, grenze = zu_langer_pfad(os.path.join(ablage, ZIEL_NEU),
1905
+ dateien + [clientmap.LIEFERUMFANG_DATEI])
1906
+ if lang:
1907
+ print(f"FEHLER: Der Pfad zum Projekt ist fuer Windows zu lang. Beim Kopieren "
1908
+ f"entstuende ein Pfad mit {len(lang)} Zeichen, erlaubt sind {grenze}:\n"
1909
+ f" {lang}\nDas Projekt braucht einen um mindestens {len(lang) - grenze} "
1910
+ f"Zeichen kuerzeren Pfad. Kopiert wird nichts (D-368).", file=sys.stderr)
1911
+ return 1
1912
+ if args.dry_run:
1913
+ print()
1914
+ print("dry-run: Es wird nichts geschrieben. Die Installation im Projekt folgt "
1915
+ "erst nach dem Kopieren und wird deshalb hier nicht vorgefuehrt.")
1916
+ return 0
1917
+
1918
+ neu = os.path.join(ablage, ZIEL_NEU)
1919
+ alt = os.path.join(ablage, ZIEL_ALT)
1920
+ for rest in (neu, alt):
1921
+ if os.path.isdir(rest):
1922
+ # Ein Rest eines abgebrochenen Laufs dieses Werkzeugs, an seinem Namen erkannt.
1923
+ _loesche_baum(rest)
1924
+ os.makedirs(ablage, exist_ok=True)
1925
+ try:
1926
+ for rel in dateien:
1927
+ dst = os.path.join(neu, *rel.split("/"))
1928
+ os.makedirs(os.path.dirname(dst), exist_ok=True)
1929
+ shutil.copyfile(os.path.join(HERE, *rel.split("/")), dst)
1930
+ with open(os.path.join(neu, clientmap.LIEFERUMFANG_DATEI), "w", encoding="utf-8",
1931
+ newline="\n") as fh:
1932
+ fh.write(umfang + "\n")
1933
+ if vorhanden:
1934
+ os.replace(zielkern, alt)
1935
+ try:
1936
+ os.replace(neu, zielkern)
1937
+ except OSError:
1938
+ if vorhanden:
1939
+ os.replace(alt, zielkern)
1940
+ raise
1941
+ except OSError as exc:
1942
+ if os.path.isdir(neu):
1943
+ _loesche_baum(neu)
1944
+ print(f"FEHLER: Der Kern liess sich nicht nach {zielkern} kopieren: {exc}\n"
1945
+ f"Ist eine Datei darin geoeffnet? Das Projekt ist unveraendert.",
1946
+ file=sys.stderr)
1947
+ return 1
1948
+
1949
+ print()
1950
+ print(f"--- Installation im Projekt, mit {clientmap.CORE_REL}/install.py des Ziels ---")
1951
+ print()
1952
+ argv = [sys.executable, os.path.join(zielkern, "install.py"), "--root", ziel]
1953
+ if args.update:
1954
+ argv.append("--update")
1955
+ if args.client:
1956
+ argv += ["--client", args.client]
1957
+ if args.overlay:
1958
+ argv += ["--overlay", args.overlay]
1959
+ sys.stdout.flush()
1960
+ rc = subprocess.run(argv).returncode
1961
+ if rc != 0:
1962
+ # Die Installation bricht vor ihrem ersten Schreibvorgang ab (Kollisionen,
1963
+ # Muster, Abbildung). Der kopierte Kern gehoert dann niemandem - zurueck damit.
1964
+ _loesche_baum(zielkern)
1965
+ if vorhanden:
1966
+ os.replace(alt, zielkern)
1967
+ print(f"\nDie Installation im Projekt ist gescheitert; der bisherige Kern "
1968
+ f"(Stand {kern_version(zielkern)}) liegt wieder an seinem Platz.",
1969
+ file=sys.stderr)
1970
+ else:
1971
+ print(f"\nDie Installation im Projekt ist gescheitert; der kopierte Kern ist "
1972
+ f"wieder entfernt.", file=sys.stderr)
1973
+ return rc
1974
+ if vorhanden:
1975
+ _loesche_baum(alt)
1976
+ print()
1977
+ print("Das Heben ist erst mit diesen Handgriffen ein Stand "
1978
+ "(RELEASE_PROCESS.md 4.1, Schritt 2):")
1979
+ print(" 1. geaenderte Overlay-Werte in allen drei Traegern nachziehen,")
1980
+ print(" 2. python .koolie/core/tests/scripts/validate-framework.py "
1981
+ "--strict-overlay,")
1982
+ print(" 3. im Projekt committen.")
1983
+ return 0
1984
+
1985
+
1986
+ def main() -> int:
1987
+ ap = argparse.ArgumentParser(
1988
+ description="Installiert die Wurzeldateien des Koolie-Frameworks in ein Projekt.")
1989
+ ap.add_argument("--root", default=None,
1990
+ help="Wurzelverzeichnis des Projekts (Standard: aktuelles Verzeichnis)")
1991
+ ap.add_argument("--target", default=None, metavar="PROJEKT",
1992
+ help="Den Kern dieses install.py in PROJEKT kopieren und dort installieren; "
1993
+ "mit --update ein vorhandenes Projekt heben (D-362)")
1994
+ ap.add_argument("--lieferumfang", choices=clientmap.LIEFERUMFAENGE, default=None,
1995
+ help="Nur mit --target: den ganzen Kern (voll) oder ohne die "
1996
+ "Nachweisschicht (nutzung) kopieren. Ohne Angabe bleibt es beim "
1997
+ "Heben beim bisherigen Umfang, sonst voll (D-367)")
1998
+ ap.add_argument("--update", action="store_true",
1999
+ help="Core-Dateien auf den Stand dieses Releases bringen; Projektdateien bleiben unberuehrt")
2000
+ ap.add_argument("--check", action="store_true",
2001
+ help="Nur pruefen: meldet fehlende und abweichende Core-Dateien, schreibt nichts")
2002
+ ap.add_argument("--dry-run", action="store_true",
2003
+ help="Zeigen, was geschehen wuerde, ohne zu schreiben")
2004
+ ap.add_argument("--probe", action="store_true",
2005
+ help="Wirksamkeitsprobe: ruft den Schutz-Hook des Projekts mit synthetischen "
2006
+ "Ereignissen auf und prueft Konfiguration und Vertrauen, ohne Modell "
2007
+ "(K-195, D-488). Exit 1, wenn eine Muss-Kontrolle fehlt")
2008
+ ap.add_argument("--ohne-client", action="store_true",
2009
+ help="Nur mit --probe: die Startmeldung des Clients nicht abrufen")
2010
+ ap.add_argument("--client", default=None,
2011
+ help=f"Client Pack, aus dem installiert wird. Ohne Angabe wird das installierte Pack erkannt; bei einer Erstinstallation gilt {DEFAULT_CLIENT}")
2012
+ ap.add_argument("--list-clients", action="store_true",
2013
+ help="Verfuegbare Client Packs auflisten und beenden")
2014
+ ap.add_argument("--list-skills", action="store_true",
2015
+ help="Skills dieser Installation auflisten: Name, Herkunft, Aufrufbarkeit, Pfad")
2016
+ ap.add_argument("--overlay", nargs="?", const="", default=None, metavar="NAME",
2017
+ help="Nur bei der Erstinstallation: ein mitgeliefertes Overlay-Muster statt "
2018
+ "des leeren Overlays (D-126). Ohne NAME: verfuegbare Muster auflisten")
2019
+ args = ap.parse_args()
2020
+
2021
+ if sys.version_info < PYTHON_MINDEST:
2022
+ print(f"FEHLER: Koolie braucht Python {'.'.join(map(str, PYTHON_MINDEST))} oder "
2023
+ f"neuer; dieses ist {sys.version.split()[0]} (D-363).", file=sys.stderr)
2024
+ return 1
2025
+
2026
+ clients = available_clients()
2027
+
2028
+ if args.overlay == "":
2029
+ muster_namen = verfuegbare_muster()
2030
+ if not muster_namen:
2031
+ print(f"Keine Overlay-Muster unter {MUSTER_ABLAGE} gefunden.")
2032
+ return 1
2033
+ print("Verfuegbare Overlay-Muster (nur bei der Erstinstallation, --overlay <name>):")
2034
+ for name in muster_namen:
2035
+ try:
2036
+ m = muster_laden(name)
2037
+ except MusterFehler as exc:
2038
+ print(f" {name} (nicht verwendbar: {exc})")
2039
+ continue
2040
+ print(f" {name} {m['version']} - fuellt {', '.join(m['werte'])}")
2041
+ print()
2042
+ print("Ohne --overlay beginnt das Projekt mit dem leeren Overlay. Was ein Muster")
2043
+ print("fuellt und was nicht, steht in .koolie/core/framework/overlay-patterns/<name>.md.")
2044
+ return 0
2045
+
2046
+ if args.list_clients:
2047
+ if not clients:
2048
+ print("Keine installierbaren Client Packs gefunden.")
2049
+ print(f"Erwartet: {CLIENT_PACKS}/<name>/root-template/")
2050
+ return 1
2051
+ print("Verfuegbare Client Packs:")
2052
+ for name in clients:
2053
+ mark = " (Standard)" if name == DEFAULT_CLIENT else ""
2054
+ print(f" {name}{mark}")
2055
+ print()
2056
+ print("Welche Zusagen des Frameworks ein Client technisch durchsetzt, steht in")
2057
+ print(".koolie/core/clients/<name>/CLIENT_PACK.md.")
2058
+ return 0
2059
+
2060
+ if args.probe and args.target is not None:
2061
+ # Die Probe schreibt nichts und kopiert keinen Kern - --target nennt hier nur das
2062
+ # Projekt. Sie prueft mit den Manifesten DIESES Kerns; gemeint ist der des Projekts.
2063
+ args.root, args.target = args.target, None
2064
+ if args.ohne_client and not args.probe:
2065
+ print("FEHLER: --ohne-client gehoert zu --probe.", file=sys.stderr)
2066
+ return 1
2067
+ if args.target is not None:
2068
+ return in_projekt_installieren(args)
2069
+ if args.lieferumfang is not None:
2070
+ # Nur --target kopiert den Kern; ohne Kopie gibt es nichts wegzulassen.
2071
+ print("FEHLER: --lieferumfang gehoert zu --target - nur dort wird der Kern "
2072
+ "kopiert.", file=sys.stderr)
2073
+ return 1
2074
+
2075
+ # Das Ziel steht vor der Clientwahl fest - denn es entscheidet sie.
2076
+ root = os.path.abspath(args.root or os.getcwd())
2077
+ if os.path.abspath(HERE) == root:
2078
+ print("FEHLER: --root zeigt auf .koolie/core/ selbst. Gemeint ist das "
2079
+ "Wurzelverzeichnis des Projekts, also eine Ebene darueber.", file=sys.stderr)
2080
+ return 1
2081
+
2082
+ erkannt = installierte_clients(root)
2083
+ if len(erkannt) > 1:
2084
+ print(f"FEHLER: In {root} liegen bereits mehrere Laufzeitschichten: "
2085
+ f"{', '.join(erkannt)}. Welche gilt, entscheidet der Client - und die "
2086
+ f"Packs kennen einander nicht. Entferne die nicht gewollte Schicht und "
2087
+ f"rufe erneut auf.", file=sys.stderr)
2088
+ return 1
2089
+
2090
+ if args.client is None:
2091
+ # Erkanntes Pack vor Vorgabe. Die Vorgabe gilt nur, wo es nichts zu erkennen
2092
+ # gibt - bei einer Erstinstallation.
2093
+ args.client = erkannt[0] if erkannt else DEFAULT_CLIENT
2094
+ elif erkannt and args.client not in erkannt:
2095
+ print(f"FEHLER: In {root} ist das Client Pack '{erkannt[0]}' installiert, "
2096
+ f"angefordert ist '{args.client}'. Eine Aktualisierung wuerde hier keine "
2097
+ f"Datei aktualisieren, sondern eine zweite Laufzeitschicht anlegen. Ein "
2098
+ f"Wechsel des Packs ist eine Entscheidung: Entferne dazu die vorhandene "
2099
+ f"Laufzeitschicht und rufe erneut auf.", file=sys.stderr)
2100
+ return 1
2101
+
2102
+ if args.client not in clients:
2103
+ print(f"FEHLER: Unbekanntes Client Pack: {args.client}", file=sys.stderr)
2104
+ if clients:
2105
+ print(f"Verfuegbar: {', '.join(clients)}", file=sys.stderr)
2106
+ else:
2107
+ print(f"Kein Pack mit root-template/ unter {CLIENT_PACKS} gefunden.", file=sys.stderr)
2108
+ return 1
2109
+
2110
+ template = client_template(args.client)
2111
+ try:
2112
+ man = load_manifest(args.client)
2113
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
2114
+ print(f"FEHLER: Manifest des Client Packs {args.client} nicht lesbar: {exc}", file=sys.stderr)
2115
+ return 1
2116
+
2117
+ if args.list_skills:
2118
+ return list_skills(root, man, args.client)
2119
+
2120
+ if args.probe:
2121
+ if not erkannt:
2122
+ print(f"FEHLER: In {root} ist kein Client Pack installiert - es gibt nichts zu "
2123
+ f"proben.", file=sys.stderr)
2124
+ return 1
2125
+ quelle = json.loads(clientmap.load_source(HERE, "hooks.json"))
2126
+ verben = [v for e in quelle.get("PreToolUse", []) for v in e.get("on", [])
2127
+ if any(h.get("enforcing") for h in e.get("hooks", []))]
2128
+ erg = wirksamkeit.probe(root, man, args.client, verben, not args.ohne_client)
2129
+ return wirksamkeit.ausgeben(erg, root, args.client)
2130
+
2131
+ mode = "check" if args.check else ("update" if args.update else "install")
2132
+
2133
+ muster = None
2134
+ if args.overlay:
2135
+ # Ein Muster ist ein Anfangszustand, kein Kanal (D-76, D-355): Es wirkt nur, wo
2136
+ # die Saat noch nicht liegt. Wer es bei --update erwartet, erwartet einen
2137
+ # Fuellschritt ueber Projektwerte - und genau den gibt es nicht.
2138
+ if mode != "install":
2139
+ print(f"FEHLER: --overlay gilt nur bei der Erstinstallation, nicht mit "
2140
+ f"--{mode}. Die Saat gehoert nach der Erstinstallation dem Projekt "
2141
+ f"(D-76).", file=sys.stderr)
2142
+ return 1
2143
+ try:
2144
+ muster = muster_laden(args.overlay)
2145
+ except MusterFehler as exc:
2146
+ print(f"FEHLER: {exc}", file=sys.stderr)
2147
+ return 1
2148
+ vorhanden = [dst_rel for _src, dst_rel in
2149
+ shared_files(man, "shared_seed") + muster_dokumentziele(muster)
2150
+ if os.path.exists(os.path.join(root, *dst_rel.split("/")))]
2151
+ if vorhanden:
2152
+ print(f"FEHLER: --overlay {args.overlay} fuellt die Saat bei der "
2153
+ f"Erstinstallation - hier liegt sie schon ({len(vorhanden)} Datei(en)):",
2154
+ file=sys.stderr)
2155
+ for rel in vorhanden[:6]:
2156
+ print(f" {rel}", file=sys.stderr)
2157
+ print("Vorhandene Saat gehoert dem Projekt und wird nie ueberschrieben. Werte "
2158
+ "aus dem Muster von Hand uebernehmen:", file=sys.stderr)
2159
+ print(f" .koolie/core/framework/overlay-patterns/{args.overlay}.md",
2160
+ file=sys.stderr)
2161
+ return 1
2162
+
2163
+ if mode == "install":
2164
+ # Vor dem ersten Schreibvorgang, nicht waehrenddessen: Ein Abbruch nach der
2165
+ # Haelfte der Dateien waere schlimmer als keiner.
2166
+ belegt = kollisionen(root, template, man)
2167
+ if belegt:
2168
+ print(f"FEHLER: In {root} liegen bereits Dateien, die das Framework "
2169
+ f"beansprucht:", file=sys.stderr)
2170
+ for rel in belegt:
2171
+ print(f" {rel}", file=sys.stderr)
2172
+ print("", file=sys.stderr)
2173
+ print("Eine Erstinstallation wuerde sie ueberschreiben. Das Framework hat in "
2174
+ "dieses\nVerzeichnis noch nie geschrieben - diese Dateien gehoeren also "
2175
+ "dem Projekt.", file=sys.stderr)
2176
+ print("", file=sys.stderr)
2177
+ print("Stammen sie aus einer frueheren Installation: --update verwenden.",
2178
+ file=sys.stderr)
2179
+ print("Sonst den Inhalt vorher uebernehmen - Projektwissen nach "
2180
+ ".koolie/project-overlay/OVERLAY.md,\nprojektspezifische Regeln in eine Regeldatei "
2181
+ "2N-overlay-<name>.md. Danach die\nDatei entfernen und erneut aufrufen "
2182
+ "(.koolie/core/docs/ADOPTION_GUIDE.md, Abschnitt 2).", file=sys.stderr)
2183
+ return 1
2184
+ if mode == "update":
2185
+ # GEMESSEN am 2026-09-30 (K-31, D-515): Die Wurzel-Anweisung gehoert dem Kern, und
2186
+ # --update schrieb sie neu - der markierte Block eines fremden Generators ging dabei
2187
+ # OHNE Meldung verloren, und der Generator schriebe ihn beim naechsten Lauf zurueck.
2188
+ # Dieselbe Bauform wie die Kollision der Erstinstallation: Abbruch vor dem ersten
2189
+ # Schreibvorgang, nicht Datenverlust.
2190
+ wurzel_rel = man.get("root_instruction_file", "")
2191
+ wurzel_pfad = os.path.join(root, *wurzel_rel.split("/")) if wurzel_rel else ""
2192
+ bloecke = (koexistenz.markierte_bloecke(read_text(wurzel_pfad))
2193
+ if wurzel_pfad and os.path.isfile(wurzel_pfad) else [])
2194
+ if bloecke:
2195
+ print(f"FEHLER: {wurzel_rel} traegt {len(bloecke)} markierte(n) Block/Bloecke "
2196
+ f"eines fremden Generators:", file=sys.stderr)
2197
+ for name, zeichen in bloecke:
2198
+ print(f" {name}: {zeichen} Zeichen", file=sys.stderr)
2199
+ print("", file=sys.stderr)
2200
+ print(f"Die Aktualisierung schreibt {wurzel_rel} neu - die Datei gehoert dem Kern "
2201
+ f"(Ebene 1).\nDer Block ginge verloren, und der Generator schriebe ihn "
2202
+ f"beim naechsten Lauf zurueck.", file=sys.stderr)
2203
+ print("", file=sys.stderr)
2204
+ print("Abgrenzung nach Gegenstand (K-31): den Generator auf eine eigene Datei "
2205
+ "umstellen,\netwa eine, die bei Bedarf laedt; den Block entfernen und "
2206
+ "erneut aufrufen\n(.koolie/core/docs/ADOPTION_GUIDE.md, Abschnitt 8).",
2207
+ file=sys.stderr)
2208
+ return 1
2209
+ version_file = os.path.join(HERE, "VERSION")
2210
+ version = open(version_file, encoding="utf-8").read().strip() if os.path.exists(version_file) else "unbekannt"
2211
+
2212
+ print(f"Framework {version}")
2213
+ print(f"Client: {args.client}")
2214
+ print(f"Vorlage: {os.path.relpath(template, root) if root in template else template}")
2215
+ print(f"Ziel: {root}")
2216
+ print(f"Modus: {mode}{' (dry-run, es wird nichts geschrieben)' if args.dry_run else ''}")
2217
+ if muster:
2218
+ print(f"Overlay: Muster {muster['name']} {muster['version']}")
2219
+ print()
2220
+
2221
+ try:
2222
+ rep = run(root, template, man, mode, args.dry_run, muster)
2223
+ except MusterFehler as exc:
2224
+ print(f"FEHLER: Das Overlay-Muster laesst sich nicht vollstaendig anwenden; es "
2225
+ f"wurde nichts geschrieben.", file=sys.stderr)
2226
+ print(f" {exc}", file=sys.stderr)
2227
+ return 1
2228
+ except clientmap.AbbildungsFehler as exc:
2229
+ # D-26/D-27: Eine Aussage der Quelle, die dieser Client nicht tragen kann, wird
2230
+ # abgebildet oder die Installation scheitert. Sie stillschweigend wegzulassen
2231
+ # waere ein Verlust der Zusage - deshalb hier ein Abbruch und keine Warnung.
2232
+ print(f"FEHLER: Die Quellen lassen sich nicht verlustfrei auf das Client Pack "
2233
+ f"{args.client} abbilden.\n {exc}", file=sys.stderr)
2234
+ return 1
2235
+
2236
+ def block(title: str, items: list[str], limit: int = 12) -> None:
2237
+ if not items:
2238
+ return
2239
+ print(f"{title} ({len(items)}):")
2240
+ for rel in items[:limit]:
2241
+ print(f" {rel}")
2242
+ if len(items) > limit:
2243
+ print(f" ... und {len(items) - limit} weitere")
2244
+ print()
2245
+
2246
+ if mode == "check":
2247
+ block("Core-Dateien fehlen", rep.missing)
2248
+ block("Core-Dateien weichen von der Vorlage ab", rep.drifted)
2249
+ print(f"Unveraendert: {len(rep.unchanged)} | Projektdateien vorhanden: {len(rep.kept)}")
2250
+ if rep.drifted:
2251
+ print()
2252
+ print("Abweichende Core-Dateien bedeuten: Es wurde an der falschen Stelle bearbeitet.")
2253
+ tpl_rel = os.path.relpath(template, HERE).replace(os.sep, "/")
2254
+ print(f"Core-Aenderungen gehoeren in .koolie/core/{tpl_rel}/ und laufen")
2255
+ print("als Aenderungsantrag (.koolie/core/governance/FEEDBACK_PROCESS.md).")
2256
+ print("Mit --update wird der Release-Stand wiederhergestellt; lokale Aenderungen gehen dabei verloren.")
2257
+ if rep.missing or rep.drifted:
2258
+ return 1
2259
+ print("\nErgebnis: Core ist auf dem Stand des Releases.")
2260
+ return 0
2261
+
2262
+ block("Angelegt", rep.created)
2263
+ block("Core aktualisiert", rep.updated)
2264
+ if rep.kept:
2265
+ print(f"Projektdateien unberuehrt gelassen: {len(rep.kept)}")
2266
+ for rel in rep.kept[:6]:
2267
+ print(f" {rel}")
2268
+ if len(rep.kept) > 6:
2269
+ print(f" ... und {len(rep.kept) - 6} weitere")
2270
+ print()
2271
+
2272
+ print(f"Zusammenfassung: {len(rep.created)} angelegt, {len(rep.updated)} aktualisiert, "
2273
+ f"{len(rep.unchanged)} unveraendert, {len(rep.kept)} Projektdateien behalten.")
2274
+
2275
+ if mode == "install" and rep.created:
2276
+ print()
2277
+ print("Naechste Schritte:")
2278
+ print(" 1. .koolie/project-overlay/OVERLAY.md ausfuellen (Platzhalter und <TBD>-Felder).")
2279
+ if muster:
2280
+ print(f" Vorbefuellt aus dem Muster {muster['name']}: "
2281
+ f"{', '.join(muster['werte'])} - in allen drei Traegern.")
2282
+ print(" Die Werte sind Vorschlaege: pruefen, anpassen, und eine Aenderung in")
2283
+ print(f" OVERLAY.md, der Laufzeitfassung und {man['permissions_file']} nachziehen.")
2284
+ if muster["dokumente"]:
2285
+ print(f" Dazu {len(muster['dokumente'])} Musterdokumente unter "
2286
+ f"{MUSTER_DOKUMENTZIEL}/, im Manifest mit")
2287
+ print(" Status entwurf registriert. Sie wirken erst, wenn der Overlay Owner "
2288
+ "sie prueft,")
2289
+ print(" freigibt und in der Laufzeitfassung als K1-Dokumente fuehrt.")
2290
+ # Den Integritaetsblock fuehrt nur eine Berechtigungsdatei im JSON-Format - auch
2291
+ # das Agentenprofil von kiro (D-414); die TOML-Datei eines Packs kennt ihn nicht
2292
+ # (K-123, D-395).
2293
+ if clientmap.permissions_format(man) in ("json", "kiro-agent"):
2294
+ print(f" 2. Werte in {man['permissions_file']} eintragen - die Kernregeln unter")
2295
+ print(" _core_rules_integrity nicht entfernen.")
2296
+ else:
2297
+ print(f" 2. Werte in {man['permissions_file']} eintragen.")
2298
+ print(" 3. .koolie/project-overlay/forbidden-terms.txt mit den realen Projekt- und")
2299
+ print(" Kundennamen fuellen (bleibt projektlokal).")
2300
+ print(" 4. .koolie/core/checklists/10-project-adoption.md abarbeiten.")
2301
+ print(" 5. python .koolie/core/tests/scripts/validate-framework.py --strict-overlay")
2302
+ nachschritte(man.get("post_install_steps", []), 6)
2303
+ print()
2304
+ print("Ob die Schutzschicht im Projekt greift, prueft ohne Modell:")
2305
+ print(" python .koolie/core/install.py --probe (K-195, D-488)")
2306
+ if mode == "update":
2307
+ print()
2308
+ print(f"Hinweis: {man['permissions_file']} wurde nicht angefasst, weil sie Projektwerte enthaelt.")
2309
+ print("Pruefe nach einem Release-Wechsel, ob die Kernregeln noch vollstaendig sind:")
2310
+ print(" python .koolie/core/tests/scripts/validate-framework.py --strict-overlay")
2311
+ print("und ob die Schutzschicht greift - auch ein neuer Hook-Matcher erreicht die Datei")
2312
+ print("nicht von selbst (D-488):")
2313
+ print(" python .koolie/core/install.py --probe")
2314
+ if man.get("post_update_steps"):
2315
+ print()
2316
+ print("Fuer dieses Client Pack ausserdem:")
2317
+ nachschritte(man["post_update_steps"], 1)
2318
+ fehlend = fehlende_zusatzschluessel(root, man)
2319
+ if fehlend:
2320
+ print()
2321
+ print(f"HINWEIS ({len(fehlend)}): Das Client Pack deklariert Schluessel, die in "
2322
+ f"{man['permissions_file']}")
2323
+ print("fehlen oder einen anderen Wert tragen. Ein Update schreibt sie nicht nach:")
2324
+ for feld, schluessel, wert in fehlend:
2325
+ ort = "oberste Ebene" if feld == "settings_extra" else "innerhalb von permissions"
2326
+ print(f" {schluessel} ({ort}): {json.dumps(wert, ensure_ascii=False)}")
2327
+ print("Von Hand nachtragen, wie im Migrationshinweis des Releases beschrieben -")
2328
+ print("oder die Abweichung in der nutzerlokalen Datei des Clients ausweisen.")
2329
+ print("Dies ist eine Auskunft und keine Schranke (D-434).")
2330
+ alt = veraltete_overlaysperre(root, man)
2331
+ if alt:
2332
+ print()
2333
+ print(f"HINWEIS ({len(alt)}): {man['permissions_file']} sperrt das Overlay noch "
2334
+ f"statisch:")
2335
+ for zeile in alt:
2336
+ print(f" {zeile}")
2337
+ print("Seit 1.17.0 sperrt es der Schutz-Hook, bis ein Mandat es freigibt (Modus M6).")
2338
+ print("Solange die Zeile steht, bleibt ein Mandat wirkungslos. Entfernen, wenn M6")
2339
+ print("genutzt werden soll - auch aus _core_rules_integrity (D-448).")
2340
+
2341
+ ignoriert = ignorierte_kerndateien(root)
2342
+ if ignoriert:
2343
+ print()
2344
+ print(f"HINWEIS ({len(ignoriert)}): Dieses Projekt IGNORIERT Kerndateien, die "
2345
+ f"soeben geschrieben wurden.")
2346
+ for rel in ignoriert[:8]:
2347
+ print(f" {rel}")
2348
+ if len(ignoriert) > 8:
2349
+ print(f" ... und {len(ignoriert) - 8} weitere")
2350
+ print("Sie liegen im Arbeitsbaum, aber nicht im Versionierten - beim naechsten")
2351
+ print("Klonen dieses Projekts fehlen sie. Abhilfe: eine Negativregel im")
2352
+ print(f"'.gitignore' des Projekts, etwa '!{clientmap.CORE_REL}/**'. Das '.gitignore'")
2353
+ print("gehoert dem Projekt; dies ist eine Auskunft und keine Schranke.")
2354
+
2355
+ kanaele = belegte_importkanaele(root, man)
2356
+ if kanaele:
2357
+ print()
2358
+ # Die Einleitung sagt seit 1.13.0 das Pack selbst (import_channels_intro): Bei
2359
+ # kiro sind es keine Quellen eines fremden Werkzeugs, sondern die eigenen
2360
+ # Ablagen des Clients im Benutzerprofil (D-414).
2361
+ einleitung = man.get("import_channels_intro") or (
2362
+ "Die Importsteuerung dieses Client Packs laesst Quellen eines fremden "
2363
+ "Werkzeugs zu,")
2364
+ print(f"HINWEIS ({len(kanaele)}): {einleitung}")
2365
+ print("und auf diesem Arbeitsplatz sind davon belegt:")
2366
+ for rel, was in kanaele:
2367
+ print(f" {rel}")
2368
+ for zeile in textwrap.wrap(was, width=76):
2369
+ print(f" {zeile}")
2370
+ print("Was dort steht, erreicht die Sitzung neben den Regeln des Frameworks. Pruefen,")
2371
+ print("ob es dorthin gehoert (CLIENT_PACK.md, Zeile R6). Dies ist eine Auskunft und")
2372
+ print("keine Schranke.")
2373
+
2374
+ if traegt_quellrepo_kennzeichen(root):
2375
+ print()
2376
+ print(f"HINWEIS: In diesem Verzeichnis liegt {QUELLREPO_KENNZEICHEN} - das "
2377
+ f"Kennzeichen des Framework-Repositoriums selbst.")
2378
+ print("Ist dies ein Projekt, wurde beim Kopieren zu viel mitgenommen: Kopiert wird")
2379
+ print(f"nur {clientmap.CORE_REL}/, nie ganz .koolie/. Solange die Datei hier liegt, haelt")
2380
+ print("der Validator das Projekt fuer das Quellrepositorium und prueft Projektdateien")
2381
+ print("mit (Pruefungen 75, 78, 79 und 81). Abhilfe: die Datei entfernen und pruefen, ob")
2382
+ print(".koolie/project-overlay/ noch der Stand des Projekts ist - eine Kopie aus dem")
2383
+ print("Arbeitsbaum des Frameworks bringt dessen Overlay mit.")
2384
+ print("Im Framework-Repositorium selbst ist nichts zu tun. Dies ist eine Auskunft und")
2385
+ print("keine Schranke.")
2386
+
2387
+ fremd = koexistenz_auskunft(root, man)
2388
+ if fremd:
2389
+ print()
2390
+ for zeile in fremd:
2391
+ print(zeile)
2392
+
2393
+ print()
2394
+ print("Diese Pfade gehoeren dem Projekt und werden von install.py nie geschrieben:")
2395
+ for hint in man.get("project_owned_hint", []):
2396
+ print(f" {hint}")
2397
+
2398
+ return 0
2399
+
2400
+
2401
+ if __name__ == "__main__":
2402
+ sys.exit(main())