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.
- solokit/__init__.py +10 -0
- solokit/__version__.py +3 -0
- solokit/cli.py +374 -0
- solokit/core/__init__.py +1 -0
- solokit/core/cache.py +102 -0
- solokit/core/command_runner.py +278 -0
- solokit/core/config.py +453 -0
- solokit/core/config_validator.py +204 -0
- solokit/core/constants.py +291 -0
- solokit/core/error_formatter.py +279 -0
- solokit/core/error_handlers.py +346 -0
- solokit/core/exceptions.py +1567 -0
- solokit/core/file_ops.py +309 -0
- solokit/core/logging_config.py +166 -0
- solokit/core/output.py +99 -0
- solokit/core/performance.py +57 -0
- solokit/core/protocols.py +141 -0
- solokit/core/types.py +312 -0
- solokit/deployment/__init__.py +1 -0
- solokit/deployment/executor.py +411 -0
- solokit/git/__init__.py +1 -0
- solokit/git/integration.py +619 -0
- solokit/init/__init__.py +41 -0
- solokit/init/claude_commands_installer.py +87 -0
- solokit/init/dependency_installer.py +313 -0
- solokit/init/docs_structure.py +90 -0
- solokit/init/env_generator.py +160 -0
- solokit/init/environment_validator.py +334 -0
- solokit/init/git_hooks_installer.py +71 -0
- solokit/init/git_setup.py +188 -0
- solokit/init/gitignore_updater.py +195 -0
- solokit/init/initial_commit.py +145 -0
- solokit/init/initial_scans.py +109 -0
- solokit/init/orchestrator.py +246 -0
- solokit/init/readme_generator.py +207 -0
- solokit/init/session_structure.py +239 -0
- solokit/init/template_installer.py +424 -0
- solokit/learning/__init__.py +1 -0
- solokit/learning/archiver.py +115 -0
- solokit/learning/categorizer.py +126 -0
- solokit/learning/curator.py +428 -0
- solokit/learning/extractor.py +352 -0
- solokit/learning/reporter.py +351 -0
- solokit/learning/repository.py +254 -0
- solokit/learning/similarity.py +342 -0
- solokit/learning/validator.py +144 -0
- solokit/project/__init__.py +1 -0
- solokit/project/init.py +1162 -0
- solokit/project/stack.py +436 -0
- solokit/project/sync_plugin.py +438 -0
- solokit/project/tree.py +375 -0
- solokit/quality/__init__.py +1 -0
- solokit/quality/api_validator.py +424 -0
- solokit/quality/checkers/__init__.py +25 -0
- solokit/quality/checkers/base.py +114 -0
- solokit/quality/checkers/context7.py +221 -0
- solokit/quality/checkers/custom.py +162 -0
- solokit/quality/checkers/deployment.py +323 -0
- solokit/quality/checkers/documentation.py +179 -0
- solokit/quality/checkers/formatting.py +161 -0
- solokit/quality/checkers/integration.py +394 -0
- solokit/quality/checkers/linting.py +159 -0
- solokit/quality/checkers/security.py +261 -0
- solokit/quality/checkers/spec_completeness.py +127 -0
- solokit/quality/checkers/tests.py +184 -0
- solokit/quality/env_validator.py +306 -0
- solokit/quality/gates.py +655 -0
- solokit/quality/reporters/__init__.py +10 -0
- solokit/quality/reporters/base.py +25 -0
- solokit/quality/reporters/console.py +98 -0
- solokit/quality/reporters/json_reporter.py +34 -0
- solokit/quality/results.py +98 -0
- solokit/session/__init__.py +1 -0
- solokit/session/briefing/__init__.py +245 -0
- solokit/session/briefing/documentation_loader.py +53 -0
- solokit/session/briefing/formatter.py +476 -0
- solokit/session/briefing/git_context.py +282 -0
- solokit/session/briefing/learning_loader.py +212 -0
- solokit/session/briefing/milestone_builder.py +78 -0
- solokit/session/briefing/orchestrator.py +137 -0
- solokit/session/briefing/stack_detector.py +51 -0
- solokit/session/briefing/tree_generator.py +52 -0
- solokit/session/briefing/work_item_loader.py +209 -0
- solokit/session/briefing.py +353 -0
- solokit/session/complete.py +1188 -0
- solokit/session/status.py +246 -0
- solokit/session/validate.py +452 -0
- solokit/templates/.claude/commands/end.md +109 -0
- solokit/templates/.claude/commands/init.md +159 -0
- solokit/templates/.claude/commands/learn-curate.md +88 -0
- solokit/templates/.claude/commands/learn-search.md +62 -0
- solokit/templates/.claude/commands/learn-show.md +69 -0
- solokit/templates/.claude/commands/learn.md +136 -0
- solokit/templates/.claude/commands/start.md +114 -0
- solokit/templates/.claude/commands/status.md +22 -0
- solokit/templates/.claude/commands/validate.md +27 -0
- solokit/templates/.claude/commands/work-delete.md +119 -0
- solokit/templates/.claude/commands/work-graph.md +139 -0
- solokit/templates/.claude/commands/work-list.md +26 -0
- solokit/templates/.claude/commands/work-new.md +114 -0
- solokit/templates/.claude/commands/work-next.md +25 -0
- solokit/templates/.claude/commands/work-show.md +24 -0
- solokit/templates/.claude/commands/work-update.md +141 -0
- solokit/templates/CHANGELOG.md +17 -0
- solokit/templates/WORK_ITEM_TYPES.md +141 -0
- solokit/templates/__init__.py +1 -0
- solokit/templates/bug_spec.md +217 -0
- solokit/templates/config.schema.json +150 -0
- solokit/templates/dashboard_refine/base/.gitignore +36 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/layout.tsx +22 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/page.tsx +68 -0
- solokit/templates/dashboard_refine/base/app/(dashboard)/users/page.tsx +77 -0
- solokit/templates/dashboard_refine/base/app/globals.css +60 -0
- solokit/templates/dashboard_refine/base/app/layout.tsx +23 -0
- solokit/templates/dashboard_refine/base/app/page.tsx +9 -0
- solokit/templates/dashboard_refine/base/components/client-refine-wrapper.tsx +21 -0
- solokit/templates/dashboard_refine/base/components/layout/header.tsx +44 -0
- solokit/templates/dashboard_refine/base/components/layout/sidebar.tsx +82 -0
- solokit/templates/dashboard_refine/base/components/ui/button.tsx +53 -0
- solokit/templates/dashboard_refine/base/components/ui/card.tsx +78 -0
- solokit/templates/dashboard_refine/base/components/ui/table.tsx +116 -0
- solokit/templates/dashboard_refine/base/components.json +16 -0
- solokit/templates/dashboard_refine/base/lib/refine.tsx +65 -0
- solokit/templates/dashboard_refine/base/lib/utils.ts +13 -0
- solokit/templates/dashboard_refine/base/next.config.ts +10 -0
- solokit/templates/dashboard_refine/base/package.json.template +40 -0
- solokit/templates/dashboard_refine/base/postcss.config.mjs +8 -0
- solokit/templates/dashboard_refine/base/providers/refine-provider.tsx +26 -0
- solokit/templates/dashboard_refine/base/tailwind.config.ts +57 -0
- solokit/templates/dashboard_refine/base/tsconfig.json +27 -0
- solokit/templates/dashboard_refine/docker/Dockerfile +57 -0
- solokit/templates/dashboard_refine/docker/docker-compose.prod.yml +31 -0
- solokit/templates/dashboard_refine/docker/docker-compose.yml +21 -0
- solokit/templates/dashboard_refine/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/dashboard_refine/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/dashboard_refine/tier-1-essential/package.json.tier1.template +57 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/setup.ts +26 -0
- solokit/templates/dashboard_refine/tier-1-essential/tests/unit/example.test.tsx +73 -0
- solokit/templates/dashboard_refine/tier-2-standard/package.json.tier2.template +62 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/eslint.config.mjs +22 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/package.json.tier3.template +79 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/stryker.conf.json +38 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/dashboard.spec.ts +88 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/e2e/user-management.spec.ts +102 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/tests/integration/dashboard.test.tsx +90 -0
- solokit/templates/dashboard_refine/tier-3-comprehensive/type-coverage.json +16 -0
- solokit/templates/dashboard_refine/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/dashboard_refine/tier-4-production/k6/dashboard-load-test.js +70 -0
- solokit/templates/dashboard_refine/tier-4-production/next.config.ts +46 -0
- solokit/templates/dashboard_refine/tier-4-production/package.json.tier4.template +89 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/dashboard_refine/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/deployment_spec.md +500 -0
- solokit/templates/feature_spec.md +248 -0
- solokit/templates/fullstack_nextjs/base/.gitignore +36 -0
- solokit/templates/fullstack_nextjs/base/app/api/example/route.ts +65 -0
- solokit/templates/fullstack_nextjs/base/app/globals.css +27 -0
- solokit/templates/fullstack_nextjs/base/app/layout.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/app/page.tsx +32 -0
- solokit/templates/fullstack_nextjs/base/components/example-component.tsx +20 -0
- solokit/templates/fullstack_nextjs/base/lib/prisma.ts +17 -0
- solokit/templates/fullstack_nextjs/base/lib/utils.ts +13 -0
- solokit/templates/fullstack_nextjs/base/lib/validations.ts +20 -0
- solokit/templates/fullstack_nextjs/base/next.config.ts +7 -0
- solokit/templates/fullstack_nextjs/base/package.json.template +32 -0
- solokit/templates/fullstack_nextjs/base/postcss.config.mjs +8 -0
- solokit/templates/fullstack_nextjs/base/prisma/schema.prisma +21 -0
- solokit/templates/fullstack_nextjs/base/tailwind.config.ts +19 -0
- solokit/templates/fullstack_nextjs/base/tsconfig.json +27 -0
- solokit/templates/fullstack_nextjs/docker/Dockerfile +60 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.prod.yml +57 -0
- solokit/templates/fullstack_nextjs/docker/docker-compose.yml +47 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/package.json.tier1.template +48 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/api/example.test.ts +88 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/fullstack_nextjs/tier-1-essential/tests/unit/example.test.tsx +22 -0
- solokit/templates/fullstack_nextjs/tier-2-standard/package.json.tier2.template +52 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/package.json.tier3.template +68 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/stryker.conf.json +33 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/e2e/flow.spec.ts +59 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/tests/integration/api.test.ts +165 -0
- solokit/templates/fullstack_nextjs/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/fullstack_nextjs/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/fullstack_nextjs/tier-4-production/k6/load-test.js +45 -0
- solokit/templates/fullstack_nextjs/tier-4-production/next.config.ts +46 -0
- solokit/templates/fullstack_nextjs/tier-4-production/package.json.tier4.template +77 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/fullstack_nextjs/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/git-hooks/prepare-commit-msg +24 -0
- solokit/templates/integration_test_spec.md +363 -0
- solokit/templates/learnings.json +15 -0
- solokit/templates/ml_ai_fastapi/base/.gitignore +104 -0
- solokit/templates/ml_ai_fastapi/base/alembic/env.py +96 -0
- solokit/templates/ml_ai_fastapi/base/alembic.ini +114 -0
- solokit/templates/ml_ai_fastapi/base/pyproject.toml.template +91 -0
- solokit/templates/ml_ai_fastapi/base/requirements.txt.template +28 -0
- solokit/templates/ml_ai_fastapi/base/src/__init__.py +5 -0
- solokit/templates/ml_ai_fastapi/base/src/api/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/dependencies.py +20 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/example.py +134 -0
- solokit/templates/ml_ai_fastapi/base/src/api/routes/health.py +66 -0
- solokit/templates/ml_ai_fastapi/base/src/core/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/core/config.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/core/database.py +50 -0
- solokit/templates/ml_ai_fastapi/base/src/main.py +64 -0
- solokit/templates/ml_ai_fastapi/base/src/models/__init__.py +7 -0
- solokit/templates/ml_ai_fastapi/base/src/models/example.py +61 -0
- solokit/templates/ml_ai_fastapi/base/src/services/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/base/src/services/example.py +115 -0
- solokit/templates/ml_ai_fastapi/docker/Dockerfile +59 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.prod.yml +112 -0
- solokit/templates/ml_ai_fastapi/docker/docker-compose.yml +77 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyproject.toml.tier1.template +112 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pyrightconfig.json +41 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/pytest.ini +69 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/requirements-dev.txt +17 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/ruff.toml +81 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/conftest.py +72 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/test_main.py +49 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-1-essential/tests/unit/test_example.py +113 -0
- solokit/templates/ml_ai_fastapi/tier-2-standard/pyproject.toml.tier2.template +130 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/locustfile.py +99 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/mutmut_config.py +53 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/pyproject.toml.tier3.template +150 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/conftest.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-3-comprehensive/tests/integration/test_api.py +131 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/pyproject.toml.tier4.template +162 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/requirements-prod.txt +25 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/api/routes/metrics.py +19 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/logging.py +74 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/monitoring.py +68 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/core/sentry.py +66 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/__init__.py +3 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/logging.py +79 -0
- solokit/templates/ml_ai_fastapi/tier-4-production/src/middleware/tracing.py +60 -0
- solokit/templates/refactor_spec.md +287 -0
- solokit/templates/saas_t3/base/.gitignore +36 -0
- solokit/templates/saas_t3/base/app/api/trpc/[trpc]/route.ts +33 -0
- solokit/templates/saas_t3/base/app/globals.css +27 -0
- solokit/templates/saas_t3/base/app/layout.tsx +23 -0
- solokit/templates/saas_t3/base/app/page.tsx +31 -0
- solokit/templates/saas_t3/base/lib/api.tsx +77 -0
- solokit/templates/saas_t3/base/lib/utils.ts +13 -0
- solokit/templates/saas_t3/base/next.config.ts +7 -0
- solokit/templates/saas_t3/base/package.json.template +38 -0
- solokit/templates/saas_t3/base/postcss.config.mjs +8 -0
- solokit/templates/saas_t3/base/prisma/schema.prisma +20 -0
- solokit/templates/saas_t3/base/server/api/root.ts +19 -0
- solokit/templates/saas_t3/base/server/api/routers/example.ts +28 -0
- solokit/templates/saas_t3/base/server/api/trpc.ts +52 -0
- solokit/templates/saas_t3/base/server/db.ts +17 -0
- solokit/templates/saas_t3/base/tailwind.config.ts +19 -0
- solokit/templates/saas_t3/base/tsconfig.json +27 -0
- solokit/templates/saas_t3/docker/Dockerfile +60 -0
- solokit/templates/saas_t3/docker/docker-compose.prod.yml +59 -0
- solokit/templates/saas_t3/docker/docker-compose.yml +49 -0
- solokit/templates/saas_t3/tier-1-essential/.eslintrc.json +7 -0
- solokit/templates/saas_t3/tier-1-essential/jest.config.ts +17 -0
- solokit/templates/saas_t3/tier-1-essential/jest.setup.ts +1 -0
- solokit/templates/saas_t3/tier-1-essential/package.json.tier1.template +54 -0
- solokit/templates/saas_t3/tier-1-essential/tests/setup.ts +22 -0
- solokit/templates/saas_t3/tier-1-essential/tests/unit/example.test.tsx +24 -0
- solokit/templates/saas_t3/tier-2-standard/package.json.tier2.template +58 -0
- solokit/templates/saas_t3/tier-3-comprehensive/eslint.config.mjs +39 -0
- solokit/templates/saas_t3/tier-3-comprehensive/package.json.tier3.template +74 -0
- solokit/templates/saas_t3/tier-3-comprehensive/playwright.config.ts +66 -0
- solokit/templates/saas_t3/tier-3-comprehensive/stryker.conf.json +34 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/e2e/home.spec.ts +41 -0
- solokit/templates/saas_t3/tier-3-comprehensive/tests/integration/api.test.ts +44 -0
- solokit/templates/saas_t3/tier-3-comprehensive/type-coverage.json +12 -0
- solokit/templates/saas_t3/tier-4-production/instrumentation.ts +9 -0
- solokit/templates/saas_t3/tier-4-production/k6/load-test.js +51 -0
- solokit/templates/saas_t3/tier-4-production/next.config.ts +46 -0
- solokit/templates/saas_t3/tier-4-production/package.json.tier4.template +83 -0
- solokit/templates/saas_t3/tier-4-production/sentry.client.config.ts +26 -0
- solokit/templates/saas_t3/tier-4-production/sentry.edge.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/sentry.server.config.ts +11 -0
- solokit/templates/saas_t3/tier-4-production/vercel.json +37 -0
- solokit/templates/security_spec.md +287 -0
- solokit/templates/stack-versions.yaml +617 -0
- solokit/templates/status_update.json +6 -0
- solokit/templates/template-registry.json +257 -0
- solokit/templates/work_items.json +11 -0
- solokit/testing/__init__.py +1 -0
- solokit/testing/integration_runner.py +550 -0
- solokit/testing/performance.py +637 -0
- solokit/visualization/__init__.py +1 -0
- solokit/visualization/dependency_graph.py +788 -0
- solokit/work_items/__init__.py +1 -0
- solokit/work_items/creator.py +217 -0
- solokit/work_items/delete.py +264 -0
- solokit/work_items/get_dependencies.py +185 -0
- solokit/work_items/get_dependents.py +113 -0
- solokit/work_items/get_metadata.py +121 -0
- solokit/work_items/get_next_recommendations.py +133 -0
- solokit/work_items/manager.py +235 -0
- solokit/work_items/milestones.py +137 -0
- solokit/work_items/query.py +376 -0
- solokit/work_items/repository.py +267 -0
- solokit/work_items/scheduler.py +184 -0
- solokit/work_items/spec_parser.py +838 -0
- solokit/work_items/spec_validator.py +493 -0
- solokit/work_items/updater.py +157 -0
- solokit/work_items/validator.py +205 -0
- solokit-0.1.1.dist-info/METADATA +640 -0
- solokit-0.1.1.dist-info/RECORD +323 -0
- solokit-0.1.1.dist-info/WHEEL +5 -0
- solokit-0.1.1.dist-info/entry_points.txt +2 -0
- solokit-0.1.1.dist-info/licenses/LICENSE +21 -0
- 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())
|