onecoder 0.0.2__tar.gz → 0.0.4__tar.gz
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.
- {onecoder-0.0.2 → onecoder-0.0.4}/PKG-INFO +6 -2
- onecoder-0.0.4/ai_sprint/__init__.py +1 -0
- onecoder-0.0.4/ai_sprint/cli.py +40 -0
- onecoder-0.0.4/ai_sprint/commands/common.py +37 -0
- onecoder-0.0.4/ai_sprint/commands/governance.py +327 -0
- onecoder-0.0.4/ai_sprint/commands/sprint.py +142 -0
- onecoder-0.0.4/ai_sprint/commands/task.py +79 -0
- onecoder-0.0.4/ai_sprint/commands/utility.py +82 -0
- onecoder-0.0.4/ai_sprint/commit.py +163 -0
- onecoder-0.0.4/ai_sprint/hooks/__init__.py +1 -0
- onecoder-0.0.4/ai_sprint/policy.py +105 -0
- onecoder-0.0.4/ai_sprint/pr_creator.py +94 -0
- onecoder-0.0.4/ai_sprint/preflight.py +270 -0
- onecoder-0.0.4/ai_sprint/spec_validator.py +97 -0
- onecoder-0.0.4/ai_sprint/state.py +118 -0
- onecoder-0.0.4/ai_sprint/submodule.py +125 -0
- onecoder-0.0.4/ai_sprint/sync_engine.py +75 -0
- onecoder-0.0.4/ai_sprint/telemetry.py +98 -0
- onecoder-0.0.4/ai_sprint/trace.py +172 -0
- onecoder-0.0.4/ai_sprint/visual_generator.py +208 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/api_client.py +31 -5
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/cli.py +34 -18
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/auth.py +53 -10
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/delegate.py +17 -1
- onecoder-0.0.4/onecoder/commands/doctor.py +59 -0
- onecoder-0.0.4/onecoder/commands/feedback.py +73 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/issue.py +8 -1
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/config_manager.py +9 -0
- onecoder-0.0.4/onecoder/governance/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/governance/probllm.py +45 -5
- onecoder-0.0.4/onecoder/governance/retry_governor.py +114 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/knowledge.py +70 -2
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/sync.py +25 -1
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/external_tools.py +1 -1
- onecoder-0.0.4/onecoder/usage_logger.py +52 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/PKG-INFO +6 -2
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/SOURCES.txt +25 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/requires.txt +5 -1
- onecoder-0.0.4/onecoder.egg-info/top_level.txt +2 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/pyproject.toml +8 -4
- onecoder-0.0.4/tests/test_bundling.py +26 -0
- onecoder-0.0.4/tests/test_probllm_guardian.py +93 -0
- onecoder-0.0.2/onecoder/commands/doctor.py +0 -40
- onecoder-0.0.2/onecoder.egg-info/top_level.txt +0 -1
- {onecoder-0.0.2 → onecoder-0.0.4}/README.md +0 -0
- {onecoder-0.0.2/onecoder/agentic_tool_search → onecoder-0.0.4/ai_sprint/commands}/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agent.py +0 -0
- {onecoder-0.0.2/onecoder/governance → onecoder-0.0.4/onecoder/agentic_tool_search}/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agentic_tool_search/dynamic_tool_search.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agentic_tool_search/registry.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/documentation_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/file_reader_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/file_writer_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/orchestrator_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/refactoring_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/research_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/task_suggestion_agent.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/alignment.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/api.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/backends/base.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/backends/local_tui.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/blackboard.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/ci.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/logs.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/project.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/server.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/constants.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/diagnostics/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/diagnostics/env_scan.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/discovery.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/distillation.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/evaluation/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/evaluation/ttu.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/hooks.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/ipc_auth.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/issues.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/jules_client.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/llm.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/logger.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/metrics.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/models/delegation.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/onboarding.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/review.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/services/delegation_service.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/services/validation_service.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/sessions.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/sprint_collector.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tmux.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/executor.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/file_tools.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/interface.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/jules_tools.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/kit_tools.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/registry.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/__init__.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/app.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/commands.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/widgets.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/worktree.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/dependency_links.txt +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/entry_points.txt +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/setup.cfg +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_agent_governance.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_blackboard.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_delegation_service.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_durable_sessions.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_error_scenarios.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_jules_tools.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_openrouter.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_textual_tui.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tmux.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_token_expiration.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_token_lifecycle.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tui_commands.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tui_session.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tui_simple.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_validation_service.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_version.py +0 -0
- {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_worktree.py +0 -0
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: onecoder
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Requires-Python: >=3.11
|
|
5
5
|
Requires-Dist: google-adk
|
|
6
6
|
Requires-Dist: litellm
|
|
7
7
|
Requires-Dist: python-dotenv
|
|
8
|
-
Requires-Dist:
|
|
8
|
+
Requires-Dist: rank-bm25
|
|
9
9
|
Requires-Dist: fastapi>=0.104.0
|
|
10
10
|
Requires-Dist: uvicorn[standard]>=0.24.0
|
|
11
11
|
Requires-Dist: httpx>=0.25.0
|
|
@@ -13,5 +13,9 @@ Requires-Dist: rich>=13.0.0
|
|
|
13
13
|
Requires-Dist: textual>=0.50.0
|
|
14
14
|
Requires-Dist: click>=8.1.0
|
|
15
15
|
Requires-Dist: requests>=2.31.0
|
|
16
|
+
Requires-Dist: python-frontmatter>=1.0.0
|
|
17
|
+
Requires-Dist: pluggy>=1.0.0
|
|
18
|
+
Requires-Dist: jsonschema>=4.0.0
|
|
16
19
|
Requires-Dist: pytest>=7.4.0
|
|
17
20
|
Requires-Dist: pytest-asyncio>=0.21.0
|
|
21
|
+
Requires-Dist: twine
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""AI Sprint CLI - Sprint lifecycle management and state tracking."""
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import sys
|
|
3
|
+
from .commands import common, sprint, task, governance, utility
|
|
4
|
+
|
|
5
|
+
@click.group()
|
|
6
|
+
@click.version_option(version="0.1.4")
|
|
7
|
+
def main():
|
|
8
|
+
"""ai-sprint: Standardize your development sprints."""
|
|
9
|
+
pass
|
|
10
|
+
|
|
11
|
+
def run():
|
|
12
|
+
"""Main entry point with telemetry wrapper."""
|
|
13
|
+
try:
|
|
14
|
+
main()
|
|
15
|
+
except Exception as e:
|
|
16
|
+
if isinstance(e, (click.exceptions.Exit, click.exceptions.Abort, click.exceptions.ClickException)):
|
|
17
|
+
raise e
|
|
18
|
+
from .telemetry import FailureModeCapture
|
|
19
|
+
capture = FailureModeCapture(common.PROJECT_ROOT)
|
|
20
|
+
capture.capture(e, context={"command_args": sys.argv[1:]})
|
|
21
|
+
raise e
|
|
22
|
+
|
|
23
|
+
# Register commands
|
|
24
|
+
main.add_command(sprint.init)
|
|
25
|
+
main.add_command(sprint.update)
|
|
26
|
+
main.add_command(sprint.status)
|
|
27
|
+
main.add_command(task.start)
|
|
28
|
+
main.add_command(task.finish)
|
|
29
|
+
main.add_command(governance.commit)
|
|
30
|
+
main.add_command(governance.verify)
|
|
31
|
+
main.add_command(governance.preflight)
|
|
32
|
+
main.add_command(governance.close)
|
|
33
|
+
main.add_command(utility.trace)
|
|
34
|
+
main.add_command(utility.audit)
|
|
35
|
+
main.add_command(utility.backlog)
|
|
36
|
+
main.add_command(utility.check_submodules, name="check-submodules")
|
|
37
|
+
main.add_command(utility.install_hooks, name="install-hooks")
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
main()
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
class LazyConsole:
|
|
6
|
+
def __init__(self):
|
|
7
|
+
self._console = None
|
|
8
|
+
|
|
9
|
+
@property
|
|
10
|
+
def inner(self):
|
|
11
|
+
if self._console is None:
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
self._console = Console()
|
|
14
|
+
return self._console
|
|
15
|
+
|
|
16
|
+
def print(self, *args, **kwargs):
|
|
17
|
+
self.inner.print(*args, **kwargs)
|
|
18
|
+
|
|
19
|
+
def __getattr__(self, name):
|
|
20
|
+
return getattr(self.inner, name)
|
|
21
|
+
|
|
22
|
+
console = LazyConsole()
|
|
23
|
+
|
|
24
|
+
def find_project_root():
|
|
25
|
+
"""Find the project root directory (containing .git)."""
|
|
26
|
+
current = Path.cwd()
|
|
27
|
+
while current != current.parent:
|
|
28
|
+
if (current / ".git").exists():
|
|
29
|
+
return current
|
|
30
|
+
current = current.parent
|
|
31
|
+
return Path.cwd()
|
|
32
|
+
|
|
33
|
+
PROJECT_ROOT = find_project_root()
|
|
34
|
+
SPRINT_DIR = PROJECT_ROOT / ".sprint"
|
|
35
|
+
|
|
36
|
+
from ..state import SprintStateManager
|
|
37
|
+
from ..commit import auto_detect_sprint_id
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
import shutil
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import List, Dict, Any
|
|
10
|
+
from .common import console, PROJECT_ROOT, SPRINT_DIR, SprintStateManager, auto_detect_sprint_id
|
|
11
|
+
from ..commit import validate_trailers, create_commit_with_trailers
|
|
12
|
+
from ..spec_validator import validate_spec_ids
|
|
13
|
+
from ..trace import trace_specifications
|
|
14
|
+
from ..preflight import SprintPreflight
|
|
15
|
+
from ..policy import PolicyEngine
|
|
16
|
+
|
|
17
|
+
@click.command()
|
|
18
|
+
@click.option("--message", "-m", required=True, help="Commit message")
|
|
19
|
+
@click.option("--task-id", help="Task identifier")
|
|
20
|
+
@click.option("--status", type=click.Choice(["planning", "in-progress", "review", "done"]), help="Task status")
|
|
21
|
+
@click.option("--validation", help="Validation status or URI")
|
|
22
|
+
@click.option("--sprint-id", help="Override sprint ID")
|
|
23
|
+
@click.option("--spec-id", help="Specification ID(s)")
|
|
24
|
+
@click.option("--component", help="Component scope")
|
|
25
|
+
@click.option("--files", "-f", help="Files to stage (comma-separated or multiple)")
|
|
26
|
+
def commit(message, task_id, status, validation, sprint_id, spec_id, component, files):
|
|
27
|
+
"""Create atomic commit with metadata trailers."""
|
|
28
|
+
trailers = {}
|
|
29
|
+
active_sprint_id = sprint_id or auto_detect_sprint_id()
|
|
30
|
+
if active_sprint_id:
|
|
31
|
+
trailers["Sprint-Id"] = active_sprint_id
|
|
32
|
+
if not component:
|
|
33
|
+
try:
|
|
34
|
+
state_manager = SprintStateManager(SPRINT_DIR / active_sprint_id)
|
|
35
|
+
component = state_manager.get_component()
|
|
36
|
+
except Exception: pass
|
|
37
|
+
if component: trailers["Component"] = component
|
|
38
|
+
|
|
39
|
+
if task_id:
|
|
40
|
+
if not re.match(r"^task-\d+$", task_id):
|
|
41
|
+
resolved = None
|
|
42
|
+
if active_sprint_id:
|
|
43
|
+
try:
|
|
44
|
+
state_manager = SprintStateManager(SPRINT_DIR / active_sprint_id)
|
|
45
|
+
resolved = state_manager.get_task_id_by_title(task_id)
|
|
46
|
+
except Exception: pass
|
|
47
|
+
if not resolved and task_id.isdigit():
|
|
48
|
+
resolved = f"task-{int(task_id):03d}"
|
|
49
|
+
if resolved:
|
|
50
|
+
console.print(f"[dim]Resolved Task ID: {task_id} -> {resolved}[/dim]")
|
|
51
|
+
task_id = resolved
|
|
52
|
+
trailers["Task-Id"] = task_id
|
|
53
|
+
|
|
54
|
+
if status:
|
|
55
|
+
if status == "done":
|
|
56
|
+
try:
|
|
57
|
+
result = subprocess.run(["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, check=True)
|
|
58
|
+
staged_files = [f.strip() for f in result.stdout.split("\n") if f.strip()]
|
|
59
|
+
has_impl_changes = any(not (f.startswith(".sprint/") or f in ["TODO.md", "RETRO.md", "README.md", "sprint.json"]) for f in staged_files)
|
|
60
|
+
is_exception = any(x in message.lower() for x in ["docs", "chore", "governance"])
|
|
61
|
+
if not has_impl_changes and not is_exception:
|
|
62
|
+
console.print("[bold red]Error:[/bold red] Implementation Integrity Violation (SPEC-GOV-008.2)")
|
|
63
|
+
sys.exit(1)
|
|
64
|
+
except Exception: pass
|
|
65
|
+
trailers["Status"] = status
|
|
66
|
+
|
|
67
|
+
if validation: trailers["Validation"] = validation
|
|
68
|
+
if spec_id:
|
|
69
|
+
is_valid, errors = validate_spec_ids(spec_id, PROJECT_ROOT)
|
|
70
|
+
if not is_valid:
|
|
71
|
+
for error in errors: console.print(f" ❌ {error}")
|
|
72
|
+
sys.exit(1)
|
|
73
|
+
trailers["Spec-Id"] = spec_id
|
|
74
|
+
|
|
75
|
+
errors = validate_trailers(trailers)
|
|
76
|
+
if errors:
|
|
77
|
+
for error in errors: console.print(f" ❌ {error}")
|
|
78
|
+
sys.exit(1)
|
|
79
|
+
|
|
80
|
+
# Automatically stage files if provided
|
|
81
|
+
file_list = []
|
|
82
|
+
if files:
|
|
83
|
+
# Support comma-separated or multiple (though click argument is single for now)
|
|
84
|
+
file_list = [f.strip() for f in files.split(",") if f.strip()]
|
|
85
|
+
if file_list:
|
|
86
|
+
console.print(f"[dim]Staging files: {', '.join(file_list)}[/dim]")
|
|
87
|
+
try:
|
|
88
|
+
subprocess.run(["git", "add"] + file_list, cwd=PROJECT_ROOT, check=True)
|
|
89
|
+
except subprocess.CalledProcessError as e:
|
|
90
|
+
console.print(f"[bold red]Error staging files:[/bold red] {e}")
|
|
91
|
+
sys.exit(1)
|
|
92
|
+
|
|
93
|
+
from ..submodule import get_unpushed_submodules
|
|
94
|
+
if get_unpushed_submodules(PROJECT_ROOT):
|
|
95
|
+
console.print("[bold red]Error:[/bold red] Unpushed submodule commits detected.")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
|
|
98
|
+
# ProbLLM Governance Check on Staged Files
|
|
99
|
+
try:
|
|
100
|
+
# Import dynamically to avoid circular dependencies if any, or just standard import
|
|
101
|
+
from onecoder.governance.probllm import ProbLLMGuardian
|
|
102
|
+
guardian = ProbLLMGuardian(PROJECT_ROOT / "governance.yaml")
|
|
103
|
+
|
|
104
|
+
# Re-fetch staged files as they might have changed if 'files' arg was used
|
|
105
|
+
res = subprocess.run(["git", "diff", "--cached", "--name-only"], capture_output=True, text=True)
|
|
106
|
+
if res.returncode == 0:
|
|
107
|
+
current_staged = [f.strip() for f in res.stdout.split("\n") if f.strip()]
|
|
108
|
+
is_safe, msg = guardian.validate_staged_files(current_staged)
|
|
109
|
+
if not is_safe:
|
|
110
|
+
console.print(f"[bold red]Governance Block (ProbLLM):[/bold red] {msg}")
|
|
111
|
+
sys.exit(1)
|
|
112
|
+
except ImportError:
|
|
113
|
+
# If onecoder package is not found (e.g. running ai-sprint standalone), skip or warn
|
|
114
|
+
# For now, we assume implicit availability in the onecoder-cli environment
|
|
115
|
+
pass
|
|
116
|
+
except Exception as e:
|
|
117
|
+
console.print(f"[yellow]Warning:[/yellow] ProbLLM check failed to run: {e}")
|
|
118
|
+
|
|
119
|
+
if create_commit_with_trailers(message, trailers):
|
|
120
|
+
console.print("[bold green]Success:[/bold green] Commit created.")
|
|
121
|
+
else:
|
|
122
|
+
sys.exit(1)
|
|
123
|
+
|
|
124
|
+
@click.command()
|
|
125
|
+
@click.option("--sprint-id", help="Override sprint ID")
|
|
126
|
+
def verify(sprint_id):
|
|
127
|
+
"""Verify sprint for technical debt (Zero Errors/Lint)."""
|
|
128
|
+
active_sprint = sprint_id or auto_detect_sprint_id()
|
|
129
|
+
if not active_sprint:
|
|
130
|
+
console.print("[bold red]Error:[/bold red] No active sprint detected.")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
target_dir = SPRINT_DIR / active_sprint
|
|
133
|
+
state_manager = SprintStateManager(target_dir)
|
|
134
|
+
component = state_manager.get_component()
|
|
135
|
+
if not component:
|
|
136
|
+
console.print("[yellow]Warning:[/yellow] No component scope defined.")
|
|
137
|
+
return
|
|
138
|
+
|
|
139
|
+
console.print(f"[cyan]Verifying component debt for [bold]{component}[/bold]...[/cyan]")
|
|
140
|
+
policy_engine = PolicyEngine(PROJECT_ROOT)
|
|
141
|
+
comp_rules = policy_engine.get_verification_rules().get(component, [])
|
|
142
|
+
|
|
143
|
+
results = {"errors": 0, "lint_violations": 0, "details": []}
|
|
144
|
+
for check in comp_rules:
|
|
145
|
+
cmd = check if isinstance(check, str) else check['cmd']
|
|
146
|
+
name = check if isinstance(check, str) else check['name']
|
|
147
|
+
res = subprocess.run(cmd, shell=True, cwd=PROJECT_ROOT / component, capture_output=True, text=True)
|
|
148
|
+
if res.returncode != 0:
|
|
149
|
+
results["errors" if "lint" not in name.lower() else "lint_violations"] += 1
|
|
150
|
+
console.print(f" ❌ [bold red]{name} failed[/bold red]")
|
|
151
|
+
else:
|
|
152
|
+
console.print(f" ✅ [green]{name} passed[/green]")
|
|
153
|
+
|
|
154
|
+
with open(target_dir / ".verification_results.json", "w") as f:
|
|
155
|
+
json.dump(results, f)
|
|
156
|
+
if results["errors"] > 0 or results["lint_violations"] > 0:
|
|
157
|
+
sys.exit(1)
|
|
158
|
+
console.print("\n[bold green]✓ Zero-Debt Verification Passed.[/bold green]")
|
|
159
|
+
|
|
160
|
+
@click.command()
|
|
161
|
+
@click.option("--fix", is_flag=True, help="Attempt to fix simple issues")
|
|
162
|
+
@click.option("--sprint-id", help="Sprint ID to preflight")
|
|
163
|
+
def preflight(fix, sprint_id):
|
|
164
|
+
"""Validate sprint readiness and governance adherence."""
|
|
165
|
+
active_sprint = sprint_id or auto_detect_sprint_id()
|
|
166
|
+
if not active_sprint:
|
|
167
|
+
console.print("[bold red]Error:[/bold red] No active sprint detected.")
|
|
168
|
+
return
|
|
169
|
+
sprint_dir = SPRINT_DIR / active_sprint
|
|
170
|
+
preflight_engine = SprintPreflight(sprint_dir, PROJECT_ROOT)
|
|
171
|
+
score, results = preflight_engine.run_all()
|
|
172
|
+
for res in results:
|
|
173
|
+
status_icon = "✅" if res["status"] == "passed" else ("❌" if res["status"] == "failed" else "⚠️")
|
|
174
|
+
color = "green" if res["status"] == "passed" else ("red" if res["status"] == "failed" else "yellow")
|
|
175
|
+
console.print(f" {status_icon} [{color}]{res['name']}[/{color}]: {res['message']}")
|
|
176
|
+
if score < 75: sys.exit(1)
|
|
177
|
+
|
|
178
|
+
@click.command()
|
|
179
|
+
@click.argument("name")
|
|
180
|
+
@click.option("--pr/--no-pr", is_flag=True, default=True, help="Create pull request after closing sprint")
|
|
181
|
+
@click.option("--apply", is_flag=True, help="Execute the closure (Apply phase)")
|
|
182
|
+
@click.option("--plan", is_flag=True, default=True, help="Only show what would be done (Plan phase, default)")
|
|
183
|
+
def close(name, pr, apply, plan):
|
|
184
|
+
"""Close a sprint if criteria are met (Plan-Apply pattern)."""
|
|
185
|
+
from ..pr_creator import create_pull_request
|
|
186
|
+
from ..visual_generator import generate_visual_assets
|
|
187
|
+
from ..policy import PolicyEngine
|
|
188
|
+
|
|
189
|
+
is_apply = apply
|
|
190
|
+
target_dir = SPRINT_DIR / name
|
|
191
|
+
if not target_dir.exists():
|
|
192
|
+
console.print(f"[bold red]Error:[/bold red] Sprint {name} does not exist.")
|
|
193
|
+
sys.exit(1)
|
|
194
|
+
|
|
195
|
+
console.print(f"[cyan]Evaluating governance policy for {name}...[/cyan]")
|
|
196
|
+
policy_engine = PolicyEngine(PROJECT_ROOT)
|
|
197
|
+
|
|
198
|
+
visual_policy = policy_engine.get_visual_policy()
|
|
199
|
+
if visual_policy.get("auto_generate_on_close"):
|
|
200
|
+
media_dir = target_dir / "media"
|
|
201
|
+
if is_apply or media_dir.exists() or plan:
|
|
202
|
+
console.print(f"[cyan]Ensuring visual assets for {name}...[/cyan]")
|
|
203
|
+
try:
|
|
204
|
+
generate_visual_assets(target_dir, name)
|
|
205
|
+
except Exception as e:
|
|
206
|
+
if "Google IDE" not in str(e):
|
|
207
|
+
console.print(f"[yellow]Warning:[/yellow] Visual generation issue: {e}")
|
|
208
|
+
|
|
209
|
+
closure_rules = policy_engine.get_closure_policy()
|
|
210
|
+
violations = policy_engine.evaluate_closure(target_dir)
|
|
211
|
+
has_debt = any("Zero-Debt" in v or "verification has not been run" in v for v in violations)
|
|
212
|
+
|
|
213
|
+
if closure_rules.get("require_zero_debt") and is_apply and has_debt:
|
|
214
|
+
console.print(f"[cyan]Verifying zero-debt status for {name}...[/cyan]")
|
|
215
|
+
try:
|
|
216
|
+
ctx = click.get_current_context()
|
|
217
|
+
ctx.invoke(verify, sprint_id=name)
|
|
218
|
+
except SystemExit as e:
|
|
219
|
+
if e.code != 0:
|
|
220
|
+
console.print("[bold red]Cannot close: Zero-Debt verification failed.[/bold red]")
|
|
221
|
+
sys.exit(1)
|
|
222
|
+
except Exception as e:
|
|
223
|
+
console.print(f"[yellow]Warning:[/yellow] Verification could not be run: {e}")
|
|
224
|
+
|
|
225
|
+
violations = policy_engine.evaluate_closure(target_dir)
|
|
226
|
+
if violations:
|
|
227
|
+
console.print("[bold red]Cannot close: Governance Policy Violations Detected![/bold red]")
|
|
228
|
+
for v in violations: console.print(f" ❌ {v}")
|
|
229
|
+
if not is_apply: console.print("\n[dim]Tip: Run with --apply to attempt automatic fixes.[/dim]")
|
|
230
|
+
sys.exit(1)
|
|
231
|
+
|
|
232
|
+
if not is_apply:
|
|
233
|
+
console.print("\n[bold cyan]--- Sprint Close PLAN ---[/bold cyan]")
|
|
234
|
+
console.print("[dim]Governance: OK[/dim]")
|
|
235
|
+
|
|
236
|
+
from ..submodule import get_unpushed_submodules
|
|
237
|
+
if get_unpushed_submodules(PROJECT_ROOT):
|
|
238
|
+
console.print("[bold red]Cannot close:[/bold red] Unpushed submodule commits detected.")
|
|
239
|
+
sys.exit(1)
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
# Check for staged changes
|
|
243
|
+
staged = subprocess.run(["git", "diff", "--cached", "--name-only"], capture_output=True, text=True, cwd=PROJECT_ROOT)
|
|
244
|
+
if staged.returncode == 0 and staged.stdout.strip():
|
|
245
|
+
console.print("[bold red]Cannot close:[/bold red] Found staged changes.")
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
# Check for unstaged changes
|
|
249
|
+
result = subprocess.run(["git", "status", "--porcelain"], capture_output=True, text=True, cwd=PROJECT_ROOT)
|
|
250
|
+
if result.returncode == 0 and result.stdout.strip():
|
|
251
|
+
uncommitted_files = [f for f in result.stdout.strip().split("\n")
|
|
252
|
+
if not (f[3:].startswith(".sprint/") or f[3:].endswith(".json") or f[3:] in ["TODO.md", "RETRO.md", "README.md", "sprint.json"])]
|
|
253
|
+
if uncommitted_files:
|
|
254
|
+
console.print(f"[bold red]Cannot close:[/bold red] Found {len(uncommitted_files)} uncommitted implementation changes.")
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
except Exception as e: console.print(f"[yellow]Warning:[/yellow] Git check: {e}")
|
|
257
|
+
|
|
258
|
+
# TODO.md check
|
|
259
|
+
todo_file = target_dir / "TODO.md"
|
|
260
|
+
if todo_file.exists():
|
|
261
|
+
with open(todo_file, "r") as f: lines = f.readlines()
|
|
262
|
+
delivery_sections = ["## High Priority", "## Implementation"]
|
|
263
|
+
in_delivery_section = True
|
|
264
|
+
incomplete_tasks = []
|
|
265
|
+
for line in lines:
|
|
266
|
+
if line.startswith("## "):
|
|
267
|
+
section_name = line.strip()
|
|
268
|
+
if section_name in delivery_sections: in_delivery_section = True
|
|
269
|
+
elif section_name in ["## Backlog", "## Future"]: in_delivery_section = False
|
|
270
|
+
else: in_delivery_section = "backlog" not in section_name.lower()
|
|
271
|
+
if in_delivery_section:
|
|
272
|
+
match = re.match(r"-\s*\[\s\]\s*(.+)", line)
|
|
273
|
+
if match and match.group(1).strip(): incomplete_tasks.append(match.group(1).strip())
|
|
274
|
+
if incomplete_tasks:
|
|
275
|
+
console.print(f"[bold red]Cannot close:[/bold red] Found {len(incomplete_tasks)} incomplete tasks.")
|
|
276
|
+
sys.exit(1)
|
|
277
|
+
|
|
278
|
+
retro_file = target_dir / "RETRO.md"
|
|
279
|
+
if not retro_file.exists() or retro_file.stat().st_size < 50:
|
|
280
|
+
console.print("[bold red]Cannot close:[/bold red] RETRO.md missing or too short.")
|
|
281
|
+
sys.exit(1)
|
|
282
|
+
|
|
283
|
+
# BAKE-IT check
|
|
284
|
+
try:
|
|
285
|
+
trace_map = trace_specifications(PROJECT_ROOT, limit=500)
|
|
286
|
+
sprint_flags = [f for f in trace_map.get("audit", []) if name in str(f.get("message", "")) or name == f.get("id") or (f.get("sprint_id") == name)]
|
|
287
|
+
if sprint_flags:
|
|
288
|
+
console.print("[bold red]Cannot close: BAKE-IT Anti-Patterns Detected![/bold red]")
|
|
289
|
+
sys.exit(1)
|
|
290
|
+
except Exception as e: console.print(f"[yellow]Warning:[/yellow] Audit check skipped: {e}")
|
|
291
|
+
|
|
292
|
+
if not is_apply:
|
|
293
|
+
console.print("\n[bold green]Plan phase complete.[/bold green]")
|
|
294
|
+
return
|
|
295
|
+
|
|
296
|
+
# Apply phase
|
|
297
|
+
(target_dir / ".status").write_text("Closed")
|
|
298
|
+
try:
|
|
299
|
+
to_add = [a for a in [str(target_dir.relative_to(PROJECT_ROOT)), "TODO.md", "RETRO.md", "README.md", "sprint.json"] if (PROJECT_ROOT / a).exists()]
|
|
300
|
+
if to_add:
|
|
301
|
+
subprocess.run(["git", "add"] + to_add, cwd=PROJECT_ROOT, check=True)
|
|
302
|
+
commit_msg = f"chore(gov): close sprint {name}\n\n[Sprint-Id: {name}]\n[Status: closed]"
|
|
303
|
+
subprocess.run(["git", "commit", "-m", commit_msg], cwd=PROJECT_ROOT, check=True)
|
|
304
|
+
except Exception as e: console.print(f"[yellow]Warning:[/yellow] Governance commit failed: {e}")
|
|
305
|
+
|
|
306
|
+
console.print(f"[bold green]Success:[/bold green] Sprint {name} closed.")
|
|
307
|
+
if pr:
|
|
308
|
+
try:
|
|
309
|
+
create_pull_request(target_dir, name)
|
|
310
|
+
console.print("[green]✓[/green] Pull request created")
|
|
311
|
+
except Exception as e: console.print(f"[bold red]Error:[/bold red] PR failed: {e}")
|
|
312
|
+
|
|
313
|
+
@click.command()
|
|
314
|
+
@click.option("--limit", default=100, help="Number of commits to trace")
|
|
315
|
+
@click.option("--json", "json_output", is_flag=True, help="Output as JSON")
|
|
316
|
+
def audit(limit, staged):
|
|
317
|
+
"""Run a comprehensive audit on current project."""
|
|
318
|
+
# Placeholder for audit logic
|
|
319
|
+
pass
|
|
320
|
+
|
|
321
|
+
@click.command()
|
|
322
|
+
@click.option("--component", help="Filter by component")
|
|
323
|
+
@click.option("--category", help="Filter by category")
|
|
324
|
+
def backlog(component, category):
|
|
325
|
+
"""View consolidated backlog."""
|
|
326
|
+
# Placeholder for backlog logic
|
|
327
|
+
pass
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import re
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from .common import console, PROJECT_ROOT, SPRINT_DIR, SprintStateManager, auto_detect_sprint_id
|
|
6
|
+
|
|
7
|
+
def create_sprint_structure(target_dir: Path, name: str, exist_ok: bool = False):
|
|
8
|
+
target_dir.mkdir(parents=True, exist_ok=exist_ok)
|
|
9
|
+
(target_dir / "planning").mkdir(exist_ok=True)
|
|
10
|
+
(target_dir / "logs").mkdir(exist_ok=True)
|
|
11
|
+
(target_dir / "context").mkdir(exist_ok=True)
|
|
12
|
+
(target_dir / "media").mkdir(exist_ok=True)
|
|
13
|
+
|
|
14
|
+
readme_file = target_dir / "README.md"
|
|
15
|
+
if not readme_file.exists():
|
|
16
|
+
with open(readme_file, "w") as f:
|
|
17
|
+
f.write(f"# Sprint: {name}\n\n## Goal\nDescribe the goal of this sprint.\n")
|
|
18
|
+
|
|
19
|
+
tasks_file = target_dir / "planning" / "tasks.md"
|
|
20
|
+
if not tasks_file.exists():
|
|
21
|
+
with open(tasks_file, "w") as f:
|
|
22
|
+
f.write("# Tasks\n\n- [ ] Initial Task\n")
|
|
23
|
+
|
|
24
|
+
todo_file = target_dir / "TODO.md"
|
|
25
|
+
if not todo_file.exists():
|
|
26
|
+
with open(todo_file, "w") as f:
|
|
27
|
+
f.write("# Sprint TODO\n\n## High Priority\n- [ ] \n\n## Backlog\n- [ ] \n")
|
|
28
|
+
|
|
29
|
+
retro_file = target_dir / "RETRO.md"
|
|
30
|
+
if not retro_file.exists():
|
|
31
|
+
with open(retro_file, "w") as f:
|
|
32
|
+
f.write(
|
|
33
|
+
"# Retrospective\n\n"
|
|
34
|
+
"## Went Well\n\n"
|
|
35
|
+
"## To Improve\n\n"
|
|
36
|
+
"## Action Items\n\n"
|
|
37
|
+
"## Visual Assets\n"
|
|
38
|
+
"<!-- Generated diagrams and flowcharts will be referenced here -->\n"
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
@click.command()
|
|
42
|
+
@click.argument("name", required=False)
|
|
43
|
+
@click.option("--name", "name_option", help="Name of the sprint directory")
|
|
44
|
+
@click.option("--branch/--no-branch", default=True, help="Automatically create and switch to a git branch")
|
|
45
|
+
@click.option("--component", help="Component scope for this sprint")
|
|
46
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompts")
|
|
47
|
+
def init(name, name_option, branch, component, yes):
|
|
48
|
+
"""Initialize a new sprint directory structure."""
|
|
49
|
+
if not name:
|
|
50
|
+
name = name_option
|
|
51
|
+
if not name:
|
|
52
|
+
name = click.prompt("Sprint Name")
|
|
53
|
+
|
|
54
|
+
if not yes and not click.confirm("Have you reviewed the pending .issues/ and backlog?"):
|
|
55
|
+
console.print("[bold yellow]Aborted.[/bold yellow]")
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
if not re.match(r"^\d{3}-", name):
|
|
59
|
+
max_num = 0
|
|
60
|
+
if SPRINT_DIR.exists():
|
|
61
|
+
for item in SPRINT_DIR.iterdir():
|
|
62
|
+
if item.is_dir():
|
|
63
|
+
match = re.match(r"^(\d{3})-", item.name)
|
|
64
|
+
if match:
|
|
65
|
+
num = int(match.group(1))
|
|
66
|
+
if num > max_num:
|
|
67
|
+
max_num = num
|
|
68
|
+
next_num = max_num + 1
|
|
69
|
+
name = f"{next_num:03d}-{name}"
|
|
70
|
+
|
|
71
|
+
target_dir = SPRINT_DIR / name
|
|
72
|
+
if target_dir.exists():
|
|
73
|
+
console.print(f"[bold red]Error:[/bold red] Directory {target_dir} already exists.")
|
|
74
|
+
return
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
create_sprint_structure(target_dir, name)
|
|
78
|
+
state_manager = SprintStateManager(target_dir)
|
|
79
|
+
initial_state = state_manager.create_initial_state(name, name, component)
|
|
80
|
+
state_manager.save(initial_state)
|
|
81
|
+
console.print(f"[bold green]Success:[/bold green] Initialized sprint at [cyan]{target_dir}[/cyan]")
|
|
82
|
+
|
|
83
|
+
if branch:
|
|
84
|
+
branch_name = f"sprint/{name}"
|
|
85
|
+
subprocess.run(["git", "checkout", "-b", branch_name], check=True, capture_output=True)
|
|
86
|
+
console.print(f"[bold green]Success:[/bold green] Created branch [cyan]{branch_name}[/cyan]")
|
|
87
|
+
|
|
88
|
+
except Exception as e:
|
|
89
|
+
console.print(f"[bold red]Error:[/bold red] Failed to initialize: {e}")
|
|
90
|
+
return
|
|
91
|
+
|
|
92
|
+
console.print("\n[cyan]Running sprint preflight check...[/cyan]")
|
|
93
|
+
from ..preflight import SprintPreflight
|
|
94
|
+
preflight = SprintPreflight(target_dir, PROJECT_ROOT)
|
|
95
|
+
score, results = preflight.run_all()
|
|
96
|
+
|
|
97
|
+
for res in results:
|
|
98
|
+
status_icon = "✅" if res["status"] == "passed" else ("❌" if res["status"] == "failed" else "⚠️")
|
|
99
|
+
color = "green" if res["status"] == "passed" else ("red" if res["status"] == "failed" else "yellow")
|
|
100
|
+
console.print(f" {status_icon} [{color}]{res['name']}[/{color}]: {res['message']}")
|
|
101
|
+
|
|
102
|
+
if score >= 75:
|
|
103
|
+
console.print(f"\n[bold green]Preflight Passed![/bold green] Score: {score}/100")
|
|
104
|
+
else:
|
|
105
|
+
console.print(f"\n[bold red]Preflight Failed![/bold red] Score: {score}/100")
|
|
106
|
+
|
|
107
|
+
@click.command()
|
|
108
|
+
@click.argument("name")
|
|
109
|
+
def update(name):
|
|
110
|
+
"""Update an existing sprint with missing templates."""
|
|
111
|
+
target_dir = SPRINT_DIR / name
|
|
112
|
+
if not target_dir.exists():
|
|
113
|
+
console.print(f"[bold red]Error:[/bold red] Sprint {name} does not exist.")
|
|
114
|
+
return
|
|
115
|
+
try:
|
|
116
|
+
create_sprint_structure(target_dir, name, exist_ok=True)
|
|
117
|
+
console.print(f"[bold green]Success:[/bold green] Updated sprint at {target_dir}")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
console.print(f"[bold red]Error:[/bold red] Failed to update: {e}")
|
|
120
|
+
|
|
121
|
+
@click.command()
|
|
122
|
+
def status():
|
|
123
|
+
"""Show the current status of the sprint."""
|
|
124
|
+
if not SPRINT_DIR.exists():
|
|
125
|
+
console.print("[yellow]No .sprint directory found.[/yellow]")
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
from rich.table import Table
|
|
129
|
+
table = Table(title="Available Sprints")
|
|
130
|
+
table.add_column("Sprint Name", style="cyan")
|
|
131
|
+
table.add_column("Status", style="magenta")
|
|
132
|
+
|
|
133
|
+
for item in sorted(SPRINT_DIR.iterdir()):
|
|
134
|
+
if item.is_dir():
|
|
135
|
+
status_file = item / ".status"
|
|
136
|
+
state = "Active"
|
|
137
|
+
if status_file.exists():
|
|
138
|
+
with open(status_file, "r") as f:
|
|
139
|
+
state = f.read().strip()
|
|
140
|
+
color = "green" if state == "Active" else "dim white"
|
|
141
|
+
table.add_row(item.name, f"[{color}]{state}[/{color}]")
|
|
142
|
+
console.print(table)
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import click
|
|
2
|
+
import datetime
|
|
3
|
+
import subprocess
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import List, Dict
|
|
6
|
+
from .common import console, PROJECT_ROOT, SPRINT_DIR, SprintStateManager, auto_detect_sprint_id
|
|
7
|
+
|
|
8
|
+
def _validate_audit_task(task: Dict, sprint_commits: List[Dict], project_root: Path) -> bool:
|
|
9
|
+
from ..trace import get_commit_files
|
|
10
|
+
for commit in sprint_commits:
|
|
11
|
+
files = get_commit_files(commit["hash"], project_root)
|
|
12
|
+
if any(f.startswith(".issues/") for f in files):
|
|
13
|
+
return True
|
|
14
|
+
return False
|
|
15
|
+
|
|
16
|
+
def _validate_artifact_task(task: Dict, sprint_commits: List[Dict], sprint_dir: Path, project_root: Path) -> bool:
|
|
17
|
+
from ..trace import get_commit_files
|
|
18
|
+
sprint_rel_path = sprint_dir.relative_to(project_root)
|
|
19
|
+
for commit in sprint_commits:
|
|
20
|
+
files = get_commit_files(commit["hash"], project_root)
|
|
21
|
+
if any(f.startswith(str(sprint_rel_path)) for f in files):
|
|
22
|
+
return True
|
|
23
|
+
return False
|
|
24
|
+
|
|
25
|
+
@click.command()
|
|
26
|
+
@click.argument("task_name")
|
|
27
|
+
@click.option("--branch/--no-branch", default=True, help="Create/switch to a sprint branch")
|
|
28
|
+
@click.option("--type", type=click.Choice(["implementation", "audit", "docs", "planning"]), help="Task type")
|
|
29
|
+
def start(task_name, branch, type):
|
|
30
|
+
"""Start working on a specific task."""
|
|
31
|
+
console.print(f"[bold blue]Starting task:[/bold blue] {task_name}")
|
|
32
|
+
active_sprint = auto_detect_sprint_id()
|
|
33
|
+
if not active_sprint:
|
|
34
|
+
console.print("[yellow]Warning:[/yellow] No active sprint detected.")
|
|
35
|
+
else:
|
|
36
|
+
sprint_dir = SPRINT_DIR / active_sprint
|
|
37
|
+
if sprint_dir.exists():
|
|
38
|
+
state_manager = SprintStateManager(sprint_dir)
|
|
39
|
+
if not type:
|
|
40
|
+
type = click.prompt("Task type", type=click.Choice(["implementation", "audit", "docs", "planning"]), default="implementation")
|
|
41
|
+
state = state_manager.load()
|
|
42
|
+
for task in state.get("tasks", []):
|
|
43
|
+
if task.get("id") == task_name or task.get("title") == task_name:
|
|
44
|
+
task["status"] = "in-progress"
|
|
45
|
+
task["type"] = type
|
|
46
|
+
if not task.get("startedAt"):
|
|
47
|
+
task["startedAt"] = datetime.datetime.now().isoformat()
|
|
48
|
+
break
|
|
49
|
+
state_manager.save(state)
|
|
50
|
+
state_manager.start_task(task_name)
|
|
51
|
+
timestamp = datetime.datetime.now().isoformat()
|
|
52
|
+
console.print(f"Recorded start time: {timestamp}")
|
|
53
|
+
|
|
54
|
+
@click.command()
|
|
55
|
+
@click.argument("task_name", required=False)
|
|
56
|
+
@click.option("--message", "-m", help="Commit message")
|
|
57
|
+
@click.option("--validation", "-v", help="Validation proof (URI, path, or description)")
|
|
58
|
+
def finish(task_name, message, validation):
|
|
59
|
+
"""Finish the current task and commit changes with validation."""
|
|
60
|
+
if not task_name:
|
|
61
|
+
task_name = click.prompt("Finished Task Name/ID")
|
|
62
|
+
if not validation:
|
|
63
|
+
validation = click.prompt("Validation Proof")
|
|
64
|
+
if not message:
|
|
65
|
+
message = click.prompt("Commit Message", default=f"feat: finish task {task_name}")
|
|
66
|
+
|
|
67
|
+
console.print(f"[bold green]Finishing task:[/bold green] {task_name}")
|
|
68
|
+
active_sprint_id = auto_detect_sprint_id()
|
|
69
|
+
resolved_task_id = task_name
|
|
70
|
+
if active_sprint_id:
|
|
71
|
+
state_manager = SprintStateManager(SPRINT_DIR / active_sprint_id)
|
|
72
|
+
if state_manager.mark_done(task_name):
|
|
73
|
+
console.print(f"Task marked as [bold green]done[/bold green]")
|
|
74
|
+
id_lookup = state_manager.get_task_id_by_title(task_name)
|
|
75
|
+
if id_lookup: resolved_task_id = id_lookup
|
|
76
|
+
|
|
77
|
+
from .governance import commit
|
|
78
|
+
ctx = click.get_current_context()
|
|
79
|
+
ctx.invoke(commit, message=message, task_id=resolved_task_id, status="done", validation=validation)
|