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.
Files changed (242) hide show
  1. spec_kitty_cli-0.12.1.dist-info/METADATA +1767 -0
  2. spec_kitty_cli-0.12.1.dist-info/RECORD +242 -0
  3. spec_kitty_cli-0.12.1.dist-info/WHEEL +4 -0
  4. spec_kitty_cli-0.12.1.dist-info/entry_points.txt +2 -0
  5. spec_kitty_cli-0.12.1.dist-info/licenses/LICENSE +21 -0
  6. specify_cli/__init__.py +171 -0
  7. specify_cli/acceptance.py +627 -0
  8. specify_cli/agent_utils/README.md +157 -0
  9. specify_cli/agent_utils/__init__.py +9 -0
  10. specify_cli/agent_utils/status.py +356 -0
  11. specify_cli/cli/__init__.py +6 -0
  12. specify_cli/cli/commands/__init__.py +46 -0
  13. specify_cli/cli/commands/accept.py +189 -0
  14. specify_cli/cli/commands/agent/__init__.py +22 -0
  15. specify_cli/cli/commands/agent/config.py +382 -0
  16. specify_cli/cli/commands/agent/context.py +191 -0
  17. specify_cli/cli/commands/agent/feature.py +1057 -0
  18. specify_cli/cli/commands/agent/release.py +11 -0
  19. specify_cli/cli/commands/agent/tasks.py +1253 -0
  20. specify_cli/cli/commands/agent/workflow.py +801 -0
  21. specify_cli/cli/commands/context.py +246 -0
  22. specify_cli/cli/commands/dashboard.py +85 -0
  23. specify_cli/cli/commands/implement.py +973 -0
  24. specify_cli/cli/commands/init.py +827 -0
  25. specify_cli/cli/commands/init_help.py +62 -0
  26. specify_cli/cli/commands/merge.py +755 -0
  27. specify_cli/cli/commands/mission.py +240 -0
  28. specify_cli/cli/commands/ops.py +265 -0
  29. specify_cli/cli/commands/orchestrate.py +640 -0
  30. specify_cli/cli/commands/repair.py +175 -0
  31. specify_cli/cli/commands/research.py +165 -0
  32. specify_cli/cli/commands/sync.py +364 -0
  33. specify_cli/cli/commands/upgrade.py +249 -0
  34. specify_cli/cli/commands/validate_encoding.py +186 -0
  35. specify_cli/cli/commands/validate_tasks.py +186 -0
  36. specify_cli/cli/commands/verify.py +310 -0
  37. specify_cli/cli/helpers.py +123 -0
  38. specify_cli/cli/step_tracker.py +91 -0
  39. specify_cli/cli/ui.py +192 -0
  40. specify_cli/core/__init__.py +53 -0
  41. specify_cli/core/agent_context.py +311 -0
  42. specify_cli/core/config.py +96 -0
  43. specify_cli/core/context_validation.py +362 -0
  44. specify_cli/core/dependency_graph.py +351 -0
  45. specify_cli/core/git_ops.py +129 -0
  46. specify_cli/core/multi_parent_merge.py +323 -0
  47. specify_cli/core/paths.py +260 -0
  48. specify_cli/core/project_resolver.py +110 -0
  49. specify_cli/core/stale_detection.py +263 -0
  50. specify_cli/core/tool_checker.py +79 -0
  51. specify_cli/core/utils.py +43 -0
  52. specify_cli/core/vcs/__init__.py +114 -0
  53. specify_cli/core/vcs/detection.py +341 -0
  54. specify_cli/core/vcs/exceptions.py +85 -0
  55. specify_cli/core/vcs/git.py +1304 -0
  56. specify_cli/core/vcs/jujutsu.py +1208 -0
  57. specify_cli/core/vcs/protocol.py +285 -0
  58. specify_cli/core/vcs/types.py +249 -0
  59. specify_cli/core/version_checker.py +261 -0
  60. specify_cli/core/worktree.py +506 -0
  61. specify_cli/dashboard/__init__.py +28 -0
  62. specify_cli/dashboard/diagnostics.py +204 -0
  63. specify_cli/dashboard/handlers/__init__.py +17 -0
  64. specify_cli/dashboard/handlers/api.py +143 -0
  65. specify_cli/dashboard/handlers/base.py +65 -0
  66. specify_cli/dashboard/handlers/features.py +390 -0
  67. specify_cli/dashboard/handlers/router.py +81 -0
  68. specify_cli/dashboard/handlers/static.py +50 -0
  69. specify_cli/dashboard/lifecycle.py +541 -0
  70. specify_cli/dashboard/scanner.py +437 -0
  71. specify_cli/dashboard/server.py +123 -0
  72. specify_cli/dashboard/static/dashboard/dashboard.css +722 -0
  73. specify_cli/dashboard/static/dashboard/dashboard.js +1424 -0
  74. specify_cli/dashboard/static/spec-kitty.png +0 -0
  75. specify_cli/dashboard/templates/__init__.py +36 -0
  76. specify_cli/dashboard/templates/index.html +258 -0
  77. specify_cli/doc_generators.py +621 -0
  78. specify_cli/doc_state.py +408 -0
  79. specify_cli/frontmatter.py +384 -0
  80. specify_cli/gap_analysis.py +915 -0
  81. specify_cli/gitignore_manager.py +300 -0
  82. specify_cli/guards.py +145 -0
  83. specify_cli/legacy_detector.py +83 -0
  84. specify_cli/manifest.py +286 -0
  85. specify_cli/merge/__init__.py +63 -0
  86. specify_cli/merge/executor.py +653 -0
  87. specify_cli/merge/forecast.py +215 -0
  88. specify_cli/merge/ordering.py +126 -0
  89. specify_cli/merge/preflight.py +230 -0
  90. specify_cli/merge/state.py +185 -0
  91. specify_cli/merge/status_resolver.py +354 -0
  92. specify_cli/mission.py +654 -0
  93. specify_cli/missions/documentation/command-templates/implement.md +309 -0
  94. specify_cli/missions/documentation/command-templates/plan.md +275 -0
  95. specify_cli/missions/documentation/command-templates/review.md +344 -0
  96. specify_cli/missions/documentation/command-templates/specify.md +206 -0
  97. specify_cli/missions/documentation/command-templates/tasks.md +189 -0
  98. specify_cli/missions/documentation/mission.yaml +113 -0
  99. specify_cli/missions/documentation/templates/divio/explanation-template.md +192 -0
  100. specify_cli/missions/documentation/templates/divio/howto-template.md +168 -0
  101. specify_cli/missions/documentation/templates/divio/reference-template.md +179 -0
  102. specify_cli/missions/documentation/templates/divio/tutorial-template.md +146 -0
  103. specify_cli/missions/documentation/templates/generators/jsdoc.json.template +18 -0
  104. specify_cli/missions/documentation/templates/generators/sphinx-conf.py.template +36 -0
  105. specify_cli/missions/documentation/templates/plan-template.md +269 -0
  106. specify_cli/missions/documentation/templates/release-template.md +222 -0
  107. specify_cli/missions/documentation/templates/spec-template.md +172 -0
  108. specify_cli/missions/documentation/templates/task-prompt-template.md +140 -0
  109. specify_cli/missions/documentation/templates/tasks-template.md +159 -0
  110. specify_cli/missions/research/command-templates/merge.md +388 -0
  111. specify_cli/missions/research/command-templates/plan.md +125 -0
  112. specify_cli/missions/research/command-templates/review.md +144 -0
  113. specify_cli/missions/research/command-templates/tasks.md +225 -0
  114. specify_cli/missions/research/mission.yaml +115 -0
  115. specify_cli/missions/research/templates/data-model-template.md +33 -0
  116. specify_cli/missions/research/templates/plan-template.md +161 -0
  117. specify_cli/missions/research/templates/research/evidence-log.csv +18 -0
  118. specify_cli/missions/research/templates/research/source-register.csv +18 -0
  119. specify_cli/missions/research/templates/research-template.md +35 -0
  120. specify_cli/missions/research/templates/spec-template.md +64 -0
  121. specify_cli/missions/research/templates/task-prompt-template.md +148 -0
  122. specify_cli/missions/research/templates/tasks-template.md +114 -0
  123. specify_cli/missions/software-dev/command-templates/accept.md +75 -0
  124. specify_cli/missions/software-dev/command-templates/analyze.md +183 -0
  125. specify_cli/missions/software-dev/command-templates/checklist.md +286 -0
  126. specify_cli/missions/software-dev/command-templates/clarify.md +157 -0
  127. specify_cli/missions/software-dev/command-templates/constitution.md +432 -0
  128. specify_cli/missions/software-dev/command-templates/dashboard.md +101 -0
  129. specify_cli/missions/software-dev/command-templates/implement.md +41 -0
  130. specify_cli/missions/software-dev/command-templates/merge.md +383 -0
  131. specify_cli/missions/software-dev/command-templates/plan.md +171 -0
  132. specify_cli/missions/software-dev/command-templates/review.md +32 -0
  133. specify_cli/missions/software-dev/command-templates/specify.md +321 -0
  134. specify_cli/missions/software-dev/command-templates/tasks.md +566 -0
  135. specify_cli/missions/software-dev/mission.yaml +100 -0
  136. specify_cli/missions/software-dev/templates/plan-template.md +132 -0
  137. specify_cli/missions/software-dev/templates/spec-template.md +116 -0
  138. specify_cli/missions/software-dev/templates/task-prompt-template.md +140 -0
  139. specify_cli/missions/software-dev/templates/tasks-template.md +159 -0
  140. specify_cli/orchestrator/__init__.py +75 -0
  141. specify_cli/orchestrator/agent_config.py +224 -0
  142. specify_cli/orchestrator/agents/__init__.py +170 -0
  143. specify_cli/orchestrator/agents/augment.py +112 -0
  144. specify_cli/orchestrator/agents/base.py +243 -0
  145. specify_cli/orchestrator/agents/claude.py +112 -0
  146. specify_cli/orchestrator/agents/codex.py +106 -0
  147. specify_cli/orchestrator/agents/copilot.py +137 -0
  148. specify_cli/orchestrator/agents/cursor.py +139 -0
  149. specify_cli/orchestrator/agents/gemini.py +115 -0
  150. specify_cli/orchestrator/agents/kilocode.py +94 -0
  151. specify_cli/orchestrator/agents/opencode.py +132 -0
  152. specify_cli/orchestrator/agents/qwen.py +96 -0
  153. specify_cli/orchestrator/config.py +455 -0
  154. specify_cli/orchestrator/executor.py +642 -0
  155. specify_cli/orchestrator/integration.py +1230 -0
  156. specify_cli/orchestrator/monitor.py +898 -0
  157. specify_cli/orchestrator/scheduler.py +832 -0
  158. specify_cli/orchestrator/state.py +508 -0
  159. specify_cli/orchestrator/testing/__init__.py +122 -0
  160. specify_cli/orchestrator/testing/availability.py +346 -0
  161. specify_cli/orchestrator/testing/fixtures.py +684 -0
  162. specify_cli/orchestrator/testing/paths.py +218 -0
  163. specify_cli/plan_validation.py +107 -0
  164. specify_cli/scripts/debug-dashboard-scan.py +61 -0
  165. specify_cli/scripts/tasks/acceptance_support.py +695 -0
  166. specify_cli/scripts/tasks/task_helpers.py +506 -0
  167. specify_cli/scripts/tasks/tasks_cli.py +848 -0
  168. specify_cli/scripts/validate_encoding.py +180 -0
  169. specify_cli/task_metadata_validation.py +274 -0
  170. specify_cli/tasks_support.py +447 -0
  171. specify_cli/template/__init__.py +47 -0
  172. specify_cli/template/asset_generator.py +206 -0
  173. specify_cli/template/github_client.py +334 -0
  174. specify_cli/template/manager.py +193 -0
  175. specify_cli/template/renderer.py +99 -0
  176. specify_cli/templates/AGENTS.md +190 -0
  177. specify_cli/templates/POWERSHELL_SYNTAX.md +229 -0
  178. specify_cli/templates/agent-file-template.md +35 -0
  179. specify_cli/templates/checklist-template.md +42 -0
  180. specify_cli/templates/claudeignore-template +58 -0
  181. specify_cli/templates/command-templates/accept.md +141 -0
  182. specify_cli/templates/command-templates/analyze.md +253 -0
  183. specify_cli/templates/command-templates/checklist.md +352 -0
  184. specify_cli/templates/command-templates/clarify.md +224 -0
  185. specify_cli/templates/command-templates/constitution.md +432 -0
  186. specify_cli/templates/command-templates/dashboard.md +175 -0
  187. specify_cli/templates/command-templates/implement.md +190 -0
  188. specify_cli/templates/command-templates/merge.md +374 -0
  189. specify_cli/templates/command-templates/plan.md +171 -0
  190. specify_cli/templates/command-templates/research.md +88 -0
  191. specify_cli/templates/command-templates/review.md +510 -0
  192. specify_cli/templates/command-templates/specify.md +321 -0
  193. specify_cli/templates/command-templates/status.md +92 -0
  194. specify_cli/templates/command-templates/tasks.md +199 -0
  195. specify_cli/templates/git-hooks/pre-commit +22 -0
  196. specify_cli/templates/git-hooks/pre-commit-agent-check +37 -0
  197. specify_cli/templates/git-hooks/pre-commit-encoding-check +142 -0
  198. specify_cli/templates/plan-template.md +108 -0
  199. specify_cli/templates/spec-template.md +118 -0
  200. specify_cli/templates/task-prompt-template.md +165 -0
  201. specify_cli/templates/tasks-template.md +161 -0
  202. specify_cli/templates/vscode-settings.json +13 -0
  203. specify_cli/text_sanitization.py +225 -0
  204. specify_cli/upgrade/__init__.py +18 -0
  205. specify_cli/upgrade/detector.py +239 -0
  206. specify_cli/upgrade/metadata.py +182 -0
  207. specify_cli/upgrade/migrations/__init__.py +65 -0
  208. specify_cli/upgrade/migrations/base.py +80 -0
  209. specify_cli/upgrade/migrations/m_0_10_0_python_only.py +359 -0
  210. specify_cli/upgrade/migrations/m_0_10_12_constitution_cleanup.py +99 -0
  211. specify_cli/upgrade/migrations/m_0_10_14_update_implement_slash_command.py +176 -0
  212. specify_cli/upgrade/migrations/m_0_10_1_populate_slash_commands.py +174 -0
  213. specify_cli/upgrade/migrations/m_0_10_2_update_slash_commands.py +172 -0
  214. specify_cli/upgrade/migrations/m_0_10_6_workflow_simplification.py +174 -0
  215. specify_cli/upgrade/migrations/m_0_10_8_fix_memory_structure.py +252 -0
  216. specify_cli/upgrade/migrations/m_0_10_9_repair_templates.py +168 -0
  217. specify_cli/upgrade/migrations/m_0_11_0_workspace_per_wp.py +182 -0
  218. specify_cli/upgrade/migrations/m_0_11_1_improved_workflow_templates.py +173 -0
  219. specify_cli/upgrade/migrations/m_0_11_1_update_implement_slash_command.py +160 -0
  220. specify_cli/upgrade/migrations/m_0_11_2_improved_workflow_templates.py +173 -0
  221. specify_cli/upgrade/migrations/m_0_11_3_workflow_agent_flag.py +114 -0
  222. specify_cli/upgrade/migrations/m_0_12_0_documentation_mission.py +155 -0
  223. specify_cli/upgrade/migrations/m_0_12_1_remove_kitty_specs_from_gitignore.py +183 -0
  224. specify_cli/upgrade/migrations/m_0_2_0_specify_to_kittify.py +80 -0
  225. specify_cli/upgrade/migrations/m_0_4_8_gitignore_agents.py +118 -0
  226. specify_cli/upgrade/migrations/m_0_5_0_encoding_hooks.py +141 -0
  227. specify_cli/upgrade/migrations/m_0_6_5_commands_rename.py +169 -0
  228. specify_cli/upgrade/migrations/m_0_6_7_ensure_missions.py +228 -0
  229. specify_cli/upgrade/migrations/m_0_7_2_worktree_commands_dedup.py +89 -0
  230. specify_cli/upgrade/migrations/m_0_7_3_update_scripts.py +114 -0
  231. specify_cli/upgrade/migrations/m_0_8_0_remove_active_mission.py +82 -0
  232. specify_cli/upgrade/migrations/m_0_8_0_worktree_agents_symlink.py +148 -0
  233. specify_cli/upgrade/migrations/m_0_9_0_frontmatter_only_lanes.py +346 -0
  234. specify_cli/upgrade/migrations/m_0_9_1_complete_lane_migration.py +656 -0
  235. specify_cli/upgrade/migrations/m_0_9_2_research_mission_templates.py +221 -0
  236. specify_cli/upgrade/registry.py +121 -0
  237. specify_cli/upgrade/runner.py +284 -0
  238. specify_cli/validators/__init__.py +14 -0
  239. specify_cli/validators/paths.py +154 -0
  240. specify_cli/validators/research.py +428 -0
  241. specify_cli/verify_enhanced.py +270 -0
  242. specify_cli/workspace_context.py +224 -0
specify_cli/cli/ui.py ADDED
@@ -0,0 +1,192 @@
1
+ """Reusable UI helpers for Spec Kitty CLI interactions."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Dict, List, Optional
6
+
7
+ import readchar
8
+ import typer
9
+ from rich.console import Console
10
+ from rich.live import Live
11
+ from rich.panel import Panel
12
+ from rich.table import Table
13
+
14
+ from .step_tracker import StepTracker
15
+
16
+
17
+ def get_key() -> str:
18
+ """Get a single keypress in a cross-platform way using readchar."""
19
+ key = readchar.readkey()
20
+
21
+ if key == readchar.key.UP or key == readchar.key.CTRL_P:
22
+ return "up"
23
+ if key == readchar.key.DOWN or key == readchar.key.CTRL_N:
24
+ return "down"
25
+
26
+ if key == readchar.key.ENTER:
27
+ return "enter"
28
+
29
+ if key == readchar.key.ESC or key == "\x1b":
30
+ return "escape"
31
+
32
+ if key == readchar.key.CTRL_C:
33
+ raise KeyboardInterrupt
34
+
35
+ return key
36
+
37
+
38
+ def _resolve_console(console: Optional[Console]) -> Console:
39
+ return console or Console()
40
+
41
+
42
+ def select_with_arrows(
43
+ options: Dict,
44
+ prompt_text: str = "Select an option",
45
+ default_key: str | None = None,
46
+ console: Console | None = None,
47
+ ) -> str:
48
+ """
49
+ Interactive selection using arrow keys with Rich Live display.
50
+ """
51
+ console = _resolve_console(console)
52
+ option_keys = list(options.keys())
53
+ if default_key and default_key in option_keys:
54
+ selected_index = option_keys.index(default_key)
55
+ else:
56
+ selected_index = 0
57
+
58
+ selected_key = None
59
+
60
+ def create_selection_panel():
61
+ """Create the selection panel with current selection highlighted."""
62
+ table = Table.grid(padding=(0, 2))
63
+ table.add_column(style="cyan", justify="left", width=3)
64
+ table.add_column(style="white", justify="left")
65
+
66
+ for i, key in enumerate(option_keys):
67
+ if i == selected_index:
68
+ table.add_row("▶", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
69
+ else:
70
+ table.add_row(" ", f"[cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
71
+
72
+ table.add_row("", "")
73
+ table.add_row("", "[dim]Use ↑/↓ to navigate, Enter to select, Esc to cancel[/dim]")
74
+
75
+ return Panel(
76
+ table,
77
+ title=f"[bold]{prompt_text}[/bold]",
78
+ border_style="cyan",
79
+ padding=(1, 2),
80
+ )
81
+
82
+ console.print()
83
+
84
+ def run_selection_loop():
85
+ nonlocal selected_key, selected_index
86
+ with Live(create_selection_panel(), console=console, transient=True, auto_refresh=False) as live:
87
+ while True:
88
+ try:
89
+ key = get_key()
90
+ if key == "up":
91
+ selected_index = (selected_index - 1) % len(option_keys)
92
+ elif key == "down":
93
+ selected_index = (selected_index + 1) % len(option_keys)
94
+ elif key == "enter":
95
+ selected_key = option_keys[selected_index]
96
+ break
97
+ elif key == "escape":
98
+ console.print("\n[yellow]Selection cancelled[/yellow]")
99
+ raise typer.Exit(1)
100
+
101
+ live.update(create_selection_panel(), refresh=True)
102
+
103
+ except KeyboardInterrupt:
104
+ console.print("\n[yellow]Selection cancelled[/yellow]")
105
+ raise typer.Exit(1)
106
+
107
+ run_selection_loop()
108
+
109
+ if selected_key is None:
110
+ console.print("\n[red]Selection failed.[/red]")
111
+ raise typer.Exit(1)
112
+
113
+ return selected_key
114
+
115
+
116
+ def multi_select_with_arrows(
117
+ options: Dict[str, str],
118
+ prompt_text: str = "Select options",
119
+ default_keys: Optional[List[str]] = None,
120
+ console: Console | None = None,
121
+ ) -> List[str]:
122
+ """Allow selecting one or more options using arrow keys + space to toggle."""
123
+
124
+ console = _resolve_console(console)
125
+ option_keys = list(options.keys())
126
+ selected_indices: set[int] = set()
127
+ if default_keys:
128
+ for key in default_keys:
129
+ if key in option_keys:
130
+ selected_indices.add(option_keys.index(key))
131
+ if not selected_indices and option_keys:
132
+ selected_indices.add(0)
133
+
134
+ cursor_index = next(iter(selected_indices)) if selected_indices else 0
135
+
136
+ def build_panel():
137
+ table = Table.grid(padding=(0, 2))
138
+ table.add_column(style="cyan", justify="left", width=3)
139
+ table.add_column(style="white", justify="left")
140
+
141
+ for i, key in enumerate(option_keys):
142
+ indicator = "[cyan]☑" if i in selected_indices else "[bright_black]☐"
143
+ pointer = "▶" if i == cursor_index else " "
144
+ table.add_row(pointer, f"{indicator} [cyan]{key}[/cyan] [dim]({options[key]})[/dim]")
145
+
146
+ table.add_row("", "")
147
+ table.add_row(
148
+ "",
149
+ "[dim]Use ↑/↓ to move, Space to toggle, Enter to confirm, Esc to cancel[/dim]",
150
+ )
151
+
152
+ return Panel(table, title=f"[bold]{prompt_text}[/bold]", border_style="cyan", padding=(1, 2))
153
+
154
+ def normalize_selection() -> List[str]:
155
+ return [option_keys[i] for i in range(len(option_keys)) if i in selected_indices]
156
+
157
+ console.print()
158
+
159
+ with Live(build_panel(), console=console, transient=True, auto_refresh=False) as live:
160
+ while True:
161
+ try:
162
+ key = get_key()
163
+ if key == "up":
164
+ cursor_index = (cursor_index - 1) % len(option_keys)
165
+ elif key == "down":
166
+ cursor_index = (cursor_index + 1) % len(option_keys)
167
+ elif key in (" ", readchar.key.SPACE):
168
+ if cursor_index in selected_indices:
169
+ selected_indices.remove(cursor_index)
170
+ else:
171
+ selected_indices.add(cursor_index)
172
+ elif key == "enter":
173
+ current = normalize_selection()
174
+ if current:
175
+ return current
176
+ elif key == "escape":
177
+ console.print("\n[yellow]Selection cancelled[/yellow]")
178
+ raise typer.Exit(1)
179
+
180
+ live.update(build_panel(), refresh=True)
181
+
182
+ except KeyboardInterrupt:
183
+ console.print("\n[yellow]Selection cancelled[/yellow]")
184
+ raise typer.Exit(1)
185
+
186
+
187
+ __all__ = [
188
+ "StepTracker",
189
+ "get_key",
190
+ "select_with_arrows",
191
+ "multi_select_with_arrows",
192
+ ]
@@ -0,0 +1,53 @@
1
+ """Core utilities and configuration exports."""
2
+
3
+ from .config import (
4
+ AGENT_COMMAND_CONFIG,
5
+ AGENT_TOOL_REQUIREMENTS,
6
+ AI_CHOICES,
7
+ BANNER,
8
+ DEFAULT_MISSION_KEY,
9
+ DEFAULT_TEMPLATE_REPO,
10
+ MISSION_CHOICES,
11
+ SCRIPT_TYPE_CHOICES,
12
+ )
13
+ from .utils import format_path, ensure_directory, safe_remove, get_platform
14
+ from .git_ops import run_command, is_git_repo, init_git_repo, get_current_branch
15
+ from .project_resolver import (
16
+ locate_project_root,
17
+ resolve_template_path,
18
+ resolve_worktree_aware_feature_dir,
19
+ get_active_mission_key,
20
+ )
21
+ from .tool_checker import (
22
+ check_tool,
23
+ check_tool_for_tracker,
24
+ check_all_tools,
25
+ get_tool_version,
26
+ )
27
+
28
+ __all__ = [
29
+ "AGENT_COMMAND_CONFIG",
30
+ "AGENT_TOOL_REQUIREMENTS",
31
+ "AI_CHOICES",
32
+ "BANNER",
33
+ "DEFAULT_MISSION_KEY",
34
+ "DEFAULT_TEMPLATE_REPO",
35
+ "MISSION_CHOICES",
36
+ "SCRIPT_TYPE_CHOICES",
37
+ "format_path",
38
+ "ensure_directory",
39
+ "safe_remove",
40
+ "get_platform",
41
+ "run_command",
42
+ "is_git_repo",
43
+ "init_git_repo",
44
+ "get_current_branch",
45
+ "locate_project_root",
46
+ "resolve_template_path",
47
+ "resolve_worktree_aware_feature_dir",
48
+ "get_active_mission_key",
49
+ "check_tool",
50
+ "check_tool_for_tracker",
51
+ "check_all_tools",
52
+ "get_tool_version",
53
+ ]
@@ -0,0 +1,311 @@
1
+ """Agent context file management for updating CLAUDE.md, GEMINI.md, etc."""
2
+
3
+ from datetime import datetime
4
+ from pathlib import Path
5
+ from typing import Dict, List, Optional
6
+ import re
7
+
8
+
9
+ # Agent types and their file paths
10
+ AGENT_CONFIGS = {
11
+ "claude": "CLAUDE.md",
12
+ "gemini": "GEMINI.md",
13
+ "copilot": ".github/copilot-instructions.md",
14
+ "cursor": ".cursor/rules/specify-rules.mdc",
15
+ "qwen": "QWEN.md",
16
+ "opencode": "AGENTS.md",
17
+ "codex": "AGENTS.md",
18
+ "windsurf": ".windsurf/rules/specify-rules.md",
19
+ "kilocode": ".kilocode/rules/specify-rules.md",
20
+ "auggie": ".augment/rules/specify-rules.md",
21
+ "roo": ".roo/rules/specify-rules.md",
22
+ "q": "AGENTS.md",
23
+ }
24
+
25
+
26
+ def parse_plan_for_tech_stack(plan_path: Path) -> Dict[str, Optional[str]]:
27
+ """
28
+ Extract tech stack information from plan.md Technical Context section.
29
+
30
+ Args:
31
+ plan_path: Path to plan.md file
32
+
33
+ Returns:
34
+ Dictionary with language, dependencies, storage, testing, project_type keys
35
+
36
+ Example return:
37
+ {
38
+ "language": "Python 3.11+",
39
+ "dependencies": "Typer, Rich, pathlib, subprocess",
40
+ "storage": "Filesystem only (no database)",
41
+ "testing": "pytest with unit + integration tests",
42
+ "project_type": "Single Python package"
43
+ }
44
+ """
45
+ if not plan_path.exists():
46
+ raise FileNotFoundError(f"Plan file not found: {plan_path}")
47
+
48
+ content = plan_path.read_text()
49
+
50
+ # Extract fields from Technical Context section using markdown patterns
51
+ def extract_field(pattern: str) -> Optional[str]:
52
+ # Match pattern like "**Language/Version**: Python 3.11+"
53
+ match = re.search(rf"\*\*{pattern}\*\*:\s*(.+?)(?:\n|$)", content, re.MULTILINE)
54
+ if match:
55
+ value = match.group(1).strip()
56
+ # Filter out placeholders
57
+ if value and value != "NEEDS CLARIFICATION" and value != "N/A":
58
+ return value
59
+ return None
60
+
61
+ return {
62
+ "language": extract_field("Language/Version"),
63
+ "dependencies": extract_field("Primary Dependencies"),
64
+ "storage": extract_field("Storage"),
65
+ "testing": extract_field("Testing"),
66
+ "project_type": extract_field("Project Type"),
67
+ }
68
+
69
+
70
+ def format_technology_stack(tech_stack: Dict[str, Optional[str]], feature_slug: str) -> List[str]:
71
+ """
72
+ Format tech stack data into markdown bullet points for Active Technologies section.
73
+
74
+ Args:
75
+ tech_stack: Dictionary from parse_plan_for_tech_stack()
76
+ feature_slug: Current feature branch/slug (e.g., "008-unified-python-cli")
77
+
78
+ Returns:
79
+ List of formatted markdown lines
80
+
81
+ Example:
82
+ ["- Python 3.11+ (existing spec-kitty requirement) (008-unified-python-cli)",
83
+ "- Filesystem only (no database) (008-unified-python-cli)"]
84
+ """
85
+ entries = []
86
+
87
+ # Add language + dependencies as one line
88
+ parts = []
89
+ if tech_stack.get("language"):
90
+ parts.append(tech_stack["language"])
91
+ if tech_stack.get("dependencies"):
92
+ parts.append(tech_stack["dependencies"])
93
+
94
+ if parts:
95
+ tech_line = " + ".join(parts)
96
+ entries.append(f"- {tech_line} ({feature_slug})")
97
+
98
+ # Add storage as separate line if present
99
+ if tech_stack.get("storage"):
100
+ entries.append(f"- {tech_stack['storage']} ({feature_slug})")
101
+
102
+ return entries
103
+
104
+
105
+ def preserve_manual_additions(old_content: str, new_content: str) -> str:
106
+ """
107
+ Preserve content between <!-- MANUAL ADDITIONS START/END --> markers.
108
+
109
+ Args:
110
+ old_content: Original file content with manual additions
111
+ new_content: New generated content
112
+
113
+ Returns:
114
+ Merged content with manual additions from old_content injected into new_content
115
+
116
+ Note:
117
+ If markers are not found in old_content, returns new_content unchanged.
118
+ If markers are not found in new_content, manual section is appended.
119
+ """
120
+ # Extract manual additions from old content
121
+ start_marker = "<!-- MANUAL ADDITIONS START -->"
122
+ end_marker = "<!-- MANUAL ADDITIONS END -->"
123
+
124
+ # Find manual additions in old content
125
+ start_idx = old_content.find(start_marker)
126
+ end_idx = old_content.find(end_marker)
127
+
128
+ if start_idx == -1 or end_idx == -1:
129
+ # No manual additions to preserve
130
+ return new_content
131
+
132
+ # Extract the manual section (including markers)
133
+ manual_section = old_content[start_idx:end_idx + len(end_marker)]
134
+
135
+ # Find where to inject in new content
136
+ new_start_idx = new_content.find(start_marker)
137
+ new_end_idx = new_content.find(end_marker)
138
+
139
+ if new_start_idx == -1 or new_end_idx == -1:
140
+ # New content doesn't have markers, append at end
141
+ return new_content.rstrip() + "\n\n" + manual_section + "\n"
142
+
143
+ # Replace the section in new content with the preserved manual section
144
+ before = new_content[:new_start_idx]
145
+ after = new_content[new_end_idx + len(end_marker):]
146
+
147
+ return before + manual_section + after
148
+
149
+
150
+ def update_agent_context(
151
+ agent_type: str,
152
+ tech_stack: Dict[str, Optional[str]],
153
+ feature_slug: str,
154
+ repo_root: Path,
155
+ feature_dir: Optional[Path] = None,
156
+ ) -> None:
157
+ """
158
+ Update agent context file with tech stack from plan.md.
159
+
160
+ Args:
161
+ agent_type: One of the keys in AGENT_CONFIGS (claude, gemini, etc.)
162
+ tech_stack: Dictionary from parse_plan_for_tech_stack()
163
+ feature_slug: Current feature branch/slug
164
+ repo_root: Repository root directory
165
+ feature_dir: Feature directory path (for worktree-local updates)
166
+
167
+ Raises:
168
+ ValueError: If agent_type is not supported
169
+ FileNotFoundError: If agent file doesn't exist
170
+ """
171
+ if agent_type not in AGENT_CONFIGS:
172
+ raise ValueError(
173
+ f"Unsupported agent type: {agent_type}. "
174
+ f"Supported types: {', '.join(AGENT_CONFIGS.keys())}"
175
+ )
176
+
177
+ agent_file_path = repo_root / AGENT_CONFIGS[agent_type]
178
+
179
+ # If it's a worktree-local file, use feature_dir
180
+ if feature_dir and agent_file_path.is_relative_to(repo_root):
181
+ worktree_agent_file = feature_dir / AGENT_CONFIGS[agent_type]
182
+ if worktree_agent_file.exists():
183
+ agent_file_path = worktree_agent_file
184
+
185
+ if not agent_file_path.exists():
186
+ raise FileNotFoundError(f"Agent file not found: {agent_file_path}")
187
+
188
+ # Read existing content
189
+ old_content = agent_file_path.read_text()
190
+
191
+ # Format new tech entries
192
+ new_tech_entries = format_technology_stack(tech_stack, feature_slug)
193
+
194
+ # Prepare change entry for Recent Changes section
195
+ tech_parts = []
196
+ if tech_stack.get("language"):
197
+ tech_parts.append(tech_stack["language"])
198
+ if tech_stack.get("dependencies"):
199
+ tech_parts.append(tech_stack["dependencies"])
200
+
201
+ tech_description = " + ".join(tech_parts) if tech_parts else tech_stack.get("storage", "")
202
+ new_change_entry = f"- {feature_slug}: Added {tech_description}" if tech_description else ""
203
+
204
+ # Process file line by line to update sections
205
+ lines = old_content.splitlines(keepends=True)
206
+ new_lines = []
207
+
208
+ in_tech_section = False
209
+ in_changes_section = False
210
+ tech_entries_added = False
211
+ existing_changes_count = 0
212
+ current_date = datetime.now().strftime("%Y-%m-%d")
213
+
214
+ for line in lines:
215
+ # Handle Active Technologies section
216
+ if line.strip() == "## Active Technologies":
217
+ new_lines.append(line)
218
+ in_tech_section = True
219
+ continue
220
+
221
+ # Handle Recent Changes section - MUST come before generic section exit checks
222
+ if line.strip() == "## Recent Changes":
223
+ # If we were in tech section, close it first
224
+ if in_tech_section and not tech_entries_added and new_tech_entries:
225
+ for entry in new_tech_entries:
226
+ new_lines.append(entry + "\n")
227
+ tech_entries_added = True
228
+ in_tech_section = False
229
+
230
+ new_lines.append(line)
231
+ in_changes_section = True
232
+ # Add new change entry right after heading
233
+ if new_change_entry:
234
+ new_lines.append(new_change_entry + "\n")
235
+ continue
236
+
237
+ # Check if we're exiting tech section
238
+ if in_tech_section and line.strip().startswith("##"):
239
+ # Add new tech entries before closing section
240
+ if not tech_entries_added and new_tech_entries:
241
+ for entry in new_tech_entries:
242
+ new_lines.append(entry + "\n")
243
+ tech_entries_added = True
244
+ new_lines.append(line)
245
+ in_tech_section = False
246
+ continue
247
+
248
+ # Check if we're exiting changes section
249
+ if in_changes_section and line.strip().startswith("##"):
250
+ new_lines.append(line)
251
+ in_changes_section = False
252
+ continue
253
+
254
+ # In changes section: keep only first 2 existing changes, skip empty lines
255
+ if in_changes_section:
256
+ if line.strip().startswith("- "):
257
+ if existing_changes_count < 2:
258
+ new_lines.append(line)
259
+ existing_changes_count += 1
260
+ continue
261
+ elif line.strip() == "":
262
+ # Skip empty lines in changes section
263
+ continue
264
+
265
+ # Update last updated timestamp
266
+ if "**Last updated**:" in line or "*Last updated*:" in line:
267
+ # Replace date in format YYYY-MM-DD
268
+ line = re.sub(r'\d{4}-\d{2}-\d{2}', current_date, line)
269
+
270
+ new_lines.append(line)
271
+
272
+ # Post-loop: if still in tech section and haven't added entries
273
+ if in_tech_section and not tech_entries_added and new_tech_entries:
274
+ for entry in new_tech_entries:
275
+ new_lines.append(entry + "\n")
276
+
277
+ new_content = "".join(new_lines)
278
+
279
+ # Preserve manual additions
280
+ final_content = preserve_manual_additions(old_content, new_content)
281
+
282
+ # Write updated content
283
+ agent_file_path.write_text(final_content)
284
+
285
+
286
+ def get_supported_agent_types() -> List[str]:
287
+ """Return list of supported agent types."""
288
+ return list(AGENT_CONFIGS.keys())
289
+
290
+
291
+ def get_agent_file_path(agent_type: str, repo_root: Path) -> Path:
292
+ """
293
+ Get the file path for a specific agent type.
294
+
295
+ Args:
296
+ agent_type: One of the keys in AGENT_CONFIGS
297
+ repo_root: Repository root directory
298
+
299
+ Returns:
300
+ Path to the agent configuration file
301
+
302
+ Raises:
303
+ ValueError: If agent_type is not supported
304
+ """
305
+ if agent_type not in AGENT_CONFIGS:
306
+ raise ValueError(
307
+ f"Unsupported agent type: {agent_type}. "
308
+ f"Supported types: {', '.join(AGENT_CONFIGS.keys())}"
309
+ )
310
+
311
+ return repo_root / AGENT_CONFIGS[agent_type]
@@ -0,0 +1,96 @@
1
+ """Configuration constants shared across the Spec Kitty CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ AI_CHOICES = {
6
+ "copilot": "GitHub Copilot",
7
+ "claude": "Claude Code",
8
+ "gemini": "Gemini CLI",
9
+ "cursor": "Cursor",
10
+ "qwen": "Qwen Code",
11
+ "opencode": "opencode",
12
+ "codex": "Codex CLI",
13
+ "windsurf": "Windsurf",
14
+ "kilocode": "Kilo Code",
15
+ "auggie": "Auggie CLI",
16
+ "roo": "Roo Code",
17
+ "q": "Amazon Q Developer CLI",
18
+ }
19
+
20
+ MISSION_CHOICES = {
21
+ "software-dev": "Software Dev Kitty",
22
+ "research": "Deep Research Kitty",
23
+ }
24
+
25
+ DEFAULT_MISSION_KEY = "software-dev"
26
+
27
+ AGENT_TOOL_REQUIREMENTS: dict[str, tuple[str, str]] = {
28
+ "claude": ("claude", "https://docs.anthropic.com/en/docs/claude-code/setup"),
29
+ "gemini": ("gemini", "https://github.com/google-gemini/gemini-cli"),
30
+ "qwen": ("qwen", "https://github.com/QwenLM/qwen-code"),
31
+ "opencode": ("opencode", "https://opencode.ai"),
32
+ "codex": ("codex", "https://github.com/openai/codex"),
33
+ "auggie": ("auggie", "https://docs.augmentcode.com/cli/setup-auggie/install-auggie-cli"),
34
+ "q": ("q", "https://aws.amazon.com/developer/learning/q-developer-cli/"),
35
+ }
36
+
37
+ SCRIPT_TYPE_CHOICES = {"sh": "POSIX Shell (bash/zsh)", "ps": "PowerShell"}
38
+
39
+ DEFAULT_TEMPLATE_REPO = "spec-kitty/spec-kitty"
40
+
41
+ # IDE-integrated agents that don't require CLI installation
42
+ IDE_AGENTS = {"cursor", "windsurf", "copilot", "kilocode"}
43
+
44
+ AGENT_COMMAND_CONFIG: dict[str, dict[str, str]] = {
45
+ "claude": {"dir": ".claude/commands", "ext": "md", "arg_format": "$ARGUMENTS"},
46
+ "gemini": {"dir": ".gemini/commands", "ext": "toml", "arg_format": "{{args}}"},
47
+ "copilot": {"dir": ".github/prompts", "ext": "prompt.md", "arg_format": "$ARGUMENTS"},
48
+ "cursor": {"dir": ".cursor/commands", "ext": "md", "arg_format": "$ARGUMENTS"},
49
+ "qwen": {"dir": ".qwen/commands", "ext": "toml", "arg_format": "{{args}}"},
50
+ "opencode": {"dir": ".opencode/command", "ext": "md", "arg_format": "$ARGUMENTS"},
51
+ "windsurf": {"dir": ".windsurf/workflows", "ext": "md", "arg_format": "$ARGUMENTS"},
52
+ "codex": {"dir": ".codex/prompts", "ext": "md", "arg_format": "$ARGUMENTS"},
53
+ "kilocode": {"dir": ".kilocode/workflows", "ext": "md", "arg_format": "$ARGUMENTS"},
54
+ "auggie": {"dir": ".augment/commands", "ext": "md", "arg_format": "$ARGUMENTS"},
55
+ "roo": {"dir": ".roo/commands", "ext": "md", "arg_format": "$ARGUMENTS"},
56
+ "q": {"dir": ".amazonq/prompts", "ext": "md", "arg_format": "$ARGUMENTS"},
57
+ }
58
+
59
+ BANNER = """
60
+ `````````````````````````````````````````````````````````
61
+
62
+ ▄█▄_ ╓▄█_
63
+ ▐█ └▀█▄_ ▄█▀▀ ╙█
64
+ █" `▀█▄ ▄█▀ █▌
65
+ ▐█ ▀█▄▄▄██████████▄▄▄█" ▐█
66
+ ║█ "` ╟█ ╫▌ █" '" █
67
+ ║█ ▀ ╚▀ ▀ J█
68
+ █ █▌
69
+ █▀ ,▄█████▄ ,▄█████▄_ █▌
70
+ █▌ ▄█" "██ ╓█▀ `▀█_ █▌
71
+ ▐█__▐▌ ▄██▄ ╙█_____╒█ ▄██, '█__'█
72
+ █▀▀▀█M ████ █▀╙\"\"\"██ ▐████ █▀▀"█▌
73
+ █─ ╟█ ╙▀▀" ██ █╕ ╙▀▀ ╓█ ║▌
74
+ ╓▄▄▄▄█▌,_ ╙█▄_ _▄█▀╒██████ ▀█╥ ▄█▀ __,██▄▄▄▄
75
+ ╚█'`" `╙▀▀▀▀▀" `▀██▀ "▀▀▀▀▀" ""▐█
76
+ _,▄▄███▀ █▌ ▀▀███▄▄,_
77
+ ▀"` ▀█_ '▀█▄▄█▀▀█▄▄█▀ ▄█" '"▀"
78
+ ╙██_ ▄█▀
79
+ └▀█▄_ ,▓█▀
80
+ └▀▀██▄,__ __╓▄██▀▀
81
+ `"▀▀▀▀▀▀▀▀▀▀▀╙"`
82
+
83
+ `````````````````````````````````````````````````````````
84
+ """
85
+
86
+ __all__ = [
87
+ "AI_CHOICES",
88
+ "MISSION_CHOICES",
89
+ "DEFAULT_MISSION_KEY",
90
+ "AGENT_TOOL_REQUIREMENTS",
91
+ "SCRIPT_TYPE_CHOICES",
92
+ "DEFAULT_TEMPLATE_REPO",
93
+ "AGENT_COMMAND_CONFIG",
94
+ "IDE_AGENTS",
95
+ "BANNER",
96
+ ]