ai-kit-python 1.4.2__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 (278) hide show
  1. ai_kit_python-1.4.2.dist-info/METADATA +132 -0
  2. ai_kit_python-1.4.2.dist-info/RECORD +278 -0
  3. ai_kit_python-1.4.2.dist-info/WHEEL +5 -0
  4. ai_kit_python-1.4.2.dist-info/entry_points.txt +2 -0
  5. ai_kit_python-1.4.2.dist-info/licenses/LICENSE +21 -0
  6. ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/security-audit/LICENSE +21 -0
  7. ai_kit_python-1.4.2.dist-info/licenses/template/ai-kit/vendor/strix/LICENSE +201 -0
  8. ai_kit_python-1.4.2.dist-info/top_level.txt +1 -0
  9. aikit/.agents/skills/go-work/SKILL.md +8 -0
  10. aikit/.agents/skills/laravel-work/SKILL.md +8 -0
  11. aikit/.agents/skills/moodle-work/SKILL.md +8 -0
  12. aikit/.agents/skills/project-continuity/SKILL.md +34 -0
  13. aikit/.agents/skills/python-work/SKILL.md +8 -0
  14. aikit/.dockerignore +90 -0
  15. aikit/.gitattributes +3 -0
  16. aikit/.github/workflows/check-kit.yml +70 -0
  17. aikit/.github/workflows/publish-pypi.yml +108 -0
  18. aikit/.gitignore +60 -0
  19. aikit/BOOTSTRAP_PROMPT.md +18 -0
  20. aikit/CHANGELOG.md +85 -0
  21. aikit/CLAUDE.md +3 -0
  22. aikit/CONTRIBUTING.md +46 -0
  23. aikit/Dockerfile +44 -0
  24. aikit/KIMI.md +3 -0
  25. aikit/LICENSE +21 -0
  26. aikit/MANIFEST.in +22 -0
  27. aikit/MANUS.md +3 -0
  28. aikit/Makefile +70 -0
  29. aikit/README.md +1161 -0
  30. aikit/SECURITY.md +8 -0
  31. aikit/SKILL.md +105 -0
  32. aikit/VERSION +1 -0
  33. aikit/__init__.py +1 -0
  34. aikit/ai-kit/ADAPTERS.md +5 -0
  35. aikit/ai-kit/CONTAINERS.md +5 -0
  36. aikit/ai-kit/ENGINEERING.md +5 -0
  37. aikit/ai-kit/INTENT.md +5 -0
  38. aikit/ai-kit/MODELS.md +5 -0
  39. aikit/ai-kit/SKILLS.md +5 -0
  40. aikit/ai-kit/stacks/GO.md +5 -0
  41. aikit/ai-kit/stacks/LARAVEL.md +5 -0
  42. aikit/ai-kit/stacks/MOODLE.md +5 -0
  43. aikit/ai-kit/stacks/PYTHON.md +5 -0
  44. aikit/aikit_cli.py +59 -0
  45. aikit/deploy/k8s/install-preview-job.yaml +56 -0
  46. aikit/docker/build-requirements.txt +4 -0
  47. aikit/docker/check_version.py +29 -0
  48. aikit/docker/pypi-requirements.txt +3 -0
  49. aikit/docs/ADAPTER_PATH_FIX.md +23 -0
  50. aikit/docs/BENCHMARK.md +106 -0
  51. aikit/docs/CONTEXT7.md +33 -0
  52. aikit/docs/DECISIONS.md +22 -0
  53. aikit/docs/DEVELOPMENT_PLAN.md +16 -0
  54. aikit/docs/DIALOGUE_MEMORY.md +70 -0
  55. aikit/docs/DOCKER.md +74 -0
  56. aikit/docs/DOCKER_HUB_OVERVIEW.md +311 -0
  57. aikit/docs/EVALUATION.md +30 -0
  58. aikit/docs/IMPLEMENTATION.md +134 -0
  59. aikit/docs/MANUS_REVIEW.md +46 -0
  60. aikit/docs/PYPI.md +87 -0
  61. aikit/docs/PYPI_DESCRIPTION.md +103 -0
  62. aikit/docs/SECURITY_AUDIT.md +26 -0
  63. aikit/docs/STRIX.md +28 -0
  64. aikit/docs/V0_5_0.md +27 -0
  65. aikit/docs/V0_6_4.md +25 -0
  66. aikit/docs/V1_4_0.md +73 -0
  67. aikit/docs/V1_4_1.md +55 -0
  68. aikit/docs/V1_4_2.md +33 -0
  69. aikit/docs/WEB_DESIGN.md +72 -0
  70. aikit/docs/WINDOWS_CI_FIX.md +39 -0
  71. aikit/docs/adr/0000-template.md +24 -0
  72. aikit/docs/adr/0001-adaptive-model-routing.md +30 -0
  73. aikit/docs/adr/0002-template-separation.md +24 -0
  74. aikit/docs/adr/0003-agent-registry-and-router.md +24 -0
  75. aikit/docs/adr/0004-onboarding-and-project-facts.md +24 -0
  76. aikit/docs/adr/0005-client-settings-ownership.md +30 -0
  77. aikit/docs/adr/0006-routing-evidence-and-local-measurements.md +26 -0
  78. aikit/docs/adr/0007-generated-client-configuration-and-presets.md +34 -0
  79. aikit/docs/adr/0008-unified-cli-and-fact-drift.md +20 -0
  80. aikit/docs/adr/0009-provider-review-doctor-fix-and-ci-self-install.md +20 -0
  81. aikit/docs/adr/0010-metrics-analysis-and-gemini-verification.md +20 -0
  82. aikit/docs/adr/0011-evidence-lifecycle-and-evaluation.md +20 -0
  83. aikit/docs/adr/0012-measurement-led-routing.md +31 -0
  84. aikit/docs/adr/0013-docker-cli-image.md +24 -0
  85. aikit/docs/adr/0014-local-dialogue-memory.md +22 -0
  86. aikit/docs/history/original-bundle-checks.md +11 -0
  87. aikit/docs/history/v0.1-changelog.txt +37 -0
  88. aikit/docs/history/v0.10-changelog.md +12 -0
  89. aikit/docs/history/v0.2-changelog.md +35 -0
  90. aikit/docs/history/v0.3-changelog.md +25 -0
  91. aikit/docs/history/v0.4-changelog.md +31 -0
  92. aikit/docs/history/v0.5-changelog.md +14 -0
  93. aikit/docs/history/v0.6.4-changelog.md +12 -0
  94. aikit/docs/history/v0.6.5-v0.9-changelog.md +35 -0
  95. aikit/docs/history/v1.1.0-changelog.md +12 -0
  96. aikit/docs/history/v1.2.0-changelog.md +11 -0
  97. aikit/docs/history/verification-before-measurements.md +53 -0
  98. aikit/integrations/CONVENTIONS.md +3 -0
  99. aikit/integrations/README.md +7 -0
  100. aikit/integrations/agents.json +136 -0
  101. aikit/integrations/ai-kit-check.yml +66 -0
  102. aikit/integrations/ai-kit-cline.md +4 -0
  103. aikit/integrations/ai-kit-roo.md +4 -0
  104. aikit/integrations/ai-kit-windsurf.md +4 -0
  105. aikit/integrations/ai-kit.mdc +6 -0
  106. aikit/integrations/claude-settings.data-ask.json +23 -0
  107. aikit/integrations/claude-settings.git-ask.json +10 -0
  108. aikit/integrations/copilot-instructions.md +3 -0
  109. aikit/integrations/security-audit.json +135 -0
  110. aikit/integrations/strix.json +69 -0
  111. aikit/pyproject.toml +60 -0
  112. aikit/scripts/__init__.py +1 -0
  113. aikit/scripts/adapters.py +184 -0
  114. aikit/scripts/adaptive.py +167 -0
  115. aikit/scripts/adr.py +61 -0
  116. aikit/scripts/benchmark.py +543 -0
  117. aikit/scripts/changelog.py +55 -0
  118. aikit/scripts/check_distribution.py +178 -0
  119. aikit/scripts/check_kit.py +406 -0
  120. aikit/scripts/context.py +184 -0
  121. aikit/scripts/docker_test_usage.py +55 -0
  122. aikit/scripts/doctor.py +313 -0
  123. aikit/scripts/evaluate.py +125 -0
  124. aikit/scripts/install.py +1584 -0
  125. aikit/scripts/learning.py +260 -0
  126. aikit/scripts/memory.py +344 -0
  127. aikit/scripts/memory_import.py +177 -0
  128. aikit/scripts/memory_store.py +125 -0
  129. aikit/scripts/metrics.py +351 -0
  130. aikit/scripts/router.py +399 -0
  131. aikit/template/.agents/hooks/session-start.md +3 -0
  132. aikit/template/.agents/hooks/session-start.sh +21 -0
  133. aikit/template/.agents/skills/ai-kit-bootstrap/SKILL.md +14 -0
  134. aikit/template/.agents/skills/ai-kit-review/SKILL.md +11 -0
  135. aikit/template/.agents/skills/ai-kit-sync-context/SKILL.md +12 -0
  136. aikit/template/.agents/skills/context7-docs/SKILL.md +11 -0
  137. aikit/template/.agents/skills/design-extract/SKILL.md +9 -0
  138. aikit/template/.agents/skills/dotnet-work/SKILL.md +9 -0
  139. aikit/template/.agents/skills/filament-work/SKILL.md +9 -0
  140. aikit/template/.agents/skills/frontend-work/SKILL.md +9 -0
  141. aikit/template/.agents/skills/go-work/SKILL.md +9 -0
  142. aikit/template/.agents/skills/infra-work/SKILL.md +9 -0
  143. aikit/template/.agents/skills/java-work/SKILL.md +9 -0
  144. aikit/template/.agents/skills/laravel-blade-ui/SKILL.md +9 -0
  145. aikit/template/.agents/skills/laravel-work/SKILL.md +10 -0
  146. aikit/template/.agents/skills/library-work/SKILL.md +9 -0
  147. aikit/template/.agents/skills/moodle-work/SKILL.md +9 -0
  148. aikit/template/.agents/skills/php-work/SKILL.md +9 -0
  149. aikit/template/.agents/skills/project-continuity/SKILL.md +21 -0
  150. aikit/template/.agents/skills/python-work/SKILL.md +9 -0
  151. aikit/template/.agents/skills/rust-work/SKILL.md +9 -0
  152. aikit/template/.agents/skills/security-audit/SKILL.md +16 -0
  153. aikit/template/.agents/skills/security-audit/agents/openai.yaml +2 -0
  154. aikit/template/.agents/skills/strix/SKILL.md +16 -0
  155. aikit/template/.agents/skills/strix/agents/openai.yaml +2 -0
  156. aikit/template/.agents/skills/ui-review/SKILL.md +9 -0
  157. aikit/template/.agents/skills/web-design/SKILL.md +9 -0
  158. aikit/template/AGENTS.md +16 -0
  159. aikit/template/CHANGELOG.md +6 -0
  160. aikit/template/CLAUDE.md +5 -0
  161. aikit/template/GEMINI.md +4 -0
  162. aikit/template/KIMI.md +4 -0
  163. aikit/template/MANUS.md +4 -0
  164. aikit/template/PROJECT_CONTEXT.md +35 -0
  165. aikit/template/README.md +6 -0
  166. aikit/template/WIKI.md +37 -0
  167. aikit/template/ai-kit/ADAPTERS.md +100 -0
  168. aikit/template/ai-kit/BOOTSTRAP.md +15 -0
  169. aikit/template/ai-kit/CONTAINERS.md +63 -0
  170. aikit/template/ai-kit/CONTEXT7.md +96 -0
  171. aikit/template/ai-kit/CORE.md +23 -0
  172. aikit/template/ai-kit/ENGINEERING.md +34 -0
  173. aikit/template/ai-kit/MCP.md +17 -0
  174. aikit/template/ai-kit/METRICS.md +83 -0
  175. aikit/template/ai-kit/SECURITY-AUDIT.md +21 -0
  176. aikit/template/ai-kit/SECURITY.md +34 -0
  177. aikit/template/ai-kit/SKILLS.md +37 -0
  178. aikit/template/ai-kit/STRIX.md +44 -0
  179. aikit/template/ai-kit/UPSTREAM.md +38 -0
  180. aikit/template/ai-kit/WEB-DESIGN.md +37 -0
  181. aikit/template/ai-kit/profiles/OWNER.md +19 -0
  182. aikit/template/ai-kit/project.json +14 -0
  183. aikit/template/ai-kit/router/POLICY.md +79 -0
  184. aikit/template/ai-kit/router/providers/anthropic.json +44 -0
  185. aikit/template/ai-kit/router/providers/anthropic.md +14 -0
  186. aikit/template/ai-kit/router/providers/gemini.json +13 -0
  187. aikit/template/ai-kit/router/providers/gemini.md +10 -0
  188. aikit/template/ai-kit/router/providers/kimi.json +26 -0
  189. aikit/template/ai-kit/router/providers/kimi.md +13 -0
  190. aikit/template/ai-kit/router/providers/local.json +18 -0
  191. aikit/template/ai-kit/router/providers/local.md +10 -0
  192. aikit/template/ai-kit/router/providers/openai.json +48 -0
  193. aikit/template/ai-kit/router/providers/openai.md +14 -0
  194. aikit/template/ai-kit/router/routing.json +11 -0
  195. aikit/template/ai-kit/router/selection.json +14 -0
  196. aikit/template/ai-kit/settings.json +11 -0
  197. aikit/template/ai-kit/stacks/DOTNET.md +13 -0
  198. aikit/template/ai-kit/stacks/FILAMENT.md +18 -0
  199. aikit/template/ai-kit/stacks/FRONTEND.md +9 -0
  200. aikit/template/ai-kit/stacks/GO.md +19 -0
  201. aikit/template/ai-kit/stacks/INFRA.md +12 -0
  202. aikit/template/ai-kit/stacks/JAVA.md +13 -0
  203. aikit/template/ai-kit/stacks/LARAVEL.md +15 -0
  204. aikit/template/ai-kit/stacks/LIBRARY.md +9 -0
  205. aikit/template/ai-kit/stacks/MOODLE.md +12 -0
  206. aikit/template/ai-kit/stacks/PHP.md +11 -0
  207. aikit/template/ai-kit/stacks/PYTHON.md +11 -0
  208. aikit/template/ai-kit/stacks/RUST.md +18 -0
  209. aikit/template/ai-kit/vendor/security-audit/AI-AND-LLM.md +83 -0
  210. aikit/template/ai-kit/vendor/security-audit/ATTACK-CLASSES.md +130 -0
  211. aikit/template/ai-kit/vendor/security-audit/CLIENT-SIDE.md +83 -0
  212. aikit/template/ai-kit/vendor/security-audit/CLOUD-AND-DEPLOYMENT.md +86 -0
  213. aikit/template/ai-kit/vendor/security-audit/DATA-ISOLATION-AND-LIFECYCLE.md +84 -0
  214. aikit/template/ai-kit/vendor/security-audit/DESKTOP-MOBILE-AND-LOCAL-IPC.md +89 -0
  215. aikit/template/ai-kit/vendor/security-audit/HUNTING.md +251 -0
  216. aikit/template/ai-kit/vendor/security-audit/LICENSE +21 -0
  217. aikit/template/ai-kit/vendor/security-audit/MEMORY-SAFETY-AND-BINARY.md +101 -0
  218. aikit/template/ai-kit/vendor/security-audit/PROTOCOLS-RPC-AND-MESSAGING.md +81 -0
  219. aikit/template/ai-kit/vendor/security-audit/RECONNAISSANCE.md +156 -0
  220. aikit/template/ai-kit/vendor/security-audit/RESOURCE-EXHAUSTION-AND-AVAILABILITY.md +78 -0
  221. aikit/template/ai-kit/vendor/security-audit/SKILL.md +192 -0
  222. aikit/template/ai-kit/vendor/security-audit/SUPPLY-CHAIN-AND-RELEASE.md +73 -0
  223. aikit/template/ai-kit/vendor/security-audit/VALIDATION-AND-REPORTING.md +186 -0
  224. aikit/template/ai-kit/vendor/security-audit/WEB-PROTOCOL-AND-AUTH.md +105 -0
  225. aikit/template/ai-kit/vendor/security-audit/report-schema.json +461 -0
  226. aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.cjs +872 -0
  227. aikit/template/ai-kit/vendor/security-audit/validate-coverage-ledger.test.cjs +740 -0
  228. aikit/template/ai-kit/vendor/security-audit/validate-findings.cjs +773 -0
  229. aikit/template/ai-kit/vendor/security-audit/validate-findings.test.cjs +652 -0
  230. aikit/template/ai-kit/vendor/strix/LICENSE +201 -0
  231. aikit/template/ai-kit/vendor/strix/api-security-testing/SKILL.md +61 -0
  232. aikit/template/ai-kit/vendor/strix/application-security-testing/SKILL.md +66 -0
  233. aikit/template/ai-kit/vendor/strix/ci-security-scanning-with-strix/SKILL.md +149 -0
  234. aikit/template/ai-kit/vendor/strix/find-security-vulnerabilities-in-code/SKILL.md +62 -0
  235. aikit/template/ai-kit/vendor/strix/fix-security-vulnerabilities-with-strix/SKILL.md +77 -0
  236. aikit/template/ai-kit/vendor/strix/managed-pentesting-with-strix/SKILL.md +321 -0
  237. aikit/template/ai-kit/vendor/strix/owasp-top-10-testing/SKILL.md +64 -0
  238. aikit/template/ai-kit/vendor/strix/penetration-testing-with-strix/SKILL.md +162 -0
  239. aikit/template/ai-kit/vendor/strix/web-app-penetration-testing/SKILL.md +54 -0
  240. aikit/template/ai-kit/web-design/LARAVEL.md +51 -0
  241. aikit/template/ai-kit/web-design/LIBRARIES.md +39 -0
  242. aikit/template/ai-kit/web-design/REVIEW.md +37 -0
  243. aikit/template/ai-kit/web-design/TOOLS.md +43 -0
  244. aikit/template/docs/DECISIONS.md +7 -0
  245. aikit/template/docs/adr/0000-template.md +20 -0
  246. aikit/template/docs/design/ACCEPTANCE.md +24 -0
  247. aikit/template/docs/design/BRIEF.md +26 -0
  248. aikit/template/docs/design/SYSTEM.md +30 -0
  249. aikit/templates/agent-secrets.ignore +19 -0
  250. aikit/templates/gitignore.private +160 -0
  251. aikit/templates/gitignore.team +139 -0
  252. aikit/tests/build_distribution.py +73 -0
  253. aikit/tests/docker_smoke.py +172 -0
  254. aikit/tests/fixtures/create-junction.ps1 +18 -0
  255. aikit/tests/fixtures/golden/claude-settings-local.json +58 -0
  256. aikit/tests/test_adapters.py +105 -0
  257. aikit/tests/test_aikit.py +79 -0
  258. aikit/tests/test_benchmark.py +241 -0
  259. aikit/tests/test_check_kit.py +203 -0
  260. aikit/tests/test_context.py +76 -0
  261. aikit/tests/test_context7.py +153 -0
  262. aikit/tests/test_context_selection.py +70 -0
  263. aikit/tests/test_distribution.py +181 -0
  264. aikit/tests/test_docker_image.py +63 -0
  265. aikit/tests/test_docker_test_usage.py +54 -0
  266. aikit/tests/test_doctor.py +148 -0
  267. aikit/tests/test_evaluate.py +66 -0
  268. aikit/tests/test_install.py +939 -0
  269. aikit/tests/test_learning.py +247 -0
  270. aikit/tests/test_memory.py +371 -0
  271. aikit/tests/test_metrics.py +209 -0
  272. aikit/tests/test_native.py +342 -0
  273. aikit/tests/test_onboarding.py +301 -0
  274. aikit/tests/test_router.py +336 -0
  275. aikit/tests/test_security_audit.py +237 -0
  276. aikit/tests/test_strix.py +245 -0
  277. aikit/tests/test_web_design.py +219 -0
  278. aikit/tests/wheel_smoke.py +104 -0
@@ -0,0 +1,184 @@
1
+ """Capture manifest evidence for an installed project's verified context."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import hashlib
6
+ import json
7
+ from pathlib import Path
8
+ import re
9
+ import sys
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
12
+ import install
13
+
14
+
15
+ PROJECT_FACTS = "ai-kit/project.json"
16
+ DESIGN_WORKFLOWS = {"web-design": "web-design", "ui-review": "ui-review", "design-extract": "design-extract"}
17
+
18
+
19
+ def target_root(target: Path) -> Path:
20
+ target = target.absolute()
21
+ if install.is_link(target):
22
+ raise ValueError("Refusing a linked project root")
23
+ target = target.resolve()
24
+ if not target.is_dir():
25
+ raise ValueError("Project must be an existing directory")
26
+ return target
27
+
28
+
29
+ def snapshot_plan(target: Path) -> dict:
30
+ """Prepare, but do not write, a manifest-evidence snapshot."""
31
+ target = target_root(target)
32
+ path = install.safe_path(target, PROJECT_FACTS)
33
+ if not path.is_file():
34
+ raise ValueError("Missing ai-kit/project.json; install AI-KIT before recording context evidence")
35
+ value = install.read_json(path, {})
36
+ if type(value.get("schema")) is not int or value["schema"] != 1:
37
+ raise ValueError("Unsupported project.json schema; expected schema 1")
38
+ if not isinstance(value.get("modules"), list) or any(not isinstance(module, dict) for module in value["modules"]):
39
+ raise ValueError("project.json modules must be a list of objects")
40
+ facts = install.detect_facts(target)
41
+ updated = dict(value)
42
+ updated["evidence"] = install.project_fact_evidence(target, facts)
43
+ data = (json.dumps(updated, indent=2) + "\n").encode("utf-8")
44
+ old = path.read_bytes()
45
+ return {"target": str(target), "path": PROJECT_FACTS, "old": old, "data": data,
46
+ "changed": old != data, "detected_modules": [module["path"] for module in facts["modules"]]}
47
+
48
+
49
+ def apply_snapshot(plan: dict) -> bool:
50
+ target = Path(plan["target"])
51
+ path = install.safe_path(target, plan["path"])
52
+ actual = path.read_bytes() if path.exists() else None
53
+ if actual != plan["old"]:
54
+ raise ValueError("project.json changed after preview")
55
+ if not plan["changed"]:
56
+ return True
57
+ install.atomic_write(path, plan["data"])
58
+ return True
59
+
60
+
61
+ def select_context(target: Path, *, path: str = ".", budget: int = 32000,
62
+ workflow: str | None = None) -> dict:
63
+ """Select instructions by declared module scope; budgets cannot remove critical rules."""
64
+ target = target_root(target)
65
+ if type(budget) is not int or budget < 1:
66
+ raise ValueError("Context budget must be a positive byte count")
67
+ if workflow is not None and workflow not in DESIGN_WORKFLOWS:
68
+ raise ValueError("Unknown context workflow: " + str(workflow))
69
+ if path == ".":
70
+ relative = ""
71
+ else:
72
+ # Probe a file rather than accepting directory links through resolve().
73
+ install.safe_path(target, path.rstrip("/") + "/__context_probe__")
74
+ relative = path.strip("/")
75
+ if path.startswith(("/", "\\")):
76
+ raise ValueError("Module path must be relative")
77
+ facts = install.read_json(install.safe_path(target, PROJECT_FACTS), {})
78
+ if facts.get("schema") != 1 or not isinstance(facts.get("modules"), list):
79
+ raise ValueError("Missing/invalid project module map")
80
+ profiles, matching, scoped = set(), [], []
81
+ for module in facts["modules"]:
82
+ if not isinstance(module, dict) or not isinstance(module.get("path"), str) or not install.MODULE_PATH.fullmatch(module["path"]):
83
+ raise ValueError("Invalid declared module path")
84
+ scope = module["path"].strip("/")
85
+ if not scope or relative == scope or relative.startswith(scope + "/"):
86
+ if not isinstance(module.get("profiles"), list) or any(not isinstance(p, str) or not re.fullmatch(r"[A-Z][A-Z0-9_]*", p) for p in module["profiles"]):
87
+ raise ValueError("Invalid module profiles")
88
+ scoped.append(module)
89
+ # A nested declared module supersedes its parent's stack, while shared instructions still apply.
90
+ deepest = max((len(m["path"]) for m in scoped), default=0)
91
+ for module in scoped:
92
+ if len(module["path"]) == deepest:
93
+ matching.append(module["path"])
94
+ profiles.update(module["profiles"])
95
+ critical = ["AGENTS.md", "ai-kit/CORE.md", "ai-kit/settings.json", "PROJECT_CONTEXT.md",
96
+ "ai-kit/ENGINEERING.md", "ai-kit/SECURITY.md", "docs/DECISIONS.md"]
97
+ settings = install.read_json(install.safe_path(target, "ai-kit/settings.json"), {})
98
+ workflow_files = []
99
+ if workflow is not None:
100
+ if settings.get("web_design") is not True:
101
+ raise ValueError("Web Design workflow is disabled; install with --with-web-design first")
102
+ workflow_files = ["ai-kit/WEB-DESIGN.md", ".agents/skills/" + DESIGN_WORKFLOWS[workflow] + "/SKILL.md",
103
+ *install.OPTIONAL_DOCUMENTS["web-design"]]
104
+ if workflow == "ui-review":
105
+ workflow_files.append("ai-kit/web-design/REVIEW.md")
106
+ elif workflow == "design-extract":
107
+ workflow_files.append("ai-kit/web-design/TOOLS.md")
108
+ elif profiles & {"LARAVEL", "FILAMENT"}:
109
+ workflow_files.extend(["ai-kit/web-design/LARAVEL.md", ".agents/skills/laravel-blade-ui/SKILL.md"])
110
+ critical.extend(workflow_files)
111
+ if settings.get("conventions") == "owner" and profiles & {"LARAVEL", "FILAMENT"}:
112
+ critical.append("ai-kit/profiles/OWNER.md")
113
+ # ADRs preserve architectural constraints between sessions; do not truncate them for a budget.
114
+ adr_dir = target / "docs/adr"
115
+ if install.is_link(adr_dir):
116
+ raise ValueError("Refusing linked ADR directory")
117
+ if adr_dir.is_dir():
118
+ critical.extend(p.relative_to(target).as_posix() for p in sorted(adr_dir.glob("*.md")) if p.name != "0000-template.md")
119
+ if relative:
120
+ parts = relative.split("/")
121
+ critical.extend("/".join(parts[:i]) + "/AGENTS.md" for i in range(1, len(parts) + 1))
122
+ required_profiles = ["ai-kit/stacks/" + p + ".md" for p in sorted(profiles)]
123
+ critical.extend(required_profiles)
124
+ selected, missing, duplicates = [], [], []
125
+ hashes: dict[str, str] = {}
126
+ for name in dict.fromkeys(critical):
127
+ candidate = install.safe_path(target, name)
128
+ if not candidate.is_file():
129
+ # Nested instructions and project decision records are optional; Core/settings are required.
130
+ if name in {"AGENTS.md", "ai-kit/CORE.md", "ai-kit/settings.json", "PROJECT_CONTEXT.md",
131
+ "ai-kit/ENGINEERING.md", "ai-kit/SECURITY.md"} or name in required_profiles or name in workflow_files:
132
+ missing.append(name)
133
+ continue
134
+ data = candidate.read_bytes()
135
+ digest = hashlib.sha256(data).hexdigest()
136
+ if digest in hashes:
137
+ duplicates.append({"path": name, "same_as": hashes[digest]})
138
+ else:
139
+ hashes[digest] = name
140
+ selected.append({"path": name, "bytes": len(data), "sha256": digest, "critical": True})
141
+ if missing:
142
+ raise ValueError("Missing required context: " + ", ".join(missing))
143
+ total = sum(item["bytes"] for item in selected)
144
+ return {"selected": selected, "duplicates": duplicates, "modules": matching, "profiles": sorted(profiles),
145
+ "workflow": workflow,
146
+ "total_bytes": total, "budget_bytes": budget, "over_budget": total > budget,
147
+ "selection_complete": bool(matching),
148
+ "note": "Declared modules may still need bootstrap verification. Byte counts are not token counts. "
149
+ "Critical rules, matching profiles and architecture decisions are retained even above budget. "
150
+ "Duplicate critical files retain their scope/relative-link meaning. "
151
+ "Unrelated profiles and instruction history are excluded; no files are changed."}
152
+
153
+
154
+ def main(argv: list[str] | None = None) -> int:
155
+ parser = argparse.ArgumentParser(description=__doc__)
156
+ sub = parser.add_subparsers(dest="command", required=True)
157
+ snapshot = sub.add_parser("snapshot", help="Preview or record current manifest evidence in project.json")
158
+ snapshot.add_argument("project", type=Path)
159
+ snapshot.add_argument("--apply", action="store_true")
160
+ select = sub.add_parser("select", help="Select module instructions and measure a byte budget without writing")
161
+ select.add_argument("project", type=Path)
162
+ select.add_argument("--path", default=".", help="Relative module/directory scope")
163
+ select.add_argument("--budget", type=int, default=32000, help="Byte budget; critical context is never dropped")
164
+ select.add_argument("--workflow", choices=sorted(DESIGN_WORKFLOWS),
165
+ help="Include the enabled design workflow and project design documents")
166
+ args = parser.parse_args(argv)
167
+ try:
168
+ if args.command == "select":
169
+ print(json.dumps(select_context(args.project, path=args.path, budget=args.budget,
170
+ workflow=args.workflow), indent=2))
171
+ return 0
172
+ plan = snapshot_plan(args.project)
173
+ print(json.dumps({"target": plan["target"], "preview": not args.apply, "changed": plan["changed"],
174
+ "path": plan["path"], "detected_modules": plan["detected_modules"]}, indent=2))
175
+ if args.apply:
176
+ apply_snapshot(plan)
177
+ return 0
178
+ except (OSError, ValueError, UnicodeError, json.JSONDecodeError) as exc:
179
+ print(f"Context snapshot refused: {exc}", file=sys.stderr)
180
+ return 1
181
+
182
+
183
+ if __name__ == "__main__":
184
+ raise SystemExit(main())
@@ -0,0 +1,55 @@
1
+ """Collect read-only Docker evidence; no pruning or volume/container deletion."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ import json
6
+ import shutil
7
+ import subprocess
8
+
9
+
10
+ def command(argv: list[str]) -> str:
11
+ result = subprocess.run(argv, capture_output=True, text=True, timeout=60, check=True)
12
+ return result.stdout
13
+
14
+
15
+ def collect(project: str, *, docker: str = "docker", run=command, builder: str | None = None) -> dict:
16
+ context = run([docker, "context", "show"]).strip()
17
+ run([docker, "info", "--format", "{{.ServerVersion}}"])
18
+ names = run([docker, "volume", "ls", "--filter", "label=com.docker.compose.project=" + project,
19
+ "--format", "{{.Name}}"]).splitlines()
20
+ volumes = []
21
+ for name in names:
22
+ # Pass exact names as arguments, never as shell fragments.
23
+ data = json.loads(run([docker, "volume", "inspect", name]))[0]
24
+ labels = data.get("Labels") or {}
25
+ refs = run([docker, "ps", "-a", "--filter", "volume=" + name, "--format", "{{.ID}}"]).splitlines()
26
+ volumes.append({"name": name, "project_label": labels.get("com.docker.compose.project"),
27
+ "declared_disposable": labels.get("ai-kit.disposable") == "true",
28
+ "container_references": refs, "eligible_without_more_evidence": False})
29
+ report = {"context": context, "project": project, "volumes": volumes,
30
+ "disk_usage_raw": run([docker, "system", "df", "-v"]),
31
+ "measured_project_bytes": None,
32
+ "note": "Human-readable usage is evidence, not exact project ownership or a deletion plan."}
33
+ if builder:
34
+ report["builder"] = builder
35
+ report["build_cache_usage_raw"] = run([docker, "buildx", "du", "--builder", builder, "--format", "json"])
36
+ report["builder_project_ownership"] = "unverified"
37
+ return report
38
+
39
+
40
+ def main(argv: list[str] | None = None) -> int:
41
+ parser = argparse.ArgumentParser(description=__doc__)
42
+ parser.add_argument("--project", required=True)
43
+ parser.add_argument("--builder")
44
+ args = parser.parse_args(argv)
45
+ if not shutil.which("docker"):
46
+ parser.exit(1, "Docker client unavailable; install/configure the actual project runtime first.\n")
47
+ try:
48
+ print(json.dumps(collect(args.project, builder=args.builder), indent=2))
49
+ return 0
50
+ except (OSError, ValueError, subprocess.SubprocessError) as exc:
51
+ parser.exit(1, f"Docker evidence unavailable: {exc}\n")
52
+
53
+
54
+ if __name__ == "__main__":
55
+ raise SystemExit(main())
@@ -0,0 +1,313 @@
1
+ """Offline health check for an AI-KIT-installed application project."""
2
+ from __future__ import annotations
3
+
4
+ import argparse
5
+ from datetime import date
6
+ import json
7
+ import os
8
+ from pathlib import Path
9
+ import re
10
+ import sys
11
+ from urllib.parse import unquote
12
+
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
14
+ import check_kit
15
+ import install
16
+ import router
17
+
18
+ BUDGETS = {"AGENTS.md": 2500, "ai-kit/CORE.md": 3000}
19
+ PROVIDER_FRESHNESS_DAYS = 90
20
+
21
+
22
+ def examine(target: Path, *, root: Path = install.ROOT) -> dict:
23
+ errors: list[str] = []
24
+ warnings: list[str] = []
25
+ info: list[str] = []
26
+ try:
27
+ target = target.absolute()
28
+ if install.is_link(target):
29
+ raise ValueError("Refusing a linked target root")
30
+ target = target.resolve()
31
+ except ValueError as exc:
32
+ return {"target": str(target), "installed_version": None, "bundle_version": None,
33
+ "errors": [str(exc)], "warnings": warnings, "info": info}
34
+ report = {"target": str(target),
35
+ "installed_version": None,
36
+ "bundle_version": (root / "VERSION").read_text(encoding="utf-8").strip(),
37
+ "errors": errors, "warnings": warnings, "info": info}
38
+ if not target.is_dir():
39
+ errors.append("Target is not a directory")
40
+ return report
41
+ settings = {}
42
+ try:
43
+ settings = install.read_json(target / "ai-kit" / "settings.json", {})
44
+ install.validate_settings(settings)
45
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
46
+ errors.append(f"Invalid installation settings: {exc}")
47
+ state_path = target / "ai-kit" / ".install-state.json"
48
+ if not state_path.is_file():
49
+ errors.append("No installer state found; is AI-KIT installed in this project?")
50
+ return report
51
+ try:
52
+ state = install.read_json(state_path, {})
53
+ install.validate_state(state, target)
54
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
55
+ errors.append(f"Invalid installer state: {exc}")
56
+ return report
57
+ report["installed_version"] = state.get("accepted_version")
58
+ if report["installed_version"] != report["bundle_version"]:
59
+ info.append("Installed baseline differs from this bundle's VERSION.")
60
+ records = state.get("files", {})
61
+ modified, adapted = [], []
62
+ for relative in sorted(records):
63
+ try:
64
+ path = install.safe_path(target, relative)
65
+ except ValueError as exc:
66
+ errors.append(f"Unsafe recorded path {relative}: {exc}")
67
+ continue
68
+ if not path.is_file():
69
+ errors.append("Missing managed file: " + relative)
70
+ elif install.digest(path.read_bytes()) != records[relative]["installed_hash"]:
71
+ (adapted if records[relative].get("local_adaptation") else modified).append(relative)
72
+ warnings.extend(f"Modified after installation; rerun the installer to review: {r}" for r in modified)
73
+ info.extend(f"Locally adapted (accepted): {r}" for r in adapted)
74
+ for relative, entries in sorted(state.get("managed_json", {}).items()):
75
+ path = target / relative
76
+ if not path.is_file():
77
+ warnings.append(f"Missing settings file with AI-KIT entries: {relative}")
78
+ continue
79
+ try:
80
+ current = json.loads(path.read_text(encoding="utf-8-sig"))
81
+ if not isinstance(current, dict):
82
+ raise ValueError("expected a JSON object")
83
+ for key, values in sorted(entries.items()):
84
+ for value in values:
85
+ if install.merge_managed_json(current, {}, {key: [value]}) != current:
86
+ warnings.append(f"AI-KIT entry missing from {relative}: {key} {value}")
87
+ except (OSError, ValueError, UnicodeError) as exc:
88
+ warnings.append(f"Cannot read AI-KIT entries in {relative}: {exc}")
89
+ for relative in sorted(install.project_documents(settings)):
90
+ try:
91
+ document = install.safe_path(target, relative)
92
+ except (OSError, ValueError) as exc:
93
+ errors.append(f"Unsafe project document {relative}: {exc}")
94
+ continue
95
+ if not document.is_file():
96
+ warnings.append("Owned project document missing: " + relative)
97
+ for relative, limit in BUDGETS.items():
98
+ path = target / relative
99
+ if path.is_file() and path.stat().st_size > limit:
100
+ warnings.append(f"Always-loaded entry exceeds budget: {relative}")
101
+ candidates_dir = target / "ai-kit" / ".upstream-cache" / "candidates"
102
+ if candidates_dir.is_dir():
103
+ pending = list(candidates_dir.glob("*/*.metadata.json"))
104
+ if pending:
105
+ warnings.append(f"Pending conflict candidates need review: {len(pending)}")
106
+ providers_dir = target / "ai-kit" / "router" / "providers"
107
+ provider_configs = sorted(providers_dir.glob("*.json")) if providers_dir.is_dir() else []
108
+ for provider in provider_configs:
109
+ try:
110
+ config = json.loads(provider.read_text(encoding="utf-8"))
111
+ except ValueError as exc:
112
+ warnings.append(f"Invalid provider configuration {provider.name}: {exc}")
113
+ continue
114
+ user_supplied = config.get("user_supplied_models") is True
115
+ pending = config.get("verification_status") == "pending"
116
+ for role, spec in config.get("roles", {}).items():
117
+ if "effort" in spec and spec["effort"] not in config.get("supported_efforts", {}).get(role, []):
118
+ warnings.append(f"Unsupported configured effort: {provider.name} role {role}")
119
+ verified = config.get("verified_documentation_date")
120
+ try:
121
+ verified_date = date.fromisoformat(verified) if isinstance(verified, str) else None
122
+ except ValueError:
123
+ verified_date = None
124
+ if user_supplied:
125
+ info.append(f"Provider {provider.name} uses project-supplied local models")
126
+ elif pending:
127
+ info.append(f"Provider {provider.name} verification is pending")
128
+ elif verified_date is None:
129
+ warnings.append(f"Provider {provider.name} lacks a valid verified_documentation_date")
130
+ elif (date.today() - verified_date).days > PROVIDER_FRESHNESS_DAYS:
131
+ warnings.append(f"Provider {provider.name} verification is older than "
132
+ f"{PROVIDER_FRESHNESS_DAYS} days")
133
+ selection_path = target / "ai-kit" / "router" / "selection.json"
134
+ if selection_path.is_file():
135
+ try:
136
+ router.validate_selection(json.loads(selection_path.read_text(encoding="utf-8")))
137
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
138
+ warnings.append(f"Invalid router selection: {exc}")
139
+ resolved_path = target / "ai-kit" / "router" / "resolved.json"
140
+ if resolved_path.is_file():
141
+ try:
142
+ resolved = json.loads(resolved_path.read_text(encoding="utf-8"))
143
+ if type(resolved.get("schema")) is not int or resolved["schema"] != 1:
144
+ raise ValueError("Unsupported resolved schema; expected schema 1")
145
+ if not isinstance(resolved.get("roles"), dict):
146
+ raise ValueError("Resolved roles must be an object")
147
+ except (ValueError, json.JSONDecodeError) as exc:
148
+ warnings.append(f"Invalid generated router config: {exc}")
149
+ project_json_path = target / "ai-kit" / "project.json"
150
+ project_value = None
151
+ if project_json_path.is_file():
152
+ try:
153
+ project_value = json.loads(project_json_path.read_text(encoding="utf-8"))
154
+ if type(project_value.get("schema")) is not int or project_value["schema"] != 1:
155
+ raise ValueError("Unsupported project schema; expected schema 1")
156
+ modules = project_value.get("modules")
157
+ if not isinstance(modules, list) or any(not isinstance(m, dict) for m in modules):
158
+ raise ValueError("Project modules must be a list of objects")
159
+ except (ValueError, json.JSONDecodeError) as exc:
160
+ warnings.append(f"Invalid project.json: {exc}")
161
+ context_path = target / "PROJECT_CONTEXT.md"
162
+ if project_value is not None and context_path.is_file():
163
+ try:
164
+ json_paths = project_module_paths(project_value)
165
+ context_paths = context_module_paths(context_path.read_text(encoding="utf-8"))
166
+ if json_paths != context_paths:
167
+ detail = []
168
+ if context_paths - json_paths:
169
+ detail.append("only in context: " + ", ".join(sorted(context_paths - json_paths)))
170
+ if json_paths - context_paths:
171
+ detail.append("only in project.json: " + ", ".join(sorted(json_paths - context_paths)))
172
+ warnings.append("project.json and PROJECT_CONTEXT.md module maps differ (" + "; ".join(detail) + ")")
173
+ except (OSError, UnicodeError) as exc:
174
+ warnings.append(f"Cannot compare module maps: {exc}")
175
+ if project_value is not None:
176
+ freshness_warnings(target, project_value, warnings, info)
177
+ warnings.extend(link_warnings(target))
178
+ return report
179
+
180
+
181
+ def project_module_paths(value: dict) -> set[str]:
182
+ """Module paths recorded in ai-kit/project.json."""
183
+ paths = set()
184
+ for module in value.get("modules", []):
185
+ if isinstance(module, dict) and isinstance(module.get("path"), str):
186
+ paths.add(module["path"])
187
+ return paths
188
+
189
+
190
+ def context_module_paths(text: str) -> set[str]:
191
+ """Module paths in the PROJECT_CONTEXT.md module-map table (canonical /-prefixed rows)."""
192
+ paths = set()
193
+ for line in text.splitlines():
194
+ stripped = line.strip()
195
+ if not stripped.startswith("| /"):
196
+ continue
197
+ cells = [cell.strip() for cell in stripped.strip("|").split("|")]
198
+ if cells and cells[0].startswith("/"):
199
+ paths.add(cells[0])
200
+ return paths
201
+
202
+
203
+ def freshness_warnings(target: Path, project: dict, warnings: list[str], info: list[str]) -> None:
204
+ """Compare recorded module facts and manifest evidence with current filesystem facts."""
205
+ try:
206
+ facts = install.detect_facts(target)
207
+ except (OSError, ValueError) as exc:
208
+ warnings.append(f"Cannot detect current project facts: {exc}")
209
+ return
210
+ detected = {module["path"]: module for module in facts["modules"]}
211
+ recorded = {module["path"]: module for module in project.get("modules", [])
212
+ if isinstance(module.get("path"), str)}
213
+ for path in sorted(set(detected) - set(recorded)):
214
+ warnings.append("Detected module missing from project facts: " + path)
215
+ for path in sorted(set(recorded) - set(detected)):
216
+ if isinstance(recorded[path].get("manifests"), list) and recorded[path]["manifests"]:
217
+ warnings.append("Recorded module no longer has detected manifests: " + path)
218
+ for path in sorted(set(detected) & set(recorded)):
219
+ expected_profiles = recorded[path].get("profiles")
220
+ if isinstance(expected_profiles, list) and sorted(expected_profiles) != detected[path]["profiles"]:
221
+ warnings.append("Detected profiles differ from project facts at " + path)
222
+ evidence = project.get("evidence")
223
+ if evidence is None:
224
+ info.append("No project fact evidence snapshot; run aikit context snapshot TARGET --apply after bootstrap.")
225
+ return
226
+ if not isinstance(evidence, dict) or evidence.get("schema") != 1 or not isinstance(evidence.get("modules"), list):
227
+ warnings.append("Invalid project fact evidence; rerun aikit context snapshot TARGET --apply.")
228
+ return
229
+ try:
230
+ current = install.project_fact_evidence(target, facts)
231
+ except (OSError, ValueError) as exc:
232
+ warnings.append(f"Cannot refresh project fact evidence: {exc}")
233
+ return
234
+ stored = {item.get("path"): item for item in evidence["modules"] if isinstance(item, dict) and
235
+ isinstance(item.get("path"), str) and isinstance(item.get("manifests"), dict)}
236
+ observed = {item["path"]: item for item in current["modules"]}
237
+ for path in sorted(set(stored) | set(observed)):
238
+ if path not in stored:
239
+ warnings.append("New detected manifest evidence at " + path + "; update context after review.")
240
+ elif path not in observed:
241
+ warnings.append("Recorded manifest evidence no longer detected at " + path + "; update context after review.")
242
+ else:
243
+ if stored[path]["manifests"] != observed[path]["manifests"]:
244
+ warnings.append("Manifest evidence changed at " + path + "; review context facts and commands.")
245
+ if stored[path].get("profiles") != observed[path].get("profiles"):
246
+ warnings.append("Detected profiles changed at " + path + "; review context facts.")
247
+ if stored[path].get("commands") != observed[path].get("commands"):
248
+ warnings.append("Detected commands changed at " + path + "; review context facts and commands.")
249
+
250
+
251
+ def link_warnings(target: Path) -> list[str]:
252
+ """Report broken relative links/anchors in the installed project's Markdown."""
253
+ problems = []
254
+ excluded = {".git", ".upstream-cache", "node_modules", "vendor", ".venv", "__pycache__"}
255
+ for directory, dirs, files in os.walk(target, followlinks=False):
256
+ dirs[:] = [d for d in dirs if d not in excluded and not install.is_link(Path(directory) / d)]
257
+ if Path(directory) == target / "ai-kit":
258
+ dirs[:] = [d for d in dirs if d != ".metrics"]
259
+ for name in files:
260
+ path = Path(directory) / name
261
+ if path.suffix != ".md":
262
+ continue
263
+ relative = path.relative_to(target).as_posix()
264
+ text = path.read_text(encoding="utf-8")
265
+ for link in check_kit.links(text):
266
+ if re.match(r"^[a-z][a-z0-9+.-]*:", link, re.I):
267
+ continue
268
+ filename, _, anchor = unquote(link).partition("#")
269
+ destination = (path.parent / filename).resolve() if filename else path.resolve()
270
+ try:
271
+ destination.relative_to(target.resolve())
272
+ except ValueError:
273
+ problems.append(f"{relative}: link escapes project: {link}")
274
+ continue
275
+ if not destination.exists():
276
+ problems.append(f"{relative}: missing link: {link}")
277
+ elif anchor and destination.is_file() and destination.suffix == ".md":
278
+ if anchor not in check_kit.headings(destination.read_text(encoding="utf-8")):
279
+ problems.append(f"{relative}: missing heading: {link}")
280
+ return problems
281
+
282
+
283
+ def main(argv: list[str] | None = None) -> int:
284
+ parser = argparse.ArgumentParser(description=__doc__)
285
+ parser.add_argument("target", type=Path)
286
+ parser.add_argument("--root", type=Path, default=install.ROOT,
287
+ help="Reference distribution used for the bundle version comparison")
288
+ parser.add_argument("--fix", action="store_true",
289
+ help="Re-run the installer to restore missing/baseline files; local edits are preserved")
290
+ args = parser.parse_args(argv)
291
+ if args.fix:
292
+ try:
293
+ plan = install.build_plan(args.target, root=args.root)
294
+ applied = install.apply_plan(plan)
295
+ report = examine(args.target, root=args.root)
296
+ except (OSError, ValueError, json.JSONDecodeError) as exc:
297
+ print(f"Fix refused: {exc}", file=sys.stderr)
298
+ return 1
299
+ print(json.dumps({"fix": {"writes": sorted(plan["actions"]),
300
+ "conflicts": sorted(plan["conflicts"]), "applied": applied},
301
+ "report": report}, indent=2))
302
+ return 2 if (plan["conflicts"] or report["errors"]) else 0
303
+ try:
304
+ report = examine(args.target, root=args.root)
305
+ except OSError as exc:
306
+ print(f"Examination failed: {exc}", file=sys.stderr)
307
+ return 1
308
+ print(json.dumps(report, indent=2))
309
+ return 1 if report["errors"] else 0
310
+
311
+
312
+ if __name__ == "__main__":
313
+ raise SystemExit(main())