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 @@
|
|
|
1
|
+
"""Work item management including specs, validation, and CRUD operations."""
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Work Item Creator - Interactive and non-interactive work item creation.
|
|
4
|
+
|
|
5
|
+
Handles user prompts, ID generation, and spec file creation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import re
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TYPE_CHECKING
|
|
13
|
+
|
|
14
|
+
from solokit.core.error_handlers import log_errors
|
|
15
|
+
from solokit.core.exceptions import (
|
|
16
|
+
ErrorCode,
|
|
17
|
+
ValidationError,
|
|
18
|
+
WorkItemAlreadyExistsError,
|
|
19
|
+
)
|
|
20
|
+
from solokit.core.logging_config import get_logger
|
|
21
|
+
from solokit.core.types import WorkItemStatus, WorkItemType
|
|
22
|
+
|
|
23
|
+
if TYPE_CHECKING:
|
|
24
|
+
from .repository import WorkItemRepository
|
|
25
|
+
from solokit.core.output import get_output
|
|
26
|
+
|
|
27
|
+
logger = get_logger(__name__)
|
|
28
|
+
output = get_output()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class WorkItemCreator:
|
|
32
|
+
"""Handles work item creation with interactive and non-interactive modes"""
|
|
33
|
+
|
|
34
|
+
WORK_ITEM_TYPES = WorkItemType.values()
|
|
35
|
+
PRIORITIES = ["critical", "high", "medium", "low"]
|
|
36
|
+
|
|
37
|
+
def __init__(self, repository: WorkItemRepository):
|
|
38
|
+
"""Initialize creator with repository
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
repository: WorkItemRepository instance for data access
|
|
42
|
+
"""
|
|
43
|
+
self.repository = repository
|
|
44
|
+
self.project_root = repository.session_dir.parent
|
|
45
|
+
self.specs_dir = repository.session_dir / "specs"
|
|
46
|
+
self.templates_dir = Path(__file__).parent.parent / "templates"
|
|
47
|
+
|
|
48
|
+
@log_errors()
|
|
49
|
+
def create_from_args(
|
|
50
|
+
self, work_type: str, title: str, priority: str = "high", dependencies: str = ""
|
|
51
|
+
) -> str:
|
|
52
|
+
"""Create work item from command-line arguments (non-interactive)
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
work_type: Type of work item (feature, bug, refactor, etc.)
|
|
56
|
+
title: Title of the work item
|
|
57
|
+
priority: Priority level (critical, high, medium, low)
|
|
58
|
+
dependencies: Comma-separated dependency IDs
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
str: The created work item ID
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
ValidationError: If work type is invalid
|
|
65
|
+
WorkItemAlreadyExistsError: If work item with generated ID already exists
|
|
66
|
+
"""
|
|
67
|
+
logger.info("Creating work item from args: type=%s, title=%s", work_type, title)
|
|
68
|
+
|
|
69
|
+
# Validate work type
|
|
70
|
+
if work_type not in self.WORK_ITEM_TYPES:
|
|
71
|
+
logger.error("Invalid work item type: %s", work_type)
|
|
72
|
+
raise ValidationError(
|
|
73
|
+
message=f"Invalid work item type '{work_type}'",
|
|
74
|
+
code=ErrorCode.INVALID_WORK_ITEM_TYPE,
|
|
75
|
+
context={"work_type": work_type, "valid_types": self.WORK_ITEM_TYPES},
|
|
76
|
+
remediation=f"Valid types: {', '.join(self.WORK_ITEM_TYPES)}",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Validate priority
|
|
80
|
+
if priority not in self.PRIORITIES:
|
|
81
|
+
logger.warning("Invalid priority '%s', using 'high'", priority)
|
|
82
|
+
logger.warning("Invalid priority '%s', using 'high'", priority)
|
|
83
|
+
priority = "high"
|
|
84
|
+
|
|
85
|
+
# Parse dependencies
|
|
86
|
+
dep_list = []
|
|
87
|
+
if dependencies:
|
|
88
|
+
dep_list = [d.strip() for d in dependencies.split(",") if d.strip()]
|
|
89
|
+
logger.debug("Parsed dependencies: %s", dep_list)
|
|
90
|
+
# Validate dependencies exist
|
|
91
|
+
for dep_id in dep_list:
|
|
92
|
+
if not self.repository.work_item_exists(dep_id):
|
|
93
|
+
logger.warning("Dependency '%s' does not exist", dep_id)
|
|
94
|
+
logger.warning("Warning: Dependency '%s' does not exist", dep_id)
|
|
95
|
+
|
|
96
|
+
# Generate ID
|
|
97
|
+
work_id = self._generate_id(work_type, title)
|
|
98
|
+
logger.debug("Generated work item ID: %s", work_id)
|
|
99
|
+
|
|
100
|
+
# Check for duplicates
|
|
101
|
+
if self.repository.work_item_exists(work_id):
|
|
102
|
+
logger.error("Work item %s already exists", work_id)
|
|
103
|
+
raise WorkItemAlreadyExistsError(work_id)
|
|
104
|
+
|
|
105
|
+
# Create specification file
|
|
106
|
+
spec_file = self._create_spec_file(work_id, work_type, title)
|
|
107
|
+
if not spec_file:
|
|
108
|
+
logger.warning("Could not create specification file for %s", work_id)
|
|
109
|
+
logger.warning("Warning: Could not create specification file")
|
|
110
|
+
|
|
111
|
+
# Add to work_items.json
|
|
112
|
+
self.repository.add_work_item(work_id, work_type, title, priority, dep_list, spec_file)
|
|
113
|
+
logger.info("Work item created: %s (type=%s, priority=%s)", work_id, work_type, priority)
|
|
114
|
+
|
|
115
|
+
# Confirm
|
|
116
|
+
self._print_creation_confirmation(work_id, work_type, priority, dep_list, spec_file)
|
|
117
|
+
|
|
118
|
+
return work_id
|
|
119
|
+
|
|
120
|
+
def _generate_id(self, work_type: str, title: str) -> str:
|
|
121
|
+
"""Generate work item ID from type and title
|
|
122
|
+
|
|
123
|
+
Args:
|
|
124
|
+
work_type: Type of work item
|
|
125
|
+
title: Work item title
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
str: Generated work item ID
|
|
129
|
+
"""
|
|
130
|
+
# Clean title: lowercase, alphanumeric + underscore only
|
|
131
|
+
clean_title = re.sub(r"[^a-z0-9]+", "_", title.lower())
|
|
132
|
+
clean_title = clean_title.strip("_")
|
|
133
|
+
|
|
134
|
+
# Truncate if too long
|
|
135
|
+
if len(clean_title) > 30:
|
|
136
|
+
clean_title = clean_title[:30]
|
|
137
|
+
|
|
138
|
+
return f"{work_type}_{clean_title}"
|
|
139
|
+
|
|
140
|
+
def _create_spec_file(self, work_id: str, work_type: str, title: str) -> str:
|
|
141
|
+
"""Create specification file from template
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
work_id: Work item ID
|
|
145
|
+
work_type: Type of work item
|
|
146
|
+
title: Work item title
|
|
147
|
+
|
|
148
|
+
Returns:
|
|
149
|
+
str: Relative path to the created spec file, or empty string if failed
|
|
150
|
+
"""
|
|
151
|
+
# Ensure specs directory exists
|
|
152
|
+
self.specs_dir.mkdir(parents=True, exist_ok=True)
|
|
153
|
+
|
|
154
|
+
# Load template
|
|
155
|
+
template_file = self.templates_dir / f"{work_type}_spec.md"
|
|
156
|
+
if not template_file.exists():
|
|
157
|
+
return ""
|
|
158
|
+
|
|
159
|
+
template_content = template_file.read_text()
|
|
160
|
+
|
|
161
|
+
# Replace title placeholder
|
|
162
|
+
if work_type == "feature":
|
|
163
|
+
spec_content = template_content.replace("[Feature Name]", title)
|
|
164
|
+
elif work_type == "bug":
|
|
165
|
+
spec_content = template_content.replace("[Bug Title]", title)
|
|
166
|
+
elif work_type == "refactor":
|
|
167
|
+
spec_content = template_content.replace("[Refactor Title]", title)
|
|
168
|
+
elif work_type == "security":
|
|
169
|
+
spec_content = template_content.replace("[Name]", title)
|
|
170
|
+
elif work_type == "integration_test":
|
|
171
|
+
spec_content = template_content.replace("[Name]", title)
|
|
172
|
+
elif work_type == "deployment":
|
|
173
|
+
spec_content = template_content.replace("[Environment]", title)
|
|
174
|
+
else:
|
|
175
|
+
spec_content = template_content
|
|
176
|
+
|
|
177
|
+
# Save spec file
|
|
178
|
+
spec_path = self.specs_dir / f"{work_id}.md"
|
|
179
|
+
spec_path.write_text(spec_content)
|
|
180
|
+
|
|
181
|
+
# Return relative path from project root
|
|
182
|
+
return f".session/specs/{work_id}.md"
|
|
183
|
+
|
|
184
|
+
def _print_creation_confirmation(
|
|
185
|
+
self,
|
|
186
|
+
work_id: str,
|
|
187
|
+
work_type: str,
|
|
188
|
+
priority: str,
|
|
189
|
+
dependencies: list[str],
|
|
190
|
+
spec_file: str,
|
|
191
|
+
) -> None:
|
|
192
|
+
"""Print creation confirmation message
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
work_id: Created work item ID
|
|
196
|
+
work_type: Work item type
|
|
197
|
+
priority: Priority level
|
|
198
|
+
dependencies: List of dependency IDs
|
|
199
|
+
spec_file: Path to spec file
|
|
200
|
+
"""
|
|
201
|
+
output.info(f"\n{'=' * 50}")
|
|
202
|
+
output.info("Work item created successfully!")
|
|
203
|
+
output.info("=" * 50)
|
|
204
|
+
output.info(f"\nID: {work_id}")
|
|
205
|
+
output.info(f"Type: {work_type}")
|
|
206
|
+
output.info(f"Priority: {priority}")
|
|
207
|
+
output.info(f"Status: {WorkItemStatus.NOT_STARTED.value}")
|
|
208
|
+
if dependencies:
|
|
209
|
+
output.info(f"Dependencies: {', '.join(dependencies)}")
|
|
210
|
+
|
|
211
|
+
if spec_file:
|
|
212
|
+
output.info(f"\nSpecification saved to: {spec_file}")
|
|
213
|
+
|
|
214
|
+
output.info("\nNext steps:")
|
|
215
|
+
output.info(f"1. Edit specification: {spec_file}")
|
|
216
|
+
output.info("2. Start working: /start")
|
|
217
|
+
output.info("")
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Work Item Deletion - Safe deletion of work items.
|
|
4
|
+
|
|
5
|
+
Handles deletion of work items with dependency checking and interactive confirmation.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
from solokit.core.error_handlers import log_errors
|
|
12
|
+
from solokit.core.exceptions import (
|
|
13
|
+
FileNotFoundError as SolokitFileNotFoundError,
|
|
14
|
+
)
|
|
15
|
+
from solokit.core.exceptions import (
|
|
16
|
+
FileOperationError,
|
|
17
|
+
ValidationError,
|
|
18
|
+
WorkItemNotFoundError,
|
|
19
|
+
)
|
|
20
|
+
from solokit.core.file_ops import load_json, save_json
|
|
21
|
+
from solokit.core.logging_config import get_logger
|
|
22
|
+
from solokit.core.output import get_output
|
|
23
|
+
from solokit.core.types import WorkItemStatus
|
|
24
|
+
|
|
25
|
+
logger = get_logger(__name__)
|
|
26
|
+
output = get_output()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def find_dependents(work_items: dict, work_item_id: str) -> list[str]:
|
|
30
|
+
"""
|
|
31
|
+
Find work items that depend on the given work item.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
work_items: Dictionary of all work items
|
|
35
|
+
work_item_id: ID to find dependents for
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
List of work item IDs that depend on this item
|
|
39
|
+
"""
|
|
40
|
+
dependents = []
|
|
41
|
+
for wid, item in work_items.items():
|
|
42
|
+
deps = item.get("dependencies", [])
|
|
43
|
+
if work_item_id in deps:
|
|
44
|
+
dependents.append(wid)
|
|
45
|
+
return dependents
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@log_errors()
|
|
49
|
+
def delete_work_item(
|
|
50
|
+
work_item_id: str, delete_spec: Optional[bool] = None, project_root: Optional[Path] = None
|
|
51
|
+
) -> bool:
|
|
52
|
+
"""
|
|
53
|
+
Delete a work item from the system.
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
work_item_id: ID of work item to delete
|
|
57
|
+
delete_spec: Whether to also delete the spec file (None for interactive prompt)
|
|
58
|
+
project_root: Project root path (defaults to current directory)
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
True if deletion successful, False if user cancels
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
SolokitFileNotFoundError: If work_items.json doesn't exist
|
|
65
|
+
WorkItemNotFoundError: If work item ID doesn't exist
|
|
66
|
+
FileOperationError: If unable to load or save work items file
|
|
67
|
+
ValidationError: If running in non-interactive mode without flags
|
|
68
|
+
"""
|
|
69
|
+
# Setup paths
|
|
70
|
+
if project_root is None:
|
|
71
|
+
project_root = Path.cwd()
|
|
72
|
+
|
|
73
|
+
session_dir = project_root / ".session"
|
|
74
|
+
work_items_file = session_dir / "tracking" / "work_items.json"
|
|
75
|
+
|
|
76
|
+
# Check if work items file exists
|
|
77
|
+
if not work_items_file.exists():
|
|
78
|
+
logger.error("Work items file not found")
|
|
79
|
+
raise SolokitFileNotFoundError(file_path=str(work_items_file), file_type="work items")
|
|
80
|
+
|
|
81
|
+
# Load work items
|
|
82
|
+
try:
|
|
83
|
+
work_items_data = load_json(work_items_file)
|
|
84
|
+
except (OSError, ValueError) as e:
|
|
85
|
+
logger.error("Failed to load work items: %s", e)
|
|
86
|
+
raise FileOperationError(
|
|
87
|
+
operation="read", file_path=str(work_items_file), details=str(e), cause=e
|
|
88
|
+
) from e
|
|
89
|
+
|
|
90
|
+
work_items = work_items_data.get("work_items", {})
|
|
91
|
+
|
|
92
|
+
# Validate work item exists
|
|
93
|
+
if work_item_id not in work_items:
|
|
94
|
+
logger.error("Work item '%s' not found", work_item_id)
|
|
95
|
+
raise WorkItemNotFoundError(work_item_id)
|
|
96
|
+
|
|
97
|
+
item = work_items[work_item_id]
|
|
98
|
+
|
|
99
|
+
# Find dependents
|
|
100
|
+
dependents = find_dependents(work_items, work_item_id)
|
|
101
|
+
|
|
102
|
+
# Show work item details
|
|
103
|
+
output.warning(f"\nThis will permanently delete work item '{work_item_id}'")
|
|
104
|
+
output.info("\nWork item details:")
|
|
105
|
+
output.info(f" Title: {item.get('title', 'N/A')}")
|
|
106
|
+
output.info(f" Type: {item.get('type', 'N/A')}")
|
|
107
|
+
output.info(f" Status: {item.get('status', 'N/A')}")
|
|
108
|
+
|
|
109
|
+
dependencies = item.get("dependencies", [])
|
|
110
|
+
if dependencies:
|
|
111
|
+
output.info(f" Dependencies: {', '.join(dependencies)}")
|
|
112
|
+
else:
|
|
113
|
+
output.info(" Dependencies: none")
|
|
114
|
+
|
|
115
|
+
if dependents:
|
|
116
|
+
output.info(
|
|
117
|
+
f" Dependents: {', '.join(dependents)} ({len(dependents)} item(s) depend on this)"
|
|
118
|
+
)
|
|
119
|
+
else:
|
|
120
|
+
output.info(" Dependents: none")
|
|
121
|
+
|
|
122
|
+
# Require explicit flag (no interactive mode)
|
|
123
|
+
if delete_spec is None:
|
|
124
|
+
logger.error("Must specify --keep-spec or --delete-spec flag")
|
|
125
|
+
raise ValidationError(
|
|
126
|
+
message="Must specify either --keep-spec or --delete-spec flag",
|
|
127
|
+
remediation=(
|
|
128
|
+
"Use command-line flags:\n"
|
|
129
|
+
" sk work-delete <work_item_id> --keep-spec (delete work item only)\n"
|
|
130
|
+
" sk work-delete <work_item_id> --delete-spec (delete work item and spec)"
|
|
131
|
+
),
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
# Show what will be done
|
|
135
|
+
if delete_spec:
|
|
136
|
+
output.info("\n→ Will delete work item and spec file")
|
|
137
|
+
else:
|
|
138
|
+
output.info("\n→ Will delete work item only (keeping spec file)")
|
|
139
|
+
|
|
140
|
+
# Perform deletion
|
|
141
|
+
logger.info("Deleting work item '%s'", work_item_id)
|
|
142
|
+
del work_items[work_item_id]
|
|
143
|
+
|
|
144
|
+
# Update metadata
|
|
145
|
+
work_items_data["work_items"] = work_items
|
|
146
|
+
if "metadata" not in work_items_data:
|
|
147
|
+
work_items_data["metadata"] = {}
|
|
148
|
+
|
|
149
|
+
work_items_data["metadata"]["total_items"] = len(work_items)
|
|
150
|
+
work_items_data["metadata"]["completed"] = sum(
|
|
151
|
+
1 for item in work_items.values() if item["status"] == WorkItemStatus.COMPLETED.value
|
|
152
|
+
)
|
|
153
|
+
work_items_data["metadata"]["in_progress"] = sum(
|
|
154
|
+
1 for item in work_items.values() if item["status"] == WorkItemStatus.IN_PROGRESS.value
|
|
155
|
+
)
|
|
156
|
+
work_items_data["metadata"]["blocked"] = sum(
|
|
157
|
+
1 for item in work_items.values() if item["status"] == WorkItemStatus.BLOCKED.value
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# Save work items
|
|
161
|
+
try:
|
|
162
|
+
save_json(work_items_file, work_items_data)
|
|
163
|
+
logger.info("Successfully updated work_items.json")
|
|
164
|
+
output.info(f"✓ Deleted work item '{work_item_id}'")
|
|
165
|
+
except OSError as e:
|
|
166
|
+
logger.error("Failed to save work items: %s", e)
|
|
167
|
+
raise FileOperationError(
|
|
168
|
+
operation="write", file_path=str(work_items_file), details=str(e), cause=e
|
|
169
|
+
) from e
|
|
170
|
+
|
|
171
|
+
# Delete spec file if requested
|
|
172
|
+
if delete_spec:
|
|
173
|
+
spec_file_path = item.get("spec_file", f".session/specs/{work_item_id}.md")
|
|
174
|
+
spec_path = project_root / spec_file_path
|
|
175
|
+
|
|
176
|
+
if spec_path.exists():
|
|
177
|
+
try:
|
|
178
|
+
spec_path.unlink()
|
|
179
|
+
logger.info("Deleted spec file: %s", spec_file_path)
|
|
180
|
+
output.info(f"✓ Deleted spec file '{spec_file_path}'")
|
|
181
|
+
except (OSError, PermissionError) as e:
|
|
182
|
+
logger.warning("Failed to delete spec file: %s", e)
|
|
183
|
+
output.warning(f"Could not delete spec file: {e}")
|
|
184
|
+
else:
|
|
185
|
+
logger.debug("Spec file not found: %s", spec_file_path)
|
|
186
|
+
output.info(f"Note: Spec file '{spec_file_path}' not found")
|
|
187
|
+
|
|
188
|
+
# Warn about dependents
|
|
189
|
+
if dependents:
|
|
190
|
+
output.warning("\nThe following work items depend on this item:")
|
|
191
|
+
for dep in dependents:
|
|
192
|
+
output.info(f" - {dep}")
|
|
193
|
+
output.info(" Update their dependencies manually if needed.")
|
|
194
|
+
|
|
195
|
+
output.info("\nDeletion successful.")
|
|
196
|
+
logger.info("Work item deletion completed successfully")
|
|
197
|
+
return True
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def main() -> int:
|
|
201
|
+
"""CLI entry point for work item deletion."""
|
|
202
|
+
import argparse
|
|
203
|
+
|
|
204
|
+
parser = argparse.ArgumentParser(description="Delete a work item")
|
|
205
|
+
parser.add_argument("work_item_id", help="ID of work item to delete")
|
|
206
|
+
parser.add_argument(
|
|
207
|
+
"--keep-spec",
|
|
208
|
+
action="store_true",
|
|
209
|
+
help="Keep the spec file (delete work item only)",
|
|
210
|
+
)
|
|
211
|
+
parser.add_argument(
|
|
212
|
+
"--delete-spec",
|
|
213
|
+
action="store_true",
|
|
214
|
+
help="Delete both work item and spec file",
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
args = parser.parse_args()
|
|
218
|
+
|
|
219
|
+
# Determine delete_spec value
|
|
220
|
+
delete_spec_value = None
|
|
221
|
+
if args.keep_spec and args.delete_spec:
|
|
222
|
+
raise ValidationError(
|
|
223
|
+
message="Cannot specify both --keep-spec and --delete-spec",
|
|
224
|
+
remediation="Choose only one option: --keep-spec OR --delete-spec",
|
|
225
|
+
)
|
|
226
|
+
elif args.keep_spec:
|
|
227
|
+
delete_spec_value = False
|
|
228
|
+
elif args.delete_spec:
|
|
229
|
+
delete_spec_value = True
|
|
230
|
+
|
|
231
|
+
# Perform deletion
|
|
232
|
+
try:
|
|
233
|
+
success = delete_work_item(args.work_item_id, delete_spec=delete_spec_value)
|
|
234
|
+
return 0 if success else 1
|
|
235
|
+
except WorkItemNotFoundError as e:
|
|
236
|
+
output.info(f"❌ Error: {e.message}")
|
|
237
|
+
if e.remediation:
|
|
238
|
+
output.info(f"\n{e.remediation}")
|
|
239
|
+
# Show available work items
|
|
240
|
+
try:
|
|
241
|
+
from pathlib import Path
|
|
242
|
+
|
|
243
|
+
work_items_file = Path.cwd() / ".session" / "tracking" / "work_items.json"
|
|
244
|
+
if work_items_file.exists():
|
|
245
|
+
work_items_data = load_json(work_items_file)
|
|
246
|
+
work_items = work_items_data.get("work_items", {})
|
|
247
|
+
if work_items:
|
|
248
|
+
output.info("\nAvailable work items:")
|
|
249
|
+
for wid in list(work_items.keys())[:5]:
|
|
250
|
+
output.info(f" - {wid}")
|
|
251
|
+
if len(work_items) > 5:
|
|
252
|
+
output.info(f" ... and {len(work_items) - 5} more")
|
|
253
|
+
except Exception: # noqa: BLE001 - This is optional enhancement, don't fail on it
|
|
254
|
+
pass
|
|
255
|
+
return e.exit_code
|
|
256
|
+
except (SolokitFileNotFoundError, FileOperationError, ValidationError) as e:
|
|
257
|
+
output.info(f"❌ Error: {e.message}")
|
|
258
|
+
if e.remediation:
|
|
259
|
+
output.info(f"\n{e.remediation}")
|
|
260
|
+
return e.exit_code
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
exit(main())
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""Fast dependency information retrieval for Claude Code integration.
|
|
2
|
+
|
|
3
|
+
This module provides optimized dependency lookups without reading full spec files.
|
|
4
|
+
Used by /work-new and /work-delete commands to fetch available dependencies.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_available_dependencies(
|
|
16
|
+
exclude_statuses: list[str] | None = None,
|
|
17
|
+
title_filter: str | None = None,
|
|
18
|
+
max_results: int = 3,
|
|
19
|
+
) -> list[dict[str, Any]]:
|
|
20
|
+
"""Get available work items that can be used as dependencies.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
exclude_statuses: List of statuses to exclude (default: ["completed"])
|
|
24
|
+
title_filter: Optional title to use for smart filtering/relevance
|
|
25
|
+
max_results: Maximum number of results to return (default: 3)
|
|
26
|
+
|
|
27
|
+
Returns:
|
|
28
|
+
List of dependency info dicts with keys: id, type, title, status
|
|
29
|
+
|
|
30
|
+
Raises:
|
|
31
|
+
FileNotFoundError: If work_items.json doesn't exist
|
|
32
|
+
json.JSONDecodeError: If work_items.json is invalid
|
|
33
|
+
"""
|
|
34
|
+
if exclude_statuses is None:
|
|
35
|
+
exclude_statuses = ["completed"]
|
|
36
|
+
|
|
37
|
+
# Find .session directory
|
|
38
|
+
session_dir = _find_session_dir()
|
|
39
|
+
if not session_dir:
|
|
40
|
+
print("Error: Not in an Solokit project (no .session directory found)", file=sys.stderr)
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
# Load work items
|
|
44
|
+
work_items_file = session_dir / "tracking" / "work_items.json"
|
|
45
|
+
if not work_items_file.exists():
|
|
46
|
+
print(f"Error: Work items file not found: {work_items_file}", file=sys.stderr)
|
|
47
|
+
return []
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with open(work_items_file) as f:
|
|
51
|
+
data = json.load(f)
|
|
52
|
+
except json.JSONDecodeError as e:
|
|
53
|
+
print(f"Error: Invalid JSON in {work_items_file}: {e}", file=sys.stderr)
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
# Extract work_items from the data structure
|
|
57
|
+
work_items = data.get("work_items", {})
|
|
58
|
+
if not work_items:
|
|
59
|
+
print("No work items found", file=sys.stderr)
|
|
60
|
+
return []
|
|
61
|
+
|
|
62
|
+
# Filter available dependencies
|
|
63
|
+
available = []
|
|
64
|
+
for work_id, item in work_items.items():
|
|
65
|
+
status = item.get("status", "unknown")
|
|
66
|
+
if status not in exclude_statuses:
|
|
67
|
+
available.append(
|
|
68
|
+
{
|
|
69
|
+
"id": work_id,
|
|
70
|
+
"type": item.get("type", "unknown"),
|
|
71
|
+
"title": item.get("title", "Untitled"),
|
|
72
|
+
"status": status,
|
|
73
|
+
}
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# Apply smart filtering if title provided
|
|
77
|
+
if title_filter and available:
|
|
78
|
+
available = _filter_by_relevance(available, title_filter)
|
|
79
|
+
|
|
80
|
+
# Limit results
|
|
81
|
+
return available[:max_results]
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _find_session_dir() -> Path | None:
|
|
85
|
+
"""Find the .session directory by walking up from current directory."""
|
|
86
|
+
current = Path.cwd()
|
|
87
|
+
while current != current.parent:
|
|
88
|
+
session_dir = current / ".session"
|
|
89
|
+
if session_dir.is_dir():
|
|
90
|
+
return session_dir
|
|
91
|
+
current = current.parent
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _filter_by_relevance(items: list[dict], title: str) -> list[dict]:
|
|
96
|
+
"""Filter and sort items by relevance to the given title.
|
|
97
|
+
|
|
98
|
+
Simple keyword-based relevance scoring:
|
|
99
|
+
- Exact word matches in title get highest score
|
|
100
|
+
- Partial matches get medium score
|
|
101
|
+
- Items with same type get bonus points
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
items: List of work item dicts
|
|
105
|
+
title: Title to compare against
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
Sorted list (most relevant first)
|
|
109
|
+
"""
|
|
110
|
+
title_lower = title.lower()
|
|
111
|
+
title_words = set(title_lower.split())
|
|
112
|
+
|
|
113
|
+
def relevance_score(item: dict) -> float:
|
|
114
|
+
item_title_lower = item["title"].lower()
|
|
115
|
+
item_words = set(item_title_lower.split())
|
|
116
|
+
|
|
117
|
+
# Exact word matches (high weight)
|
|
118
|
+
word_matches = len(title_words & item_words)
|
|
119
|
+
|
|
120
|
+
# Partial matches (medium weight)
|
|
121
|
+
partial_matches = sum(1 for tw in title_words for iw in item_words if tw in iw or iw in tw)
|
|
122
|
+
|
|
123
|
+
# Calculate score
|
|
124
|
+
score = (word_matches * 3.0) + (partial_matches * 1.5)
|
|
125
|
+
|
|
126
|
+
return score
|
|
127
|
+
|
|
128
|
+
# Score and sort
|
|
129
|
+
scored = [(item, relevance_score(item)) for item in items]
|
|
130
|
+
scored.sort(key=lambda x: x[1], reverse=True)
|
|
131
|
+
|
|
132
|
+
# Return only items with non-zero scores, or all if none match
|
|
133
|
+
relevant = [item for item, score in scored if score > 0]
|
|
134
|
+
return relevant if relevant else items
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def main() -> None:
|
|
138
|
+
"""CLI entry point for get_dependencies script.
|
|
139
|
+
|
|
140
|
+
Usage:
|
|
141
|
+
python -m solokit.work_items.get_dependencies
|
|
142
|
+
python -m solokit.work_items.get_dependencies --title "My new feature"
|
|
143
|
+
python -m solokit.work_items.get_dependencies --title "Bug fix" --max 5
|
|
144
|
+
python -m solokit.work_items.get_dependencies --exclude-status not_started,in_progress
|
|
145
|
+
"""
|
|
146
|
+
import argparse
|
|
147
|
+
|
|
148
|
+
parser = argparse.ArgumentParser(description="Get available dependencies for work items")
|
|
149
|
+
parser.add_argument("--title", help="Optional title for smart filtering by relevance")
|
|
150
|
+
parser.add_argument("--max", type=int, default=3, help="Maximum number of results (default: 3)")
|
|
151
|
+
parser.add_argument(
|
|
152
|
+
"--exclude-status", help="Comma-separated list of statuses to exclude (default: completed)"
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
args = parser.parse_args()
|
|
156
|
+
|
|
157
|
+
# Parse exclude statuses
|
|
158
|
+
exclude = None
|
|
159
|
+
if args.exclude_status:
|
|
160
|
+
exclude = [s.strip() for s in args.exclude_status.split(",")]
|
|
161
|
+
|
|
162
|
+
# Get dependencies
|
|
163
|
+
dependencies = get_available_dependencies(
|
|
164
|
+
exclude_statuses=exclude,
|
|
165
|
+
title_filter=args.title,
|
|
166
|
+
max_results=args.max,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
# Output results
|
|
170
|
+
if not dependencies:
|
|
171
|
+
print("No available dependencies found")
|
|
172
|
+
sys.exit(1)
|
|
173
|
+
|
|
174
|
+
print(f"Found {len(dependencies)} available dependencies:")
|
|
175
|
+
print()
|
|
176
|
+
for dep in dependencies:
|
|
177
|
+
print(f"ID: {dep['id']}")
|
|
178
|
+
print(f"Type: {dep['type']}")
|
|
179
|
+
print(f"Title: {dep['title']}")
|
|
180
|
+
print(f"Status: {dep['status']}")
|
|
181
|
+
print()
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
if __name__ == "__main__":
|
|
185
|
+
main()
|