claude-code-kit 0.7.0__py3-none-any.whl
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.
- claude_code_kit-0.7.0.dist-info/METADATA +384 -0
- claude_code_kit-0.7.0.dist-info/RECORD +209 -0
- claude_code_kit-0.7.0.dist-info/WHEEL +4 -0
- claude_code_kit-0.7.0.dist-info/entry_points.txt +4 -0
- claude_code_kit-0.7.0.dist-info/licenses/LICENSE +21 -0
- claude_kit/__init__.py +10 -0
- claude_kit/__main__.py +8 -0
- claude_kit/_payload/agents/acceptance-reviewer.md +60 -0
- claude_kit/_payload/agents/auditor.md +76 -0
- claude_kit/_payload/agents/dependency-scanner.md +84 -0
- claude_kit/_payload/agents/developer.md +187 -0
- claude_kit/_payload/agents/devils-advocate.md +62 -0
- claude_kit/_payload/agents/devops-engineer.md +134 -0
- claude_kit/_payload/agents/e2e-tester.md +152 -0
- claude_kit/_payload/agents/em-reviewer.md +105 -0
- claude_kit/_payload/agents/incident-responder.md +64 -0
- claude_kit/_payload/agents/merge-reviewer.md +194 -0
- claude_kit/_payload/agents/observability-engineer.md +94 -0
- claude_kit/_payload/agents/orchestrator.md +551 -0
- claude_kit/_payload/agents/owasp-reviewer.md +76 -0
- claude_kit/_payload/agents/policy-validator.md +63 -0
- claude_kit/_payload/agents/pr-raiser.md +138 -0
- claude_kit/_payload/agents/risk-classifier.md +50 -0
- claude_kit/_payload/agents/sdlc-code-reviewer.md +196 -0
- claude_kit/_payload/agents/secret-scanner.md +70 -0
- claude_kit/_payload/agents/security-reviewer.md +80 -0
- claude_kit/_payload/agents/senior-backend-dev.md +199 -0
- claude_kit/_payload/agents/senior-frontend-dev.md +181 -0
- claude_kit/_payload/agents/senior-tester.md +206 -0
- claude_kit/_payload/agents/spec-doc-writer.md +331 -0
- claude_kit/_payload/agents/story-planner.md +56 -0
- claude_kit/_payload/agents/technical-architect.md +139 -0
- claude_kit/_payload/agents/tester.md +193 -0
- claude_kit/_payload/agents/ui-designer.md +73 -0
- claude_kit/_payload/agents/unit-tester.md +119 -0
- claude_kit/_payload/catalog/mcp.yaml +54 -0
- claude_kit/_payload/catalog/org.yaml +145 -0
- claude_kit/_payload/catalog/profiles.yaml +96 -0
- claude_kit/_payload/catalog/stacks.yaml +96 -0
- claude_kit/_payload/commands/init.md +36 -0
- claude_kit/_payload/commands/sdlc.md +18 -0
- claude_kit/_payload/commands/status.md +20 -0
- claude_kit/_payload/hooks/hooks.json +58 -0
- claude_kit/_payload/hooks/scripts/audit-log.sh +18 -0
- claude_kit/_payload/hooks/scripts/guard-secrets.sh +26 -0
- claude_kit/_payload/hooks/scripts/lint-fix.sh +38 -0
- claude_kit/_payload/hooks/scripts/load-continuity.sh +32 -0
- claude_kit/_payload/hooks/scripts/load-learnings.sh +40 -0
- claude_kit/_payload/hooks/scripts/type-check.sh +23 -0
- claude_kit/_payload/hooks/scripts/validate-frontmatter.sh +34 -0
- claude_kit/_payload/hooks/scripts/validate-settings.sh +21 -0
- claude_kit/_payload/hooks/scripts/warn-large-edits.sh +24 -0
- claude_kit/_payload/hooks/scripts/warn-missing-tests.sh +24 -0
- claude_kit/_payload/hooks/scripts/warn-sensitive-files.sh +30 -0
- claude_kit/_payload/hooks/scripts/warn-shared-modules.sh +33 -0
- claude_kit/_payload/rules/agent-guardrails.md +83 -0
- claude_kit/_payload/rules/agent-memory.md +106 -0
- claude_kit/_payload/rules/agent-resilience.md +61 -0
- claude_kit/_payload/rules/autonomy-levels.md +30 -0
- claude_kit/_payload/rules/code-organization.md +312 -0
- claude_kit/_payload/rules/continuity.md +84 -0
- claude_kit/_payload/rules/design-patterns.md +422 -0
- claude_kit/_payload/rules/devops-observability.md +57 -0
- claude_kit/_payload/rules/documentation.md +326 -0
- claude_kit/_payload/rules/evals.md +62 -0
- claude_kit/_payload/rules/frontend-best-practices.md +157 -0
- claude_kit/_payload/rules/goal-setting-and-monitoring.md +72 -0
- claude_kit/_payload/rules/human-in-the-loop.md +64 -0
- claude_kit/_payload/rules/linting-and-formatting.md +220 -0
- claude_kit/_payload/rules/mandatory-workflow.md +309 -0
- claude_kit/_payload/rules/model-tiers.md +34 -0
- claude_kit/_payload/rules/quality-gates.md +107 -0
- claude_kit/_payload/rules/rarv-cycle.md +31 -0
- claude_kit/_payload/rules/reasoning-techniques.md +62 -0
- claude_kit/_payload/rules/responsive-and-accessibility.md +353 -0
- claude_kit/_payload/rules/risk-classification.md +36 -0
- claude_kit/_payload/rules/testing.md +417 -0
- claude_kit/_payload/rules/tool-design.md +66 -0
- claude_kit/_payload/skills/_references/accessibility-checklist.md +160 -0
- claude_kit/_payload/skills/_references/orchestration-patterns.md +405 -0
- claude_kit/_payload/skills/_references/performance-checklist.md +153 -0
- claude_kit/_payload/skills/_references/security-checklist.md +134 -0
- claude_kit/_payload/skills/_references/testing-patterns.md +236 -0
- claude_kit/_payload/skills/accessibility-review/SKILL.md +56 -0
- claude_kit/_payload/skills/api-and-interface-design/SKILL.md +294 -0
- claude_kit/_payload/skills/api-integration/SKILL.md +348 -0
- claude_kit/_payload/skills/archive-sprint/SKILL.md +31 -0
- claude_kit/_payload/skills/backlog/SKILL.md +41 -0
- claude_kit/_payload/skills/backlog/item-template.md +20 -0
- claude_kit/_payload/skills/browser-testing-with-devtools/SKILL.md +302 -0
- claude_kit/_payload/skills/ci-cd-and-automation/SKILL.md +402 -0
- claude_kit/_payload/skills/code-review-and-quality/SKILL.md +347 -0
- claude_kit/_payload/skills/code-simplification/SKILL.md +331 -0
- claude_kit/_payload/skills/component-design/SKILL.md +171 -0
- claude_kit/_payload/skills/consolidate-learnings/SKILL.md +55 -0
- claude_kit/_payload/skills/context-engineering/SKILL.md +321 -0
- claude_kit/_payload/skills/debugging-and-error-recovery/SKILL.md +300 -0
- claude_kit/_payload/skills/decision/SKILL.md +46 -0
- claude_kit/_payload/skills/decision/adr-template.md +36 -0
- claude_kit/_payload/skills/deprecation-and-migration/SKILL.md +207 -0
- claude_kit/_payload/skills/documentation-and-adrs/SKILL.md +299 -0
- claude_kit/_payload/skills/doubt-driven-development/SKILL.md +243 -0
- claude_kit/_payload/skills/execute/SKILL.md +27 -0
- claude_kit/_payload/skills/frontend-ui-engineering/SKILL.md +328 -0
- claude_kit/_payload/skills/git-workflow-and-versioning/SKILL.md +300 -0
- claude_kit/_payload/skills/idea-refine/SKILL.md +178 -0
- claude_kit/_payload/skills/idea-refine/examples.md +238 -0
- claude_kit/_payload/skills/idea-refine/frameworks.md +99 -0
- claude_kit/_payload/skills/idea-refine/refinement-criteria.md +113 -0
- claude_kit/_payload/skills/idea-refine/scripts/idea-refine.sh +15 -0
- claude_kit/_payload/skills/incident-postmortem/SKILL.md +74 -0
- claude_kit/_payload/skills/incremental-implementation/SKILL.md +245 -0
- claude_kit/_payload/skills/interview-me/SKILL.md +221 -0
- claude_kit/_payload/skills/load-testing/SKILL.md +83 -0
- claude_kit/_payload/skills/manual-test/SKILL.md +516 -0
- claude_kit/_payload/skills/performance-optimization/SKILL.md +277 -0
- claude_kit/_payload/skills/planning-and-task-breakdown/SKILL.md +223 -0
- claude_kit/_payload/skills/playwright-verification/SKILL.md +205 -0
- claude_kit/_payload/skills/refresh-docs/SKILL.md +63 -0
- claude_kit/_payload/skills/remember/SKILL.md +96 -0
- claude_kit/_payload/skills/scope/SKILL.md +52 -0
- claude_kit/_payload/skills/scope/scope-template.md +82 -0
- claude_kit/_payload/skills/sdlc/SKILL.md +83 -0
- claude_kit/_payload/skills/security-and-hardening/SKILL.md +368 -0
- claude_kit/_payload/skills/security-verification/SKILL.md +209 -0
- claude_kit/_payload/skills/shipping-and-launch/SKILL.md +309 -0
- claude_kit/_payload/skills/smoke-test/SKILL.md +78 -0
- claude_kit/_payload/skills/source-driven-development/SKILL.md +195 -0
- claude_kit/_payload/skills/spec-driven-development/SKILL.md +200 -0
- claude_kit/_payload/skills/sprint/SKILL.md +67 -0
- claude_kit/_payload/skills/sprint/sprint-template.md +90 -0
- claude_kit/_payload/skills/test-driven-development/SKILL.md +383 -0
- claude_kit/_payload/skills/threat-model/SKILL.md +60 -0
- claude_kit/_payload/skills/triage/SKILL.md +87 -0
- claude_kit/_payload/skills/ui-ux-design/SKILL.md +71 -0
- claude_kit/_payload/skills/unit-test/SKILL.md +237 -0
- claude_kit/_payload/skills/using-agent-skills/SKILL.md +180 -0
- claude_kit/_payload/templates/CLAUDE.md +238 -0
- claude_kit/_payload/templates/CLAUDE.stack.md.tmpl +53 -0
- claude_kit/_payload/templates/CONTINUITY.template.md +35 -0
- claude_kit/_payload/templates/README.claude-sdlc.md.tmpl +219 -0
- claude_kit/_payload/templates/agent-memory/MEMORY.md +30 -0
- claude_kit/_payload/templates/agent-memory/api/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/architecture/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/debugging/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/gotchas/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/patterns/.gitkeep +0 -0
- claude_kit/_payload/templates/agent-memory/performance/.gitkeep +0 -0
- claude_kit/_payload/templates/artifacts/adr.md +18 -0
- claude_kit/_payload/templates/artifacts/feature-spec.md +29 -0
- claude_kit/_payload/templates/artifacts/release-plan.md +23 -0
- claude_kit/_payload/templates/artifacts/runbook.md +24 -0
- claude_kit/_payload/templates/artifacts/security-review.md +23 -0
- claude_kit/_payload/templates/artifacts/test-plan.md +22 -0
- claude_kit/_payload/templates/org/README.md +53 -0
- claude_kit/_payload/templates/org/agents/data-workflow-agent.md +59 -0
- claude_kit/_payload/templates/org/agents/founder-prototype-agent.md +61 -0
- claude_kit/_payload/templates/org/agents/internal-tools-builder.md +63 -0
- claude_kit/_payload/templates/org/agents/pm-copilot.md +60 -0
- claude_kit/_payload/templates/org/agents/support-ticket-engineer.md +63 -0
- claude_kit/_payload/templates/org/packs/devops-and-release/README.md +46 -0
- claude_kit/_payload/templates/org/packs/devops-and-release/pack.yaml +32 -0
- claude_kit/_payload/templates/org/packs/engineering-core/README.md +46 -0
- claude_kit/_payload/templates/org/packs/engineering-core/pack.yaml +44 -0
- claude_kit/_payload/templates/org/packs/non-engineer-builder/README.md +53 -0
- claude_kit/_payload/templates/org/packs/non-engineer-builder/pack.yaml +39 -0
- claude_kit/_payload/templates/org/packs/onboarding-and-docs/README.md +49 -0
- claude_kit/_payload/templates/org/packs/onboarding-and-docs/pack.yaml +26 -0
- claude_kit/_payload/templates/org/packs/product-to-code/README.md +50 -0
- claude_kit/_payload/templates/org/packs/product-to-code/pack.yaml +34 -0
- claude_kit/_payload/templates/org/packs/quality-and-review/README.md +53 -0
- claude_kit/_payload/templates/org/packs/quality-and-review/pack.yaml +40 -0
- claude_kit/_payload/templates/org/packs/security-and-compliance/README.md +50 -0
- claude_kit/_payload/templates/org/packs/security-and-compliance/pack.yaml +36 -0
- claude_kit/_payload/templates/org/rules/ai-working-agreement.md +45 -0
- claude_kit/_payload/templates/org/rules/ambiguity-resolution.md +36 -0
- claude_kit/_payload/templates/org/rules/branch-and-pr-policy.md +41 -0
- claude_kit/_payload/templates/org/rules/compliance-policy.md +50 -0
- claude_kit/_payload/templates/org/rules/non-engineer-safe-coding.md +37 -0
- claude_kit/_payload/templates/org/rules/pii-policy.md +46 -0
- claude_kit/_payload/templates/org/rules/production-data-policy.md +35 -0
- claude_kit/_payload/templates/org/rules/prompt-to-task-conversion.md +30 -0
- claude_kit/_payload/templates/org/rules/prototype-boundaries.md +40 -0
- claude_kit/_payload/templates/org/rules/secrets-policy.md +34 -0
- claude_kit/_payload/templates/org/skills/customer-issue-to-fix/SKILL.md +61 -0
- claude_kit/_payload/templates/org/skills/feature-from-idea/SKILL.md +56 -0
- claude_kit/_payload/templates/org/skills/prompt-to-safe-task/SKILL.md +59 -0
- claude_kit/_payload/templates/org/skills/prototype-to-production/SKILL.md +61 -0
- claude_kit/_payload/templates/org/skills/repo-onboarding/SKILL.md +60 -0
- claude_kit/_payload/templates/settings.json +53 -0
- claude_kit/_payload/templates/stacks/backend/python/fastapi/rules/fastapi-patterns.md +64 -0
- claude_kit/_payload/templates/stacks/db/mongodb/agents/migration-specialist.md +61 -0
- claude_kit/_payload/templates/stacks/db/mongodb/agents/mongodb-specialist.md +59 -0
- claude_kit/_payload/templates/stacks/db/mongodb/rules/mongodb-patterns.md +39 -0
- claude_kit/_payload/templates/stacks/db/postgres/agents/db-performance-reviewer.md +66 -0
- claude_kit/_payload/templates/stacks/db/postgres/agents/migration-specialist.md +56 -0
- claude_kit/_payload/templates/stacks/db/postgres/agents/postgres-specialist.md +58 -0
- claude_kit/_payload/templates/stacks/db/postgres/rules/database-performance.md +64 -0
- claude_kit/_payload/templates/stacks/db/postgres/rules/postgres-patterns.md +43 -0
- claude_kit/_payload/templates/stacks/frontend/react/rules/react-patterns.md +63 -0
- claude_kit/catalog.py +476 -0
- claude_kit/cli.py +327 -0
- claude_kit/hooks.py +246 -0
- claude_kit/models.py +205 -0
- claude_kit/prompts.py +209 -0
- claude_kit/render.py +146 -0
- claude_kit/scaffold.py +492 -0
- claude_kit/upgrader.py +294 -0
- claude_kit/validator.py +197 -0
claude_kit/catalog.py
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
"""Catalog loader + resolver — turns a :class:`~claude_kit.models.Selection` into a concrete
|
|
2
|
+
:class:`~claude_kit.models.ResolvedPlan` with **no hard-coded branching**.
|
|
3
|
+
|
|
4
|
+
Everything selectable lives in ``catalog/{stacks,profiles,mcp}.yaml``. Adding a framework, database,
|
|
5
|
+
profile, or MCP server is a YAML edit (plus a ``templates/stacks/<stack_dir>/`` folder for overlay
|
|
6
|
+
content); this module never needs to change.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
import yaml
|
|
15
|
+
|
|
16
|
+
from claude_kit import hooks as hooks_mod
|
|
17
|
+
from claude_kit.models import OrgPlan, ResolvedPlan, Selection
|
|
18
|
+
|
|
19
|
+
#: Canonical backend command keys surfaced in CLAUDE.md (defaulted to "" so templates never break).
|
|
20
|
+
_BACKEND_CMD_KEYS = (
|
|
21
|
+
"install",
|
|
22
|
+
"dev",
|
|
23
|
+
"test",
|
|
24
|
+
"lint",
|
|
25
|
+
"format",
|
|
26
|
+
"migrate",
|
|
27
|
+
"make_migration",
|
|
28
|
+
)
|
|
29
|
+
#: Canonical frontend command keys surfaced in CLAUDE.md.
|
|
30
|
+
_FRONTEND_CMD_KEYS = ("install", "dev", "test", "lint", "typecheck", "build")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def catalog_dir(payload_root: Path) -> Path:
|
|
34
|
+
"""Return the ``catalog/`` directory inside a payload root."""
|
|
35
|
+
return Path(payload_root) / "catalog"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _load(payload_root: Path, name: str) -> dict[str, Any]:
|
|
39
|
+
"""Load and parse a catalog YAML file by name (e.g. ``"stacks.yaml"``)."""
|
|
40
|
+
path = catalog_dir(payload_root) / name
|
|
41
|
+
if not path.is_file():
|
|
42
|
+
raise FileNotFoundError(f"catalog file not found: {path}")
|
|
43
|
+
data = yaml.safe_load(path.read_text(encoding="utf-8"))
|
|
44
|
+
if not isinstance(data, dict):
|
|
45
|
+
raise ValueError(f"catalog file {name} did not parse to a mapping")
|
|
46
|
+
return data
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _dedup(items: list[str]) -> list[str]:
|
|
50
|
+
"""Return ``items`` with duplicates removed, order preserved."""
|
|
51
|
+
seen: set[str] = set()
|
|
52
|
+
out: list[str] = []
|
|
53
|
+
for item in items:
|
|
54
|
+
if item not in seen:
|
|
55
|
+
seen.add(item)
|
|
56
|
+
out.append(item)
|
|
57
|
+
return out
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def available(payload_root: Path) -> dict[str, list[str]]:
|
|
61
|
+
"""Discover the installable agents/skills/hooks present in a payload root.
|
|
62
|
+
|
|
63
|
+
Used to expand the ``all`` token in profiles and to validate profile membership.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
Mapping with keys ``agents`` (agent file stems), ``skills`` (skill dir names, excluding
|
|
67
|
+
``_references``), and ``hooks`` (every registered hook id).
|
|
68
|
+
"""
|
|
69
|
+
root = Path(payload_root)
|
|
70
|
+
agents_dir = root / "agents"
|
|
71
|
+
skills_dir = root / "skills"
|
|
72
|
+
agents = (
|
|
73
|
+
sorted(p.stem for p in agents_dir.glob("*.md")) if agents_dir.is_dir() else []
|
|
74
|
+
)
|
|
75
|
+
skills = (
|
|
76
|
+
sorted(
|
|
77
|
+
p.name
|
|
78
|
+
for p in skills_dir.iterdir()
|
|
79
|
+
if p.is_dir() and not p.name.startswith("_") and (p / "SKILL.md").is_file()
|
|
80
|
+
)
|
|
81
|
+
if skills_dir.is_dir()
|
|
82
|
+
else []
|
|
83
|
+
)
|
|
84
|
+
return {"agents": agents, "skills": skills, "hooks": hooks_mod.all_ids()}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --- selection lookups (raise ValueError on unknown / not-yet-shipped choices) --------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _frontend(stacks: dict[str, Any], framework: str) -> dict[str, Any]:
|
|
91
|
+
fws = stacks.get("frontend", {}).get("frameworks", {})
|
|
92
|
+
fw = fws.get(framework)
|
|
93
|
+
if fw is None:
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"unknown frontend framework {framework!r} (choices: {', '.join(fws)})"
|
|
96
|
+
)
|
|
97
|
+
if fw.get("status") == "planned":
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"frontend framework {framework!r} is planned but not yet available"
|
|
100
|
+
)
|
|
101
|
+
return fw
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _backend(
|
|
105
|
+
stacks: dict[str, Any], language: str, framework: str
|
|
106
|
+
) -> tuple[dict[str, Any], dict[str, Any]]:
|
|
107
|
+
langs = stacks.get("backend", {}).get("languages", {})
|
|
108
|
+
lang = langs.get(language)
|
|
109
|
+
if lang is None:
|
|
110
|
+
raise ValueError(
|
|
111
|
+
f"unknown backend language {language!r} (choices: {', '.join(langs)})"
|
|
112
|
+
)
|
|
113
|
+
if lang.get("status") == "planned":
|
|
114
|
+
raise ValueError(
|
|
115
|
+
f"backend language {language!r} is planned but not yet available"
|
|
116
|
+
)
|
|
117
|
+
fws = lang.get("frameworks", {})
|
|
118
|
+
fw = fws.get(framework)
|
|
119
|
+
if fw is None:
|
|
120
|
+
raise ValueError(
|
|
121
|
+
f"unknown backend framework {framework!r} for {language} (choices: {', '.join(fws)})"
|
|
122
|
+
)
|
|
123
|
+
if fw.get("status") == "planned":
|
|
124
|
+
raise ValueError(
|
|
125
|
+
f"backend framework {framework!r} is planned but not yet available"
|
|
126
|
+
)
|
|
127
|
+
return lang, fw
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _database(stacks: dict[str, Any], database: str) -> dict[str, Any]:
|
|
131
|
+
opts = stacks.get("database", {}).get("options", {})
|
|
132
|
+
db = opts.get(database)
|
|
133
|
+
if db is None:
|
|
134
|
+
raise ValueError(f"unknown database {database!r} (choices: {', '.join(opts)})")
|
|
135
|
+
return db
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _resolve_profile(
|
|
139
|
+
profiles: dict[str, Any], name: str, avail: dict[str, list[str]]
|
|
140
|
+
) -> dict[str, list[str]]:
|
|
141
|
+
"""Resolve a profile's agents/skills/gates/hooks, honouring ``inherit:`` and the ``all`` token."""
|
|
142
|
+
table = profiles.get("profiles", {})
|
|
143
|
+
prof = table.get(name)
|
|
144
|
+
if prof is None:
|
|
145
|
+
raise ValueError(f"unknown profile {name!r} (choices: {', '.join(table)})")
|
|
146
|
+
|
|
147
|
+
base: dict[str, list[str]] = {"agents": [], "skills": [], "gates": [], "hooks": []}
|
|
148
|
+
if prof.get("inherit"):
|
|
149
|
+
base = _resolve_profile(profiles, prof["inherit"], avail)
|
|
150
|
+
|
|
151
|
+
out: dict[str, list[str]] = {}
|
|
152
|
+
for key in ("agents", "skills", "gates", "hooks"):
|
|
153
|
+
val = prof.get(key)
|
|
154
|
+
if val == "all":
|
|
155
|
+
out[key] = list(avail.get(key, base[key]))
|
|
156
|
+
elif val is None:
|
|
157
|
+
out[key] = list(base[key])
|
|
158
|
+
else:
|
|
159
|
+
out[key] = _dedup(list(base[key]) + list(val))
|
|
160
|
+
return out
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _build_context(
|
|
164
|
+
sel: Selection,
|
|
165
|
+
frontend: dict[str, Any],
|
|
166
|
+
backend_lang: dict[str, Any],
|
|
167
|
+
backend_fw: dict[str, Any],
|
|
168
|
+
database: dict[str, Any],
|
|
169
|
+
profiles: dict[str, Any],
|
|
170
|
+
) -> dict[str, str]:
|
|
171
|
+
"""Build the flat string context used to render CLAUDE.md / README from the selection."""
|
|
172
|
+
fe_cmds = frontend.get("commands", {})
|
|
173
|
+
be_cmds = backend_fw.get("commands", {})
|
|
174
|
+
profile_label = (
|
|
175
|
+
profiles.get("profiles", {}).get(sel.profile, {}).get("label", sel.profile)
|
|
176
|
+
)
|
|
177
|
+
ctx: dict[str, str] = {
|
|
178
|
+
"frontend_framework": sel.frontend_framework,
|
|
179
|
+
"frontend_label": str(frontend.get("label", sel.frontend_framework)),
|
|
180
|
+
"frontend_language": sel.frontend_language,
|
|
181
|
+
"backend_language": sel.backend_language,
|
|
182
|
+
"backend_language_label": str(backend_lang.get("label", sel.backend_language)),
|
|
183
|
+
"backend_framework": sel.backend_framework,
|
|
184
|
+
"backend_label": str(backend_fw.get("label", sel.backend_framework)),
|
|
185
|
+
"database": sel.database,
|
|
186
|
+
"db": sel.database,
|
|
187
|
+
"db_label": str(database.get("label", sel.database)),
|
|
188
|
+
"profile": sel.profile,
|
|
189
|
+
"profile_label": str(profile_label),
|
|
190
|
+
"mcp_list": ", ".join(sel.mcp) if sel.mcp else "none",
|
|
191
|
+
"frontend_overlay_rule": (frontend.get("overlay_rules") or [""])[0],
|
|
192
|
+
"backend_overlay_rule": (backend_fw.get("overlay_rules") or [""])[0],
|
|
193
|
+
"db_overlay_rule": (database.get("overlay_rules") or [""])[0],
|
|
194
|
+
"scope": sel.scope,
|
|
195
|
+
"teams_list": ", ".join(sel.teams) if sel.teams else "all",
|
|
196
|
+
"autonomy": sel.autonomy,
|
|
197
|
+
"review_strictness": sel.review_strictness,
|
|
198
|
+
"org_packs": "yes" if sel.org_packs else "no",
|
|
199
|
+
"is_org": "yes" if sel.scope == "organization" else "no",
|
|
200
|
+
# Overwritten with the resolved policy in organization scope (see resolve()).
|
|
201
|
+
"autonomy_policy": "",
|
|
202
|
+
}
|
|
203
|
+
for key in _BACKEND_CMD_KEYS:
|
|
204
|
+
ctx[f"backend_{key}_cmd"] = str(be_cmds.get(key, ""))
|
|
205
|
+
for key in _FRONTEND_CMD_KEYS:
|
|
206
|
+
ctx[f"frontend_{key}_cmd"] = str(fe_cmds.get(key, ""))
|
|
207
|
+
return ctx
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _resolve_org(
|
|
211
|
+
payload_root: Path, selection: Selection, avail: dict[str, list[str]]
|
|
212
|
+
) -> OrgPlan | None:
|
|
213
|
+
"""Resolve the org capability layer from ``org.yaml`` (only for ``scope == organization``).
|
|
214
|
+
|
|
215
|
+
Returns ``None`` for individual/team scope, leaving the rest of the plan untouched. Otherwise
|
|
216
|
+
looks up the chosen autonomy level + strictness (data, no branching), validates that the hooks and
|
|
217
|
+
core agents they reference exist, and returns an :class:`OrgPlan`. The new skills/agents/rules and
|
|
218
|
+
pack manifests are installed comprehensively when ``org_packs`` is true.
|
|
219
|
+
|
|
220
|
+
Raises:
|
|
221
|
+
ValueError: If the chosen autonomy/strictness id is unknown, or a referenced hook/agent is not
|
|
222
|
+
registered/available (a misconfigured ``org.yaml``).
|
|
223
|
+
"""
|
|
224
|
+
if selection.scope != "organization":
|
|
225
|
+
return None
|
|
226
|
+
org = _load(payload_root, "org.yaml")
|
|
227
|
+
|
|
228
|
+
autonomy = selection.autonomy or org.get("autonomy", {}).get("default", "assisted")
|
|
229
|
+
levels = org.get("autonomy", {}).get("levels", {})
|
|
230
|
+
if autonomy not in levels:
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"unknown autonomy level {autonomy!r} (choices: {', '.join(levels)})"
|
|
233
|
+
)
|
|
234
|
+
level = levels[autonomy]
|
|
235
|
+
|
|
236
|
+
strictness = selection.review_strictness or org.get("strictness", {}).get(
|
|
237
|
+
"default", "standard"
|
|
238
|
+
)
|
|
239
|
+
strict_levels = org.get("strictness", {}).get("levels", {})
|
|
240
|
+
if strictness not in strict_levels:
|
|
241
|
+
raise ValueError(
|
|
242
|
+
f"unknown review strictness {strictness!r} (choices: {', '.join(strict_levels)})"
|
|
243
|
+
)
|
|
244
|
+
strict = strict_levels[strictness]
|
|
245
|
+
|
|
246
|
+
added_hooks = _dedup(list(level.get("hooks", [])) + list(strict.get("hooks", [])))
|
|
247
|
+
valid_hooks = set(hooks_mod.all_ids())
|
|
248
|
+
unknown_hooks = [h for h in added_hooks if h not in valid_hooks]
|
|
249
|
+
if unknown_hooks:
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"org.yaml references unknown hook(s): {', '.join(unknown_hooks)}"
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
core_agents_added = list(org.get("core_agents_added", []))
|
|
255
|
+
unknown_agents = [a for a in core_agents_added if a not in set(avail["agents"])]
|
|
256
|
+
if unknown_agents:
|
|
257
|
+
raise ValueError(
|
|
258
|
+
f"org.yaml core_agents_added not found in agents/: {', '.join(unknown_agents)}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
want_packs = bool(selection.org_packs)
|
|
262
|
+
pack_ids = [p["id"] for p in org.get("packs", [])] if want_packs else []
|
|
263
|
+
return OrgPlan(
|
|
264
|
+
scope="organization",
|
|
265
|
+
teams=list(selection.teams),
|
|
266
|
+
autonomy=autonomy,
|
|
267
|
+
autonomy_policy=str(level.get("policy", "")),
|
|
268
|
+
review_strictness=strictness,
|
|
269
|
+
packs=pack_ids,
|
|
270
|
+
org_skills=list(org.get("new_skills", [])) if want_packs else [],
|
|
271
|
+
org_agents=list(org.get("new_agents", [])) if want_packs else [],
|
|
272
|
+
org_rules=list(org.get("new_rules", [])) if want_packs else [],
|
|
273
|
+
added_hooks=added_hooks,
|
|
274
|
+
added_agents=core_agents_added,
|
|
275
|
+
extra_gates=list(strict.get("extra_gates", [])),
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def resolve(payload_root: str | Path, selection: Selection) -> ResolvedPlan:
|
|
280
|
+
"""Resolve a :class:`Selection` into a concrete :class:`ResolvedPlan`.
|
|
281
|
+
|
|
282
|
+
Args:
|
|
283
|
+
payload_root: Payload root containing ``catalog/``, ``agents/``, ``skills/``.
|
|
284
|
+
selection: The user's choices.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
The install plan: agent/skill/hook subsets, overlay rules+agents, gates, MCP configs,
|
|
288
|
+
per-stack dirs, the render context, and (organization scope only) the org capability layer.
|
|
289
|
+
|
|
290
|
+
Raises:
|
|
291
|
+
ValueError: If any selected stack/profile is unknown or not yet shipped.
|
|
292
|
+
FileNotFoundError: If a catalog file is missing.
|
|
293
|
+
"""
|
|
294
|
+
payload_root = Path(payload_root)
|
|
295
|
+
stacks = _load(payload_root, "stacks.yaml")
|
|
296
|
+
profiles = _load(payload_root, "profiles.yaml")
|
|
297
|
+
mcp = _load(payload_root, "mcp.yaml")
|
|
298
|
+
|
|
299
|
+
frontend = _frontend(stacks, selection.frontend_framework)
|
|
300
|
+
backend_lang, backend_fw = _backend(
|
|
301
|
+
stacks, selection.backend_language, selection.backend_framework
|
|
302
|
+
)
|
|
303
|
+
database = _database(stacks, selection.database)
|
|
304
|
+
|
|
305
|
+
avail = available(payload_root)
|
|
306
|
+
prof = _resolve_profile(profiles, selection.profile, avail)
|
|
307
|
+
|
|
308
|
+
overlay_rules = _dedup(
|
|
309
|
+
list(frontend.get("overlay_rules", []))
|
|
310
|
+
+ list(backend_fw.get("overlay_rules", []))
|
|
311
|
+
+ list(database.get("overlay_rules", []))
|
|
312
|
+
)
|
|
313
|
+
overlay_agents = _dedup(
|
|
314
|
+
list(frontend.get("overlay_agents", []))
|
|
315
|
+
+ list(backend_fw.get("overlay_agents", []))
|
|
316
|
+
+ list(database.get("overlay_agents", []))
|
|
317
|
+
)
|
|
318
|
+
skills = _dedup(
|
|
319
|
+
prof["skills"]
|
|
320
|
+
+ list(frontend.get("skills", []))
|
|
321
|
+
+ list(backend_fw.get("skills", []))
|
|
322
|
+
+ list(database.get("skills", []))
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
mcp_servers: dict[str, dict[str, Any]] = {}
|
|
326
|
+
servers = mcp.get("servers", {})
|
|
327
|
+
for sid in selection.mcp:
|
|
328
|
+
if sid not in servers:
|
|
329
|
+
raise ValueError(
|
|
330
|
+
f"unknown MCP server {sid!r} (choices: {', '.join(servers)})"
|
|
331
|
+
)
|
|
332
|
+
mcp_servers[sid] = servers[sid]["config"]
|
|
333
|
+
|
|
334
|
+
stack_dirs = {
|
|
335
|
+
"frontend": str(frontend.get("stack_dir", "")),
|
|
336
|
+
"backend": str(backend_fw.get("stack_dir", "")),
|
|
337
|
+
"database": str(database.get("stack_dir", "")),
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
org = _resolve_org(payload_root, selection, avail)
|
|
341
|
+
agents = _dedup(prof["agents"] + (org.added_agents if org else []))
|
|
342
|
+
hooks = _dedup(prof["hooks"] + (org.added_hooks if org else []))
|
|
343
|
+
gates = _dedup(prof["gates"] + (org.extra_gates if org else []))
|
|
344
|
+
|
|
345
|
+
context = _build_context(
|
|
346
|
+
selection, frontend, backend_lang, backend_fw, database, profiles
|
|
347
|
+
)
|
|
348
|
+
if org is not None:
|
|
349
|
+
context["autonomy_policy"] = org.autonomy_policy
|
|
350
|
+
context["org_packs_list"] = ", ".join(org.packs) or "none"
|
|
351
|
+
|
|
352
|
+
return ResolvedPlan(
|
|
353
|
+
selection=selection,
|
|
354
|
+
agents=agents,
|
|
355
|
+
skills=skills,
|
|
356
|
+
overlay_rules=overlay_rules,
|
|
357
|
+
overlay_agents=overlay_agents,
|
|
358
|
+
hooks=hooks,
|
|
359
|
+
gates=gates,
|
|
360
|
+
mcp_servers=mcp_servers,
|
|
361
|
+
context=context,
|
|
362
|
+
stack_dirs=stack_dirs,
|
|
363
|
+
org=org,
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def org_options(payload_root: str | Path) -> dict[str, Any]:
|
|
368
|
+
"""Return the org-layer choices for the prompt/menu layer (scopes, teams, autonomy, strictness).
|
|
369
|
+
|
|
370
|
+
Each list is ``[{"id", "label"}, ...]`` with the catalog defaults surfaced under ``defaults``.
|
|
371
|
+
"""
|
|
372
|
+
org = _load(Path(payload_root), "org.yaml")
|
|
373
|
+
autonomy = org.get("autonomy", {})
|
|
374
|
+
strictness = org.get("strictness", {})
|
|
375
|
+
return {
|
|
376
|
+
"scopes": list(org.get("scopes", [])),
|
|
377
|
+
"teams": list(org.get("teams", [])),
|
|
378
|
+
"autonomy": [
|
|
379
|
+
{"id": lid, "label": lvl.get("label", lid)}
|
|
380
|
+
for lid, lvl in autonomy.get("levels", {}).items()
|
|
381
|
+
],
|
|
382
|
+
"strictness": [
|
|
383
|
+
{"id": lid, "label": lvl.get("label", lid)}
|
|
384
|
+
for lid, lvl in strictness.get("levels", {}).items()
|
|
385
|
+
],
|
|
386
|
+
"defaults": {
|
|
387
|
+
"scope": org.get("default_scope", "team"),
|
|
388
|
+
"autonomy": autonomy.get("default", "assisted"),
|
|
389
|
+
"strictness": strictness.get("default", "standard"),
|
|
390
|
+
},
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def defaults(payload_root: str | Path) -> Selection:
|
|
395
|
+
"""Return the default :class:`Selection` (every catalog default, no MCP)."""
|
|
396
|
+
payload_root = Path(payload_root)
|
|
397
|
+
stacks = _load(payload_root, "stacks.yaml")
|
|
398
|
+
profiles = _load(payload_root, "profiles.yaml")
|
|
399
|
+
fe_default = stacks["frontend"]["default"]
|
|
400
|
+
be_lang_default = stacks["backend"]["default"]
|
|
401
|
+
be_lang = stacks["backend"]["languages"][be_lang_default]
|
|
402
|
+
be_fw_default = be_lang["default_framework"]
|
|
403
|
+
fe = stacks["frontend"]["frameworks"][fe_default]
|
|
404
|
+
return Selection(
|
|
405
|
+
frontend_framework=fe_default,
|
|
406
|
+
frontend_language=fe.get("languages", {}).get("default", "typescript"),
|
|
407
|
+
backend_language=be_lang_default,
|
|
408
|
+
backend_framework=be_fw_default,
|
|
409
|
+
database=stacks["database"]["default"],
|
|
410
|
+
profile=profiles.get("default", "standard"),
|
|
411
|
+
mcp=[],
|
|
412
|
+
)
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def list_options(payload_root: str | Path) -> dict[str, Any]:
|
|
416
|
+
"""Return a structured view of every selectable option (for ``list-options`` and prompts)."""
|
|
417
|
+
payload_root = Path(payload_root)
|
|
418
|
+
stacks = _load(payload_root, "stacks.yaml")
|
|
419
|
+
profiles = _load(payload_root, "profiles.yaml")
|
|
420
|
+
mcp = _load(payload_root, "mcp.yaml")
|
|
421
|
+
|
|
422
|
+
def _live(entry: dict[str, Any]) -> bool:
|
|
423
|
+
return entry.get("status") != "planned"
|
|
424
|
+
|
|
425
|
+
frontends = [
|
|
426
|
+
{
|
|
427
|
+
"id": fid,
|
|
428
|
+
"label": fw.get("label", fid),
|
|
429
|
+
"status": fw.get("status", "live"),
|
|
430
|
+
"languages": fw.get("languages", {}).get("options", []),
|
|
431
|
+
"default_language": fw.get("languages", {}).get("default", ""),
|
|
432
|
+
}
|
|
433
|
+
for fid, fw in stacks["frontend"]["frameworks"].items()
|
|
434
|
+
]
|
|
435
|
+
backends = []
|
|
436
|
+
for lid, lang in stacks["backend"]["languages"].items():
|
|
437
|
+
backends.append(
|
|
438
|
+
{
|
|
439
|
+
"id": lid,
|
|
440
|
+
"label": lang.get("label", lid),
|
|
441
|
+
"status": lang.get("status", "live"),
|
|
442
|
+
"default_framework": lang.get("default_framework", ""),
|
|
443
|
+
"frameworks": [
|
|
444
|
+
{
|
|
445
|
+
"id": fid,
|
|
446
|
+
"label": fw.get("label", fid),
|
|
447
|
+
"status": fw.get("status", "live"),
|
|
448
|
+
}
|
|
449
|
+
for fid, fw in lang.get("frameworks", {}).items()
|
|
450
|
+
],
|
|
451
|
+
}
|
|
452
|
+
)
|
|
453
|
+
databases = [
|
|
454
|
+
{"id": did, "label": db.get("label", did)}
|
|
455
|
+
for did, db in stacks["database"]["options"].items()
|
|
456
|
+
]
|
|
457
|
+
profile_list = [
|
|
458
|
+
{"id": pid, "label": p.get("label", pid)}
|
|
459
|
+
for pid, p in profiles["profiles"].items()
|
|
460
|
+
]
|
|
461
|
+
mcp_list = [
|
|
462
|
+
{"id": sid, "label": s.get("label", sid)}
|
|
463
|
+
for sid, s in mcp.get("servers", {}).items()
|
|
464
|
+
]
|
|
465
|
+
return {
|
|
466
|
+
"frontend": frontends,
|
|
467
|
+
"backend": backends,
|
|
468
|
+
"database": databases,
|
|
469
|
+
"profiles": profile_list,
|
|
470
|
+
"mcp": mcp_list,
|
|
471
|
+
"live": {
|
|
472
|
+
"frontend": [f for f in frontends if _live(f)],
|
|
473
|
+
"databases": databases,
|
|
474
|
+
"profiles": profile_list,
|
|
475
|
+
},
|
|
476
|
+
}
|