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.
Files changed (209) hide show
  1. claude_code_kit-0.7.0.dist-info/METADATA +384 -0
  2. claude_code_kit-0.7.0.dist-info/RECORD +209 -0
  3. claude_code_kit-0.7.0.dist-info/WHEEL +4 -0
  4. claude_code_kit-0.7.0.dist-info/entry_points.txt +4 -0
  5. claude_code_kit-0.7.0.dist-info/licenses/LICENSE +21 -0
  6. claude_kit/__init__.py +10 -0
  7. claude_kit/__main__.py +8 -0
  8. claude_kit/_payload/agents/acceptance-reviewer.md +60 -0
  9. claude_kit/_payload/agents/auditor.md +76 -0
  10. claude_kit/_payload/agents/dependency-scanner.md +84 -0
  11. claude_kit/_payload/agents/developer.md +187 -0
  12. claude_kit/_payload/agents/devils-advocate.md +62 -0
  13. claude_kit/_payload/agents/devops-engineer.md +134 -0
  14. claude_kit/_payload/agents/e2e-tester.md +152 -0
  15. claude_kit/_payload/agents/em-reviewer.md +105 -0
  16. claude_kit/_payload/agents/incident-responder.md +64 -0
  17. claude_kit/_payload/agents/merge-reviewer.md +194 -0
  18. claude_kit/_payload/agents/observability-engineer.md +94 -0
  19. claude_kit/_payload/agents/orchestrator.md +551 -0
  20. claude_kit/_payload/agents/owasp-reviewer.md +76 -0
  21. claude_kit/_payload/agents/policy-validator.md +63 -0
  22. claude_kit/_payload/agents/pr-raiser.md +138 -0
  23. claude_kit/_payload/agents/risk-classifier.md +50 -0
  24. claude_kit/_payload/agents/sdlc-code-reviewer.md +196 -0
  25. claude_kit/_payload/agents/secret-scanner.md +70 -0
  26. claude_kit/_payload/agents/security-reviewer.md +80 -0
  27. claude_kit/_payload/agents/senior-backend-dev.md +199 -0
  28. claude_kit/_payload/agents/senior-frontend-dev.md +181 -0
  29. claude_kit/_payload/agents/senior-tester.md +206 -0
  30. claude_kit/_payload/agents/spec-doc-writer.md +331 -0
  31. claude_kit/_payload/agents/story-planner.md +56 -0
  32. claude_kit/_payload/agents/technical-architect.md +139 -0
  33. claude_kit/_payload/agents/tester.md +193 -0
  34. claude_kit/_payload/agents/ui-designer.md +73 -0
  35. claude_kit/_payload/agents/unit-tester.md +119 -0
  36. claude_kit/_payload/catalog/mcp.yaml +54 -0
  37. claude_kit/_payload/catalog/org.yaml +145 -0
  38. claude_kit/_payload/catalog/profiles.yaml +96 -0
  39. claude_kit/_payload/catalog/stacks.yaml +96 -0
  40. claude_kit/_payload/commands/init.md +36 -0
  41. claude_kit/_payload/commands/sdlc.md +18 -0
  42. claude_kit/_payload/commands/status.md +20 -0
  43. claude_kit/_payload/hooks/hooks.json +58 -0
  44. claude_kit/_payload/hooks/scripts/audit-log.sh +18 -0
  45. claude_kit/_payload/hooks/scripts/guard-secrets.sh +26 -0
  46. claude_kit/_payload/hooks/scripts/lint-fix.sh +38 -0
  47. claude_kit/_payload/hooks/scripts/load-continuity.sh +32 -0
  48. claude_kit/_payload/hooks/scripts/load-learnings.sh +40 -0
  49. claude_kit/_payload/hooks/scripts/type-check.sh +23 -0
  50. claude_kit/_payload/hooks/scripts/validate-frontmatter.sh +34 -0
  51. claude_kit/_payload/hooks/scripts/validate-settings.sh +21 -0
  52. claude_kit/_payload/hooks/scripts/warn-large-edits.sh +24 -0
  53. claude_kit/_payload/hooks/scripts/warn-missing-tests.sh +24 -0
  54. claude_kit/_payload/hooks/scripts/warn-sensitive-files.sh +30 -0
  55. claude_kit/_payload/hooks/scripts/warn-shared-modules.sh +33 -0
  56. claude_kit/_payload/rules/agent-guardrails.md +83 -0
  57. claude_kit/_payload/rules/agent-memory.md +106 -0
  58. claude_kit/_payload/rules/agent-resilience.md +61 -0
  59. claude_kit/_payload/rules/autonomy-levels.md +30 -0
  60. claude_kit/_payload/rules/code-organization.md +312 -0
  61. claude_kit/_payload/rules/continuity.md +84 -0
  62. claude_kit/_payload/rules/design-patterns.md +422 -0
  63. claude_kit/_payload/rules/devops-observability.md +57 -0
  64. claude_kit/_payload/rules/documentation.md +326 -0
  65. claude_kit/_payload/rules/evals.md +62 -0
  66. claude_kit/_payload/rules/frontend-best-practices.md +157 -0
  67. claude_kit/_payload/rules/goal-setting-and-monitoring.md +72 -0
  68. claude_kit/_payload/rules/human-in-the-loop.md +64 -0
  69. claude_kit/_payload/rules/linting-and-formatting.md +220 -0
  70. claude_kit/_payload/rules/mandatory-workflow.md +309 -0
  71. claude_kit/_payload/rules/model-tiers.md +34 -0
  72. claude_kit/_payload/rules/quality-gates.md +107 -0
  73. claude_kit/_payload/rules/rarv-cycle.md +31 -0
  74. claude_kit/_payload/rules/reasoning-techniques.md +62 -0
  75. claude_kit/_payload/rules/responsive-and-accessibility.md +353 -0
  76. claude_kit/_payload/rules/risk-classification.md +36 -0
  77. claude_kit/_payload/rules/testing.md +417 -0
  78. claude_kit/_payload/rules/tool-design.md +66 -0
  79. claude_kit/_payload/skills/_references/accessibility-checklist.md +160 -0
  80. claude_kit/_payload/skills/_references/orchestration-patterns.md +405 -0
  81. claude_kit/_payload/skills/_references/performance-checklist.md +153 -0
  82. claude_kit/_payload/skills/_references/security-checklist.md +134 -0
  83. claude_kit/_payload/skills/_references/testing-patterns.md +236 -0
  84. claude_kit/_payload/skills/accessibility-review/SKILL.md +56 -0
  85. claude_kit/_payload/skills/api-and-interface-design/SKILL.md +294 -0
  86. claude_kit/_payload/skills/api-integration/SKILL.md +348 -0
  87. claude_kit/_payload/skills/archive-sprint/SKILL.md +31 -0
  88. claude_kit/_payload/skills/backlog/SKILL.md +41 -0
  89. claude_kit/_payload/skills/backlog/item-template.md +20 -0
  90. claude_kit/_payload/skills/browser-testing-with-devtools/SKILL.md +302 -0
  91. claude_kit/_payload/skills/ci-cd-and-automation/SKILL.md +402 -0
  92. claude_kit/_payload/skills/code-review-and-quality/SKILL.md +347 -0
  93. claude_kit/_payload/skills/code-simplification/SKILL.md +331 -0
  94. claude_kit/_payload/skills/component-design/SKILL.md +171 -0
  95. claude_kit/_payload/skills/consolidate-learnings/SKILL.md +55 -0
  96. claude_kit/_payload/skills/context-engineering/SKILL.md +321 -0
  97. claude_kit/_payload/skills/debugging-and-error-recovery/SKILL.md +300 -0
  98. claude_kit/_payload/skills/decision/SKILL.md +46 -0
  99. claude_kit/_payload/skills/decision/adr-template.md +36 -0
  100. claude_kit/_payload/skills/deprecation-and-migration/SKILL.md +207 -0
  101. claude_kit/_payload/skills/documentation-and-adrs/SKILL.md +299 -0
  102. claude_kit/_payload/skills/doubt-driven-development/SKILL.md +243 -0
  103. claude_kit/_payload/skills/execute/SKILL.md +27 -0
  104. claude_kit/_payload/skills/frontend-ui-engineering/SKILL.md +328 -0
  105. claude_kit/_payload/skills/git-workflow-and-versioning/SKILL.md +300 -0
  106. claude_kit/_payload/skills/idea-refine/SKILL.md +178 -0
  107. claude_kit/_payload/skills/idea-refine/examples.md +238 -0
  108. claude_kit/_payload/skills/idea-refine/frameworks.md +99 -0
  109. claude_kit/_payload/skills/idea-refine/refinement-criteria.md +113 -0
  110. claude_kit/_payload/skills/idea-refine/scripts/idea-refine.sh +15 -0
  111. claude_kit/_payload/skills/incident-postmortem/SKILL.md +74 -0
  112. claude_kit/_payload/skills/incremental-implementation/SKILL.md +245 -0
  113. claude_kit/_payload/skills/interview-me/SKILL.md +221 -0
  114. claude_kit/_payload/skills/load-testing/SKILL.md +83 -0
  115. claude_kit/_payload/skills/manual-test/SKILL.md +516 -0
  116. claude_kit/_payload/skills/performance-optimization/SKILL.md +277 -0
  117. claude_kit/_payload/skills/planning-and-task-breakdown/SKILL.md +223 -0
  118. claude_kit/_payload/skills/playwright-verification/SKILL.md +205 -0
  119. claude_kit/_payload/skills/refresh-docs/SKILL.md +63 -0
  120. claude_kit/_payload/skills/remember/SKILL.md +96 -0
  121. claude_kit/_payload/skills/scope/SKILL.md +52 -0
  122. claude_kit/_payload/skills/scope/scope-template.md +82 -0
  123. claude_kit/_payload/skills/sdlc/SKILL.md +83 -0
  124. claude_kit/_payload/skills/security-and-hardening/SKILL.md +368 -0
  125. claude_kit/_payload/skills/security-verification/SKILL.md +209 -0
  126. claude_kit/_payload/skills/shipping-and-launch/SKILL.md +309 -0
  127. claude_kit/_payload/skills/smoke-test/SKILL.md +78 -0
  128. claude_kit/_payload/skills/source-driven-development/SKILL.md +195 -0
  129. claude_kit/_payload/skills/spec-driven-development/SKILL.md +200 -0
  130. claude_kit/_payload/skills/sprint/SKILL.md +67 -0
  131. claude_kit/_payload/skills/sprint/sprint-template.md +90 -0
  132. claude_kit/_payload/skills/test-driven-development/SKILL.md +383 -0
  133. claude_kit/_payload/skills/threat-model/SKILL.md +60 -0
  134. claude_kit/_payload/skills/triage/SKILL.md +87 -0
  135. claude_kit/_payload/skills/ui-ux-design/SKILL.md +71 -0
  136. claude_kit/_payload/skills/unit-test/SKILL.md +237 -0
  137. claude_kit/_payload/skills/using-agent-skills/SKILL.md +180 -0
  138. claude_kit/_payload/templates/CLAUDE.md +238 -0
  139. claude_kit/_payload/templates/CLAUDE.stack.md.tmpl +53 -0
  140. claude_kit/_payload/templates/CONTINUITY.template.md +35 -0
  141. claude_kit/_payload/templates/README.claude-sdlc.md.tmpl +219 -0
  142. claude_kit/_payload/templates/agent-memory/MEMORY.md +30 -0
  143. claude_kit/_payload/templates/agent-memory/api/.gitkeep +0 -0
  144. claude_kit/_payload/templates/agent-memory/architecture/.gitkeep +0 -0
  145. claude_kit/_payload/templates/agent-memory/debugging/.gitkeep +0 -0
  146. claude_kit/_payload/templates/agent-memory/gotchas/.gitkeep +0 -0
  147. claude_kit/_payload/templates/agent-memory/patterns/.gitkeep +0 -0
  148. claude_kit/_payload/templates/agent-memory/performance/.gitkeep +0 -0
  149. claude_kit/_payload/templates/artifacts/adr.md +18 -0
  150. claude_kit/_payload/templates/artifacts/feature-spec.md +29 -0
  151. claude_kit/_payload/templates/artifacts/release-plan.md +23 -0
  152. claude_kit/_payload/templates/artifacts/runbook.md +24 -0
  153. claude_kit/_payload/templates/artifacts/security-review.md +23 -0
  154. claude_kit/_payload/templates/artifacts/test-plan.md +22 -0
  155. claude_kit/_payload/templates/org/README.md +53 -0
  156. claude_kit/_payload/templates/org/agents/data-workflow-agent.md +59 -0
  157. claude_kit/_payload/templates/org/agents/founder-prototype-agent.md +61 -0
  158. claude_kit/_payload/templates/org/agents/internal-tools-builder.md +63 -0
  159. claude_kit/_payload/templates/org/agents/pm-copilot.md +60 -0
  160. claude_kit/_payload/templates/org/agents/support-ticket-engineer.md +63 -0
  161. claude_kit/_payload/templates/org/packs/devops-and-release/README.md +46 -0
  162. claude_kit/_payload/templates/org/packs/devops-and-release/pack.yaml +32 -0
  163. claude_kit/_payload/templates/org/packs/engineering-core/README.md +46 -0
  164. claude_kit/_payload/templates/org/packs/engineering-core/pack.yaml +44 -0
  165. claude_kit/_payload/templates/org/packs/non-engineer-builder/README.md +53 -0
  166. claude_kit/_payload/templates/org/packs/non-engineer-builder/pack.yaml +39 -0
  167. claude_kit/_payload/templates/org/packs/onboarding-and-docs/README.md +49 -0
  168. claude_kit/_payload/templates/org/packs/onboarding-and-docs/pack.yaml +26 -0
  169. claude_kit/_payload/templates/org/packs/product-to-code/README.md +50 -0
  170. claude_kit/_payload/templates/org/packs/product-to-code/pack.yaml +34 -0
  171. claude_kit/_payload/templates/org/packs/quality-and-review/README.md +53 -0
  172. claude_kit/_payload/templates/org/packs/quality-and-review/pack.yaml +40 -0
  173. claude_kit/_payload/templates/org/packs/security-and-compliance/README.md +50 -0
  174. claude_kit/_payload/templates/org/packs/security-and-compliance/pack.yaml +36 -0
  175. claude_kit/_payload/templates/org/rules/ai-working-agreement.md +45 -0
  176. claude_kit/_payload/templates/org/rules/ambiguity-resolution.md +36 -0
  177. claude_kit/_payload/templates/org/rules/branch-and-pr-policy.md +41 -0
  178. claude_kit/_payload/templates/org/rules/compliance-policy.md +50 -0
  179. claude_kit/_payload/templates/org/rules/non-engineer-safe-coding.md +37 -0
  180. claude_kit/_payload/templates/org/rules/pii-policy.md +46 -0
  181. claude_kit/_payload/templates/org/rules/production-data-policy.md +35 -0
  182. claude_kit/_payload/templates/org/rules/prompt-to-task-conversion.md +30 -0
  183. claude_kit/_payload/templates/org/rules/prototype-boundaries.md +40 -0
  184. claude_kit/_payload/templates/org/rules/secrets-policy.md +34 -0
  185. claude_kit/_payload/templates/org/skills/customer-issue-to-fix/SKILL.md +61 -0
  186. claude_kit/_payload/templates/org/skills/feature-from-idea/SKILL.md +56 -0
  187. claude_kit/_payload/templates/org/skills/prompt-to-safe-task/SKILL.md +59 -0
  188. claude_kit/_payload/templates/org/skills/prototype-to-production/SKILL.md +61 -0
  189. claude_kit/_payload/templates/org/skills/repo-onboarding/SKILL.md +60 -0
  190. claude_kit/_payload/templates/settings.json +53 -0
  191. claude_kit/_payload/templates/stacks/backend/python/fastapi/rules/fastapi-patterns.md +64 -0
  192. claude_kit/_payload/templates/stacks/db/mongodb/agents/migration-specialist.md +61 -0
  193. claude_kit/_payload/templates/stacks/db/mongodb/agents/mongodb-specialist.md +59 -0
  194. claude_kit/_payload/templates/stacks/db/mongodb/rules/mongodb-patterns.md +39 -0
  195. claude_kit/_payload/templates/stacks/db/postgres/agents/db-performance-reviewer.md +66 -0
  196. claude_kit/_payload/templates/stacks/db/postgres/agents/migration-specialist.md +56 -0
  197. claude_kit/_payload/templates/stacks/db/postgres/agents/postgres-specialist.md +58 -0
  198. claude_kit/_payload/templates/stacks/db/postgres/rules/database-performance.md +64 -0
  199. claude_kit/_payload/templates/stacks/db/postgres/rules/postgres-patterns.md +43 -0
  200. claude_kit/_payload/templates/stacks/frontend/react/rules/react-patterns.md +63 -0
  201. claude_kit/catalog.py +476 -0
  202. claude_kit/cli.py +327 -0
  203. claude_kit/hooks.py +246 -0
  204. claude_kit/models.py +205 -0
  205. claude_kit/prompts.py +209 -0
  206. claude_kit/render.py +146 -0
  207. claude_kit/scaffold.py +492 -0
  208. claude_kit/upgrader.py +294 -0
  209. claude_kit/validator.py +197 -0
claude_kit/upgrader.py ADDED
@@ -0,0 +1,294 @@
1
+ """Diff and safe-upgrade of a scaffolded claude-kit configuration.
2
+
3
+ The strategy is **render-and-compare**: re-render a pristine reference install of the *recorded*
4
+ selection into a throwaway temp dir (reusing :func:`claude_kit.scaffold.install_sdlc`, so no install
5
+ logic is duplicated), then compare that reference tree against the live ``target`` tree. Each file's
6
+ recorded ``owner`` (kit / overlay / user-editable) plus whether it was modified since install (live
7
+ checksum vs. the checksum in ``.claude/config/init-options.json``) decides the action:
8
+
9
+ * **kit** / **overlay** files are refreshed to the new content (a user-modified one is backed up first).
10
+ * **user-editable** files (``CLAUDE.md``, ``settings.json``, ``.mcp.json``, ``CONTINUITY.md``,
11
+ ``agent-memory/``) are *never* clobbered: if the user changed one, the new version is written
12
+ alongside as a ``.claude-kit`` sidecar so they can merge it (``--force`` overwrites instead).
13
+ * Files the current kit no longer ships (orphans) are backed up and removed — but only kit/overlay
14
+ ones; a user's own files are left untouched.
15
+
16
+ ``diff`` previews these actions and writes nothing; ``upgrade`` applies them and then refreshes
17
+ ``init-options.json`` with the new checksums and kit version. Both return the ``(ok, messages)``
18
+ contract shared by the other lifecycle commands.
19
+ """
20
+
21
+ from __future__ import annotations
22
+
23
+ import hashlib
24
+ import shutil
25
+ import tempfile
26
+ from contextlib import ExitStack
27
+ from dataclasses import dataclass
28
+ from pathlib import Path
29
+
30
+ from claude_kit import catalog, scaffold
31
+ from claude_kit.models import InitOptions
32
+ from claude_kit.validator import _load_init_options
33
+
34
+ #: Sidecar suffix for a new version of a user-modified, protected file.
35
+ _SIDECAR_SUFFIX = ".claude-kit"
36
+
37
+
38
+ @dataclass(frozen=True)
39
+ class _Action:
40
+ """One planned change to a single file, relative to the project root."""
41
+
42
+ rel: str
43
+ kind: str # "add" | "update" | "keep" | "remove"
44
+ owner: str # "kit" | "overlay" | "user-editable"
45
+ user_modified: bool = False
46
+
47
+
48
+ @dataclass
49
+ class _Comparison:
50
+ """The result of comparing a freshly-rendered reference tree against the live install."""
51
+
52
+ target: Path
53
+ old: InitOptions
54
+ plan: object # ResolvedPlan
55
+ ref_root: Path
56
+ actions: list[_Action]
57
+
58
+
59
+ def _sha256(path: Path) -> str:
60
+ """Return the hex SHA-256 of a file's bytes."""
61
+ h = hashlib.sha256()
62
+ h.update(path.read_bytes())
63
+ return h.hexdigest()
64
+
65
+
66
+ def _compare(src: Path, target: Path) -> _Comparison | str:
67
+ """Render a reference install and diff it against ``target``.
68
+
69
+ Returns a :class:`_Comparison`, or a short error string (``"not-installed"`` /
70
+ ``"no-options"``) the callers turn into a ``FAIL`` message. The caller owns cleanup of
71
+ ``ref_root`` (via :func:`_cleanup`).
72
+ """
73
+ target = Path(target).expanduser().resolve()
74
+ claude = target / ".claude"
75
+ if not claude.is_dir():
76
+ return "not-installed"
77
+ old = _load_init_options(claude)
78
+ if old is None:
79
+ return "no-options"
80
+
81
+ plan = catalog.resolve(src, old.selection)
82
+ # Render the reference under the REAL project name so CLAUDE.md/README don't diff spuriously.
83
+ plan.context["project_name"] = target.name
84
+ ref_root = Path(tempfile.mkdtemp(prefix="claude-kit-ref-"))
85
+ scaffold.install_sdlc(src, ref_root, plan, force=True, log=[])
86
+
87
+ ref_opts = _load_init_options(ref_root / ".claude")
88
+ ref = {r.path: r for r in ref_opts.files} if ref_opts else {}
89
+ old_map = {r.path: r for r in old.files}
90
+
91
+ actions: list[_Action] = []
92
+ for rel, rrec in sorted(ref.items()):
93
+ live = target / rel
94
+ if not live.is_file():
95
+ actions.append(_Action(rel, "add", rrec.owner))
96
+ continue
97
+ if _sha256(live) == rrec.sha256:
98
+ continue # already identical to the new reference
99
+ old_sha = old_map[rel].sha256 if rel in old_map else None
100
+ user_modified = old_sha is not None and _sha256(live) != old_sha
101
+ if rrec.owner == "user-editable":
102
+ actions.append(
103
+ _Action(
104
+ rel,
105
+ "keep" if user_modified else "update",
106
+ rrec.owner,
107
+ user_modified,
108
+ )
109
+ )
110
+ else:
111
+ actions.append(_Action(rel, "update", rrec.owner, user_modified))
112
+
113
+ # Orphans: recorded kit/overlay files the current kit no longer ships for this selection.
114
+ for rel, orec in sorted(old_map.items()):
115
+ if rel in ref or orec.owner == "user-editable":
116
+ continue
117
+ if (target / rel).is_file():
118
+ actions.append(_Action(rel, "remove", orec.owner))
119
+
120
+ return _Comparison(
121
+ target=target, old=old, plan=plan, ref_root=ref_root, actions=actions
122
+ )
123
+
124
+
125
+ def _cleanup(ref_root: Path) -> None:
126
+ """Remove the throwaway reference render."""
127
+ shutil.rmtree(ref_root, ignore_errors=True)
128
+
129
+
130
+ def _next_backup_dir(target: Path) -> Path:
131
+ """Return a fresh, non-existing ``.claude-kit.bak-N/`` directory under ``target``."""
132
+ n = 1
133
+ while (target / f".claude-kit.bak-{n}").exists():
134
+ n += 1
135
+ return target / f".claude-kit.bak-{n}"
136
+
137
+
138
+ def _format_preview(cmp: _Comparison) -> list[str]:
139
+ """Build the human-readable diff report from a comparison (no side effects)."""
140
+ from claude_kit import __version__
141
+
142
+ msgs: list[str] = []
143
+ if cmp.old.claude_kit_version != __version__:
144
+ msgs.append(f"INFO kit version {cmp.old.claude_kit_version} -> {__version__}")
145
+ else:
146
+ msgs.append(f"INFO kit version {__version__} (unchanged)")
147
+
148
+ if not cmp.actions:
149
+ msgs.append("OK everything up to date — nothing to upgrade")
150
+ return msgs
151
+
152
+ order = {"add": 0, "update": 1, "keep": 2, "remove": 3}
153
+ verbs = {
154
+ "add": "add",
155
+ "update": "update",
156
+ "keep": "keep (sidecar new version)",
157
+ "remove": "remove (orphan)",
158
+ }
159
+ for act in sorted(cmp.actions, key=lambda a: (order[a.kind], a.rel)):
160
+ note = ""
161
+ if act.kind == "update" and act.user_modified and act.owner != "user-editable":
162
+ note = " [local changes will be backed up]"
163
+ elif act.kind == "keep":
164
+ note = " [your edits kept; new version as .claude-kit]"
165
+ msgs.append(f" {verbs[act.kind]:<28} {act.rel} ({act.owner}){note}")
166
+
167
+ counts: dict[str, int] = {}
168
+ for act in cmp.actions:
169
+ counts[act.kind] = counts.get(act.kind, 0) + 1
170
+ summary = ", ".join(f"{counts[k]} {k}" for k in order if k in counts)
171
+ msgs.append(f"INFO {summary}")
172
+ return msgs
173
+
174
+
175
+ def diff(target: str | Path) -> tuple[bool, list[str]]:
176
+ """Preview what an upgrade would change (no writes). Returns ``(ok, messages)``."""
177
+ with ExitStack() as stack:
178
+ src = scaffold.payload_dir(stack)
179
+ result = _compare(src, target)
180
+ if isinstance(result, str):
181
+ return _explain_error(result, target)
182
+ try:
183
+ return True, _format_preview(result)
184
+ finally:
185
+ _cleanup(result.ref_root)
186
+
187
+
188
+ def upgrade(target: str | Path, *, force: bool = False) -> tuple[bool, list[str]]:
189
+ """Apply the upgrade: refresh kit/overlay files, protect user edits, prune orphans.
190
+
191
+ Args:
192
+ target: Project root to upgrade.
193
+ force: Overwrite user-modified *user-editable* files instead of writing sidecars.
194
+
195
+ Returns:
196
+ ``(ok, messages)``.
197
+ """
198
+ with ExitStack() as stack:
199
+ src = scaffold.payload_dir(stack)
200
+ result = _compare(src, target)
201
+ if isinstance(result, str):
202
+ return _explain_error(result, target)
203
+ try:
204
+ return _apply(result, force=force)
205
+ finally:
206
+ _cleanup(result.ref_root)
207
+
208
+
209
+ def _apply(cmp: _Comparison, *, force: bool) -> tuple[bool, list[str]]:
210
+ """Carry out the planned actions and refresh ``init-options.json``."""
211
+ msgs: list[str] = []
212
+ if not cmp.actions:
213
+ msgs.append("OK everything up to date — nothing to upgrade")
214
+ return True, msgs
215
+
216
+ target, ref_root = cmp.target, cmp.ref_root
217
+ backup_dir = _next_backup_dir(target)
218
+ backed_up = 0
219
+
220
+ def _backup(rel: str) -> None:
221
+ nonlocal backed_up
222
+ live = target / rel
223
+ if not live.is_file():
224
+ return
225
+ dest = backup_dir / rel
226
+ dest.parent.mkdir(parents=True, exist_ok=True)
227
+ shutil.copy2(live, dest)
228
+ backed_up += 1
229
+
230
+ def _copy_ref(rel: str) -> None:
231
+ live = target / rel
232
+ live.parent.mkdir(parents=True, exist_ok=True)
233
+ shutil.copy2(ref_root / rel, live)
234
+
235
+ for act in cmp.actions:
236
+ live = target / act.rel
237
+ if act.kind == "add":
238
+ _copy_ref(act.rel)
239
+ msgs.append(f" + {act.rel}")
240
+ elif act.kind == "update":
241
+ if act.user_modified:
242
+ _backup(act.rel)
243
+ if act.owner == "user-editable" and act.user_modified and not force:
244
+ # Protect: keep the user's file, drop the new version beside it.
245
+ shutil.copy2(
246
+ ref_root / act.rel, live.with_name(live.name + _SIDECAR_SUFFIX)
247
+ )
248
+ msgs.append(
249
+ f" ~ {act.rel} (kept; new version -> {live.name}{_SIDECAR_SUFFIX})"
250
+ )
251
+ else:
252
+ _copy_ref(act.rel)
253
+ msgs.append(f" ✓ {act.rel}")
254
+ elif act.kind == "keep":
255
+ shutil.copy2(
256
+ ref_root / act.rel, live.with_name(live.name + _SIDECAR_SUFFIX)
257
+ )
258
+ msgs.append(
259
+ f" ~ {act.rel} (kept; new version -> {live.name}{_SIDECAR_SUFFIX})"
260
+ )
261
+ elif act.kind == "remove":
262
+ _backup(act.rel)
263
+ live.unlink(missing_ok=True)
264
+ msgs.append(f" - {act.rel} (orphan removed)")
265
+
266
+ # Adopt the reference's config verbatim as the new baseline. Recording the kit's CANONICAL
267
+ # checksums (not the live ones) is what keeps a *kept* user-editable file detectable as
268
+ # user-modified on the next upgrade — re-recording its live sha would make the next run treat
269
+ # it as pristine and clobber the user's edits.
270
+ ref_config = ref_root / ".claude" / "config"
271
+ dst_config = target / ".claude" / "config"
272
+ dst_config.mkdir(parents=True, exist_ok=True)
273
+ for name in ("init-options.json", "stack-catalog.snapshot.yaml"):
274
+ if (ref_config / name).is_file():
275
+ shutil.copy2(ref_config / name, dst_config / name)
276
+
277
+ if backed_up:
278
+ msgs.append(
279
+ f"INFO backed up {backed_up} modified/removed file(s) -> {backup_dir.name}/"
280
+ )
281
+ msgs.append("OK upgrade complete")
282
+ return True, msgs
283
+
284
+
285
+ def _explain_error(code: str, target: str | Path) -> tuple[bool, list[str]]:
286
+ """Translate a ``_compare`` error code into a ``(False, [FAIL …])`` report."""
287
+ if code == "not-installed":
288
+ return False, [
289
+ f"FAIL no .claude/ at {Path(target).expanduser().resolve()} — run `claude-kit init` first"
290
+ ]
291
+ return False, [
292
+ "FAIL no .claude/config/init-options.json — this install predates upgrade tracking; "
293
+ "re-run `claude-kit init --force` to start tracking"
294
+ ]
@@ -0,0 +1,197 @@
1
+ """Validation and health checks for a scaffolded claude-kit configuration.
2
+
3
+ ``validate`` performs structural checks (files present, JSON parses, frontmatter complete,
4
+ referenced overlays installed). ``doctor`` adds environment checks (git/jq available, hook scripts
5
+ executable, runtime dirs gitignored). Both return ``(ok, messages)`` so the CLI can print a report
6
+ and choose an exit code.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import shutil
13
+ from pathlib import Path
14
+
15
+ from claude_kit.models import InitOptions
16
+
17
+
18
+ def _parse_frontmatter(text: str) -> dict[str, str] | None:
19
+ """Return the frontmatter key/values at the top of a markdown file, or None if absent.
20
+
21
+ Uses lenient line-based parsing (``key: value`` at column 0), deliberately mirroring Claude
22
+ Code's own frontmatter reader rather than strict YAML. Real agent/skill files routinely carry
23
+ a colon inside a ``description`` ("Read-only: routes fixes…") or a bracketed ``argument-hint``
24
+ (``[optional: "x"]``); ``yaml.safe_load`` rejects both even though Claude Code accepts them, so
25
+ validating with strict YAML would fail on valid files. Indented continuation lines, blanks, and
26
+ comments are skipped — only the top-level scalar fields this module checks (``name``,
27
+ ``description``) need to be recovered.
28
+ """
29
+ if not text.startswith("---"):
30
+ return None
31
+ end = text.find("\n---", 3)
32
+ if end == -1:
33
+ return None
34
+ data: dict[str, str] = {}
35
+ for line in text[3:end].splitlines():
36
+ if not line.strip() or line[0] in (" ", "\t", "#"):
37
+ continue
38
+ key, sep, value = line.partition(":")
39
+ if sep:
40
+ data[key.strip()] = value.strip()
41
+ return data
42
+
43
+
44
+ def _load_init_options(claude_dir: Path) -> InitOptions | None:
45
+ """Load and parse ``.claude/config/init-options.json`` if present."""
46
+ path = claude_dir / "config" / "init-options.json"
47
+ if not path.is_file():
48
+ return None
49
+ try:
50
+ return InitOptions.from_dict(json.loads(path.read_text(encoding="utf-8")))
51
+ except (json.JSONDecodeError, TypeError, ValueError):
52
+ return None
53
+
54
+
55
+ def validate(target: str | Path) -> tuple[bool, list[str]]:
56
+ """Structurally validate the claude-kit config at ``target``.
57
+
58
+ Returns:
59
+ ``(ok, messages)`` where each message is prefixed ``OK``/``WARN``/``FAIL`` and ``ok`` is
60
+ False if any ``FAIL`` was recorded.
61
+ """
62
+ target = Path(target).expanduser().resolve()
63
+ claude = target / ".claude"
64
+ msgs: list[str] = []
65
+ ok = True
66
+
67
+ def fail(m: str) -> None:
68
+ nonlocal ok
69
+ ok = False
70
+ msgs.append(f"FAIL {m}")
71
+
72
+ def warn(m: str) -> None:
73
+ msgs.append(f"WARN {m}")
74
+
75
+ def good(m: str) -> None:
76
+ msgs.append(f"OK {m}")
77
+
78
+ if not claude.is_dir():
79
+ fail(f"no .claude/ directory in {target} — run `claude-kit init` here")
80
+ return ok, msgs
81
+
82
+ options = _load_init_options(claude)
83
+ if options is None:
84
+ warn(
85
+ "missing or unreadable .claude/config/init-options.json (validate/upgrade limited)"
86
+ )
87
+ else:
88
+ good(
89
+ f"init-options.json (schema v{options.schema_version}, kit {options.claude_kit_version})"
90
+ )
91
+ for rec in options.files:
92
+ if not (target / rec.path).exists():
93
+ fail(f"recorded file missing: {rec.path}")
94
+ good(f"tracked files present ({len(options.files)} recorded)")
95
+
96
+ settings = claude / "settings.json"
97
+ if settings.is_file():
98
+ try:
99
+ json.loads(settings.read_text(encoding="utf-8"))
100
+ good("settings.json is valid JSON")
101
+ except json.JSONDecodeError as exc:
102
+ fail(f"settings.json is invalid JSON: {exc}")
103
+ else:
104
+ warn("no .claude/settings.json (hooks not configured)")
105
+
106
+ agents_dir = claude / "agents"
107
+ if agents_dir.is_dir():
108
+ bad = [
109
+ p.name
110
+ for p in agents_dir.glob("*.md")
111
+ if not (_parse_frontmatter(p.read_text(encoding="utf-8")) or {}).get("name")
112
+ or not (_parse_frontmatter(p.read_text(encoding="utf-8")) or {}).get(
113
+ "description"
114
+ )
115
+ ]
116
+ if bad:
117
+ fail(
118
+ f"agents missing name/description frontmatter: {', '.join(sorted(bad))}"
119
+ )
120
+ else:
121
+ good(
122
+ f"agents/ frontmatter complete ({sum(1 for _ in agents_dir.glob('*.md'))} agents)"
123
+ )
124
+ else:
125
+ warn("no .claude/agents/")
126
+
127
+ skills_dir = claude / "skills"
128
+ if skills_dir.is_dir():
129
+ bad_skills = [
130
+ d.name
131
+ for d in skills_dir.iterdir()
132
+ if d.is_dir()
133
+ and (d / "SKILL.md").is_file()
134
+ and not (
135
+ _parse_frontmatter((d / "SKILL.md").read_text(encoding="utf-8")) or {}
136
+ ).get("description")
137
+ ]
138
+ if bad_skills:
139
+ fail(f"skills missing description: {', '.join(sorted(bad_skills))}")
140
+ else:
141
+ good(
142
+ f"skills/ descriptions present "
143
+ f"({sum(1 for d in skills_dir.iterdir() if d.is_dir() and (d / 'SKILL.md').is_file())} skills)"
144
+ )
145
+
146
+ rules_dir = claude / "rules"
147
+ if not rules_dir.is_dir() or not any(rules_dir.glob("*.md")):
148
+ fail("no .claude/rules/ content")
149
+ else:
150
+ good(f"rules/ present ({sum(1 for _ in rules_dir.glob('*.md'))} rules)")
151
+
152
+ return ok, msgs
153
+
154
+
155
+ def doctor(target: str | Path) -> tuple[bool, list[str]]:
156
+ """Run :func:`validate` plus environment/health checks.
157
+
158
+ Returns:
159
+ ``(ok, messages)``; environment issues are warnings (do not fail) unless they break config.
160
+ """
161
+ ok, msgs = validate(target)
162
+ target = Path(target).expanduser().resolve()
163
+ claude = target / ".claude"
164
+
165
+ for tool, why in (
166
+ ("git", "version control"),
167
+ ("jq", "command hooks parse tool input with jq"),
168
+ ):
169
+ if shutil.which(tool):
170
+ msgs.append(f"OK {tool} found ({why})")
171
+ else:
172
+ msgs.append(f"WARN {tool} not on PATH — {why}")
173
+
174
+ hooks_dir = claude / "hooks"
175
+ if hooks_dir.is_dir():
176
+ nonexec = [
177
+ p.name for p in hooks_dir.glob("*.sh") if not (p.stat().st_mode & 0o111)
178
+ ]
179
+ if nonexec:
180
+ msgs.append(
181
+ f"WARN hook scripts not executable: {', '.join(sorted(nonexec))} "
182
+ f"(run: chmod +x .claude/hooks/*.sh)"
183
+ )
184
+ elif any(hooks_dir.glob("*.sh")):
185
+ msgs.append("OK hook scripts are executable")
186
+
187
+ gitignore = target / ".gitignore"
188
+ gi = gitignore.read_text(encoding="utf-8") if gitignore.is_file() else ""
189
+ for entry in (".claude/state/", ".claude/tmp/"):
190
+ if entry in gi:
191
+ msgs.append(f"OK {entry} is gitignored")
192
+ else:
193
+ msgs.append(
194
+ f"WARN {entry} not gitignored (runtime artifacts may be committed)"
195
+ )
196
+
197
+ return ok, msgs