@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.
- package/.gitattributes +16 -0
- package/.gitignore +51 -0
- package/.koolie/QUELLREPOSITORIUM.md +18 -0
- package/.koolie/core/CHANGELOG.md +11832 -0
- package/.koolie/core/LICENSE +674 -0
- package/.koolie/core/LICENSE-HINWEIS.md +98 -0
- package/.koolie/core/OWNERS.md +36 -0
- package/.koolie/core/VERSION +1 -0
- package/.koolie/core/banner.py +397 -0
- package/.koolie/core/build/README.md +34 -0
- package/.koolie/core/build/assemble.py +354 -0
- package/.koolie/core/build/build-docx.py +194 -0
- package/.koolie/core/build/doc/00-kopf.md +18 -0
- package/.koolie/core/build/doc/01-executive-summary.md +24 -0
- package/.koolie/core/build/doc/02-zielbild.md +13 -0
- package/.koolie/core/build/doc/03-ziele-nichtziele.md +28 -0
- package/.koolie/core/build/doc/04-geltungsbereich.md +25 -0
- package/.koolie/core/build/doc/05-glossar.md +47 -0
- package/.koolie/core/build/doc/06-leitprinzipien.md +5 -0
- package/.koolie/core/build/doc/07-architektur.md +60 -0
- package/.koolie/core/build/doc/07a-abbildungsschicht.md +66 -0
- package/.koolie/core/build/doc/08-trennung.md +24 -0
- package/.koolie/core/build/doc/09-betriebsmodi.md +18 -0
- package/.koolie/core/build/doc/10-arbeitsablauf.md +5 -0
- package/.koolie/core/build/doc/11-datenschutz.md +5 -0
- package/.koolie/core/build/doc/12-sicherheit.md +5 -0
- package/.koolie/core/build/doc/13-risiko.md +5 -0
- package/.koolie/core/build/doc/14-rollen.md +9 -0
- package/.koolie/core/build/doc/15-referenzstruktur.md +124 -0
- package/.koolie/core/build/doc/16-agentenanweisung.md +8 -0
- package/.koolie/core/build/doc/17-overlay.md +29 -0
- package/.koolie/core/build/doc/18-skill-standard.md +5 -0
- package/.koolie/core/build/doc/19-skill-template.md +5 -0
- package/.koolie/core/build/doc/20-referenz-skills.md +70 -0
- package/.koolie/core/build/doc/21-prompts.md +53 -0
- package/.koolie/core/build/doc/22-checklisten.md +49 -0
- package/.koolie/core/build/doc/23-entscheidungsbaeume.md +29 -0
- package/.koolie/core/build/doc/24-onboarding.md +33 -0
- package/.koolie/core/build/doc/25-governance.md +35 -0
- package/.koolie/core/build/doc/26-qs-test.md +9 -0
- package/.koolie/core/build/doc/27-pilot.md +13 -0
- package/.koolie/core/build/doc/28-uebernahme.md +5 -0
- package/.koolie/core/build/doc/29-grenzen.md +87 -0
- package/.koolie/core/build/doc/30-roadmap.md +5 -0
- package/.koolie/core/build/doc/31-anhaenge.md +151 -0
- package/.koolie/core/build/doc/32-abschluss.md +92 -0
- package/.koolie/core/build/ref-a4.docx +0 -0
- package/.koolie/core/checklists/01-preflight.md +54 -0
- package/.koolie/core/checklists/02-privacy-context.md +51 -0
- package/.koolie/core/checklists/03-before-code-change.md +51 -0
- package/.koolie/core/checklists/04-review-ai-code.md +58 -0
- package/.koolie/core/checklists/05-testing.md +48 -0
- package/.koolie/core/checklists/06-security.md +50 -0
- package/.koolie/core/checklists/07-new-dependency.md +57 -0
- package/.koolie/core/checklists/08-merge-request.md +47 -0
- package/.koolie/core/checklists/09-onboarding.md +52 -0
- package/.koolie/core/checklists/10-project-adoption.md +74 -0
- package/.koolie/core/checklists/11-framework-release.md +71 -0
- package/.koolie/core/checklists/README.md +28 -0
- package/.koolie/core/clientmap.py +1358 -0
- package/.koolie/core/clients/README.md +113 -0
- package/.koolie/core/clients/_template/CLIENT_PACK.md +197 -0
- package/.koolie/core/clients/claude-code/CLIENT_PACK.md +383 -0
- package/.koolie/core/clients/claude-code/manifest.json +232 -0
- package/.koolie/core/clients/claude-code/root-template/.claude/README.md +127 -0
- package/.koolie/core/clients/cursor/CLIENT_PACK.md +229 -0
- package/.koolie/core/clients/cursor/manifest.json +330 -0
- package/.koolie/core/clients/cursor/root-template/.cursor/README.md +36 -0
- package/.koolie/core/clients/devin-desktop/CLIENT_PACK.md +251 -0
- package/.koolie/core/clients/devin-desktop/manifest.json +163 -0
- package/.koolie/core/clients/devin-desktop/root-template/.devin/README.md +79 -0
- package/.koolie/core/clients/kiro/CLIENT_PACK.md +223 -0
- package/.koolie/core/clients/kiro/manifest.json +300 -0
- package/.koolie/core/clients/kiro/root-template/.kiro/README.md +37 -0
- package/.koolie/core/clients/openai-codex/CLIENT_PACK.md +257 -0
- package/.koolie/core/clients/openai-codex/manifest.json +208 -0
- package/.koolie/core/clients/openai-codex/root-template/.codex/README.md +35 -0
- package/.koolie/core/decision-trees/01-context-allowed.md +46 -0
- package/.koolie/core/decision-trees/02-may-ai-do-task.md +47 -0
- package/.koolie/core/decision-trees/03-analyze-or-modify.md +50 -0
- package/.koolie/core/decision-trees/04-required-review.md +49 -0
- package/.koolie/core/decision-trees/05-stop-or-escalate.md +47 -0
- package/.koolie/core/decision-trees/06-rule-placement.md +58 -0
- package/.koolie/core/decision-trees/README.md +14 -0
- package/.koolie/core/docs/ADOPTION_GUIDE.md +582 -0
- package/.koolie/core/docs/DOCUMENTATION_STANDARD.md +91 -0
- package/.koolie/core/docs/PLACEHOLDER_REGISTRY.md +79 -0
- package/.koolie/core/docs/ROADMAP.md +848 -0
- package/.koolie/core/docs/RUNTIME_GLOSSARY.md +113 -0
- package/.koolie/core/examples/README.md +11 -0
- package/.koolie/core/examples/example-ergebnisbericht.md +31 -0
- package/.koolie/core/examples/example-mr-description.md +31 -0
- package/.koolie/core/examples/example-overlay-runtime.md +48 -0
- package/.koolie/core/framework/core/00-principles.md +93 -0
- package/.koolie/core/framework/core/01-governance.md +58 -0
- package/.koolie/core/framework/core/02-privacy.md +141 -0
- package/.koolie/core/framework/core/03-security.md +86 -0
- package/.koolie/core/framework/core/04-quality.md +48 -0
- package/.koolie/core/framework/core/05-working-model.md +199 -0
- package/.koolie/core/framework/core/06-prompting-rules.md +51 -0
- package/.koolie/core/framework/core/07-review-rules.md +52 -0
- package/.koolie/core/framework/core/08-skill-conventions.md +115 -0
- package/.koolie/core/framework/core/09-risk-model.md +87 -0
- package/.koolie/core/framework/core/10-error-escalation.md +53 -0
- package/.koolie/core/framework/org-policies/MAPPING_CLASSIFICATION.md +16 -0
- package/.koolie/core/framework/org-policies/README.md +29 -0
- package/.koolie/core/framework/overlay-patterns/general/documents/branching-strategy/muster-general.md +43 -0
- package/.koolie/core/framework/overlay-patterns/general/documents/coding-guidelines/muster-general.md +82 -0
- package/.koolie/core/framework/overlay-patterns/general/documents/definition-of-done/muster-general.md +52 -0
- package/.koolie/core/framework/overlay-patterns/general/documents/definition-of-ready/muster-general.md +54 -0
- package/.koolie/core/framework/overlay-patterns/general/documents/quality/muster-general.md +59 -0
- package/.koolie/core/framework/overlay-patterns/general/documents/security/muster-general.md +52 -0
- package/.koolie/core/framework/overlay-patterns/general.md +150 -0
- package/.koolie/core/framework/role-packs/README.md +63 -0
- package/.koolie/core/framework/role-packs/_template/ROLE_PACK.md +58 -0
- package/.koolie/core/framework/role-packs/requirements-engineering/ROLE_PACK.md +158 -0
- package/.koolie/core/framework/role-packs/requirements-engineering/runtime/30-role-requirements-engineering.md +88 -0
- package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/CHANGELOG.md +10 -0
- package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/EXAMPLES.md +148 -0
- package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/SKILL.md +195 -0
- package/.koolie/core/framework/role-packs/requirements-engineering/skills/role-re-ticket/TESTS.md +22 -0
- package/.koolie/core/framework/role-packs/software-development/ROLE_PACK.md +95 -0
- package/.koolie/core/framework/role-packs/software-development/runtime/30-role-software-development.md +26 -0
- package/.koolie/core/framework/runtime/agents/fw-reviewer.md +34 -0
- package/.koolie/core/framework/runtime/client-settings.json +3 -0
- package/.koolie/core/framework/runtime/hooks.json +20 -0
- package/.koolie/core/framework/runtime/mcp-config.example.json +4 -0
- package/.koolie/core/framework/runtime/permissions.json +106 -0
- package/.koolie/core/framework/runtime/root-instruction-local.example.md +19 -0
- package/.koolie/core/framework/runtime/root-instruction.md +116 -0
- package/.koolie/core/framework/runtime/rules/00-framework-core.md +49 -0
- package/.koolie/core/framework/runtime/rules/10-privacy-security.md +49 -0
- package/.koolie/core/framework/runtime/rules/15-development-rules.md +41 -0
- package/.koolie/core/framework/runtime/rules/16-plan-spezifikation.md +32 -0
- package/.koolie/core/framework/runtime/rules/20-project-overlay.md +60 -0
- package/.koolie/core/framework/skills/fw-bugfix-prepare/CHANGELOG.md +14 -0
- package/.koolie/core/framework/skills/fw-bugfix-prepare/EXAMPLES.md +57 -0
- package/.koolie/core/framework/skills/fw-bugfix-prepare/SKILL.md +177 -0
- package/.koolie/core/framework/skills/fw-bugfix-prepare/TESTS.md +15 -0
- package/.koolie/core/framework/skills/fw-change-analyze/CHANGELOG.md +12 -0
- package/.koolie/core/framework/skills/fw-change-analyze/EXAMPLES.md +66 -0
- package/.koolie/core/framework/skills/fw-change-analyze/SKILL.md +167 -0
- package/.koolie/core/framework/skills/fw-change-analyze/TESTS.md +16 -0
- package/.koolie/core/framework/skills/fw-change-small/CHANGELOG.md +13 -0
- package/.koolie/core/framework/skills/fw-change-small/EXAMPLES.md +68 -0
- package/.koolie/core/framework/skills/fw-change-small/SKILL.md +175 -0
- package/.koolie/core/framework/skills/fw-change-small/TESTS.md +13 -0
- package/.koolie/core/framework/skills/fw-code-explain/CHANGELOG.md +11 -0
- package/.koolie/core/framework/skills/fw-code-explain/EXAMPLES.md +61 -0
- package/.koolie/core/framework/skills/fw-code-explain/SKILL.md +163 -0
- package/.koolie/core/framework/skills/fw-code-explain/TESTS.md +11 -0
- package/.koolie/core/framework/skills/fw-docs-update/CHANGELOG.md +10 -0
- package/.koolie/core/framework/skills/fw-docs-update/EXAMPLES.md +59 -0
- package/.koolie/core/framework/skills/fw-docs-update/SKILL.md +160 -0
- package/.koolie/core/framework/skills/fw-docs-update/TESTS.md +12 -0
- package/.koolie/core/framework/skills/fw-error-analyze/CHANGELOG.md +10 -0
- package/.koolie/core/framework/skills/fw-error-analyze/EXAMPLES.md +67 -0
- package/.koolie/core/framework/skills/fw-error-analyze/SKILL.md +160 -0
- package/.koolie/core/framework/skills/fw-error-analyze/TESTS.md +12 -0
- package/.koolie/core/framework/skills/fw-mr-description/CHANGELOG.md +12 -0
- package/.koolie/core/framework/skills/fw-mr-description/EXAMPLES.md +69 -0
- package/.koolie/core/framework/skills/fw-mr-description/SKILL.md +170 -0
- package/.koolie/core/framework/skills/fw-mr-description/TESTS.md +12 -0
- package/.koolie/core/framework/skills/fw-overlay-pflege/CHANGELOG.md +7 -0
- package/.koolie/core/framework/skills/fw-overlay-pflege/EXAMPLES.md +48 -0
- package/.koolie/core/framework/skills/fw-overlay-pflege/SKILL.md +147 -0
- package/.koolie/core/framework/skills/fw-overlay-pflege/TESTS.md +13 -0
- package/.koolie/core/framework/skills/fw-plan/CHANGELOG.md +14 -0
- package/.koolie/core/framework/skills/fw-plan/EXAMPLES.md +69 -0
- package/.koolie/core/framework/skills/fw-plan/SKILL.md +168 -0
- package/.koolie/core/framework/skills/fw-plan/TESTS.md +14 -0
- package/.koolie/core/framework/skills/fw-refactor/CHANGELOG.md +10 -0
- package/.koolie/core/framework/skills/fw-refactor/EXAMPLES.md +70 -0
- package/.koolie/core/framework/skills/fw-refactor/SKILL.md +173 -0
- package/.koolie/core/framework/skills/fw-refactor/TESTS.md +14 -0
- package/.koolie/core/framework/skills/fw-repo-analyze/CHANGELOG.md +10 -0
- package/.koolie/core/framework/skills/fw-repo-analyze/EXAMPLES.md +52 -0
- package/.koolie/core/framework/skills/fw-repo-analyze/SKILL.md +149 -0
- package/.koolie/core/framework/skills/fw-repo-analyze/TESTS.md +11 -0
- package/.koolie/core/framework/skills/fw-review-support/CHANGELOG.md +12 -0
- package/.koolie/core/framework/skills/fw-review-support/EXAMPLES.md +69 -0
- package/.koolie/core/framework/skills/fw-review-support/SKILL.md +179 -0
- package/.koolie/core/framework/skills/fw-review-support/TESTS.md +13 -0
- package/.koolie/core/framework/skills/fw-tests/CHANGELOG.md +9 -0
- package/.koolie/core/framework/skills/fw-tests/EXAMPLES.md +65 -0
- package/.koolie/core/framework/skills/fw-tests/SKILL.md +169 -0
- package/.koolie/core/framework/skills/fw-tests/TESTS.md +12 -0
- package/.koolie/core/framework/tech-packs/README.md +15 -0
- package/.koolie/core/framework/tech-packs/_template/TECH_PACK.md +61 -0
- package/.koolie/core/governance/ADOPTION_REGISTRY.md +89 -0
- package/.koolie/core/governance/CHANGE_REQUEST_TEMPLATE.md +60 -0
- package/.koolie/core/governance/DECISION_LOG.md +779 -0
- package/.koolie/core/governance/EXCEPTION_PROCESS.md +31 -0
- package/.koolie/core/governance/FEEDBACK_PROCESS.md +34 -0
- package/.koolie/core/governance/FRAMEWORK_DEV_PROFILE.md +135 -0
- package/.koolie/core/governance/INCIDENT_HANDLING.md +47 -0
- package/.koolie/core/governance/PRIORITY_HIERARCHY.md +61 -0
- package/.koolie/core/governance/RACI.md +37 -0
- package/.koolie/core/governance/RELEASE_PROCESS.md +183 -0
- package/.koolie/core/governance/change-requests/CR-2026-001-definition-release-1-0-0.md +67 -0
- package/.koolie/core/governance/change-requests/CR-2026-002-client-packs.md +63 -0
- package/.koolie/core/governance/change-requests/CR-2026-003-install-client-schalter.md +70 -0
- package/.koolie/core/governance/change-requests/CR-2026-004-client-pack-claude-code.md +82 -0
- package/.koolie/core/governance/change-requests/CR-2026-005-kern-neutralisierung.md +75 -0
- package/.koolie/core/governance/change-requests/CR-2026-006-gemeinsame-skill-quelle.md +77 -0
- package/.koolie/core/governance/change-requests/CR-2026-007-geteilte-kernbestandteile.md +80 -0
- package/.koolie/core/governance/change-requests/CR-2026-008-berechtigungen-und-hooks.md +85 -0
- package/.koolie/core/governance/change-requests/CR-2026-009-umbenennung-leitwerk.md +77 -0
- package/.koolie/core/governance/change-requests/CR-2026-010-restduplikation.md +73 -0
- package/.koolie/core/governance/change-requests/CR-2026-011-hauptdokument.md +72 -0
- package/.koolie/core/governance/change-requests/CR-2026-012-schreibschutz-kernverzeichnis.md +106 -0
- package/.koolie/core/governance/change-requests/CR-2026-013-validator-blindstellen.md +128 -0
- package/.koolie/core/governance/change-requests/CR-2026-014-kurzform-langform.md +125 -0
- package/.koolie/core/governance/change-requests/CR-2026-015-versionskette.md +150 -0
- package/.koolie/core/governance/change-requests/CR-2026-016-ap2-claude-code.md +127 -0
- package/.koolie/core/governance/change-requests/CR-2026-017-ladebedingungen-claude-code.md +173 -0
- package/.koolie/core/governance/change-requests/CR-2026-018-quellenliste-claude-code.md +121 -0
- package/.koolie/core/governance/change-requests/CR-2026-019-strukturentscheidungen-fortschreiben.md +143 -0
- package/.koolie/core/governance/change-requests/CR-2026-020-akteursbezeichnung.md +118 -0
- package/.koolie/core/governance/change-requests/CR-2026-021-hook-interpreter.md +113 -0
- package/.koolie/core/governance/change-requests/CR-2026-022-artefaktversionen.md +73 -0
- package/.koolie/core/governance/change-requests/CR-2026-023-shell-lesesperre.md +128 -0
- package/.koolie/core/governance/change-requests/CR-2026-024-clientbindungen.md +78 -0
- package/.koolie/core/governance/change-requests/CR-2026-025-betriebsmodi.md +110 -0
- package/.koolie/core/governance/change-requests/CR-2026-026-fail-closed.md +134 -0
- package/.koolie/core/governance/change-requests/CR-2026-027-zeichenlimit-einstufung.md +105 -0
- package/.koolie/core/governance/change-requests/CR-2026-028-berechtigungsmodi.md +130 -0
- package/.koolie/core/governance/change-requests/CR-2026-029-hook-ablageort.md +115 -0
- package/.koolie/core/governance/change-requests/CR-2026-030-lesende-werkzeuge.md +143 -0
- package/.koolie/core/governance/change-requests/CR-2026-031-regelquellen-ausserhalb.md +182 -0
- package/.koolie/core/governance/change-requests/CR-2026-032-fremde-skill-ablage.md +151 -0
- package/.koolie/core/governance/change-requests/CR-2026-033-modusabhaengige-durchsetzung.md +131 -0
- package/.koolie/core/governance/change-requests/CR-2026-034-nachweisbedingungen.md +116 -0
- package/.koolie/core/governance/change-requests/CR-2026-035-readme-in-der-regelablage.md +115 -0
- package/.koolie/core/governance/change-requests/CR-2026-036-ortsangaben-hook-konfiguration.md +108 -0
- package/.koolie/core/governance/change-requests/CR-2026-037-meldender-hook-clientgebunden.md +133 -0
- package/.koolie/core/governance/change-requests/CR-2026-038-importsteuerung.md +159 -0
- package/.koolie/core/governance/change-requests/CR-2026-039-normative-saetze-in-kommentaren.md +124 -0
- package/.koolie/core/governance/change-requests/CR-2026-040-erh01-ist-clientgebunden.md +115 -0
- package/.koolie/core/governance/change-requests/CR-2026-041-s5-nicht-abbildbar.md +128 -0
- package/.koolie/core/governance/change-requests/CR-2026-042-abwesenheitsnachweis-suchwerkzeug.md +102 -0
- package/.koolie/core/governance/change-requests/CR-2026-043-validator-gibt-trefferwerte-aus.md +135 -0
- package/.koolie/core/governance/change-requests/CR-2026-044-aktivierungspruefung-clientgebunden.md +112 -0
- package/.koolie/core/governance/change-requests/CR-2026-045-update-verliert-die-clientwahl.md +116 -0
- package/.koolie/core/governance/change-requests/CR-2026-046-erstinstallation-ueberschreibt-projektdatei.md +125 -0
- package/.koolie/core/governance/change-requests/CR-2026-047-zusagen-je-zugriffskanal.md +133 -0
- package/.koolie/core/governance/change-requests/CR-2026-048-modusgrenze-ohne-durchsetzung.md +137 -0
- package/.koolie/core/governance/change-requests/CR-2026-049-sondenlauf-kodierungsabhaengig.md +126 -0
- package/.koolie/core/governance/change-requests/CR-2026-050-allowed-tools-und-verworfene-permissions.md +110 -0
- package/.koolie/core/governance/change-requests/CR-2026-051-quellenkarte-und-ueberholter-stand.md +110 -0
- package/.koolie/core/governance/change-requests/CR-2026-052-drei-regelkonflikte.md +166 -0
- package/.koolie/core/governance/change-requests/CR-2026-053-lesesperre-und-entwicklungsprofil.md +144 -0
- package/.koolie/core/governance/change-requests/CR-2026-054-aktivierung-verlangt-aktivitaet.md +124 -0
- package/.koolie/core/governance/change-requests/CR-2026-055-domain-ausnahme-ohne-mechanismus.md +173 -0
- package/.koolie/core/governance/change-requests/CR-2026-056-eingabeschema-und-pfadidentitaet.md +245 -0
- package/.koolie/core/governance/change-requests/CR-2026-057-disallowed-tools-und-s3.md +148 -0
- package/.koolie/core/governance/change-requests/CR-2026-058-unteragent-reichweite.md +203 -0
- package/.koolie/core/governance/change-requests/CR-2026-059-unteragent-tiefe-und-widerspruch.md +125 -0
- package/.koolie/core/governance/change-requests/CR-2026-060-stumme-brueche.md +170 -0
- package/.koolie/core/governance/change-requests/CR-2026-061-berechtigungsdatei-ohne-pruefung.md +169 -0
- package/.koolie/core/governance/change-requests/CR-2026-062-werkzeugabbildung-des-frontmatters.md +178 -0
- package/.koolie/core/governance/change-requests/CR-2026-063-skillaufruf-ohne-freigabe.md +207 -0
- package/.koolie/core/governance/change-requests/CR-2026-064-pruefregister-nicht-nachgezaehlt.md +141 -0
- package/.koolie/core/governance/change-requests/CR-2026-065-devin-suchwerkzeuge-erhoben.md +170 -0
- package/.koolie/core/governance/change-requests/CR-2026-066-schlitzdeckung-ohne-abgleich.md +153 -0
- package/.koolie/core/governance/change-requests/CR-2026-067-hookblock-und-praeparationsregister.md +140 -0
- package/.koolie/core/governance/change-requests/CR-2026-068-sondenlauf-nebenlaeufig.md +106 -0
- package/.koolie/core/governance/change-requests/CR-2026-069-bytecode-des-kerns.md +108 -0
- package/.koolie/core/governance/change-requests/CR-2026-070-d11-zaehler.md +114 -0
- package/.koolie/core/governance/change-requests/CR-2026-071-strukturentscheidungen-bestaetigen.md +322 -0
- package/.koolie/core/governance/change-requests/CR-2026-072-modulstatus-heben.md +315 -0
- package/.koolie/core/governance/change-requests/CR-2026-073-nicht-skill-traeger-heben.md +246 -0
- package/.koolie/core/governance/change-requests/CR-2026-074-restliche-nicht-skill-traeger-heben.md +271 -0
- package/.koolie/core/governance/change-requests/CR-2026-075-ap2-zielversion-devin-desktop.md +335 -0
- package/.koolie/core/governance/change-requests/CR-2026-076-erster-sitzungstest.md +455 -0
- package/.koolie/core/governance/change-requests/CR-2026-077-sitzungstest-schranken.md +425 -0
- package/.koolie/core/governance/change-requests/CR-2026-078-planung-nach-1-0-0.md +294 -0
- package/.koolie/core/governance/change-requests/CR-2026-079-umbenennung-vorziehen.md +233 -0
- package/.koolie/core/governance/change-requests/CR-2026-080-clientbindung-des-kerns.md +321 -0
- package/.koolie/core/governance/change-requests/CR-2026-081-produktnamen-im-kern.md +297 -0
- package/.koolie/core/governance/change-requests/CR-2026-082-sitzungstest-pi-ds-2.md +145 -0
- package/.koolie/core/governance/change-requests/CR-2026-083-sitzungstest-ne-sc.md +300 -0
- package/.koolie/core/governance/change-requests/CR-2026-084-trockenlauf-gegen-den-committeten-stand.md +117 -0
- package/.koolie/core/governance/change-requests/CR-2026-085-vorbedingungen-sitzungstest-5.md +182 -0
- package/.koolie/core/governance/change-requests/CR-2026-086-grenzfaelle-gegen-die-fassungen.md +164 -0
- package/.koolie/core/governance/change-requests/CR-2026-087-produktbeobachtung-quellenliste.md +206 -0
- package/.koolie/core/governance/change-requests/CR-2026-088-vorbedingungen-testblaetter.md +127 -0
- package/.koolie/core/governance/change-requests/CR-2026-089-herrichtung-uebungsrepositorium.md +226 -0
- package/.koolie/core/governance/change-requests/CR-2026-090-vorbedingungen-sitzungstest-5-zweiter-durchgang.md +255 -0
- package/.koolie/core/governance/change-requests/CR-2026-091-sitzungstest-5.md +163 -0
- package/.koolie/core/governance/change-requests/CR-2026-092-pruefmittelwort-und-buendelschnitt.md +226 -0
- package/.koolie/core/governance/change-requests/CR-2026-093-sechzehnte-praeparation.md +151 -0
- package/.koolie/core/governance/change-requests/CR-2026-094-testblaetter-buendel-1.md +113 -0
- package/.koolie/core/governance/change-requests/CR-2026-095-pruefapparat-filter.md +65 -0
- package/.koolie/core/governance/change-requests/CR-2026-096-vorbedingungen-buendel-2.md +225 -0
- package/.koolie/core/governance/change-requests/CR-2026-097-testblaetter-buendel-2.md +162 -0
- package/.koolie/core/governance/change-requests/CR-2026-098-anforderungen-auslieferung.md +93 -0
- package/.koolie/core/governance/change-requests/CR-2026-099-k74-und-vorbedingungen-buendel-3.md +119 -0
- package/.koolie/core/governance/change-requests/CR-2026-100-testblaetter-buendel-3.md +120 -0
- package/.koolie/core/governance/change-requests/CR-2026-101-k77-eigener-posten.md +165 -0
- package/.koolie/core/governance/change-requests/CR-2026-102-k77-entschieden.md +118 -0
- package/.koolie/core/governance/change-requests/CR-2026-103-vorbedingungen-buendel-4.md +76 -0
- package/.koolie/core/governance/change-requests/CR-2026-104-herrichtung-buendel-4.md +82 -0
- package/.koolie/core/governance/change-requests/CR-2026-105-messapparat-buendel-4.md +227 -0
- package/.koolie/core/governance/change-requests/CR-2026-106-uebergabe-einchecken.md +91 -0
- package/.koolie/core/governance/change-requests/CR-2026-107-uebergabe-im-release-commit.md +111 -0
- package/.koolie/core/governance/change-requests/CR-2026-108-testblaetter-buendel-4.md +118 -0
- package/.koolie/core/governance/change-requests/CR-2026-109-berichtsweg-des-aufraeumers.md +108 -0
- package/.koolie/core/governance/change-requests/CR-2026-110-vorbedingungen-des-nachlaufs.md +184 -0
- package/.koolie/core/governance/change-requests/CR-2026-111-der-apparat-auf-dem-weg-der-wiederaufnahme.md +127 -0
- package/.koolie/core/governance/change-requests/CR-2026-112-der-arbeitsplatz-im-kern.md +103 -0
- package/.koolie/core/governance/change-requests/CR-2026-113-nachlauf-buendel-4.md +103 -0
- package/.koolie/core/governance/change-requests/CR-2026-114-vorbedingungen-buendel-5.md +317 -0
- package/.koolie/core/governance/change-requests/CR-2026-115-herrichtung-buendel-5.md +244 -0
- package/.koolie/core/governance/change-requests/CR-2026-116-messtag-buendel-5.md +178 -0
- package/.koolie/core/governance/change-requests/CR-2026-117-sammelzellen-zentraler-katalog.md +251 -0
- package/.koolie/core/governance/change-requests/CR-2026-118-quellenzuordnung-matrixzeilen.md +115 -0
- package/.koolie/core/governance/change-requests/CR-2026-119-umbenennung-koolie.md +204 -0
- package/.koolie/core/governance/change-requests/CR-2026-120-ap2-rest-devin-desktop.md +96 -0
- package/.koolie/core/governance/change-requests/CR-2026-121-verify-marker-abschaffen.md +185 -0
- package/.koolie/core/governance/change-requests/CR-2026-122-umbenennung-koolie-lauf.md +131 -0
- package/.koolie/core/governance/change-requests/CR-2026-123-vortrag-und-namensabsatz.md +170 -0
- package/.koolie/core/governance/change-requests/CR-2026-124-hauptdokument-ap11.md +237 -0
- package/.koolie/core/governance/change-requests/CR-2026-125-word-fassung-und-blinder-fleck.md +204 -0
- package/.koolie/core/governance/change-requests/CR-2026-126-lizenz-gpl3.md +112 -0
- package/.koolie/core/governance/change-requests/CR-2026-127-gegenzeichnung-rollenfrage.md +96 -0
- package/.koolie/core/governance/change-requests/CR-2026-128-freigabelauf-1.0.0.md +278 -0
- package/.koolie/core/governance/change-requests/CR-2026-129-archiv-zeilenenden.md +90 -0
- package/.koolie/core/governance/change-requests/CR-2026-130-reihenfolge-des-hebens.md +204 -0
- package/.koolie/core/governance/change-requests/CR-2026-131-chronik-und-vorlage.md +194 -0
- package/.koolie/core/governance/change-requests/CR-2026-132-erhebung-openai-codex.md +185 -0
- package/.koolie/core/governance/change-requests/CR-2026-133-bau-openai-codex.md +97 -0
- package/.koolie/core/governance/change-requests/CR-2026-134-uebergabe-lokal.md +117 -0
- package/.koolie/core/governance/change-requests/CR-2026-135-pfadlisten-nul-getrennt.md +139 -0
- package/.koolie/core/governance/change-requests/CR-2026-136-overlay-werte-einordnung.md +156 -0
- package/.koolie/core/governance/change-requests/CR-2026-137-kopierweg-kern.md +144 -0
- package/.koolie/core/governance/change-requests/CR-2026-138-overlay-muster-general.md +118 -0
- package/.koolie/core/governance/change-requests/CR-2026-139-overlay-muster-dokumente.md +105 -0
- package/.koolie/core/governance/change-requests/CR-2026-140-installer-je-zielsystem.md +120 -0
- package/.koolie/core/governance/change-requests/CR-2026-141-lieferumfang.md +123 -0
- package/.koolie/core/governance/change-requests/CR-2026-142-dokumentationsstandard.md +90 -0
- package/.koolie/core/governance/change-requests/CR-2026-143-klasse-b-core-laufzeit.md +89 -0
- package/.koolie/core/governance/change-requests/CR-2026-144-klasse-b-governance.md +90 -0
- package/.koolie/core/governance/change-requests/CR-2026-145-klasse-b-skills.md +81 -0
- package/.koolie/core/governance/change-requests/CR-2026-146-code-pack-posten.md +84 -0
- package/.koolie/core/governance/change-requests/CR-2026-147-regel-register-posten.md +78 -0
- package/.koolie/core/governance/change-requests/CR-2026-148-mehrprojekt-tokenlast.md +70 -0
- package/.koolie/core/governance/change-requests/CR-2026-149-regelablage-pfadtoken.md +71 -0
- package/.koolie/core/governance/change-requests/CR-2026-150-client-pack-kiro.md +76 -0
- package/.koolie/core/governance/change-requests/CR-2026-151-skill-anweisungen.md +71 -0
- package/.koolie/core/governance/change-requests/CR-2026-152-testblaetter-modellwechsel.md +78 -0
- package/.koolie/core/governance/change-requests/CR-2026-153-attributionszeile.md +88 -0
- package/.koolie/core/governance/change-requests/CR-2026-154-auffindbarkeit.md +102 -0
- package/.koolie/core/governance/change-requests/CR-2026-155-client-pack-cursor.md +80 -0
- package/.koolie/core/governance/change-requests/CR-2026-156-mandat-und-reibung.md +100 -0
- package/.koolie/core/governance/change-requests/CR-2026-157-mcp-anbindung.md +93 -0
- package/.koolie/core/governance/change-requests/CR-2026-158-registerpflege.md +69 -0
- package/.koolie/core/governance/change-requests/CR-2026-159-powershell.md +49 -0
- package/.koolie/core/governance/change-requests/CR-2026-160-messapparat.md +59 -0
- package/.koolie/core/governance/change-requests/CR-2026-161-pruefwerkzeuge.md +52 -0
- package/.koolie/core/governance/change-requests/CR-2026-162-schutzschicht.md +65 -0
- package/.koolie/core/governance/change-requests/CR-2026-163-pfadsemantik.md +61 -0
- package/.koolie/core/governance/change-requests/CR-2026-164-modi-ausnahmen.md +65 -0
- package/.koolie/core/governance/change-requests/CR-2026-165-installer-banner.md +406 -0
- package/.koolie/core/governance/change-requests/CR-2026-166-nachlauf-aufzeichnungen.md +59 -0
- package/.koolie/core/governance/change-requests/CR-2026-167-einsatzarchitektur.md +60 -0
- package/.koolie/core/governance/change-requests/CR-2026-168-paketquellen.md +64 -0
- package/.koolie/core/governance/change-requests/CR-2026-169-modusbindung-messfragen.md +58 -0
- package/.koolie/core/governance/change-requests/CR-2026-170-erste-veroeffentlichung.md +62 -0
- package/.koolie/core/governance/change-requests/CR-2026-171-befehl-im-projektverzeichnis.md +57 -0
- package/.koolie/core/governance/change-requests/CR-2026-172-auftritt-npm-suche.md +58 -0
- package/.koolie/core/install.py +2402 -0
- package/.koolie/core/install_dialog.py +227 -0
- package/.koolie/core/koexistenz.py +112 -0
- package/.koolie/core/mandat.py +605 -0
- package/.koolie/core/onboarding/COMPLETION_CRITERIA.md +37 -0
- package/.koolie/core/onboarding/GUIDE.md +103 -0
- package/.koolie/core/onboarding/KNOWLEDGE_CHECK.md +48 -0
- package/.koolie/core/onboarding/MENTOR_CHECKLIST.md +40 -0
- package/.koolie/core/onboarding/QUICKSTART.md +50 -0
- package/.koolie/core/onboarding/REFERENCE.md +69 -0
- package/.koolie/core/onboarding/exercises/EXERCISES.md +78 -0
- package/.koolie/core/onboarding/exercises/README.md +120 -0
- package/.koolie/core/pilot/METRICS.md +59 -0
- package/.koolie/core/pilot/PILOT_CONCEPT.md +42 -0
- package/.koolie/core/prompts/01-understand-codebase.md +99 -0
- package/.koolie/core/prompts/02-impact-analysis.md +104 -0
- package/.koolie/core/prompts/03-implementation-planning.md +102 -0
- package/.koolie/core/prompts/04-code-generation.md +100 -0
- package/.koolie/core/prompts/05-test-generation.md +103 -0
- package/.koolie/core/prompts/06-refactoring.md +102 -0
- package/.koolie/core/prompts/07-debugging.md +97 -0
- package/.koolie/core/prompts/08-security-review.md +92 -0
- package/.koolie/core/prompts/09-performance-analysis.md +89 -0
- package/.koolie/core/prompts/10-documentation.md +91 -0
- package/.koolie/core/prompts/11-merge-request-review.md +94 -0
- package/.koolie/core/prompts/12-developer-training.md +90 -0
- package/.koolie/core/prompts/README.md +88 -0
- package/.koolie/core/templates/MR_AI_DISCLOSURE.md +37 -0
- package/.koolie/core/templates/PLAN_TEMPLATE.md +57 -0
- package/.koolie/core/templates/SKILL_TEMPLATE.md +159 -0
- package/.koolie/core/templates/project-overlay/OVERLAY.md +315 -0
- package/.koolie/core/templates/project-overlay/documents/README.md +43 -0
- package/.koolie/core/templates/project-overlay/documents/ai-governance/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/ai-process-model/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/architecture/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/architecture/decisions/README.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/branching-strategy/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/coding-guidelines/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/definition-of-done/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/definition-of-ready/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/deployment/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/glossary/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/quality/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/roadmap/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/roles/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/documents/security/.gitkeep.md +3 -0
- package/.koolie/core/templates/project-overlay/exceptions/EXCEPTIONS.md +9 -0
- package/.koolie/core/templates/project-overlay/forbidden-terms.txt +8 -0
- package/.koolie/core/templates/project-overlay/overlay-manifest.yaml +73 -0
- package/.koolie/core/templates/rules/21-overlay-TEMPLATE.md.template +28 -0
- package/.koolie/core/templates/rules/40-tech-TEMPLATE.md.template +33 -0
- package/.koolie/core/tests/EDGE_CASES.md +67 -0
- package/.koolie/core/tests/TEST_CATALOG.md +170 -0
- package/.koolie/core/tests/erhebungen/README.md +326 -0
- package/.koolie/core/tests/erhebungen/ablage.py +357 -0
- package/.koolie/core/tests/erhebungen/apparat/__init__.py +22 -0
- package/.koolie/core/tests/erhebungen/apparat/baum.py +214 -0
- package/.koolie/core/tests/erhebungen/apparat/belege.py +91 -0
- package/.koolie/core/tests/erhebungen/apparat/clients.py +321 -0
- package/.koolie/core/tests/erhebungen/apparat/freigabe.py +74 -0
- package/.koolie/core/tests/erhebungen/apparat/kontingent.py +64 -0
- package/.koolie/core/tests/erhebungen/apparat/laeufer.py +155 -0
- package/.koolie/core/tests/erhebungen/apparat/reihe.py +202 -0
- package/.koolie/core/tests/erhebungen/apparat/selbsttest.py +280 -0
- package/.koolie/core/tests/erhebungen/apparat/stand.py +30 -0
- package/.koolie/core/tests/erhebungen/apparat/vorpruefung.py +70 -0
- package/.koolie/core/tests/erhebungen/auswerten-b4.py +468 -0
- package/.koolie/core/tests/erhebungen/auswerten-b5.py +517 -0
- package/.koolie/core/tests/erhebungen/auswerten-dd.py +172 -0
- package/.koolie/core/tests/erhebungen/baeume-b4.py +334 -0
- package/.koolie/core/tests/erhebungen/baeume-b5.py +339 -0
- package/.koolie/core/tests/erhebungen/baeume_loeschen.py +138 -0
- package/.koolie/core/tests/erhebungen/cc-overlay-fuellen.py +277 -0
- package/.koolie/core/tests/erhebungen/dossier-b4.py +178 -0
- package/.koolie/core/tests/erhebungen/dossier-b5.py +202 -0
- package/.koolie/core/tests/erhebungen/historie-bauen-b4.py +642 -0
- package/.koolie/core/tests/erhebungen/k-bauen-b3.py +841 -0
- package/.koolie/core/tests/erhebungen/lauf-dd.py +146 -0
- package/.koolie/core/tests/erhebungen/lauf.py +114 -0
- package/.koolie/core/tests/erhebungen/mcp-waechter.py +278 -0
- package/.koolie/core/tests/erhebungen/messbaum-schnitt.py +256 -0
- package/.koolie/core/tests/erhebungen/messen.py +72 -0
- package/.koolie/core/tests/erhebungen/node-waechter.py +74 -0
- package/.koolie/core/tests/erhebungen/packaktivierung.py +403 -0
- package/.koolie/core/tests/erhebungen/prompts-schreiben-b4.py +210 -0
- package/.koolie/core/tests/erhebungen/prompts-schreiben-b5.py +251 -0
- package/.koolie/core/tests/erhebungen/reihe-b4.py +171 -0
- package/.koolie/core/tests/erhebungen/reihe-b5.py +172 -0
- package/.koolie/core/tests/erhebungen/stand-b4.py +135 -0
- package/.koolie/core/tests/erhebungen/stand-b5.py +137 -0
- package/.koolie/core/tests/erhebungen/trust-b4.py +56 -0
- package/.koolie/core/tests/erhebungen/trust-b5.py +56 -0
- package/.koolie/core/tests/erhebungen/turn2-schreiben-b4.py +105 -0
- package/.koolie/core/tests/erhebungen/umgebungen-bauen-b4.py +377 -0
- package/.koolie/core/tests/erhebungen/umgebungen-bauen-b5.py +363 -0
- package/.koolie/core/tests/erhebungen/zaehlen46.py +74 -0
- package/.koolie/core/tests/erhebungen/zustand-b4.py +82 -0
- package/.koolie/core/tests/erhebungen/zustand-b5.py +82 -0
- package/.koolie/core/tests/protocols/2026-09-10-AP2-claude-code-wirkungsnachweise.md +194 -0
- package/.koolie/core/tests/protocols/2026-09-10-AP2-claude-code.md +468 -0
- package/.koolie/core/tests/protocols/2026-09-10-CR-2026-020-akteursbezeichnung.md +99 -0
- package/.koolie/core/tests/protocols/2026-09-10-CR-2026-021-hook-interpreter.md +138 -0
- package/.koolie/core/tests/protocols/2026-09-10-CR-2026-022-artefaktversionen.md +87 -0
- package/.koolie/core/tests/protocols/2026-09-10-CR-2026-023-shell-lesesperre.md +135 -0
- package/.koolie/core/tests/protocols/2026-09-10-CR-2026-024-clientbindungen.md +98 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-DS-03.md +90 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-KO-01.md +105 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-KO-02.md +123 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-KO-04.md +73 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-RE-02.md +99 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-VN-01-wiederholung.md +108 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-VN-01.md +249 -0
- package/.koolie/core/tests/protocols/2026-09-10-FW-ZA-05.md +99 -0
- package/.koolie/core/tests/protocols/2026-09-11-AP2-devin-desktop.md +257 -0
- package/.koolie/core/tests/protocols/2026-09-11-CR-2026-026-fail-closed.md +113 -0
- package/.koolie/core/tests/protocols/2026-09-11-erhebungen-K21-K26.md +243 -0
- package/.koolie/core/tests/protocols/2026-09-11-wirkungsnachweise-0.26.0.md +101 -0
- package/.koolie/core/tests/protocols/2026-09-12-B01-allowed-tools.md +152 -0
- package/.koolie/core/tests/protocols/2026-09-12-B04-B05-gegenpruefung.md +204 -0
- package/.koolie/core/tests/protocols/2026-09-12-erhebungen-K28-S5-B9-bypass.md +268 -0
- package/.koolie/core/tests/protocols/2026-09-12-mehrprojekt-arbeitsbereich.md +154 -0
- package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.26.1.md +126 -0
- package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.27.0.md +114 -0
- package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.28.0.md +129 -0
- package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.29.0.md +121 -0
- package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.30.0.md +112 -0
- package/.koolie/core/tests/protocols/2026-09-12-wirkungsnachweise-0.31.0.md +107 -0
- package/.koolie/core/tests/protocols/2026-09-13-B06-gegenpruefung.md +370 -0
- package/.koolie/core/tests/protocols/2026-09-13-B07-B09-gegenpruefung.md +211 -0
- package/.koolie/core/tests/protocols/2026-09-13-B08-B11-gegenpruefung.md +179 -0
- package/.koolie/core/tests/protocols/2026-09-13-erhebung-disallowed-tools.md +147 -0
- package/.koolie/core/tests/protocols/2026-09-13-erhebung-unteragent-tiefe.md +139 -0
- package/.koolie/core/tests/protocols/2026-09-13-erhebung-unteragent.md +214 -0
- package/.koolie/core/tests/protocols/2026-09-13-gegenpruefung-berechtigungsdatei.md +253 -0
- package/.koolie/core/tests/protocols/2026-09-13-gegenpruefung-stumme-brueche.md +202 -0
- package/.koolie/core/tests/protocols/2026-09-13-gegenpruefung-werkzeugabbildung.md +219 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.32.0.md +133 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.33.0.md +169 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.34.0.md +187 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.35.0.md +187 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.36.0.md +137 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.37.0.md +112 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.38.0.md +154 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.39.0.md +188 -0
- package/.koolie/core/tests/protocols/2026-09-13-wirkungsnachweise-0.40.0.md +214 -0
- package/.koolie/core/tests/protocols/2026-09-14-erhebung-devin-werkzeuge.md +267 -0
- package/.koolie/core/tests/protocols/2026-09-14-erhebung-skillaufruf.md +238 -0
- package/.koolie/core/tests/protocols/2026-09-14-gegenpruefung-pruefregister.md +156 -0
- package/.koolie/core/tests/protocols/2026-09-14-gegenpruefung-schlitzdeckung.md +206 -0
- package/.koolie/core/tests/protocols/2026-09-14-gegenpruefung-skillwahl.md +127 -0
- package/.koolie/core/tests/protocols/2026-09-14-migrationslauf-pilot.md +134 -0
- package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.41.0.md +175 -0
- package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.42.0.md +180 -0
- package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.43.0.md +165 -0
- package/.koolie/core/tests/protocols/2026-09-14-wirkungsnachweise-0.44.0.md +130 -0
- package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-d11-zaehlregeln.md +311 -0
- package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-modulstatus.md +524 -0
- package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-nicht-skill-traeger.md +225 -0
- package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-restliche-nicht-skill-traeger.md +345 -0
- package/.koolie/core/tests/protocols/2026-09-15-gegenpruefung-strukturentscheidungen.md +392 -0
- package/.koolie/core/tests/protocols/2026-09-15-herrichtung-uebungsrepositorium.md +213 -0
- package/.koolie/core/tests/protocols/2026-09-15-migrationslauf-pilot-0.46.0.md +143 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.45.0.md +127 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.46.0.md +193 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.47.0.md +149 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.49.0.md +202 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.50.0.md +183 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.51.0.md +202 -0
- package/.koolie/core/tests/protocols/2026-09-15-wirkungsnachweise-0.52.0.md +168 -0
- package/.koolie/core/tests/protocols/2026-09-16-AP2-zielversion-devin-desktop.md +231 -0
- package/.koolie/core/tests/protocols/2026-09-16-wirkungsnachweise-0.53.0.md +350 -0
- package/.koolie/core/tests/protocols/2026-09-17-nachtrag-migrationshinweis-0.54.1.md +121 -0
- package/.koolie/core/tests/protocols/2026-09-17-sitzungstest-pi-ds.md +523 -0
- package/.koolie/core/tests/protocols/2026-09-17-sitzungstest-schranken.md +433 -0
- package/.koolie/core/tests/protocols/2026-09-17-wirkungsnachweise-0.54.0.md +159 -0
- package/.koolie/core/tests/protocols/2026-09-18-FW-AK-01.md +442 -0
- package/.koolie/core/tests/protocols/2026-09-18-FW-KO-05.md +231 -0
- package/.koolie/core/tests/protocols/2026-09-18-herrichtung-uebungsrepositorium.md +273 -0
- package/.koolie/core/tests/protocols/2026-09-18-nachtrag-preis-umbenennung-0.56.1.md +102 -0
- package/.koolie/core/tests/protocols/2026-09-18-sitzungstest-5.md +349 -0
- package/.koolie/core/tests/protocols/2026-09-18-sitzungstest-ne-sc.md +430 -0
- package/.koolie/core/tests/protocols/2026-09-18-sitzungstest-pi-ds-2.md +499 -0
- package/.koolie/core/tests/protocols/2026-09-18-vorbedingungen-sitzungstest-5-zweiter-durchgang.md +170 -0
- package/.koolie/core/tests/protocols/2026-09-18-vorbedingungen-testblaetter.md +184 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.55.0.md +202 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.56.0.md +135 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.56.2.md +154 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.57.0.md +228 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.57.1.md +262 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.58.0.md +135 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.59.0.md +150 -0
- package/.koolie/core/tests/protocols/2026-09-18-wirkungsnachweise-0.60.0.md +340 -0
- package/.koolie/core/tests/protocols/2026-09-19-herrichtung-buendel-4.md +235 -0
- package/.koolie/core/tests/protocols/2026-09-19-k77-zurechenbarkeit.md +236 -0
- package/.koolie/core/tests/protocols/2026-09-19-pruefapparat-filter.md +50 -0
- package/.koolie/core/tests/protocols/2026-09-19-pruefmittelwort-und-buendelschnitt.md +239 -0
- package/.koolie/core/tests/protocols/2026-09-19-sechzehnte-praeparation.md +135 -0
- package/.koolie/core/tests/protocols/2026-09-19-testblaetter-buendel-1.md +250 -0
- package/.koolie/core/tests/protocols/2026-09-19-testblaetter-buendel-2.md +229 -0
- package/.koolie/core/tests/protocols/2026-09-19-testblaetter-buendel-3.md +293 -0
- package/.koolie/core/tests/protocols/2026-09-19-vorbedingungen-buendel-2.md +236 -0
- package/.koolie/core/tests/protocols/2026-09-19-vorbedingungen-buendel-3.md +253 -0
- package/.koolie/core/tests/protocols/2026-09-19-vorbedingungen-buendel-4.md +138 -0
- package/.koolie/core/tests/protocols/2026-09-20-messapparat-buendel-4.md +433 -0
- package/.koolie/core/tests/protocols/2026-09-20-testblaetter-buendel-4.md +321 -0
- package/.koolie/core/tests/protocols/2026-09-20-uebergabe-im-release-commit.md +146 -0
- package/.koolie/core/tests/protocols/2026-09-20-vorbedingungen-nachlauf-b4.md +259 -0
- package/.koolie/core/tests/protocols/2026-09-21-arbeitsplatz-im-kern.md +127 -0
- package/.koolie/core/tests/protocols/2026-09-21-nachlauf-buendel-4.md +157 -0
- package/.koolie/core/tests/protocols/2026-09-21-vorbedingungen-buendel-5.md +247 -0
- package/.koolie/core/tests/protocols/2026-09-21-wiederaufnahme-nachlauf-b4.md +190 -0
- package/.koolie/core/tests/protocols/2026-09-22-ap2-rest-devin-desktop.md +614 -0
- package/.koolie/core/tests/protocols/2026-09-22-hauptdokument-ap11.md +443 -0
- package/.koolie/core/tests/protocols/2026-09-22-herrichtung-buendel-5.md +224 -0
- package/.koolie/core/tests/protocols/2026-09-22-messtag-buendel-5.md +327 -0
- package/.koolie/core/tests/protocols/2026-09-22-quellenzuordnung-matrixzeilen.md +230 -0
- package/.koolie/core/tests/protocols/2026-09-22-sammelzellen-zentraler-katalog.md +354 -0
- package/.koolie/core/tests/protocols/2026-09-22-umbenennung-koolie.md +296 -0
- package/.koolie/core/tests/protocols/2026-09-22-verify-marker-abschaffen.md +283 -0
- package/.koolie/core/tests/protocols/2026-09-22-vortrag-und-name.md +261 -0
- package/.koolie/core/tests/protocols/2026-09-22-wirkungsnachweise-0.86.0.md +158 -0
- package/.koolie/core/tests/protocols/2026-09-23-archiv-zeilenenden.md +113 -0
- package/.koolie/core/tests/protocols/2026-09-23-bau-openai-codex.md +168 -0
- package/.koolie/core/tests/protocols/2026-09-23-chronik-und-vorlage.md +224 -0
- package/.koolie/core/tests/protocols/2026-09-23-erhebung-openai-codex.md +309 -0
- package/.koolie/core/tests/protocols/2026-09-23-freigabelauf-1.0.0-vorbereitung.md +119 -0
- package/.koolie/core/tests/protocols/2026-09-23-freigabelauf-1.0.0.md +150 -0
- package/.koolie/core/tests/protocols/2026-09-23-gegenzeichnung-rollenfrage.md +198 -0
- package/.koolie/core/tests/protocols/2026-09-23-reihenfolge-des-hebens.md +220 -0
- package/.koolie/core/tests/protocols/2026-09-23-word-fassung-und-blinder-fleck.md +307 -0
- package/.koolie/core/tests/protocols/2026-09-24-kopierweg-kern.md +98 -0
- package/.koolie/core/tests/protocols/2026-09-24-overlay-werte-einordnung.md +111 -0
- package/.koolie/core/tests/protocols/2026-09-24-pfadlisten-nul-getrennt.md +128 -0
- package/.koolie/core/tests/protocols/2026-09-24-uebergabe-lokal.md +85 -0
- package/.koolie/core/tests/protocols/2026-09-25-code-pack-posten.md +86 -0
- package/.koolie/core/tests/protocols/2026-09-25-dokumentationsstandard.md +101 -0
- package/.koolie/core/tests/protocols/2026-09-25-installer-je-zielsystem.md +134 -0
- package/.koolie/core/tests/protocols/2026-09-25-klasse-b-core-laufzeit.md +126 -0
- package/.koolie/core/tests/protocols/2026-09-25-klasse-b-governance.md +133 -0
- package/.koolie/core/tests/protocols/2026-09-25-klasse-b-skills.md +91 -0
- package/.koolie/core/tests/protocols/2026-09-25-lieferumfang.md +76 -0
- package/.koolie/core/tests/protocols/2026-09-25-overlay-muster-dokumente.md +107 -0
- package/.koolie/core/tests/protocols/2026-09-25-overlay-muster-general.md +100 -0
- package/.koolie/core/tests/protocols/2026-09-25-regel-register-posten.md +87 -0
- package/.koolie/core/tests/protocols/2026-09-26-attributionszeile.md +83 -0
- package/.koolie/core/tests/protocols/2026-09-26-auffindbarkeit.md +85 -0
- package/.koolie/core/tests/protocols/2026-09-26-bau-cursor.md +68 -0
- package/.koolie/core/tests/protocols/2026-09-26-bau-kiro.md +75 -0
- package/.koolie/core/tests/protocols/2026-09-26-mehrprojekt-tokenlast.md +174 -0
- package/.koolie/core/tests/protocols/2026-09-26-regelablage-pfadtoken.md +133 -0
- package/.koolie/core/tests/protocols/2026-09-26-skill-anweisungen.md +111 -0
- package/.koolie/core/tests/protocols/2026-09-26-testblaetter-modellwechsel.md +106 -0
- package/.koolie/core/tests/protocols/2026-09-27-mandat-und-reibung.md +58 -0
- package/.koolie/core/tests/protocols/2026-09-28-mcp-anbindung.md +72 -0
- package/.koolie/core/tests/protocols/2026-09-29-messapparat.md +75 -0
- package/.koolie/core/tests/protocols/2026-09-29-powershell.md +58 -0
- package/.koolie/core/tests/protocols/2026-09-29-pruefwerkzeuge.md +64 -0
- package/.koolie/core/tests/protocols/2026-09-29-registerpflege.md +228 -0
- package/.koolie/core/tests/protocols/2026-09-29-schutzschicht.md +95 -0
- package/.koolie/core/tests/protocols/2026-09-30-einsatzarchitektur.md +104 -0
- package/.koolie/core/tests/protocols/2026-09-30-modi-ausnahmen.md +77 -0
- package/.koolie/core/tests/protocols/2026-09-30-nachlauf-banner.md +85 -0
- package/.koolie/core/tests/protocols/2026-09-30-paketquellen.md +65 -0
- package/.koolie/core/tests/protocols/2026-09-30-pfadsemantik.md +79 -0
- package/.koolie/core/tests/protocols/2026-10-01-auftritt-und-suche.md +58 -0
- package/.koolie/core/tests/protocols/2026-10-01-befehl-im-projektverzeichnis.md +47 -0
- package/.koolie/core/tests/protocols/2026-10-01-erste-veroeffentlichung.md +60 -0
- package/.koolie/core/tests/protocols/2026-10-01-modusbindung-messfragen.md +107 -0
- package/.koolie/core/tests/protocols/README.md +14 -0
- package/.koolie/core/tests/scripts/hook-check-secrets.py +1307 -0
- package/.koolie/core/tests/scripts/hook-overlay-status.py +120 -0
- package/.koolie/core/tests/scripts/mermaid_renderer.py +67 -0
- package/.koolie/core/tests/scripts/overlay_status.py +130 -0
- package/.koolie/core/tests/scripts/probe-pruefungen.py +110 -0
- package/.koolie/core/tests/scripts/pruefungen/__init__.py +4 -0
- package/.koolie/core/tests/scripts/pruefungen/berechtigungen.py +1559 -0
- package/.koolie/core/tests/scripts/pruefungen/bestand.py +1433 -0
- package/.koolie/core/tests/scripts/pruefungen/dokumente.py +1262 -0
- package/.koolie/core/tests/scripts/pruefungen/gemeinsam.py +784 -0
- package/.koolie/core/tests/scripts/pruefungen/hooks.py +1188 -0
- package/.koolie/core/tests/scripts/pruefungen/overlay.py +1251 -0
- package/.koolie/core/tests/scripts/pruefungen/packs.py +1314 -0
- package/.koolie/core/tests/scripts/pruefungen/register.py +950 -0
- package/.koolie/core/tests/scripts/pruefungen/testkatalog.py +1202 -0
- package/.koolie/core/tests/scripts/pruefungen/werkzeuge.py +957 -0
- package/.koolie/core/tests/scripts/sonden/__init__.py +4 -0
- package/.koolie/core/tests/scripts/sonden/apparat.py +653 -0
- package/.koolie/core/tests/scripts/sonden/teil01_grundbestand_und_installation.py +999 -0
- package/.koolie/core/tests/scripts/sonden/teil02_packs_mandat_mcp.py +942 -0
- package/.koolie/core/tests/scripts/sonden/teil03_pruefungen_26_bis_36.py +1184 -0
- package/.koolie/core/tests/scripts/sonden/teil04_pruefungen_37_bis_45.py +1491 -0
- package/.koolie/core/tests/scripts/sonden/teil05_pruefungen_46_bis_55.py +1447 -0
- package/.koolie/core/tests/scripts/sonden/teil06_pruefungen_57_bis_65.py +739 -0
- package/.koolie/core/tests/scripts/sonden/teil07_overlay_und_lieferung.py +1014 -0
- package/.koolie/core/tests/scripts/sonden/teil08_pruefungen_66_bis_80.py +1325 -0
- package/.koolie/core/tests/scripts/sonden/teil09_pruefmittel_81_und_82.py +505 -0
- package/.koolie/core/tests/scripts/sonden/teil10_pruefungen_83_bis_95.py +1218 -0
- package/.koolie/core/tests/scripts/sonden/teil11_pruefungen_104_und_105.py +149 -0
- package/.koolie/core/tests/scripts/sonden/teil12_pruefungen_106_und_107.py +136 -0
- package/.koolie/core/tests/scripts/sonden/teil13_pfad_und_mustersemantik.py +150 -0
- package/.koolie/core/tests/scripts/sonden/teil14_modi_ausnahmen_skills.py +204 -0
- package/.koolie/core/tests/scripts/sonden/teil15_banner_und_nachlauf.py +193 -0
- package/.koolie/core/tests/scripts/sonden/teil16_koexistenz.py +114 -0
- package/.koolie/core/tests/scripts/sonden/teil17_paketquellen.py +115 -0
- package/.koolie/core/tests/scripts/sonden/teil18_modusbindung_m3_m5.py +113 -0
- package/.koolie/core/tests/scripts/validate-framework.py +1104 -0
- package/.koolie/core/tests/scripts/validate-output.py +296 -0
- package/.koolie/core/wirksamkeit.py +430 -0
- package/CONTRIBUTING.md +119 -0
- package/LICENSE +674 -0
- package/QUICKSTART.en.md +135 -0
- package/QUICKSTART.md +129 -0
- package/README.en.md +108 -0
- package/README.md +107 -0
- package/install.cmd +44 -0
- package/install.command +46 -0
- package/package.json +21 -0
- package/paketquellen/README.md +41 -0
- package/paketquellen/bauen.py +411 -0
- package/paketquellen/koolie.cmd +23 -0
- package/paketquellen/koolie_befehl.py +82 -0
- 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())
|