solokit 0.1.1__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 (323) hide show
  1. solokit/__init__.py +10 -0
  2. solokit/__version__.py +3 -0
  3. solokit/cli.py +374 -0
  4. solokit/core/__init__.py +1 -0
  5. solokit/core/cache.py +102 -0
  6. solokit/core/command_runner.py +278 -0
  7. solokit/core/config.py +453 -0
  8. solokit/core/config_validator.py +204 -0
  9. solokit/core/constants.py +291 -0
  10. solokit/core/error_formatter.py +279 -0
  11. solokit/core/error_handlers.py +346 -0
  12. solokit/core/exceptions.py +1567 -0
  13. solokit/core/file_ops.py +309 -0
  14. solokit/core/logging_config.py +166 -0
  15. solokit/core/output.py +99 -0
  16. solokit/core/performance.py +57 -0
  17. solokit/core/protocols.py +141 -0
  18. solokit/core/types.py +312 -0
  19. solokit/deployment/__init__.py +1 -0
  20. solokit/deployment/executor.py +411 -0
  21. solokit/git/__init__.py +1 -0
  22. solokit/git/integration.py +619 -0
  23. solokit/init/__init__.py +41 -0
  24. solokit/init/claude_commands_installer.py +87 -0
  25. solokit/init/dependency_installer.py +313 -0
  26. solokit/init/docs_structure.py +90 -0
  27. solokit/init/env_generator.py +160 -0
  28. solokit/init/environment_validator.py +334 -0
  29. solokit/init/git_hooks_installer.py +71 -0
  30. solokit/init/git_setup.py +188 -0
  31. solokit/init/gitignore_updater.py +195 -0
  32. solokit/init/initial_commit.py +145 -0
  33. solokit/init/initial_scans.py +109 -0
  34. solokit/init/orchestrator.py +246 -0
  35. solokit/init/readme_generator.py +207 -0
  36. solokit/init/session_structure.py +239 -0
  37. solokit/init/template_installer.py +424 -0
  38. solokit/learning/__init__.py +1 -0
  39. solokit/learning/archiver.py +115 -0
  40. solokit/learning/categorizer.py +126 -0
  41. solokit/learning/curator.py +428 -0
  42. solokit/learning/extractor.py +352 -0
  43. solokit/learning/reporter.py +351 -0
  44. solokit/learning/repository.py +254 -0
  45. solokit/learning/similarity.py +342 -0
  46. solokit/learning/validator.py +144 -0
  47. solokit/project/__init__.py +1 -0
  48. solokit/project/init.py +1162 -0
  49. solokit/project/stack.py +436 -0
  50. solokit/project/sync_plugin.py +438 -0
  51. solokit/project/tree.py +375 -0
  52. solokit/quality/__init__.py +1 -0
  53. solokit/quality/api_validator.py +424 -0
  54. solokit/quality/checkers/__init__.py +25 -0
  55. solokit/quality/checkers/base.py +114 -0
  56. solokit/quality/checkers/context7.py +221 -0
  57. solokit/quality/checkers/custom.py +162 -0
  58. solokit/quality/checkers/deployment.py +323 -0
  59. solokit/quality/checkers/documentation.py +179 -0
  60. solokit/quality/checkers/formatting.py +161 -0
  61. solokit/quality/checkers/integration.py +394 -0
  62. solokit/quality/checkers/linting.py +159 -0
  63. solokit/quality/checkers/security.py +261 -0
  64. solokit/quality/checkers/spec_completeness.py +127 -0
  65. solokit/quality/checkers/tests.py +184 -0
  66. solokit/quality/env_validator.py +306 -0
  67. solokit/quality/gates.py +655 -0
  68. solokit/quality/reporters/__init__.py +10 -0
  69. solokit/quality/reporters/base.py +25 -0
  70. solokit/quality/reporters/console.py +98 -0
  71. solokit/quality/reporters/json_reporter.py +34 -0
  72. solokit/quality/results.py +98 -0
  73. solokit/session/__init__.py +1 -0
  74. solokit/session/briefing/__init__.py +245 -0
  75. solokit/session/briefing/documentation_loader.py +53 -0
  76. solokit/session/briefing/formatter.py +476 -0
  77. solokit/session/briefing/git_context.py +282 -0
  78. solokit/session/briefing/learning_loader.py +212 -0
  79. solokit/session/briefing/milestone_builder.py +78 -0
  80. solokit/session/briefing/orchestrator.py +137 -0
  81. solokit/session/briefing/stack_detector.py +51 -0
  82. solokit/session/briefing/tree_generator.py +52 -0
  83. solokit/session/briefing/work_item_loader.py +209 -0
  84. solokit/session/briefing.py +353 -0
  85. solokit/session/complete.py +1188 -0
  86. solokit/session/status.py +246 -0
  87. solokit/session/validate.py +452 -0
  88. solokit/templates/.claude/commands/end.md +109 -0
  89. solokit/templates/.claude/commands/init.md +159 -0
  90. solokit/templates/.claude/commands/learn-curate.md +88 -0
  91. solokit/templates/.claude/commands/learn-search.md +62 -0
  92. solokit/templates/.claude/commands/learn-show.md +69 -0
  93. solokit/templates/.claude/commands/learn.md +136 -0
  94. solokit/templates/.claude/commands/start.md +114 -0
  95. solokit/templates/.claude/commands/status.md +22 -0
  96. solokit/templates/.claude/commands/validate.md +27 -0
  97. solokit/templates/.claude/commands/work-delete.md +119 -0
  98. solokit/templates/.claude/commands/work-graph.md +139 -0
  99. solokit/templates/.claude/commands/work-list.md +26 -0
  100. solokit/templates/.claude/commands/work-new.md +114 -0
  101. solokit/templates/.claude/commands/work-next.md +25 -0
  102. solokit/templates/.claude/commands/work-show.md +24 -0
  103. solokit/templates/.claude/commands/work-update.md +141 -0
  104. solokit/templates/CHANGELOG.md +17 -0
  105. solokit/templates/WORK_ITEM_TYPES.md +141 -0
  106. solokit/templates/__init__.py +1 -0
  107. solokit/templates/bug_spec.md +217 -0
  108. solokit/templates/config.schema.json +150 -0
  109. solokit/templates/dashboard_refine/base/.gitignore +36 -0
  110. solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
  111. solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
  112. solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
  113. solokit/templates/dashboard_refine/base/app/globals.css +60 -0
  114. solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
  115. solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
  116. solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
  117. solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
  118. solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
  119. solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
  120. solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
  121. solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
  122. solokit/templates/dashboard_refine/base/components.json +16 -0
  123. solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
  124. solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
  125. solokit/templates/dashboard_refine/base/next.config.ts +10 -0
  126. solokit/templates/dashboard_refine/base/package.json.template +40 -0
  127. solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
  128. solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
  129. solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
  130. solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
  131. solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
  132. solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
  133. solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
  134. solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
  135. solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
  136. solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
  137. solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
  138. solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
  139. solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
  140. solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
  141. solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
  142. solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
  143. solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
  144. solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
  145. solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
  146. solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
  147. solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
  148. solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
  149. solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
  150. solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
  151. solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
  152. solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
  153. solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
  154. solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
  155. solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
  156. solokit/templates/deployment_spec.md +500 -0
  157. solokit/templates/feature_spec.md +248 -0
  158. solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
  159. solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
  160. solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
  161. solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
  162. solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
  163. solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
  164. solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
  165. solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
  166. solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
  167. solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
  168. solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
  169. solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
  170. solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
  171. solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
  172. solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
  173. solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
  174. solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
  175. solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
  176. solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
  177. solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
  178. solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
  179. solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
  180. solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
  181. solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
  182. solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
  183. solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
  184. solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
  185. solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
  186. solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
  187. solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
  188. solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
  189. solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
  190. solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
  191. solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
  192. solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
  193. solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
  194. solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
  195. solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
  196. solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
  197. solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
  198. solokit/templates/git-hooks/prepare-commit-msg +24 -0
  199. solokit/templates/integration_test_spec.md +363 -0
  200. solokit/templates/learnings.json +15 -0
  201. solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
  202. solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
  203. solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
  204. solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
  205. solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
  206. solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
  207. solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
  208. solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
  209. solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
  210. solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
  211. solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
  212. solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
  213. solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
  214. solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
  215. solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
  216. solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
  217. solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
  218. solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
  219. solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
  220. solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
  221. solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
  222. solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
  223. solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
  224. solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
  225. solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
  226. solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
  227. solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
  228. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
  229. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
  230. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
  231. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
  232. solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
  233. solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
  234. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
  235. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
  236. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
  237. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
  238. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
  239. solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
  240. solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
  241. solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
  242. solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
  243. solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
  244. solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
  245. solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
  246. solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
  247. solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
  248. solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
  249. solokit/templates/refactor_spec.md +287 -0
  250. solokit/templates/saas_t3/base/.gitignore +36 -0
  251. solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
  252. solokit/templates/saas_t3/base/app/globals.css +27 -0
  253. solokit/templates/saas_t3/base/app/layout.tsx +23 -0
  254. solokit/templates/saas_t3/base/app/page.tsx +31 -0
  255. solokit/templates/saas_t3/base/lib/api.tsx +77 -0
  256. solokit/templates/saas_t3/base/lib/utils.ts +13 -0
  257. solokit/templates/saas_t3/base/next.config.ts +7 -0
  258. solokit/templates/saas_t3/base/package.json.template +38 -0
  259. solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
  260. solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
  261. solokit/templates/saas_t3/base/server/api/root.ts +19 -0
  262. solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
  263. solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
  264. solokit/templates/saas_t3/base/server/db.ts +17 -0
  265. solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
  266. solokit/templates/saas_t3/base/tsconfig.json +27 -0
  267. solokit/templates/saas_t3/docker/Dockerfile +60 -0
  268. solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
  269. solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
  270. solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
  271. solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
  272. solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
  273. solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
  274. solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
  275. solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
  276. solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
  277. solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
  278. solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
  279. solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
  280. solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
  281. solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
  282. solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
  283. solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
  284. solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
  285. solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
  286. solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
  287. solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
  288. solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
  289. solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
  290. solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
  291. solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
  292. solokit/templates/security_spec.md +287 -0
  293. solokit/templates/stack-versions.yaml +617 -0
  294. solokit/templates/status_update.json +6 -0
  295. solokit/templates/template-registry.json +257 -0
  296. solokit/templates/work_items.json +11 -0
  297. solokit/testing/__init__.py +1 -0
  298. solokit/testing/integration_runner.py +550 -0
  299. solokit/testing/performance.py +637 -0
  300. solokit/visualization/__init__.py +1 -0
  301. solokit/visualization/dependency_graph.py +788 -0
  302. solokit/work_items/__init__.py +1 -0
  303. solokit/work_items/creator.py +217 -0
  304. solokit/work_items/delete.py +264 -0
  305. solokit/work_items/get_dependencies.py +185 -0
  306. solokit/work_items/get_dependents.py +113 -0
  307. solokit/work_items/get_metadata.py +121 -0
  308. solokit/work_items/get_next_recommendations.py +133 -0
  309. solokit/work_items/manager.py +235 -0
  310. solokit/work_items/milestones.py +137 -0
  311. solokit/work_items/query.py +376 -0
  312. solokit/work_items/repository.py +267 -0
  313. solokit/work_items/scheduler.py +184 -0
  314. solokit/work_items/spec_parser.py +838 -0
  315. solokit/work_items/spec_validator.py +493 -0
  316. solokit/work_items/updater.py +157 -0
  317. solokit/work_items/validator.py +205 -0
  318. solokit-0.1.1.dist-info/METADATA +640 -0
  319. solokit-0.1.1.dist-info/RECORD +323 -0
  320. solokit-0.1.1.dist-info/WHEEL +5 -0
  321. solokit-0.1.1.dist-info/entry_points.txt +2 -0
  322. solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
  323. solokit-0.1.1.dist-info/top_level.txt +1 -0
@@ -0,0 +1,246 @@
1
+ #!/usr/bin/env python3
2
+ """Display current session status."""
3
+
4
+ import json
5
+ from datetime import datetime
6
+ from pathlib import Path
7
+
8
+ from solokit.core.command_runner import CommandRunner
9
+ from solokit.core.constants import SESSION_STATUS_TIMEOUT
10
+ from solokit.core.exceptions import (
11
+ FileNotFoundError,
12
+ FileOperationError,
13
+ SessionNotFoundError,
14
+ ValidationError,
15
+ WorkItemNotFoundError,
16
+ )
17
+ from solokit.core.logging_config import get_logger
18
+ from solokit.core.output import get_output
19
+ from solokit.core.types import Priority, WorkItemStatus
20
+
21
+ logger = get_logger(__name__)
22
+ output = get_output()
23
+
24
+
25
+ def get_session_status() -> int:
26
+ """
27
+ Get current session status.
28
+
29
+ Loads and displays the current session status including:
30
+ - Work item information
31
+ - Time elapsed
32
+ - Git changes
33
+ - Milestone progress
34
+ - Next items
35
+
36
+ Returns:
37
+ int: Exit code (0 for success, error code for failure)
38
+
39
+ Raises:
40
+ SessionNotFoundError: If no active session exists
41
+ FileNotFoundError: If required session files are missing
42
+ FileOperationError: If file read operations fail
43
+ ValidationError: If session data is invalid
44
+ WorkItemNotFoundError: If work item doesn't exist
45
+ """
46
+ logger.debug("Fetching session status")
47
+ session_dir = Path(".session")
48
+ status_file = session_dir / "tracking" / "status_update.json"
49
+
50
+ if not status_file.exists():
51
+ logger.info("No active session file found")
52
+ raise SessionNotFoundError()
53
+
54
+ # Load status
55
+ logger.debug("Loading session status from: %s", status_file)
56
+ try:
57
+ status = json.loads(status_file.read_text())
58
+ except json.JSONDecodeError as e:
59
+ raise FileOperationError(
60
+ operation="read",
61
+ file_path=str(status_file),
62
+ details=f"Invalid JSON: {e}",
63
+ cause=e,
64
+ )
65
+ except OSError as e:
66
+ raise FileOperationError(
67
+ operation="read",
68
+ file_path=str(status_file),
69
+ details=str(e),
70
+ cause=e,
71
+ )
72
+
73
+ work_item_id = status.get("current_work_item")
74
+
75
+ if not work_item_id:
76
+ logger.warning("No active work item in session")
77
+ raise ValidationError(
78
+ message="No active work item in this session",
79
+ context={"status_file": str(status_file)},
80
+ remediation="Start a work item with 'sk start <work_item_id>'",
81
+ )
82
+
83
+ logger.debug("Current work item: %s", work_item_id)
84
+
85
+ # Load work item
86
+ work_items_file = session_dir / "tracking" / "work_items.json"
87
+ logger.debug("Loading work items from: %s", work_items_file)
88
+
89
+ if not work_items_file.exists():
90
+ raise FileNotFoundError(
91
+ file_path=str(work_items_file),
92
+ file_type="work items",
93
+ )
94
+
95
+ try:
96
+ data = json.loads(work_items_file.read_text())
97
+ except json.JSONDecodeError as e:
98
+ raise FileOperationError(
99
+ operation="read",
100
+ file_path=str(work_items_file),
101
+ details=f"Invalid JSON: {e}",
102
+ cause=e,
103
+ )
104
+ except OSError as e:
105
+ raise FileOperationError(
106
+ operation="read",
107
+ file_path=str(work_items_file),
108
+ details=str(e),
109
+ cause=e,
110
+ )
111
+
112
+ item = data["work_items"].get(work_item_id)
113
+
114
+ if not item:
115
+ logger.error("Work item not found: %s", work_item_id)
116
+ raise WorkItemNotFoundError(work_item_id)
117
+
118
+ logger.info("Displaying session status for work item: %s", work_item_id)
119
+
120
+ output.section("Current Session Status")
121
+
122
+ # Work item info
123
+ output.info(f"Work Item: {work_item_id}")
124
+ output.info(f"Type: {item['type']}")
125
+ output.info(f"Priority: {item['priority']}")
126
+
127
+ sessions = len(item.get("sessions", []))
128
+ estimated = item.get("estimated_effort", "Unknown")
129
+ output.info(f"Session: {sessions} (of estimated {estimated})")
130
+ output.info("")
131
+
132
+ # Time elapsed (if session start time recorded)
133
+ session_start = status.get("session_start")
134
+ if session_start:
135
+ start_time = datetime.fromisoformat(session_start)
136
+ elapsed = datetime.now() - start_time
137
+ hours = int(elapsed.total_seconds() // 3600)
138
+ minutes = int((elapsed.total_seconds() % 3600) // 60)
139
+ output.info(f"Time Elapsed: {hours}h {minutes}m")
140
+ output.info("")
141
+ logger.debug("Session elapsed time: %dh %dm", hours, minutes)
142
+
143
+ # Git changes
144
+ try:
145
+ logger.debug("Fetching git changes")
146
+ runner = CommandRunner(default_timeout=SESSION_STATUS_TIMEOUT)
147
+ result = runner.run(["git", "diff", "--name-status", "HEAD"])
148
+
149
+ if result.success and result.stdout:
150
+ lines = result.stdout.strip().split("\n")
151
+ output.info(f"Files Changed ({len(lines)}):")
152
+ for line in lines[:10]: # Show first 10
153
+ output.info(f" {line}")
154
+ if len(lines) > 10:
155
+ output.info(f" ... and {len(lines) - 10} more")
156
+ output.info("")
157
+ logger.debug("Found %d changed files", len(lines))
158
+ except Exception as e:
159
+ # Git operations are optional for status display
160
+ # Log but don't fail if git command fails
161
+ logger.debug("Failed to get git changes: %s", e)
162
+
163
+ # Git branch
164
+ git_info = item.get("git", {})
165
+ if git_info:
166
+ branch = git_info.get("branch", "N/A")
167
+ commits = len(git_info.get("commits", []))
168
+ output.info(f"Git Branch: {branch}")
169
+ output.info(f"Commits: {commits}")
170
+ output.info("")
171
+ logger.debug("Git info - branch: %s, commits: %d", branch, commits)
172
+
173
+ # Milestone
174
+ milestone_name = item.get("milestone")
175
+ if milestone_name:
176
+ logger.debug("Processing milestone: %s", milestone_name)
177
+ milestones = data.get("milestones", {})
178
+ milestone = milestones.get(milestone_name)
179
+ if milestone:
180
+ # Calculate progress (simplified)
181
+ milestone_items = [
182
+ i for i in data["work_items"].values() if i.get("milestone") == milestone_name
183
+ ]
184
+ total = len(milestone_items)
185
+ completed = sum(
186
+ 1 for i in milestone_items if i["status"] == WorkItemStatus.COMPLETED.value
187
+ )
188
+ percent = int((completed / total) * 100) if total > 0 else 0
189
+
190
+ in_prog = sum(
191
+ 1 for i in milestone_items if i["status"] == WorkItemStatus.IN_PROGRESS.value
192
+ )
193
+ not_started = sum(
194
+ 1 for i in milestone_items if i["status"] == WorkItemStatus.NOT_STARTED.value
195
+ )
196
+
197
+ output.info(f"Milestone: {milestone_name} ({percent}% complete)")
198
+ output.info(f" Related items: {in_prog} in progress, {not_started} not started")
199
+ output.info("")
200
+ logger.info(
201
+ "Milestone %s: %d%% complete (%d/%d items)",
202
+ milestone_name,
203
+ percent,
204
+ completed,
205
+ total,
206
+ )
207
+
208
+ # Next items
209
+ output.info("Next up:")
210
+ items = data["work_items"]
211
+ next_items = [
212
+ (wid, i) for wid, i in items.items() if i["status"] == WorkItemStatus.NOT_STARTED.value
213
+ ][:3]
214
+
215
+ priority_emoji = {
216
+ Priority.CRITICAL.value: "🔴",
217
+ Priority.HIGH.value: "🟠",
218
+ Priority.MEDIUM.value: "🟡",
219
+ Priority.LOW.value: "🟢",
220
+ }
221
+
222
+ logger.debug("Found %d next items to display", len(next_items))
223
+ for wid, i in next_items:
224
+ emoji = priority_emoji.get(i["priority"], "")
225
+ # Check if blocked
226
+ blocked = any(
227
+ items.get(dep_id, {}).get("status") != WorkItemStatus.COMPLETED.value
228
+ for dep_id in i.get("dependencies", [])
229
+ )
230
+ status_str = "(blocked)" if blocked else "(ready)"
231
+ output.info(f" {emoji} {wid} {status_str}")
232
+ output.info("")
233
+
234
+ # Quick actions
235
+ output.info("Quick actions:")
236
+ output.info(" - Validate session: /validate")
237
+ output.info(" - Complete session: /end")
238
+ output.info(f" - View work item: /work-show {work_item_id}")
239
+ output.info("")
240
+
241
+ logger.info("Session status displayed successfully")
242
+ return 0
243
+
244
+
245
+ if __name__ == "__main__":
246
+ exit(get_session_status())
@@ -0,0 +1,452 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Session validation - pre-flight check before completion.
4
+
5
+ Validates all conditions required for successful /end without
6
+ actually making any changes.
7
+
8
+ Updated in Phase 5.7.3 to use spec_parser for checking work item completeness.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ from pathlib import Path
16
+
17
+ from solokit.core.command_runner import CommandRunner
18
+ from solokit.core.constants import GIT_QUICK_TIMEOUT, get_config_file, get_session_dir
19
+ from solokit.core.error_handlers import log_errors
20
+ from solokit.core.exceptions import (
21
+ ErrorCode,
22
+ FileOperationError,
23
+ GitError,
24
+ NotAGitRepoError,
25
+ SessionNotFoundError,
26
+ SpecValidationError,
27
+ ValidationError,
28
+ )
29
+ from solokit.core.exceptions import (
30
+ FileNotFoundError as SolokitFileNotFoundError,
31
+ )
32
+ from solokit.core.logging_config import get_logger
33
+ from solokit.core.output import get_output
34
+ from solokit.core.types import WorkItemType
35
+ from solokit.quality.gates import QualityGates
36
+ from solokit.work_items import spec_parser
37
+
38
+ logger = get_logger(__name__)
39
+ output = get_output()
40
+
41
+
42
+ class SessionValidator:
43
+ """Validate session readiness for completion."""
44
+
45
+ def __init__(self, project_root: Path | None = None):
46
+ """Initialize SessionValidator with project root path."""
47
+ self.project_root = project_root or Path.cwd()
48
+ self.session_dir = get_session_dir(self.project_root)
49
+ self.quality_gates = QualityGates(get_config_file(self.project_root))
50
+ self.runner = CommandRunner(
51
+ default_timeout=GIT_QUICK_TIMEOUT, working_dir=self.project_root
52
+ )
53
+
54
+ @log_errors()
55
+ def check_git_status(self) -> dict:
56
+ """Check git working directory status.
57
+
58
+ Returns:
59
+ dict: Validation result with 'passed', 'message', and optionally 'details'.
60
+ This returns a dict (not raises) because it's validation output,
61
+ not an error condition.
62
+
63
+ Raises:
64
+ NotAGitRepoError: If not in a git repository
65
+ GitError: If git command fails unexpectedly
66
+ """
67
+ # Check if clean or has expected changes
68
+ result = self.runner.run(["git", "status", "--porcelain"])
69
+
70
+ if not result.success:
71
+ raise NotAGitRepoError(path=str(self.project_root))
72
+
73
+ # Check branch
74
+ branch_result = self.runner.run(["git", "branch", "--show-current"])
75
+ if not branch_result.success:
76
+ raise GitError(
77
+ message="Failed to get current branch",
78
+ code=ErrorCode.GIT_COMMAND_FAILED,
79
+ context={"stderr": branch_result.stderr},
80
+ )
81
+
82
+ current_branch = branch_result.stdout.strip()
83
+
84
+ # Get status lines
85
+ status_lines = [line for line in result.stdout.split("\n") if line.strip()]
86
+
87
+ # Check for tracking file changes
88
+ tracking_changes = [line for line in status_lines if ".session/tracking/" in line]
89
+
90
+ if tracking_changes:
91
+ return {
92
+ "passed": False,
93
+ "message": f"Uncommitted tracking files: {len(tracking_changes)} files",
94
+ }
95
+
96
+ return {
97
+ "passed": True,
98
+ "message": f"Working directory ready, branch: {current_branch}",
99
+ "details": {"branch": current_branch, "changes": len(status_lines)},
100
+ }
101
+
102
+ def preview_quality_gates(self, auto_fix: bool = False) -> dict:
103
+ """Preview quality gate results.
104
+
105
+ Args:
106
+ auto_fix: If True, automatically fix linting and formatting issues.
107
+ When True, skips tests since they cannot be auto-fixed.
108
+ """
109
+ gates = {}
110
+
111
+ # Skip tests when auto_fix=True since they cannot be automatically fixed
112
+ # Use QualityGates to run tests (respects config)
113
+ test_config = self.quality_gates.config.test_execution
114
+ if test_config.enabled and not auto_fix:
115
+ test_passed, test_results = self.quality_gates.run_tests()
116
+ # Check if tests are required
117
+ if test_config.required:
118
+ gates["tests"] = {
119
+ "passed": test_passed,
120
+ "message": test_results.get(
121
+ "reason", "Tests pass" if test_passed else "Tests fail"
122
+ ),
123
+ }
124
+ else:
125
+ # If not required, always mark as passed but include status info
126
+ gates["tests"] = {
127
+ "passed": True,
128
+ "message": f"Tests {test_results.get('status', 'unknown')} (not required)",
129
+ }
130
+
131
+ # Use QualityGates for linting (respects config)
132
+ lint_config = self.quality_gates.config.linting
133
+ if lint_config.enabled:
134
+ lint_passed, lint_results = self.quality_gates.run_linting(auto_fix=auto_fix)
135
+ if lint_config.required:
136
+ message = "No linting issues" if lint_passed else "Linting issues found"
137
+ if auto_fix and lint_results.get("fixed"):
138
+ message = "Linting issues auto-fixed"
139
+ gates["linting"] = {
140
+ "passed": lint_passed,
141
+ "message": message,
142
+ }
143
+ else:
144
+ gates["linting"] = {
145
+ "passed": True,
146
+ "message": f"Linting {lint_results.get('status', 'unknown')} (not required)",
147
+ }
148
+
149
+ # Use QualityGates for formatting (respects config)
150
+ fmt_config = self.quality_gates.config.formatting
151
+ if fmt_config.enabled:
152
+ fmt_passed, fmt_results = self.quality_gates.run_formatting(auto_fix=auto_fix)
153
+ if fmt_config.required:
154
+ message = "All files properly formatted" if fmt_passed else "Files need formatting"
155
+ if auto_fix and fmt_results.get("formatted"):
156
+ message = "Files auto-formatted"
157
+ gates["formatting"] = {
158
+ "passed": fmt_passed,
159
+ "message": message,
160
+ }
161
+ else:
162
+ gates["formatting"] = {
163
+ "passed": True,
164
+ "message": f"Formatting {fmt_results.get('status', 'unknown')} (not required)",
165
+ }
166
+
167
+ all_passed = all(g["passed"] for g in gates.values())
168
+
169
+ return {
170
+ "passed": all_passed,
171
+ "message": "All quality gates pass" if all_passed else "Some quality gates fail",
172
+ "gates": gates,
173
+ }
174
+
175
+ @log_errors()
176
+ def validate_work_item_criteria(self) -> dict:
177
+ """
178
+ Check if work item spec is complete and valid.
179
+
180
+ Updated in Phase 5.7.3 to check spec file completeness instead of
181
+ deprecated implementation_paths and test_paths fields.
182
+
183
+ Returns:
184
+ dict: Validation result with 'passed', 'message', and optionally
185
+ 'missing_sections'. Returns dict (not raises) for validation
186
+ results that should be displayed to user.
187
+
188
+ Raises:
189
+ SessionNotFoundError: If no active session exists
190
+ ValidationError: If no current work item is set
191
+ FileNotFoundError: If spec file is missing
192
+ FileOperationError: If file operations fail
193
+ SpecValidationError: If spec file parsing fails
194
+ """
195
+ # Load current work item
196
+ status_file = self.session_dir / "tracking" / "status_update.json"
197
+ if not status_file.exists():
198
+ raise SessionNotFoundError()
199
+
200
+ try:
201
+ with open(status_file) as f:
202
+ status = json.load(f)
203
+ except (OSError, json.JSONDecodeError) as e:
204
+ raise FileOperationError(
205
+ operation="read",
206
+ file_path=str(status_file),
207
+ details="Failed to read or parse status file",
208
+ cause=e,
209
+ )
210
+
211
+ if not status.get("current_work_item"):
212
+ raise ValidationError(
213
+ message="No current work item is set in session",
214
+ code=ErrorCode.MISSING_REQUIRED_FIELD,
215
+ context={"status_file": str(status_file)},
216
+ remediation="Start a work item with 'sk start <work_item_id>'",
217
+ )
218
+
219
+ # Load work items
220
+ work_items_file = self.session_dir / "tracking" / "work_items.json"
221
+ try:
222
+ with open(work_items_file) as f:
223
+ work_items_data = json.load(f)
224
+ except FileNotFoundError as e:
225
+ raise SolokitFileNotFoundError(
226
+ file_path=str(work_items_file),
227
+ file_type="work items",
228
+ ) from e
229
+ except (OSError, json.JSONDecodeError) as e:
230
+ raise FileOperationError(
231
+ operation="read",
232
+ file_path=str(work_items_file),
233
+ details="Failed to read or parse work items file",
234
+ cause=e,
235
+ )
236
+
237
+ work_item = work_items_data["work_items"][status["current_work_item"]]
238
+ work_id = work_item.get("id")
239
+
240
+ # Check spec file exists and is valid
241
+ # Use spec_file from work item configuration (supports custom filenames)
242
+ spec_file_path = work_item.get("spec_file", f".session/specs/{work_id}.md")
243
+ spec_file = self.project_root / spec_file_path
244
+ if not spec_file.exists():
245
+ raise SolokitFileNotFoundError(
246
+ file_path=str(spec_file),
247
+ file_type="spec",
248
+ )
249
+
250
+ # Parse spec file - pass full work_item dict to support custom spec filenames
251
+ try:
252
+ parsed_spec = spec_parser.parse_spec_file(work_item)
253
+ except Exception as e:
254
+ raise SpecValidationError(
255
+ work_item_id=work_id,
256
+ errors=[str(e)],
257
+ remediation=f"Check spec file format at {spec_file}",
258
+ )
259
+
260
+ # Check that spec has required sections based on work item type
261
+ work_type = work_item.get("type")
262
+ missing_sections = []
263
+
264
+ # Common sections for all types
265
+ if (
266
+ not parsed_spec.get("acceptance_criteria")
267
+ or len(parsed_spec.get("acceptance_criteria", [])) < 3
268
+ ):
269
+ missing_sections.append("Acceptance Criteria (at least 3 items)")
270
+
271
+ # Type-specific sections
272
+ if work_type == WorkItemType.FEATURE.value:
273
+ if not parsed_spec.get("overview"):
274
+ missing_sections.append("Overview")
275
+ if not parsed_spec.get("implementation_details"):
276
+ missing_sections.append("Implementation Details")
277
+
278
+ elif work_type == WorkItemType.BUG.value:
279
+ if not parsed_spec.get("description"):
280
+ missing_sections.append("Description")
281
+ if not parsed_spec.get("fix_approach"):
282
+ missing_sections.append("Fix Approach")
283
+
284
+ elif work_type == WorkItemType.INTEGRATION_TEST.value:
285
+ if not parsed_spec.get("scope"):
286
+ missing_sections.append("Scope")
287
+ if (
288
+ not parsed_spec.get("test_scenarios")
289
+ or len(parsed_spec.get("test_scenarios", [])) == 0
290
+ ):
291
+ missing_sections.append("Test Scenarios (at least 1)")
292
+
293
+ elif work_type == WorkItemType.DEPLOYMENT.value:
294
+ if not parsed_spec.get("deployment_scope"):
295
+ missing_sections.append("Deployment Scope")
296
+ if not parsed_spec.get("deployment_procedure"):
297
+ missing_sections.append("Deployment Procedure")
298
+
299
+ if missing_sections:
300
+ return {
301
+ "passed": False,
302
+ "message": "Spec file incomplete",
303
+ "missing_sections": missing_sections,
304
+ }
305
+
306
+ return {"passed": True, "message": "Work item spec is complete"}
307
+
308
+ def check_tracking_updates(self) -> dict:
309
+ """Preview tracking file updates."""
310
+ changes = {
311
+ "stack": self._check_stack_changes(),
312
+ "tree": self._check_tree_changes(),
313
+ }
314
+
315
+ return {
316
+ "passed": True, # Tracking updates don't fail validation
317
+ "message": "Tracking updates detected"
318
+ if any(c["has_changes"] for c in changes.values())
319
+ else "No tracking updates",
320
+ "changes": changes,
321
+ }
322
+
323
+ def _check_stack_changes(self) -> dict:
324
+ """Check if stack has changed."""
325
+ # This would run stack detection logic
326
+ # For now, simplified
327
+ return {"has_changes": False, "message": "No stack changes"}
328
+
329
+ def _check_tree_changes(self) -> dict:
330
+ """Check if tree structure has changed."""
331
+ # This would run tree detection logic
332
+ return {"has_changes": False, "message": "No structural changes"}
333
+
334
+ @log_errors()
335
+ def validate(self, auto_fix: bool = False) -> dict:
336
+ """Run all validation checks.
337
+
338
+ Args:
339
+ auto_fix: If True, automatically fix linting and formatting issues
340
+
341
+ Returns:
342
+ dict: Validation results with 'ready' boolean and 'checks' dict
343
+
344
+ Raises:
345
+ SolokitError: Any Solokit exception (GitError, ValidationError, FileOperationError, etc.)
346
+ that occurs during validation will be raised to the caller
347
+ """
348
+ logger.info("Starting session validation (auto_fix=%s)", auto_fix)
349
+ output.info("Running session validation...\n")
350
+
351
+ # Run all checks - exceptions will propagate to caller
352
+ checks = {
353
+ "git_status": self.check_git_status(),
354
+ "quality_gates": self.preview_quality_gates(auto_fix=auto_fix),
355
+ "work_item_criteria": self.validate_work_item_criteria(),
356
+ "tracking_updates": self.check_tracking_updates(),
357
+ }
358
+
359
+ logger.debug("Validation checks completed: %d checks run", len(checks))
360
+
361
+ # Display results
362
+ for check_name, result in checks.items():
363
+ status = "✓" if result["passed"] else "✗"
364
+ output.info(f"{status} {check_name.replace('_', ' ').title()}: {result['message']}")
365
+ logger.debug("Check %s: passed=%s", check_name, result["passed"])
366
+
367
+ # Show details for failed checks
368
+ if not result["passed"] and check_name == "quality_gates":
369
+ for gate_name, gate_result in result["gates"].items():
370
+ if not gate_result["passed"]:
371
+ output.info(f" ✗ {gate_name}: {gate_result['message']}")
372
+ logger.warning(
373
+ "Quality gate failed: %s - %s", gate_name, gate_result["message"]
374
+ )
375
+ if "issues" in gate_result:
376
+ for issue in gate_result["issues"][:5]:
377
+ output.info(f" - {issue}")
378
+
379
+ # Show missing paths for work item criteria
380
+ if not result["passed"] and check_name == "work_item_criteria":
381
+ if "missing_impl" in result and result["missing_impl"]:
382
+ output.info(" Missing implementation paths:")
383
+ for path in result["missing_impl"]:
384
+ output.info(f" - {path}")
385
+ logger.warning("Missing implementation paths: %d", len(result["missing_impl"]))
386
+ if "missing_tests" in result and result["missing_tests"]:
387
+ output.info(" Missing test paths:")
388
+ for path in result["missing_tests"]:
389
+ output.info(f" - {path}")
390
+ logger.warning("Missing test paths: %d", len(result["missing_tests"]))
391
+
392
+ all_passed = all(c["passed"] for c in checks.values())
393
+
394
+ output.info("")
395
+ if all_passed:
396
+ output.success("Session ready to complete!")
397
+ output.info("Run /end to complete the session.")
398
+ logger.info("Session validation passed - ready to complete")
399
+ else:
400
+ output.warning("Session not ready to complete")
401
+ output.info("\nFix the issues above before running /end")
402
+ logger.warning("Session validation failed - not ready to complete")
403
+
404
+ return {"ready": all_passed, "checks": checks}
405
+
406
+
407
+ def main() -> int:
408
+ """CLI entry point.
409
+
410
+ Returns:
411
+ int: Exit code (0 for success/ready, 1 for validation failures, 2+ for errors)
412
+
413
+ Note:
414
+ SDDErrors are caught and formatted for user display.
415
+ The exit code corresponds to the error category.
416
+ """
417
+ parser = argparse.ArgumentParser(description="Validate session readiness for completion")
418
+ parser.add_argument(
419
+ "--fix",
420
+ action="store_true",
421
+ help="Automatically fix linting and formatting issues",
422
+ )
423
+ args = parser.parse_args()
424
+
425
+ try:
426
+ validator = SessionValidator()
427
+ result = validator.validate(auto_fix=args.fix)
428
+ return 0 if result["ready"] else 1
429
+ except (
430
+ SessionNotFoundError,
431
+ ValidationError,
432
+ SolokitFileNotFoundError,
433
+ FileOperationError,
434
+ SpecValidationError,
435
+ NotAGitRepoError,
436
+ GitError,
437
+ ) as e:
438
+ # Handle Solokit exceptions gracefully
439
+ output.error(f"Error: {e.message}")
440
+ if e.remediation:
441
+ output.info(f"Remediation: {e.remediation}")
442
+ logger.error("Validation error: %s", e.message, exc_info=True)
443
+ return e.exit_code
444
+ except Exception as e:
445
+ # Unexpected error
446
+ output.error(f"Unexpected error during validation: {e}")
447
+ logger.error("Unexpected validation error", exc_info=True)
448
+ return 1
449
+
450
+
451
+ if __name__ == "__main__":
452
+ exit(main())