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.
Files changed (122) hide show
  1. {onecoder-0.0.2 → onecoder-0.0.4}/PKG-INFO +6 -2
  2. onecoder-0.0.4/ai_sprint/__init__.py +1 -0
  3. onecoder-0.0.4/ai_sprint/cli.py +40 -0
  4. onecoder-0.0.4/ai_sprint/commands/common.py +37 -0
  5. onecoder-0.0.4/ai_sprint/commands/governance.py +327 -0
  6. onecoder-0.0.4/ai_sprint/commands/sprint.py +142 -0
  7. onecoder-0.0.4/ai_sprint/commands/task.py +79 -0
  8. onecoder-0.0.4/ai_sprint/commands/utility.py +82 -0
  9. onecoder-0.0.4/ai_sprint/commit.py +163 -0
  10. onecoder-0.0.4/ai_sprint/hooks/__init__.py +1 -0
  11. onecoder-0.0.4/ai_sprint/policy.py +105 -0
  12. onecoder-0.0.4/ai_sprint/pr_creator.py +94 -0
  13. onecoder-0.0.4/ai_sprint/preflight.py +270 -0
  14. onecoder-0.0.4/ai_sprint/spec_validator.py +97 -0
  15. onecoder-0.0.4/ai_sprint/state.py +118 -0
  16. onecoder-0.0.4/ai_sprint/submodule.py +125 -0
  17. onecoder-0.0.4/ai_sprint/sync_engine.py +75 -0
  18. onecoder-0.0.4/ai_sprint/telemetry.py +98 -0
  19. onecoder-0.0.4/ai_sprint/trace.py +172 -0
  20. onecoder-0.0.4/ai_sprint/visual_generator.py +208 -0
  21. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/api_client.py +31 -5
  22. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/cli.py +34 -18
  23. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/auth.py +53 -10
  24. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/delegate.py +17 -1
  25. onecoder-0.0.4/onecoder/commands/doctor.py +59 -0
  26. onecoder-0.0.4/onecoder/commands/feedback.py +73 -0
  27. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/issue.py +8 -1
  28. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/config_manager.py +9 -0
  29. onecoder-0.0.4/onecoder/governance/__init__.py +0 -0
  30. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/governance/probllm.py +45 -5
  31. onecoder-0.0.4/onecoder/governance/retry_governor.py +114 -0
  32. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/knowledge.py +70 -2
  33. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/sync.py +25 -1
  34. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/external_tools.py +1 -1
  35. onecoder-0.0.4/onecoder/usage_logger.py +52 -0
  36. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/PKG-INFO +6 -2
  37. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/SOURCES.txt +25 -0
  38. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/requires.txt +5 -1
  39. onecoder-0.0.4/onecoder.egg-info/top_level.txt +2 -0
  40. {onecoder-0.0.2 → onecoder-0.0.4}/pyproject.toml +8 -4
  41. onecoder-0.0.4/tests/test_bundling.py +26 -0
  42. onecoder-0.0.4/tests/test_probllm_guardian.py +93 -0
  43. onecoder-0.0.2/onecoder/commands/doctor.py +0 -40
  44. onecoder-0.0.2/onecoder.egg-info/top_level.txt +0 -1
  45. {onecoder-0.0.2 → onecoder-0.0.4}/README.md +0 -0
  46. {onecoder-0.0.2/onecoder/agentic_tool_search → onecoder-0.0.4/ai_sprint/commands}/__init__.py +0 -0
  47. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agent.py +0 -0
  48. {onecoder-0.0.2/onecoder/governance → onecoder-0.0.4/onecoder/agentic_tool_search}/__init__.py +0 -0
  49. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agentic_tool_search/dynamic_tool_search.py +0 -0
  50. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agentic_tool_search/registry.py +0 -0
  51. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/__init__.py +0 -0
  52. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/documentation_agent.py +0 -0
  53. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/file_reader_agent.py +0 -0
  54. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/file_writer_agent.py +0 -0
  55. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/orchestrator_agent.py +0 -0
  56. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/refactoring_agent.py +0 -0
  57. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/research_agent.py +0 -0
  58. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/agents/task_suggestion_agent.py +0 -0
  59. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/alignment.py +0 -0
  60. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/api.py +0 -0
  61. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/backends/base.py +0 -0
  62. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/backends/local_tui.py +0 -0
  63. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/blackboard.py +0 -0
  64. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/__init__.py +0 -0
  65. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/ci.py +0 -0
  66. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/logs.py +0 -0
  67. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/project.py +0 -0
  68. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/commands/server.py +0 -0
  69. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/constants.py +0 -0
  70. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/diagnostics/__init__.py +0 -0
  71. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/diagnostics/env_scan.py +0 -0
  72. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/discovery.py +0 -0
  73. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/distillation.py +0 -0
  74. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/evaluation/__init__.py +0 -0
  75. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/evaluation/ttu.py +0 -0
  76. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/hooks.py +0 -0
  77. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/ipc_auth.py +0 -0
  78. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/issues.py +0 -0
  79. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/jules_client.py +0 -0
  80. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/llm.py +0 -0
  81. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/logger.py +0 -0
  82. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/metrics.py +0 -0
  83. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/models/delegation.py +0 -0
  84. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/onboarding.py +0 -0
  85. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/review.py +0 -0
  86. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/services/delegation_service.py +0 -0
  87. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/services/validation_service.py +0 -0
  88. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/sessions.py +0 -0
  89. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/sprint_collector.py +0 -0
  90. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tmux.py +0 -0
  91. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/__init__.py +0 -0
  92. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/executor.py +0 -0
  93. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/file_tools.py +0 -0
  94. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/interface.py +0 -0
  95. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/jules_tools.py +0 -0
  96. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/kit_tools.py +0 -0
  97. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tools/registry.py +0 -0
  98. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/__init__.py +0 -0
  99. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/app.py +0 -0
  100. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/commands.py +0 -0
  101. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/tui/widgets.py +0 -0
  102. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder/worktree.py +0 -0
  103. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/dependency_links.txt +0 -0
  104. {onecoder-0.0.2 → onecoder-0.0.4}/onecoder.egg-info/entry_points.txt +0 -0
  105. {onecoder-0.0.2 → onecoder-0.0.4}/setup.cfg +0 -0
  106. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_agent_governance.py +0 -0
  107. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_blackboard.py +0 -0
  108. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_delegation_service.py +0 -0
  109. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_durable_sessions.py +0 -0
  110. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_error_scenarios.py +0 -0
  111. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_jules_tools.py +0 -0
  112. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_openrouter.py +0 -0
  113. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_textual_tui.py +0 -0
  114. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tmux.py +0 -0
  115. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_token_expiration.py +0 -0
  116. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_token_lifecycle.py +0 -0
  117. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tui_commands.py +0 -0
  118. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tui_session.py +0 -0
  119. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_tui_simple.py +0 -0
  120. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_validation_service.py +0 -0
  121. {onecoder-0.0.2 → onecoder-0.0.4}/tests/test_version.py +0 -0
  122. {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.2
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: rank_bm25
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)