spec-kitty-cli 0.12.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.
- spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
- spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
- spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
- spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
- spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
- specify_cli/__init__.py +171 -0
- specify_cli/acceptance.py +627 -0
- specify_cli/agent_utils/README.md +157 -0
- specify_cli/agent_utils/__init__.py +9 -0
- specify_cli/agent_utils/status.py +356 -0
- specify_cli/cli/__init__.py +6 -0
- specify_cli/cli/commands/__init__.py +46 -0
- specify_cli/cli/commands/accept.py +189 -0
- specify_cli/cli/commands/agent/__init__.py +22 -0
- specify_cli/cli/commands/agent/config.py +382 -0
- specify_cli/cli/commands/agent/context.py +191 -0
- specify_cli/cli/commands/agent/feature.py +1057 -0
- specify_cli/cli/commands/agent/release.py +11 -0
- specify_cli/cli/commands/agent/tasks.py +1253 -0
- specify_cli/cli/commands/agent/workflow.py +801 -0
- specify_cli/cli/commands/context.py +246 -0
- specify_cli/cli/commands/dashboard.py +85 -0
- specify_cli/cli/commands/implement.py +973 -0
- specify_cli/cli/commands/init.py +827 -0
- specify_cli/cli/commands/init_help.py +62 -0
- specify_cli/cli/commands/merge.py +755 -0
- specify_cli/cli/commands/mission.py +240 -0
- specify_cli/cli/commands/ops.py +265 -0
- specify_cli/cli/commands/orchestrate.py +640 -0
- specify_cli/cli/commands/repair.py +175 -0
- specify_cli/cli/commands/research.py +165 -0
- specify_cli/cli/commands/sync.py +364 -0
- specify_cli/cli/commands/upgrade.py +249 -0
- specify_cli/cli/commands/validate_encoding.py +186 -0
- specify_cli/cli/commands/validate_tasks.py +186 -0
- specify_cli/cli/commands/verify.py +310 -0
- specify_cli/cli/helpers.py +123 -0
- specify_cli/cli/step_tracker.py +91 -0
- specify_cli/cli/ui.py +192 -0
- specify_cli/core/__init__.py +53 -0
- specify_cli/core/agent_context.py +311 -0
- specify_cli/core/config.py +96 -0
- specify_cli/core/context_validation.py +362 -0
- specify_cli/core/dependency_graph.py +351 -0
- specify_cli/core/git_ops.py +129 -0
- specify_cli/core/multi_parent_merge.py +323 -0
- specify_cli/core/paths.py +260 -0
- specify_cli/core/project_resolver.py +110 -0
- specify_cli/core/stale_detection.py +263 -0
- specify_cli/core/tool_checker.py +79 -0
- specify_cli/core/utils.py +43 -0
- specify_cli/core/vcs/__init__.py +114 -0
- specify_cli/core/vcs/detection.py +341 -0
- specify_cli/core/vcs/exceptions.py +85 -0
- specify_cli/core/vcs/git.py +1304 -0
- specify_cli/core/vcs/jujutsu.py +1208 -0
- specify_cli/core/vcs/protocol.py +285 -0
- specify_cli/core/vcs/types.py +249 -0
- specify_cli/core/version_checker.py +261 -0
- specify_cli/core/worktree.py +506 -0
- specify_cli/dashboard/__init__.py +28 -0
- specify_cli/dashboard/diagnostics.py +204 -0
- specify_cli/dashboard/handlers/__init__.py +17 -0
- specify_cli/dashboard/handlers/api.py +143 -0
- specify_cli/dashboard/handlers/base.py +65 -0
- specify_cli/dashboard/handlers/features.py +390 -0
- specify_cli/dashboard/handlers/router.py +81 -0
- specify_cli/dashboard/handlers/static.py +50 -0
- specify_cli/dashboard/lifecycle.py +541 -0
- specify_cli/dashboard/scanner.py +437 -0
- specify_cli/dashboard/server.py +123 -0
- specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
- specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
- specify_cli/dashboard/static/spec-kitty.png +0 -0
- specify_cli/dashboard/templates/__init__.py +36 -0
- specify_cli/dashboard/templates/index.html +258 -0
- specify_cli/doc_generators.py +621 -0
- specify_cli/doc_state.py +408 -0
- specify_cli/frontmatter.py +384 -0
- specify_cli/gap_analysis.py +915 -0
- specify_cli/gitignore_manager.py +300 -0
- specify_cli/guards.py +145 -0
- specify_cli/legacy_detector.py +83 -0
- specify_cli/manifest.py +286 -0
- specify_cli/merge/__init__.py +63 -0
- specify_cli/merge/executor.py +653 -0
- specify_cli/merge/forecast.py +215 -0
- specify_cli/merge/ordering.py +126 -0
- specify_cli/merge/preflight.py +230 -0
- specify_cli/merge/state.py +185 -0
- specify_cli/merge/status_resolver.py +354 -0
- specify_cli/mission.py +654 -0
- specify_cli/missions/documentation/command-templates/implement.md +309 -0
- specify_cli/missions/documentation/command-templates/plan.md +275 -0
- specify_cli/missions/documentation/command-templates/review.md +344 -0
- specify_cli/missions/documentation/command-templates/specify.md +206 -0
- specify_cli/missions/documentation/command-templates/tasks.md +189 -0
- specify_cli/missions/documentation/mission.yaml +113 -0
- specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
- specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
- specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
- specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
- specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
- specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
- specify_cli/missions/documentation/templates/plan-template.md +269 -0
- specify_cli/missions/documentation/templates/release-template.md +222 -0
- specify_cli/missions/documentation/templates/spec-template.md +172 -0
- specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
- specify_cli/missions/documentation/templates/tasks-template.md +159 -0
- specify_cli/missions/research/command-templates/merge.md +388 -0
- specify_cli/missions/research/command-templates/plan.md +125 -0
- specify_cli/missions/research/command-templates/review.md +144 -0
- specify_cli/missions/research/command-templates/tasks.md +225 -0
- specify_cli/missions/research/mission.yaml +115 -0
- specify_cli/missions/research/templates/data-model-template.md +33 -0
- specify_cli/missions/research/templates/plan-template.md +161 -0
- specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
- specify_cli/missions/research/templates/research/source-register.csv +18 -0
- specify_cli/missions/research/templates/research-template.md +35 -0
- specify_cli/missions/research/templates/spec-template.md +64 -0
- specify_cli/missions/research/templates/task-prompt-template.md +148 -0
- specify_cli/missions/research/templates/tasks-template.md +114 -0
- specify_cli/missions/software-dev/command-templates/accept.md +75 -0
- specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
- specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
- specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
- specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
- specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
- specify_cli/missions/software-dev/command-templates/implement.md +41 -0
- specify_cli/missions/software-dev/command-templates/merge.md +383 -0
- specify_cli/missions/software-dev/command-templates/plan.md +171 -0
- specify_cli/missions/software-dev/command-templates/review.md +32 -0
- specify_cli/missions/software-dev/command-templates/specify.md +321 -0
- specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
- specify_cli/missions/software-dev/mission.yaml +100 -0
- specify_cli/missions/software-dev/templates/plan-template.md +132 -0
- specify_cli/missions/software-dev/templates/spec-template.md +116 -0
- specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
- specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
- specify_cli/orchestrator/__init__.py +75 -0
- specify_cli/orchestrator/agent_config.py +224 -0
- specify_cli/orchestrator/agents/__init__.py +170 -0
- specify_cli/orchestrator/agents/augment.py +112 -0
- specify_cli/orchestrator/agents/base.py +243 -0
- specify_cli/orchestrator/agents/claude.py +112 -0
- specify_cli/orchestrator/agents/codex.py +106 -0
- specify_cli/orchestrator/agents/copilot.py +137 -0
- specify_cli/orchestrator/agents/cursor.py +139 -0
- specify_cli/orchestrator/agents/gemini.py +115 -0
- specify_cli/orchestrator/agents/kilocode.py +94 -0
- specify_cli/orchestrator/agents/opencode.py +132 -0
- specify_cli/orchestrator/agents/qwen.py +96 -0
- specify_cli/orchestrator/config.py +455 -0
- specify_cli/orchestrator/executor.py +642 -0
- specify_cli/orchestrator/integration.py +1230 -0
- specify_cli/orchestrator/monitor.py +898 -0
- specify_cli/orchestrator/scheduler.py +832 -0
- specify_cli/orchestrator/state.py +508 -0
- specify_cli/orchestrator/testing/__init__.py +122 -0
- specify_cli/orchestrator/testing/availability.py +346 -0
- specify_cli/orchestrator/testing/fixtures.py +684 -0
- specify_cli/orchestrator/testing/paths.py +218 -0
- specify_cli/plan_validation.py +107 -0
- specify_cli/scripts/debug-dashboard-scan.py +61 -0
- specify_cli/scripts/tasks/acceptance_support.py +695 -0
- specify_cli/scripts/tasks/task_helpers.py +506 -0
- specify_cli/scripts/tasks/tasks_cli.py +848 -0
- specify_cli/scripts/validate_encoding.py +180 -0
- specify_cli/task_metadata_validation.py +274 -0
- specify_cli/tasks_support.py +447 -0
- specify_cli/template/__init__.py +47 -0
- specify_cli/template/asset_generator.py +206 -0
- specify_cli/template/github_client.py +334 -0
- specify_cli/template/manager.py +193 -0
- specify_cli/template/renderer.py +99 -0
- specify_cli/templates/AGENTS.md +190 -0
- specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
- specify_cli/templates/agent-file-template.md +35 -0
- specify_cli/templates/checklist-template.md +42 -0
- specify_cli/templates/claudeignore-template +58 -0
- specify_cli/templates/command-templates/accept.md +141 -0
- specify_cli/templates/command-templates/analyze.md +253 -0
- specify_cli/templates/command-templates/checklist.md +352 -0
- specify_cli/templates/command-templates/clarify.md +224 -0
- specify_cli/templates/command-templates/constitution.md +432 -0
- specify_cli/templates/command-templates/dashboard.md +175 -0
- specify_cli/templates/command-templates/implement.md +190 -0
- specify_cli/templates/command-templates/merge.md +374 -0
- specify_cli/templates/command-templates/plan.md +171 -0
- specify_cli/templates/command-templates/research.md +88 -0
- specify_cli/templates/command-templates/review.md +510 -0
- specify_cli/templates/command-templates/specify.md +321 -0
- specify_cli/templates/command-templates/status.md +92 -0
- specify_cli/templates/command-templates/tasks.md +199 -0
- specify_cli/templates/git-hooks/pre-commit +22 -0
- specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
- specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
- specify_cli/templates/plan-template.md +108 -0
- specify_cli/templates/spec-template.md +118 -0
- specify_cli/templates/task-prompt-template.md +165 -0
- specify_cli/templates/tasks-template.md +161 -0
- specify_cli/templates/vscode-settings.json +13 -0
- specify_cli/text_sanitization.py +225 -0
- specify_cli/upgrade/__init__.py +18 -0
- specify_cli/upgrade/detector.py +239 -0
- specify_cli/upgrade/metadata.py +182 -0
- specify_cli/upgrade/migrations/__init__.py +65 -0
- specify_cli/upgrade/migrations/base.py +80 -0
- specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
- specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
- specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
- specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
- specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
- specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
- specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
- specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
- specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
- specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
- specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
- specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
- specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
- specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
- specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
- specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
- specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
- specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
- specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
- specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
- specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
- specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
- specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
- specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
- specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
- specify_cli/upgrade/registry.py +121 -0
- specify_cli/upgrade/runner.py +284 -0
- specify_cli/validators/__init__.py +14 -0
- specify_cli/validators/paths.py +154 -0
- specify_cli/validators/research.py +428 -0
- specify_cli/verify_enhanced.py +270 -0
- specify_cli/workspace_context.py +224 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Project path resolution helpers for Spec Kitty."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from specify_cli.core.config import DEFAULT_MISSION_KEY
|
|
12
|
+
|
|
13
|
+
ConsoleType = Console | None
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _resolve_console(console: ConsoleType) -> Console:
|
|
17
|
+
return console if console is not None else Console()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def locate_project_root(start: Path | None = None) -> Optional[Path]:
|
|
21
|
+
"""Walk upwards from *start* (or CWD) to find the directory that owns .kittify."""
|
|
22
|
+
current = (start or Path.cwd()).resolve()
|
|
23
|
+
for candidate in [current, *current.parents]:
|
|
24
|
+
if (candidate / ".kittify").is_dir():
|
|
25
|
+
return candidate
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def resolve_template_path(project_root: Path, mission_key: str, template_subpath: str | Path) -> Optional[Path]:
|
|
30
|
+
"""Resolve a template path, preferring mission overrides before global templates."""
|
|
31
|
+
subpath = Path(template_subpath)
|
|
32
|
+
candidates = [
|
|
33
|
+
project_root / ".kittify" / "missions" / mission_key / "templates" / subpath,
|
|
34
|
+
project_root / ".kittify" / "templates" / subpath,
|
|
35
|
+
project_root / "templates" / subpath,
|
|
36
|
+
]
|
|
37
|
+
for candidate in candidates:
|
|
38
|
+
if candidate.exists():
|
|
39
|
+
return candidate
|
|
40
|
+
return None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def resolve_worktree_aware_feature_dir(
|
|
44
|
+
repo_root: Path,
|
|
45
|
+
feature_slug: str,
|
|
46
|
+
cwd: Path | None = None,
|
|
47
|
+
console: ConsoleType = None,
|
|
48
|
+
) -> Path:
|
|
49
|
+
"""Resolve the correct feature directory, preferring worktree locations when available."""
|
|
50
|
+
resolved_console = _resolve_console(console)
|
|
51
|
+
current_dir = (cwd or Path.cwd()).resolve()
|
|
52
|
+
|
|
53
|
+
parts = current_dir.parts
|
|
54
|
+
for idx, part in enumerate(parts):
|
|
55
|
+
if part == ".worktrees" and idx + 1 < len(parts) and parts[idx + 1] == feature_slug:
|
|
56
|
+
worktree_root = Path(*parts[: idx + 2])
|
|
57
|
+
feature_dir = worktree_root / "kitty-specs" / feature_slug
|
|
58
|
+
resolved_console.print(f"[green]✓[/green] Using worktree location: {feature_dir}")
|
|
59
|
+
return feature_dir
|
|
60
|
+
|
|
61
|
+
worktree_path = repo_root / ".worktrees" / feature_slug
|
|
62
|
+
if worktree_path.exists():
|
|
63
|
+
feature_dir = worktree_path / "kitty-specs" / feature_slug
|
|
64
|
+
resolved_console.print(f"[green]✓[/green] Found worktree, using: {feature_dir}")
|
|
65
|
+
resolved_console.print(f"[yellow]Tip:[/yellow] Run commands from {worktree_path} for better isolation")
|
|
66
|
+
return feature_dir
|
|
67
|
+
|
|
68
|
+
feature_dir = repo_root / "kitty-specs" / feature_slug
|
|
69
|
+
resolved_console.print(f"[yellow]⚠[/yellow] No worktree found, using root location: {feature_dir}")
|
|
70
|
+
resolved_console.print(
|
|
71
|
+
f"[yellow]Tip:[/yellow] Consider creating a worktree with: git worktree add .worktrees/{feature_slug} {feature_slug}"
|
|
72
|
+
)
|
|
73
|
+
return feature_dir
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def get_active_mission_key(project_path: Path) -> str:
|
|
77
|
+
"""Return the mission key stored in .kittify/active-mission, falling back to default."""
|
|
78
|
+
active_path = project_path / ".kittify" / "active-mission"
|
|
79
|
+
if not active_path.exists():
|
|
80
|
+
return DEFAULT_MISSION_KEY
|
|
81
|
+
|
|
82
|
+
if active_path.is_symlink():
|
|
83
|
+
try:
|
|
84
|
+
target = Path(os.readlink(active_path))
|
|
85
|
+
key = target.name
|
|
86
|
+
if key:
|
|
87
|
+
return key
|
|
88
|
+
except OSError:
|
|
89
|
+
pass
|
|
90
|
+
resolved = active_path.resolve()
|
|
91
|
+
if resolved.parent.name == "missions":
|
|
92
|
+
return resolved.name
|
|
93
|
+
|
|
94
|
+
if active_path.is_file():
|
|
95
|
+
try:
|
|
96
|
+
key = active_path.read_text(encoding="utf-8-sig").strip()
|
|
97
|
+
if key:
|
|
98
|
+
return key
|
|
99
|
+
except OSError:
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
return DEFAULT_MISSION_KEY
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
__all__ = [
|
|
106
|
+
"get_active_mission_key",
|
|
107
|
+
"locate_project_root",
|
|
108
|
+
"resolve_template_path",
|
|
109
|
+
"resolve_worktree_aware_feature_dir",
|
|
110
|
+
]
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Stale Work Package Detection
|
|
3
|
+
============================
|
|
4
|
+
|
|
5
|
+
Detects work packages that are in "doing" lane but have no recent VCS activity,
|
|
6
|
+
indicating the agent may have stopped without transitioning the WP.
|
|
7
|
+
|
|
8
|
+
Uses git/jj commit timestamps as a "heartbeat" - if no commits for a threshold
|
|
9
|
+
period, the WP is considered stale.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
from specify_cli.core.vcs import get_vcs, VCSError
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class StaleCheckResult:
|
|
23
|
+
"""Result of checking a work package for staleness."""
|
|
24
|
+
|
|
25
|
+
wp_id: str
|
|
26
|
+
is_stale: bool
|
|
27
|
+
last_commit_time: datetime | None
|
|
28
|
+
minutes_since_commit: float | None
|
|
29
|
+
worktree_exists: bool
|
|
30
|
+
error: str | None = None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def get_last_meaningful_commit_time(worktree_path: Path) -> tuple[datetime | None, bool]:
|
|
34
|
+
"""
|
|
35
|
+
Get the timestamp of the most recent meaningful commit in a worktree.
|
|
36
|
+
|
|
37
|
+
A "meaningful" commit is one made ON THIS BRANCH since it diverged from main.
|
|
38
|
+
This prevents false staleness when a worktree is just created but no commits
|
|
39
|
+
have been made yet (HEAD points to parent branch's old commit).
|
|
40
|
+
|
|
41
|
+
For worktrees, we always use git to check the branch-specific history,
|
|
42
|
+
even in jj colocated repos. This is because:
|
|
43
|
+
- jj's shared history includes commits from ALL workspaces
|
|
44
|
+
- jj continuously auto-snapshots the working copy
|
|
45
|
+
- We need the last commit on THIS worktree's branch, not the shared history
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
worktree_path: Path to the worktree
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Tuple of (datetime of last commit on this branch, has_own_commits).
|
|
52
|
+
has_own_commits is False if the branch has no commits since diverging from main.
|
|
53
|
+
"""
|
|
54
|
+
import subprocess
|
|
55
|
+
|
|
56
|
+
if not worktree_path.exists():
|
|
57
|
+
return None, False
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
# First, check if this branch has any commits since diverging from main
|
|
61
|
+
# This prevents false staleness when worktree was just created
|
|
62
|
+
merge_base_result = subprocess.run(
|
|
63
|
+
["git", "merge-base", "HEAD", "main"],
|
|
64
|
+
cwd=worktree_path,
|
|
65
|
+
capture_output=True,
|
|
66
|
+
text=True,
|
|
67
|
+
timeout=10,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if merge_base_result.returncode == 0:
|
|
71
|
+
merge_base = merge_base_result.stdout.strip()
|
|
72
|
+
|
|
73
|
+
# Count commits on this branch since the merge base
|
|
74
|
+
count_result = subprocess.run(
|
|
75
|
+
["git", "rev-list", "--count", f"{merge_base}..HEAD"],
|
|
76
|
+
cwd=worktree_path,
|
|
77
|
+
capture_output=True,
|
|
78
|
+
text=True,
|
|
79
|
+
timeout=10,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
if count_result.returncode == 0:
|
|
83
|
+
commit_count = int(count_result.stdout.strip())
|
|
84
|
+
if commit_count == 0:
|
|
85
|
+
# No commits on this branch yet - worktree just created
|
|
86
|
+
# Don't flag as stale since agent just started
|
|
87
|
+
return None, False
|
|
88
|
+
|
|
89
|
+
# Get the last commit time on this branch
|
|
90
|
+
result = subprocess.run(
|
|
91
|
+
["git", "log", "-1", "--format=%cI"],
|
|
92
|
+
cwd=worktree_path,
|
|
93
|
+
capture_output=True,
|
|
94
|
+
text=True,
|
|
95
|
+
timeout=10,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
if result.returncode != 0 or not result.stdout.strip():
|
|
99
|
+
return None, False
|
|
100
|
+
|
|
101
|
+
# Parse ISO format timestamp
|
|
102
|
+
timestamp_str = result.stdout.strip()
|
|
103
|
+
return datetime.fromisoformat(timestamp_str), True
|
|
104
|
+
|
|
105
|
+
except subprocess.TimeoutExpired:
|
|
106
|
+
return None, False
|
|
107
|
+
except Exception:
|
|
108
|
+
return None, False
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def check_wp_staleness(
|
|
112
|
+
wp_id: str,
|
|
113
|
+
worktree_path: Path,
|
|
114
|
+
threshold_minutes: int = 10,
|
|
115
|
+
) -> StaleCheckResult:
|
|
116
|
+
"""
|
|
117
|
+
Check if a work package is stale based on VCS activity.
|
|
118
|
+
|
|
119
|
+
A WP is considered stale if:
|
|
120
|
+
- Its worktree exists
|
|
121
|
+
- The branch has commits since diverging from main (agent has done work)
|
|
122
|
+
- The last commit is older than threshold_minutes
|
|
123
|
+
|
|
124
|
+
A WP with a worktree but NO commits since diverging is NOT stale - the agent
|
|
125
|
+
just started and hasn't committed yet.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
wp_id: Work package ID (e.g., "WP01")
|
|
129
|
+
worktree_path: Path to the WP's worktree
|
|
130
|
+
threshold_minutes: Minutes of inactivity before considered stale
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
StaleCheckResult with staleness status
|
|
134
|
+
"""
|
|
135
|
+
if not worktree_path.exists():
|
|
136
|
+
return StaleCheckResult(
|
|
137
|
+
wp_id=wp_id,
|
|
138
|
+
is_stale=False,
|
|
139
|
+
last_commit_time=None,
|
|
140
|
+
minutes_since_commit=None,
|
|
141
|
+
worktree_exists=False,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
last_commit, has_own_commits = get_last_meaningful_commit_time(worktree_path)
|
|
146
|
+
|
|
147
|
+
if last_commit is None:
|
|
148
|
+
# Can't determine commit time, or no commits on this branch yet
|
|
149
|
+
# If no commits yet (has_own_commits=False), agent just started - not stale
|
|
150
|
+
return StaleCheckResult(
|
|
151
|
+
wp_id=wp_id,
|
|
152
|
+
is_stale=False,
|
|
153
|
+
last_commit_time=None,
|
|
154
|
+
minutes_since_commit=None,
|
|
155
|
+
worktree_exists=True,
|
|
156
|
+
error=None if not has_own_commits else "Could not determine last commit time",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
now = datetime.now(timezone.utc)
|
|
160
|
+
# Ensure last_commit is timezone-aware
|
|
161
|
+
if last_commit.tzinfo is None:
|
|
162
|
+
last_commit = last_commit.replace(tzinfo=timezone.utc)
|
|
163
|
+
|
|
164
|
+
delta = now - last_commit
|
|
165
|
+
minutes_since = delta.total_seconds() / 60
|
|
166
|
+
|
|
167
|
+
is_stale = minutes_since > threshold_minutes
|
|
168
|
+
|
|
169
|
+
return StaleCheckResult(
|
|
170
|
+
wp_id=wp_id,
|
|
171
|
+
is_stale=is_stale,
|
|
172
|
+
last_commit_time=last_commit,
|
|
173
|
+
minutes_since_commit=round(minutes_since, 1),
|
|
174
|
+
worktree_exists=True,
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
except Exception as e:
|
|
178
|
+
return StaleCheckResult(
|
|
179
|
+
wp_id=wp_id,
|
|
180
|
+
is_stale=False,
|
|
181
|
+
last_commit_time=None,
|
|
182
|
+
minutes_since_commit=None,
|
|
183
|
+
worktree_exists=True,
|
|
184
|
+
error=str(e),
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def find_worktree_for_wp(
|
|
189
|
+
main_repo_root: Path,
|
|
190
|
+
feature_slug: str,
|
|
191
|
+
wp_id: str,
|
|
192
|
+
) -> Path | None:
|
|
193
|
+
"""
|
|
194
|
+
Find the worktree path for a given work package.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
main_repo_root: Root of the main repository
|
|
198
|
+
feature_slug: Feature slug (e.g., "001-my-feature")
|
|
199
|
+
wp_id: Work package ID (e.g., "WP01")
|
|
200
|
+
|
|
201
|
+
Returns:
|
|
202
|
+
Path to worktree if found, None otherwise
|
|
203
|
+
"""
|
|
204
|
+
worktrees_dir = main_repo_root / ".worktrees"
|
|
205
|
+
if not worktrees_dir.exists():
|
|
206
|
+
return None
|
|
207
|
+
|
|
208
|
+
# Expected pattern: feature_slug-WP01
|
|
209
|
+
expected_name = f"{feature_slug}-{wp_id}"
|
|
210
|
+
worktree_path = worktrees_dir / expected_name
|
|
211
|
+
|
|
212
|
+
if worktree_path.exists():
|
|
213
|
+
return worktree_path
|
|
214
|
+
|
|
215
|
+
# Try case-insensitive search
|
|
216
|
+
for item in worktrees_dir.iterdir():
|
|
217
|
+
if item.is_dir() and item.name.lower() == expected_name.lower():
|
|
218
|
+
return item
|
|
219
|
+
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def check_doing_wps_for_staleness(
|
|
224
|
+
main_repo_root: Path,
|
|
225
|
+
feature_slug: str,
|
|
226
|
+
doing_wps: list[dict],
|
|
227
|
+
threshold_minutes: int = 10,
|
|
228
|
+
) -> dict[str, StaleCheckResult]:
|
|
229
|
+
"""
|
|
230
|
+
Check all WPs in "doing" lane for staleness.
|
|
231
|
+
|
|
232
|
+
Args:
|
|
233
|
+
main_repo_root: Root of the main repository
|
|
234
|
+
feature_slug: Feature slug
|
|
235
|
+
doing_wps: List of WP dicts with at least 'id' key
|
|
236
|
+
threshold_minutes: Minutes of inactivity threshold
|
|
237
|
+
|
|
238
|
+
Returns:
|
|
239
|
+
Dict mapping WP ID to StaleCheckResult
|
|
240
|
+
"""
|
|
241
|
+
results = {}
|
|
242
|
+
|
|
243
|
+
for wp in doing_wps:
|
|
244
|
+
wp_id = wp.get("id") or wp.get("work_package_id")
|
|
245
|
+
if not wp_id:
|
|
246
|
+
continue
|
|
247
|
+
|
|
248
|
+
worktree_path = find_worktree_for_wp(main_repo_root, feature_slug, wp_id)
|
|
249
|
+
|
|
250
|
+
if worktree_path:
|
|
251
|
+
result = check_wp_staleness(wp_id, worktree_path, threshold_minutes)
|
|
252
|
+
else:
|
|
253
|
+
result = StaleCheckResult(
|
|
254
|
+
wp_id=wp_id,
|
|
255
|
+
is_stale=False,
|
|
256
|
+
last_commit_time=None,
|
|
257
|
+
minutes_since_commit=None,
|
|
258
|
+
worktree_exists=False,
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
results[wp_id] = result
|
|
262
|
+
|
|
263
|
+
return results
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Tool availability helpers for Spec Kitty."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import subprocess
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Dict, Mapping, Tuple, TYPE_CHECKING
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from specify_cli.cli import StepTracker
|
|
13
|
+
|
|
14
|
+
from specify_cli.core.config import AGENT_TOOL_REQUIREMENTS, IDE_AGENTS
|
|
15
|
+
|
|
16
|
+
CLAUDE_LOCAL_PATH = Path.home() / ".claude" / "local" / "claude"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def check_tool_for_tracker(tool: str, tracker: "StepTracker") -> bool:
|
|
20
|
+
"""Check if a tool is installed and update the provided StepTracker instance."""
|
|
21
|
+
if shutil.which(tool):
|
|
22
|
+
tracker.complete(tool, "available")
|
|
23
|
+
return True
|
|
24
|
+
tracker.error(tool, "not found")
|
|
25
|
+
return False
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check_tool(tool: str, install_hint: str, agent_name: str | None = None) -> bool:
|
|
29
|
+
"""Return True when the tool is available on PATH (with Claude CLI override and IDE agent bypass).
|
|
30
|
+
|
|
31
|
+
IDE-integrated agents (cursor, windsurf, copilot, kilocode) don't require CLI
|
|
32
|
+
installation, so we skip the availability check for them.
|
|
33
|
+
"""
|
|
34
|
+
# Skip CLI checks for IDE agents - they run within the IDE, not as CLI tools
|
|
35
|
+
if agent_name and agent_name in IDE_AGENTS:
|
|
36
|
+
return True
|
|
37
|
+
|
|
38
|
+
# Special case: Claude local installation
|
|
39
|
+
if tool == "claude" and CLAUDE_LOCAL_PATH.exists() and CLAUDE_LOCAL_PATH.is_file():
|
|
40
|
+
return True
|
|
41
|
+
|
|
42
|
+
return shutil.which(tool) is not None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def get_tool_version(command: str) -> str | None:
|
|
46
|
+
"""Return the version string for a tool if the convention '<tool> --version' succeeds."""
|
|
47
|
+
try:
|
|
48
|
+
result = subprocess.run(
|
|
49
|
+
[command, "--version"],
|
|
50
|
+
check=True,
|
|
51
|
+
capture_output=True,
|
|
52
|
+
text=True,
|
|
53
|
+
)
|
|
54
|
+
except (subprocess.CalledProcessError, FileNotFoundError, OSError):
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
output = (result.stdout or result.stderr or "").strip()
|
|
58
|
+
return output or None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def check_all_tools(
|
|
62
|
+
requirements: Mapping[str, Tuple[str, str]] | None = None,
|
|
63
|
+
) -> Dict[str, Tuple[bool, str]]:
|
|
64
|
+
"""Check tool availability for all known agents, returning {agent: (ok, detail)}."""
|
|
65
|
+
results: Dict[str, Tuple[bool, str]] = {}
|
|
66
|
+
entries = requirements or AGENT_TOOL_REQUIREMENTS
|
|
67
|
+
for agent, (tool, url) in entries.items():
|
|
68
|
+
ok = check_tool(tool, url)
|
|
69
|
+
detail = get_tool_version(tool) if ok else url
|
|
70
|
+
results[agent] = (ok, detail or url)
|
|
71
|
+
return results
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"check_all_tools",
|
|
76
|
+
"check_tool",
|
|
77
|
+
"check_tool_for_tracker",
|
|
78
|
+
"get_tool_version",
|
|
79
|
+
]
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Shared utility helpers used across Spec Kitty modules."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shutil
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def format_path(path: Path, relative_to: Path | None = None) -> str:
|
|
11
|
+
"""Return a string path, optionally relative to another directory."""
|
|
12
|
+
target = path
|
|
13
|
+
if relative_to is not None:
|
|
14
|
+
try:
|
|
15
|
+
target = path.relative_to(relative_to)
|
|
16
|
+
except ValueError:
|
|
17
|
+
target = path
|
|
18
|
+
return str(target)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def ensure_directory(path: Path) -> Path:
|
|
22
|
+
"""Create a directory (and parents) if it does not exist and return the Path."""
|
|
23
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
24
|
+
return path
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def safe_remove(path: Path) -> bool:
|
|
28
|
+
"""Remove a file or directory tree if it exists, returning True when something was removed."""
|
|
29
|
+
if not path.exists():
|
|
30
|
+
return False
|
|
31
|
+
if path.is_dir() and not path.is_symlink():
|
|
32
|
+
shutil.rmtree(path)
|
|
33
|
+
else:
|
|
34
|
+
path.unlink()
|
|
35
|
+
return True
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def get_platform() -> str:
|
|
39
|
+
"""Return the current platform identifier (linux/darwin/win32)."""
|
|
40
|
+
return sys.platform
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
__all__ = ["format_path", "ensure_directory", "safe_remove", "get_platform"]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
VCS Abstraction Package
|
|
3
|
+
=======================
|
|
4
|
+
|
|
5
|
+
This package provides a unified interface for Version Control System operations,
|
|
6
|
+
supporting both Git and Jujutsu (jj) backends.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
from specify_cli.core.vcs import (
|
|
10
|
+
get_vcs,
|
|
11
|
+
VCSProtocol,
|
|
12
|
+
VCSBackend,
|
|
13
|
+
VCSCapabilities,
|
|
14
|
+
GIT_CAPABILITIES,
|
|
15
|
+
JJ_CAPABILITIES,
|
|
16
|
+
is_jj_available,
|
|
17
|
+
is_git_available,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
# Get appropriate VCS implementation
|
|
21
|
+
vcs = get_vcs(feature_path) # Auto-detect, prefers jj
|
|
22
|
+
vcs = get_vcs(feature_path, backend=VCSBackend.GIT) # Explicit git
|
|
23
|
+
|
|
24
|
+
See kitty-specs/015-first-class-jujutsu-vcs-integration/ for full documentation.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
# Enums
|
|
30
|
+
from .types import (
|
|
31
|
+
ConflictType,
|
|
32
|
+
SyncStatus,
|
|
33
|
+
VCSBackend,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
# Dataclasses
|
|
37
|
+
from .types import (
|
|
38
|
+
ChangeInfo,
|
|
39
|
+
ConflictInfo,
|
|
40
|
+
FeatureVCSConfig,
|
|
41
|
+
OperationInfo,
|
|
42
|
+
ProjectVCSConfig,
|
|
43
|
+
SyncResult,
|
|
44
|
+
VCSCapabilities,
|
|
45
|
+
WorkspaceCreateResult,
|
|
46
|
+
WorkspaceInfo,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
# Capability constants
|
|
50
|
+
from .types import (
|
|
51
|
+
GIT_CAPABILITIES,
|
|
52
|
+
JJ_CAPABILITIES,
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# Protocol
|
|
56
|
+
from .protocol import VCSProtocol
|
|
57
|
+
|
|
58
|
+
# Exceptions
|
|
59
|
+
from .exceptions import (
|
|
60
|
+
VCSBackendMismatchError,
|
|
61
|
+
VCSCapabilityError,
|
|
62
|
+
VCSConflictError,
|
|
63
|
+
VCSError,
|
|
64
|
+
VCSLockError,
|
|
65
|
+
VCSNotFoundError,
|
|
66
|
+
VCSSyncError,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
# Detection and factory functions
|
|
70
|
+
from .detection import (
|
|
71
|
+
detect_available_backends,
|
|
72
|
+
get_git_version,
|
|
73
|
+
get_jj_version,
|
|
74
|
+
get_vcs,
|
|
75
|
+
is_git_available,
|
|
76
|
+
is_jj_available,
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
__all__ = [
|
|
80
|
+
# Enums
|
|
81
|
+
"VCSBackend",
|
|
82
|
+
"SyncStatus",
|
|
83
|
+
"ConflictType",
|
|
84
|
+
# Dataclasses
|
|
85
|
+
"VCSCapabilities",
|
|
86
|
+
"ChangeInfo",
|
|
87
|
+
"ConflictInfo",
|
|
88
|
+
"SyncResult",
|
|
89
|
+
"WorkspaceInfo",
|
|
90
|
+
"OperationInfo",
|
|
91
|
+
"WorkspaceCreateResult",
|
|
92
|
+
"ProjectVCSConfig",
|
|
93
|
+
"FeatureVCSConfig",
|
|
94
|
+
# Capability constants
|
|
95
|
+
"GIT_CAPABILITIES",
|
|
96
|
+
"JJ_CAPABILITIES",
|
|
97
|
+
# Protocol
|
|
98
|
+
"VCSProtocol",
|
|
99
|
+
# Exceptions
|
|
100
|
+
"VCSError",
|
|
101
|
+
"VCSNotFoundError",
|
|
102
|
+
"VCSCapabilityError",
|
|
103
|
+
"VCSBackendMismatchError",
|
|
104
|
+
"VCSLockError",
|
|
105
|
+
"VCSConflictError",
|
|
106
|
+
"VCSSyncError",
|
|
107
|
+
# Detection and factory
|
|
108
|
+
"get_vcs",
|
|
109
|
+
"is_jj_available",
|
|
110
|
+
"is_git_available",
|
|
111
|
+
"get_jj_version",
|
|
112
|
+
"get_git_version",
|
|
113
|
+
"detect_available_backends",
|
|
114
|
+
]
|