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,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Git context and status checking.
|
|
4
|
+
Part of the briefing module decomposition.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any, Optional
|
|
10
|
+
|
|
11
|
+
from solokit.core.command_runner import CommandRunner
|
|
12
|
+
from solokit.core.constants import GIT_QUICK_TIMEOUT
|
|
13
|
+
from solokit.core.error_handlers import log_errors
|
|
14
|
+
from solokit.core.exceptions import ErrorCode, GitError, SystemError
|
|
15
|
+
from solokit.core.logging_config import get_logger
|
|
16
|
+
from solokit.core.output import get_output
|
|
17
|
+
from solokit.core.types import GitStatus, WorkItemStatus
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
output = get_output()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class GitContext:
|
|
24
|
+
"""Handle git status and branch information."""
|
|
25
|
+
|
|
26
|
+
def __init__(self) -> None:
|
|
27
|
+
"""Initialize git context handler."""
|
|
28
|
+
self.runner = CommandRunner(default_timeout=GIT_QUICK_TIMEOUT)
|
|
29
|
+
|
|
30
|
+
@log_errors()
|
|
31
|
+
def check_git_status(self) -> dict[str, Any]:
|
|
32
|
+
"""Check git status for session start.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Dict with keys: clean (bool), status (str), branch (str or None)
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
GitError: If git command fails
|
|
39
|
+
NotAGitRepoError: If not in a git repository
|
|
40
|
+
SystemError: If git workflow import or execution fails
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
# Import git workflow from new location
|
|
44
|
+
from solokit.git.integration import GitWorkflow
|
|
45
|
+
|
|
46
|
+
workflow = GitWorkflow()
|
|
47
|
+
is_clean: bool
|
|
48
|
+
status_msg: str
|
|
49
|
+
is_clean, status_msg = workflow.check_git_status() # type: ignore[misc]
|
|
50
|
+
current_branch = workflow.get_current_branch()
|
|
51
|
+
|
|
52
|
+
return {"clean": is_clean, "status": status_msg, "branch": current_branch}
|
|
53
|
+
except ImportError as e:
|
|
54
|
+
raise SystemError(
|
|
55
|
+
message="Failed to import GitWorkflow",
|
|
56
|
+
code=ErrorCode.IMPORT_FAILED,
|
|
57
|
+
context={"module": "solokit.git.integration", "error": str(e)},
|
|
58
|
+
remediation="Ensure git integration module is properly installed",
|
|
59
|
+
cause=e,
|
|
60
|
+
) from e
|
|
61
|
+
except GitError:
|
|
62
|
+
# Re-raise GitError as-is (already standardized)
|
|
63
|
+
raise
|
|
64
|
+
except Exception as e:
|
|
65
|
+
# Wrap unexpected errors
|
|
66
|
+
raise GitError(
|
|
67
|
+
message=f"Failed to check git status: {e}",
|
|
68
|
+
code=ErrorCode.GIT_COMMAND_FAILED,
|
|
69
|
+
context={"error": str(e)},
|
|
70
|
+
remediation="Check git installation and repository state",
|
|
71
|
+
cause=e,
|
|
72
|
+
) from e
|
|
73
|
+
|
|
74
|
+
@log_errors()
|
|
75
|
+
def determine_git_branch_final_status(self, branch_name: str, git_info: dict) -> str:
|
|
76
|
+
"""Determine the final status of a git branch by inspecting actual git state.
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
branch_name: Name of the git branch
|
|
80
|
+
git_info: Git info dict from work item
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
One of: "merged", "pr_created", "pr_closed", "ready_for_pr", "deleted"
|
|
84
|
+
|
|
85
|
+
Raises:
|
|
86
|
+
GitError: If git commands fail critically
|
|
87
|
+
SystemError: If JSON parsing or other operations fail
|
|
88
|
+
"""
|
|
89
|
+
parent_branch = git_info.get("parent_branch", "main")
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
# Check 1: Is branch merged?
|
|
93
|
+
result = self.runner.run(["git", "branch", "--merged", parent_branch])
|
|
94
|
+
if result.success and branch_name in result.stdout:
|
|
95
|
+
logger.debug(f"Branch {branch_name} is merged to {parent_branch}")
|
|
96
|
+
return GitStatus.MERGED.value
|
|
97
|
+
|
|
98
|
+
# Check 2: Does PR exist? (requires gh CLI)
|
|
99
|
+
result = self.runner.run(
|
|
100
|
+
[
|
|
101
|
+
"gh",
|
|
102
|
+
"pr",
|
|
103
|
+
"list",
|
|
104
|
+
"--head",
|
|
105
|
+
branch_name,
|
|
106
|
+
"--state",
|
|
107
|
+
"all",
|
|
108
|
+
"--json",
|
|
109
|
+
"number,state",
|
|
110
|
+
]
|
|
111
|
+
)
|
|
112
|
+
if result.success and result.stdout.strip():
|
|
113
|
+
try:
|
|
114
|
+
prs = json.loads(result.stdout)
|
|
115
|
+
if prs:
|
|
116
|
+
pr = prs[0] # Get first/most recent PR
|
|
117
|
+
pr_state = pr.get("state", "").upper()
|
|
118
|
+
|
|
119
|
+
if pr_state == "MERGED":
|
|
120
|
+
logger.debug(f"Branch {branch_name} has merged PR")
|
|
121
|
+
return GitStatus.MERGED.value
|
|
122
|
+
elif pr_state == "CLOSED":
|
|
123
|
+
logger.debug(f"Branch {branch_name} has closed (unmerged) PR")
|
|
124
|
+
return GitStatus.PR_CLOSED.value
|
|
125
|
+
elif pr_state == "OPEN":
|
|
126
|
+
logger.debug(f"Branch {branch_name} has open PR")
|
|
127
|
+
return GitStatus.PR_CREATED.value
|
|
128
|
+
except json.JSONDecodeError as e:
|
|
129
|
+
# Log but don't fail - gh CLI output may be malformed
|
|
130
|
+
logger.debug(f"Error parsing PR JSON: {e}")
|
|
131
|
+
# Continue to check branch existence
|
|
132
|
+
else:
|
|
133
|
+
logger.debug("gh CLI not available or no PRs found")
|
|
134
|
+
|
|
135
|
+
# Check 3: Does branch still exist locally?
|
|
136
|
+
result = self.runner.run(["git", "show-ref", "--verify", f"refs/heads/{branch_name}"])
|
|
137
|
+
if result.success:
|
|
138
|
+
logger.debug(f"Branch {branch_name} exists locally")
|
|
139
|
+
# Branch exists locally, no PR found
|
|
140
|
+
return GitStatus.READY_FOR_PR.value
|
|
141
|
+
|
|
142
|
+
# Check 4: Does branch exist remotely?
|
|
143
|
+
result = self.runner.run(["git", "ls-remote", "--heads", "origin", branch_name])
|
|
144
|
+
if result.success and result.stdout.strip():
|
|
145
|
+
logger.debug(f"Branch {branch_name} exists remotely")
|
|
146
|
+
# Branch exists remotely, no PR found
|
|
147
|
+
return GitStatus.READY_FOR_PR.value
|
|
148
|
+
|
|
149
|
+
# Branch doesn't exist and no PR found
|
|
150
|
+
logger.debug(f"Branch {branch_name} not found locally or remotely")
|
|
151
|
+
return GitStatus.DELETED.value
|
|
152
|
+
|
|
153
|
+
except GitError:
|
|
154
|
+
# Re-raise GitError from CommandRunner
|
|
155
|
+
raise
|
|
156
|
+
except Exception as e:
|
|
157
|
+
# Wrap unexpected errors
|
|
158
|
+
raise GitError(
|
|
159
|
+
message=f"Failed to determine git branch status for '{branch_name}': {e}",
|
|
160
|
+
code=ErrorCode.GIT_COMMAND_FAILED,
|
|
161
|
+
context={
|
|
162
|
+
"branch_name": branch_name,
|
|
163
|
+
"parent_branch": parent_branch,
|
|
164
|
+
"error": str(e),
|
|
165
|
+
},
|
|
166
|
+
remediation="Check git installation and repository state",
|
|
167
|
+
cause=e,
|
|
168
|
+
) from e
|
|
169
|
+
|
|
170
|
+
@log_errors()
|
|
171
|
+
def finalize_previous_work_item_git_status(
|
|
172
|
+
self, work_items_data: dict, current_work_item_id: str
|
|
173
|
+
) -> Optional[str]:
|
|
174
|
+
"""Finalize git status for previous completed work item when starting a new one.
|
|
175
|
+
|
|
176
|
+
This handles the case where:
|
|
177
|
+
- Previous work item was completed
|
|
178
|
+
- User performed git operations externally (pushed, created PR, merged)
|
|
179
|
+
- Starting a new work item (not resuming the previous one)
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
work_items_data: Loaded work items data
|
|
183
|
+
current_work_item_id: ID of work item being started
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
Previous work item ID if finalized, None otherwise
|
|
187
|
+
|
|
188
|
+
Raises:
|
|
189
|
+
GitError: If git operations fail
|
|
190
|
+
SystemError: If file operations fail
|
|
191
|
+
"""
|
|
192
|
+
work_items = work_items_data.get("work_items", {})
|
|
193
|
+
|
|
194
|
+
# Find previously active work item
|
|
195
|
+
previous_work_item = None
|
|
196
|
+
previous_work_item_id = None
|
|
197
|
+
|
|
198
|
+
for wid, wi in work_items.items():
|
|
199
|
+
# Skip current work item
|
|
200
|
+
if wid == current_work_item_id:
|
|
201
|
+
continue
|
|
202
|
+
|
|
203
|
+
# Find work item with git branch in "in_progress" status
|
|
204
|
+
git_info = wi.get("git", {})
|
|
205
|
+
if git_info.get("status") == GitStatus.IN_PROGRESS.value:
|
|
206
|
+
# Only finalize if work item itself is completed
|
|
207
|
+
if wi.get("status") == WorkItemStatus.COMPLETED.value:
|
|
208
|
+
previous_work_item = wi
|
|
209
|
+
previous_work_item_id = wid
|
|
210
|
+
break
|
|
211
|
+
|
|
212
|
+
if not previous_work_item:
|
|
213
|
+
# No previous work item to finalize
|
|
214
|
+
logger.debug("No previous work item with stale git status found")
|
|
215
|
+
return None
|
|
216
|
+
|
|
217
|
+
git_info = previous_work_item.get("git", {})
|
|
218
|
+
branch_name = git_info.get("branch")
|
|
219
|
+
|
|
220
|
+
if not branch_name:
|
|
221
|
+
logger.debug(f"Previous work item {previous_work_item_id} has no git branch")
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
logger.info(f"Finalizing git status for completed work item: {previous_work_item_id}")
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
# Inspect actual git state
|
|
228
|
+
final_status = self.determine_git_branch_final_status(branch_name, git_info)
|
|
229
|
+
|
|
230
|
+
# Update git status
|
|
231
|
+
work_items[previous_work_item_id]["git"]["status"] = final_status
|
|
232
|
+
|
|
233
|
+
# Save updated work items
|
|
234
|
+
work_items_file = Path(".session/tracking/work_items.json")
|
|
235
|
+
try:
|
|
236
|
+
with open(work_items_file, "w") as f:
|
|
237
|
+
json.dump(work_items_data, f, indent=2)
|
|
238
|
+
except OSError as e:
|
|
239
|
+
raise SystemError(
|
|
240
|
+
message=f"Failed to save work items file: {work_items_file}",
|
|
241
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
242
|
+
context={
|
|
243
|
+
"file_path": str(work_items_file),
|
|
244
|
+
"work_item_id": previous_work_item_id,
|
|
245
|
+
"error": str(e),
|
|
246
|
+
},
|
|
247
|
+
remediation="Check file permissions and disk space",
|
|
248
|
+
cause=e,
|
|
249
|
+
) from e
|
|
250
|
+
|
|
251
|
+
logger.info(
|
|
252
|
+
"Updated git status for %s: in_progress → %s",
|
|
253
|
+
previous_work_item_id,
|
|
254
|
+
final_status,
|
|
255
|
+
)
|
|
256
|
+
output.success(
|
|
257
|
+
f"Finalized git status for previous work item: "
|
|
258
|
+
f"{previous_work_item_id} → {final_status}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
return previous_work_item_id
|
|
262
|
+
|
|
263
|
+
except GitError:
|
|
264
|
+
# Re-raise GitError from determine_git_branch_final_status
|
|
265
|
+
raise
|
|
266
|
+
except SystemError:
|
|
267
|
+
# Re-raise SystemError from file operations
|
|
268
|
+
raise
|
|
269
|
+
except Exception as e:
|
|
270
|
+
# Wrap unexpected errors
|
|
271
|
+
raise SystemError(
|
|
272
|
+
message=f"Failed to finalize git status for work item '{previous_work_item_id}': {e}",
|
|
273
|
+
code=ErrorCode.FILE_OPERATION_FAILED,
|
|
274
|
+
context={
|
|
275
|
+
"work_item_id": previous_work_item_id,
|
|
276
|
+
"current_work_item_id": current_work_item_id,
|
|
277
|
+
"branch_name": branch_name,
|
|
278
|
+
"error": str(e),
|
|
279
|
+
},
|
|
280
|
+
remediation="Check work items data integrity and git repository state",
|
|
281
|
+
cause=e,
|
|
282
|
+
) from e
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Learning loading and relevance scoring.
|
|
4
|
+
Part of the briefing module decomposition.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import re
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from solokit.core.constants import MAX_SPEC_KEYWORDS
|
|
16
|
+
from solokit.core.exceptions import FileOperationError
|
|
17
|
+
from solokit.core.logging_config import get_logger
|
|
18
|
+
|
|
19
|
+
logger = get_logger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class LearningLoader:
|
|
23
|
+
"""Load and score learnings based on relevance."""
|
|
24
|
+
|
|
25
|
+
def __init__(self, session_dir: Path | None = None):
|
|
26
|
+
"""Initialize learning loader.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
session_dir: Path to .session directory (defaults to .session)
|
|
30
|
+
"""
|
|
31
|
+
self.session_dir = session_dir or Path(".session")
|
|
32
|
+
self.learnings_file = self.session_dir / "tracking" / "learnings.json"
|
|
33
|
+
|
|
34
|
+
def load_learnings(self) -> dict[str, Any]:
|
|
35
|
+
"""Load learnings from tracking file.
|
|
36
|
+
|
|
37
|
+
Returns:
|
|
38
|
+
Learnings data structure
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
FileOperationError: If file read or JSON parsing fails
|
|
42
|
+
"""
|
|
43
|
+
if not self.learnings_file.exists():
|
|
44
|
+
return {"learnings": []}
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
with open(self.learnings_file) as f:
|
|
48
|
+
return json.load(f) # type: ignore[no-any-return]
|
|
49
|
+
except json.JSONDecodeError as e:
|
|
50
|
+
raise FileOperationError(
|
|
51
|
+
operation="parse",
|
|
52
|
+
file_path=str(self.learnings_file),
|
|
53
|
+
details=f"Invalid JSON format: {e.msg} at line {e.lineno}, column {e.colno}",
|
|
54
|
+
cause=e,
|
|
55
|
+
)
|
|
56
|
+
except OSError as e:
|
|
57
|
+
raise FileOperationError(
|
|
58
|
+
operation="read",
|
|
59
|
+
file_path=str(self.learnings_file),
|
|
60
|
+
details=str(e),
|
|
61
|
+
cause=e,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
def get_relevant_learnings(
|
|
65
|
+
self, learnings_data: dict, work_item: dict, spec_content: str = ""
|
|
66
|
+
) -> list[dict]:
|
|
67
|
+
"""Get learnings relevant to this work item using multi-factor scoring.
|
|
68
|
+
|
|
69
|
+
Enhancement #11 Phase 4: Uses intelligent scoring algorithm instead of
|
|
70
|
+
simple tag matching. Considers keyword matching, type-based relevance,
|
|
71
|
+
recency weighting, and category bonuses.
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
learnings_data: Full learnings.json data structure
|
|
75
|
+
work_item: Work item dictionary with title, type, tags
|
|
76
|
+
spec_content: Optional spec content for keyword extraction
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
Top 10 scored learnings
|
|
80
|
+
"""
|
|
81
|
+
# Flatten all learnings from categories structure
|
|
82
|
+
all_learnings = []
|
|
83
|
+
categories = learnings_data.get("categories", {})
|
|
84
|
+
|
|
85
|
+
# Handle both old format (learnings list) and new format (categories dict)
|
|
86
|
+
if not categories and "learnings" in learnings_data:
|
|
87
|
+
# Old format compatibility
|
|
88
|
+
all_learnings = learnings_data.get("learnings", [])
|
|
89
|
+
for learning in all_learnings:
|
|
90
|
+
if "category" not in learning:
|
|
91
|
+
learning["category"] = "general"
|
|
92
|
+
else:
|
|
93
|
+
# New format with categories
|
|
94
|
+
for category, learnings in categories.items():
|
|
95
|
+
for learning in learnings:
|
|
96
|
+
learning_copy = learning.copy()
|
|
97
|
+
learning_copy["category"] = category
|
|
98
|
+
all_learnings.append(learning_copy)
|
|
99
|
+
|
|
100
|
+
if not all_learnings:
|
|
101
|
+
return []
|
|
102
|
+
|
|
103
|
+
# Extract keywords from work item
|
|
104
|
+
title_keywords = self._extract_keywords(work_item.get("title", ""))
|
|
105
|
+
spec_keywords = self._extract_keywords(spec_content[:MAX_SPEC_KEYWORDS])
|
|
106
|
+
work_type = work_item.get("type", "")
|
|
107
|
+
work_tags = set(work_item.get("tags", []))
|
|
108
|
+
|
|
109
|
+
scored = []
|
|
110
|
+
for learning in all_learnings:
|
|
111
|
+
score: float = 0
|
|
112
|
+
content_lower = learning.get("content", "").lower()
|
|
113
|
+
context_lower = learning.get("context", "").lower()
|
|
114
|
+
learning_tags = set(learning.get("tags", []))
|
|
115
|
+
category = learning.get("category", "general")
|
|
116
|
+
|
|
117
|
+
# 1. Keyword matching (title and spec)
|
|
118
|
+
content_keywords = self._extract_keywords(content_lower)
|
|
119
|
+
title_matches = len(title_keywords & content_keywords)
|
|
120
|
+
spec_matches = len(spec_keywords & content_keywords)
|
|
121
|
+
score += title_matches * 3 # Title match is worth more
|
|
122
|
+
score += spec_matches * 1.5
|
|
123
|
+
|
|
124
|
+
# 2. Type-based matching
|
|
125
|
+
if work_type in content_lower or work_type in context_lower:
|
|
126
|
+
score += 5
|
|
127
|
+
|
|
128
|
+
# 3. Tag matching (legacy support)
|
|
129
|
+
tag_overlap = len(work_tags & learning_tags)
|
|
130
|
+
score += tag_overlap * 2
|
|
131
|
+
|
|
132
|
+
# 4. Category bonuses
|
|
133
|
+
category_bonuses = {
|
|
134
|
+
"best_practices": 3,
|
|
135
|
+
"patterns": 2,
|
|
136
|
+
"gotchas": 2,
|
|
137
|
+
"architecture": 2,
|
|
138
|
+
}
|
|
139
|
+
score += category_bonuses.get(category, 0)
|
|
140
|
+
|
|
141
|
+
# 5. Recency weighting (decay over time)
|
|
142
|
+
created_at = learning.get("created_at", "")
|
|
143
|
+
if created_at:
|
|
144
|
+
days_ago = self._calculate_days_ago(created_at)
|
|
145
|
+
if days_ago < 7:
|
|
146
|
+
score += 3 # Very recent
|
|
147
|
+
elif days_ago < 30:
|
|
148
|
+
score += 2 # Recent
|
|
149
|
+
elif days_ago < 90:
|
|
150
|
+
score += 1 # Moderately recent
|
|
151
|
+
|
|
152
|
+
# Only include if score > 0
|
|
153
|
+
if score > 0:
|
|
154
|
+
scored.append((score, learning))
|
|
155
|
+
|
|
156
|
+
# Sort by score (descending) and return top 10
|
|
157
|
+
scored.sort(key=lambda x: x[0], reverse=True)
|
|
158
|
+
return [learning for score, learning in scored[:10]]
|
|
159
|
+
|
|
160
|
+
def _extract_keywords(self, text: str) -> set[str]:
|
|
161
|
+
"""Extract meaningful keywords from text (lowercase, >3 chars).
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
text: Text to extract keywords from
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
Set of lowercase keywords longer than 3 characters
|
|
168
|
+
"""
|
|
169
|
+
words = re.findall(r"\b\w+\b", text.lower())
|
|
170
|
+
# Filter stop words and short words
|
|
171
|
+
stop_words = {
|
|
172
|
+
"the",
|
|
173
|
+
"this",
|
|
174
|
+
"that",
|
|
175
|
+
"with",
|
|
176
|
+
"from",
|
|
177
|
+
"have",
|
|
178
|
+
"will",
|
|
179
|
+
"for",
|
|
180
|
+
"and",
|
|
181
|
+
"or",
|
|
182
|
+
"not",
|
|
183
|
+
"but",
|
|
184
|
+
"was",
|
|
185
|
+
"are",
|
|
186
|
+
"been",
|
|
187
|
+
}
|
|
188
|
+
return {w for w in words if len(w) > 3 and w not in stop_words}
|
|
189
|
+
|
|
190
|
+
def _calculate_days_ago(self, timestamp: str) -> int:
|
|
191
|
+
"""Calculate days since timestamp.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
timestamp: ISO format timestamp string
|
|
195
|
+
|
|
196
|
+
Returns:
|
|
197
|
+
Number of days ago (defaults to 365 if parsing fails)
|
|
198
|
+
|
|
199
|
+
Note:
|
|
200
|
+
Returns 365 (considered "old") if timestamp is empty or invalid.
|
|
201
|
+
This is intentional to avoid breaking the scoring algorithm.
|
|
202
|
+
"""
|
|
203
|
+
if not timestamp:
|
|
204
|
+
return 365 # Empty timestamp considered old
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
ts = datetime.fromisoformat(timestamp.replace("Z", "+00:00"))
|
|
208
|
+
delta = datetime.now() - ts
|
|
209
|
+
return delta.days
|
|
210
|
+
except (ValueError, TypeError) as e:
|
|
211
|
+
logger.debug(f"Failed to parse timestamp '{timestamp}': {e}. Treating as old learning.")
|
|
212
|
+
return 365 # Default to old if parsing fails
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Milestone context building.
|
|
4
|
+
Part of the briefing module decomposition.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from solokit.core.logging_config import get_logger
|
|
13
|
+
from solokit.core.types import WorkItemStatus
|
|
14
|
+
|
|
15
|
+
logger = get_logger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class MilestoneBuilder:
|
|
19
|
+
"""Build milestone context for briefings."""
|
|
20
|
+
|
|
21
|
+
def __init__(self, session_dir: Path | None = None):
|
|
22
|
+
"""Initialize milestone builder.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
session_dir: Path to .session directory (defaults to .session)
|
|
26
|
+
"""
|
|
27
|
+
self.session_dir = session_dir or Path(".session")
|
|
28
|
+
self.work_items_file = self.session_dir / "tracking" / "work_items.json"
|
|
29
|
+
|
|
30
|
+
def load_milestone_context(self, work_item: dict) -> dict | None:
|
|
31
|
+
"""Load milestone context for briefing.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
work_item: Work item dictionary
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Milestone context dict or None if not in a milestone
|
|
38
|
+
"""
|
|
39
|
+
milestone_name = work_item.get("milestone")
|
|
40
|
+
if not milestone_name:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
if not self.work_items_file.exists():
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
with open(self.work_items_file) as f:
|
|
47
|
+
data = json.load(f)
|
|
48
|
+
|
|
49
|
+
milestones = data.get("milestones", {})
|
|
50
|
+
milestone = milestones.get(milestone_name)
|
|
51
|
+
|
|
52
|
+
if not milestone:
|
|
53
|
+
return None
|
|
54
|
+
|
|
55
|
+
# Calculate progress
|
|
56
|
+
items = data.get("work_items", {})
|
|
57
|
+
milestone_items = [
|
|
58
|
+
{**item, "id": item_id}
|
|
59
|
+
for item_id, item in items.items()
|
|
60
|
+
if item.get("milestone") == milestone_name
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
total = len(milestone_items)
|
|
64
|
+
completed = sum(
|
|
65
|
+
1 for item in milestone_items if item["status"] == WorkItemStatus.COMPLETED.value
|
|
66
|
+
)
|
|
67
|
+
percent = int((completed / total) * 100) if total > 0 else 0
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
"name": milestone_name,
|
|
71
|
+
"title": milestone["title"],
|
|
72
|
+
"description": milestone["description"],
|
|
73
|
+
"target_date": milestone.get("target_date", ""),
|
|
74
|
+
"progress": percent,
|
|
75
|
+
"total_items": total,
|
|
76
|
+
"completed_items": completed,
|
|
77
|
+
"milestone_items": milestone_items,
|
|
78
|
+
}
|