kingdom-cli 0.3.2__tar.gz → 0.5.0__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.
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/PKG-INFO +1 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/pyproject.toml +1 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/agent.py +75 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/__init__.py +117 -4
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/council.py +343 -97
- kingdom_cli-0.5.0/src/kingdom/cli/hook.py +225 -0
- kingdom_cli-0.5.0/src/kingdom/cli/lord.py +382 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/peasant.py +364 -117
- kingdom_cli-0.5.0/src/kingdom/cli/plugin.py +151 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/ticket.py +186 -7
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/config.py +69 -3
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/council/base.py +8 -2
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/harness.py +36 -21
- kingdom_cli-0.5.0/src/kingdom/lord_harness.py +936 -0
- kingdom_cli-0.5.0/src/kingdom/lord_worker.py +50 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/session.py +63 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/skill/SKILL.md +23 -3
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/skill/references/council.md +5 -4
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/thread.py +129 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/ticket.py +59 -5
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/app.py +4 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/worktree.py +39 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom_cli.egg-info/PKG-INFO +1 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom_cli.egg-info/SOURCES.txt +10 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_agent.py +54 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli.py +112 -1
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_council.py +277 -29
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_peasant.py +809 -2
- kingdom_cli-0.5.0/tests/test_cli_plugin.py +237 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_current.py +71 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_deps.py +68 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_lifecycle.py +246 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_config.py +81 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_council.py +36 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_done.py +106 -0
- kingdom_cli-0.5.0/tests/test_epic.py +236 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_harness.py +6 -0
- kingdom_cli-0.5.0/tests/test_hook.py +441 -0
- kingdom_cli-0.5.0/tests/test_lord.py +1278 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_skill.py +3 -4
- kingdom_cli-0.5.0/tests/test_stop_blocker.py +122 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_ticket.py +161 -11
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui.py +39 -59
- kingdom_cli-0.5.0/tests/test_worktree.py +114 -0
- kingdom_cli-0.3.2/tests/test_worktree.py +0 -25
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/LICENSE +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/README.md +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/setup.cfg +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/__init__.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/__main__.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/breakdown.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/config.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/design.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/display.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/cli/helpers.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/council/__init__.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/council/bundle.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/council/council.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/council/worker.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/design.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/parsing.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/skill/__init__.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/skill/references/__init__.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/skill/references/peasants.md +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/skill/references/tickets.md +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/state.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/synthesis.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/__init__.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/chat.tcss +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/clipboard.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/poll.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/terminal.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/tui/widgets.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom/worker.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom_cli.egg-info/dependency_links.txt +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom_cli.egg-info/entry_points.txt +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom_cli.egg-info/requires.txt +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/src/kingdom_cli.egg-info/top_level.txt +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_breakdown.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_design_breakdown.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_status.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_links.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_list.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_show.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_cli_ticket_worklog.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_design.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_init.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_parsing.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_session.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_state.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_synthesis.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_thread.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_clipboard.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_integration.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_poll.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_reply.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_terminal.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_thinking_persistence.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_tui_widgets.py +0 -0
- {kingdom_cli-0.3.2 → kingdom_cli-0.5.0}/tests/test_worker.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kingdom-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: A markdown file-based CLI for software development with multi-agent council design, markdown tickets, and background worker loops.
|
|
5
5
|
Author: James Robinson-Bohnslav
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kingdom-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "A markdown file-based CLI for software development with multi-agent council design, markdown tickets, and background worker loops."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -62,6 +62,7 @@ class AgentConfig:
|
|
|
62
62
|
install_hint: str = ""
|
|
63
63
|
model: str = ""
|
|
64
64
|
extra_flags: list[str] = field(default_factory=list)
|
|
65
|
+
reasoning_effort: str = ""
|
|
65
66
|
|
|
66
67
|
|
|
67
68
|
def resolve_agent(name: str, agent_def: AgentDef) -> AgentConfig:
|
|
@@ -85,6 +86,7 @@ def resolve_agent(name: str, agent_def: AgentDef) -> AgentConfig:
|
|
|
85
86
|
install_hint=defaults["install_hint"],
|
|
86
87
|
model=agent_def.model,
|
|
87
88
|
extra_flags=list(agent_def.extra_flags),
|
|
89
|
+
reasoning_effort=agent_def.reasoning_effort,
|
|
88
90
|
)
|
|
89
91
|
|
|
90
92
|
|
|
@@ -310,6 +312,63 @@ RESPONSE_PARSERS: dict[str, ResponseParser] = {
|
|
|
310
312
|
}
|
|
311
313
|
|
|
312
314
|
|
|
315
|
+
def extract_codex_error(stdout: str, stderr: str, code: int) -> str | None:
|
|
316
|
+
"""Extract a human-meaningful error from Codex JSONL output.
|
|
317
|
+
|
|
318
|
+
Prefers terminal failure events over transient reconnect notices so callers
|
|
319
|
+
can surface the actual cause instead of a generic exit code.
|
|
320
|
+
"""
|
|
321
|
+
del code # Error extraction only depends on emitted output.
|
|
322
|
+
|
|
323
|
+
turn_failed: list[str] = []
|
|
324
|
+
item_errors: list[str] = []
|
|
325
|
+
event_errors: list[str] = []
|
|
326
|
+
|
|
327
|
+
for line in stdout.strip().split("\n"):
|
|
328
|
+
if not line:
|
|
329
|
+
continue
|
|
330
|
+
try:
|
|
331
|
+
event = json.loads(line)
|
|
332
|
+
except json.JSONDecodeError:
|
|
333
|
+
continue
|
|
334
|
+
if not isinstance(event, dict):
|
|
335
|
+
continue
|
|
336
|
+
|
|
337
|
+
event_type = event.get("type")
|
|
338
|
+
if event_type == "turn.failed":
|
|
339
|
+
error = event.get("error", {})
|
|
340
|
+
if isinstance(error, dict):
|
|
341
|
+
message = error.get("message")
|
|
342
|
+
if isinstance(message, str) and message:
|
|
343
|
+
turn_failed.append(message)
|
|
344
|
+
elif event_type == "item.completed":
|
|
345
|
+
item = event.get("item", {})
|
|
346
|
+
if isinstance(item, dict) and item.get("type") == "error":
|
|
347
|
+
message = item.get("message") or item.get("text")
|
|
348
|
+
if isinstance(message, str) and message:
|
|
349
|
+
item_errors.append(message)
|
|
350
|
+
elif event_type == "error":
|
|
351
|
+
message = event.get("message")
|
|
352
|
+
if isinstance(message, str) and message:
|
|
353
|
+
event_errors.append(message)
|
|
354
|
+
|
|
355
|
+
for candidates in (turn_failed, item_errors, event_errors):
|
|
356
|
+
if candidates:
|
|
357
|
+
return candidates[-1]
|
|
358
|
+
|
|
359
|
+
stripped = stderr.strip()
|
|
360
|
+
if stripped:
|
|
361
|
+
return stripped
|
|
362
|
+
return None
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
ErrorExtractor = Callable[[str, str, int], str | None]
|
|
366
|
+
|
|
367
|
+
ERROR_EXTRACTORS: dict[str, ErrorExtractor] = {
|
|
368
|
+
"codex": extract_codex_error,
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
|
|
313
372
|
# ---------------------------------------------------------------------------
|
|
314
373
|
# Backend-specific command builders
|
|
315
374
|
# ---------------------------------------------------------------------------
|
|
@@ -373,6 +432,8 @@ def build_codex_command(
|
|
|
373
432
|
parts.extend(["-c", 'sandbox_permissions=["disk-full-read-access"]'])
|
|
374
433
|
if config.model:
|
|
375
434
|
parts.extend(["--model", config.model])
|
|
435
|
+
if config.reasoning_effort:
|
|
436
|
+
parts.extend(["-c", f"model_reasoning_effort={config.reasoning_effort}"])
|
|
376
437
|
if config.extra_flags:
|
|
377
438
|
parts.extend(config.extra_flags)
|
|
378
439
|
if session_id:
|
|
@@ -493,6 +554,20 @@ def parse_response(config: AgentConfig, stdout: str, stderr: str, code: int) ->
|
|
|
493
554
|
return parser(stdout, stderr, code)
|
|
494
555
|
|
|
495
556
|
|
|
557
|
+
def extract_error(config: AgentConfig, stdout: str, stderr: str, code: int) -> str | None:
|
|
558
|
+
"""Extract a backend-aware error message from CLI output."""
|
|
559
|
+
extractor = ERROR_EXTRACTORS.get(config.backend)
|
|
560
|
+
if extractor is not None:
|
|
561
|
+
error = extractor(stdout, stderr, code)
|
|
562
|
+
if error:
|
|
563
|
+
return error
|
|
564
|
+
|
|
565
|
+
stripped = stderr.strip()
|
|
566
|
+
if stripped:
|
|
567
|
+
return stripped
|
|
568
|
+
return None
|
|
569
|
+
|
|
570
|
+
|
|
496
571
|
# ---------------------------------------------------------------------------
|
|
497
572
|
# Stream text extractors — extract text from individual NDJSON lines
|
|
498
573
|
# ---------------------------------------------------------------------------
|
|
@@ -9,6 +9,7 @@ from __future__ import annotations
|
|
|
9
9
|
import json
|
|
10
10
|
import os
|
|
11
11
|
import subprocess
|
|
12
|
+
import sys
|
|
12
13
|
from pathlib import Path
|
|
13
14
|
from typing import Annotated
|
|
14
15
|
|
|
@@ -40,6 +41,8 @@ from .council import council_app
|
|
|
40
41
|
from .design import design_app, get_branch_paths, get_doc_status # noqa: F401 (re-export)
|
|
41
42
|
from .display import error_console, print_error, styled_echo
|
|
42
43
|
from .helpers import install_skill, is_git_repo, require_project_root, verbose_echo # noqa: F401
|
|
44
|
+
from .hook import hook_app
|
|
45
|
+
from .lord import lord_app
|
|
43
46
|
from .peasant import ( # noqa: F401
|
|
44
47
|
PeasantContext,
|
|
45
48
|
launch_work_background,
|
|
@@ -47,6 +50,7 @@ from .peasant import ( # noqa: F401
|
|
|
47
50
|
peasant_app,
|
|
48
51
|
resolve_peasant_context,
|
|
49
52
|
)
|
|
53
|
+
from .plugin import plugin_app
|
|
50
54
|
from .ticket import format_ticket_line, format_ticket_summary, get_tickets_dir, ticket_app # noqa: F401
|
|
51
55
|
|
|
52
56
|
NO_COLOR = "NO_COLOR" in os.environ or os.environ.get("TERM") == "dumb"
|
|
@@ -78,6 +82,9 @@ app.add_typer(council_app, name="council")
|
|
|
78
82
|
app.add_typer(design_app, name="design")
|
|
79
83
|
app.add_typer(peasant_app, name="peasant")
|
|
80
84
|
app.add_typer(config_app, name="config")
|
|
85
|
+
app.add_typer(hook_app, name="hook")
|
|
86
|
+
app.add_typer(lord_app, name="lord")
|
|
87
|
+
app.add_typer(plugin_app, name="plugin")
|
|
81
88
|
app.add_typer(ticket_app, name="ticket")
|
|
82
89
|
app.add_typer(ticket_app, name="tk", hidden=True) # Alias for muscle memory
|
|
83
90
|
|
|
@@ -133,7 +140,10 @@ def start(
|
|
|
133
140
|
if current_path.exists() and not force:
|
|
134
141
|
existing = current_path.read_text(encoding="utf-8").strip()
|
|
135
142
|
print_error(f"A session is already active: {existing}")
|
|
136
|
-
error_console.print(
|
|
143
|
+
error_console.print(
|
|
144
|
+
" If that branch is finished, run `kd done` to clean it up before starting a new session.\n"
|
|
145
|
+
" If you need to switch mid-work, use `kd start --force` to override."
|
|
146
|
+
)
|
|
137
147
|
raise typer.Exit(code=1)
|
|
138
148
|
|
|
139
149
|
# Determine branch name
|
|
@@ -168,6 +178,109 @@ def start(
|
|
|
168
178
|
typer.echo(f" Design: {design_path}")
|
|
169
179
|
|
|
170
180
|
|
|
181
|
+
@app.command(help="Switch the active kd session to another branch.")
|
|
182
|
+
def switch(
|
|
183
|
+
branch: Annotated[str | None, typer.Argument(help="Branch name to switch to.")] = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
"""Switch the active kd session without changing git branch.
|
|
186
|
+
|
|
187
|
+
With an argument: validate the branch exists in .kd/branches/ and update .kd/current.
|
|
188
|
+
Without arguments: list all tracked branches, marking the current session and git branch.
|
|
189
|
+
"""
|
|
190
|
+
base = require_project_root()
|
|
191
|
+
console = Console()
|
|
192
|
+
|
|
193
|
+
if branch is None:
|
|
194
|
+
# List mode: show all tracked branches
|
|
195
|
+
branches_dir = base / ".kd" / "branches"
|
|
196
|
+
if not branches_dir.exists() or not any(branches_dir.iterdir()):
|
|
197
|
+
typer.echo("No tracked branches. Use `kd start <branch>` to create one.")
|
|
198
|
+
return
|
|
199
|
+
|
|
200
|
+
# Determine current kd session
|
|
201
|
+
current_session: str | None = None
|
|
202
|
+
current_path = base / ".kd" / "current"
|
|
203
|
+
if current_path.exists():
|
|
204
|
+
current_session = current_path.read_text(encoding="utf-8").strip() or None
|
|
205
|
+
|
|
206
|
+
# Determine current git branch
|
|
207
|
+
git_branch = get_current_git_branch()
|
|
208
|
+
git_normalized = normalize_branch_name(git_branch) if git_branch else None
|
|
209
|
+
|
|
210
|
+
for branch_dir in sorted(branches_dir.iterdir()):
|
|
211
|
+
if not branch_dir.is_dir():
|
|
212
|
+
continue
|
|
213
|
+
name = branch_dir.name
|
|
214
|
+
|
|
215
|
+
# Read original branch name from state.json
|
|
216
|
+
state_path = branch_dir / "state.json"
|
|
217
|
+
original = name
|
|
218
|
+
branch_status = ""
|
|
219
|
+
if state_path.exists():
|
|
220
|
+
try:
|
|
221
|
+
state = read_json(state_path)
|
|
222
|
+
original = state.get("branch", name)
|
|
223
|
+
if state.get("status") == "done":
|
|
224
|
+
branch_status = " [dim](done)[/dim]"
|
|
225
|
+
except (FileNotFoundError, KeyError):
|
|
226
|
+
pass
|
|
227
|
+
|
|
228
|
+
# Count open tickets
|
|
229
|
+
tickets_dir = branch_dir / "tickets"
|
|
230
|
+
ticket_count = 0
|
|
231
|
+
if tickets_dir.exists():
|
|
232
|
+
ticket_count = sum(1 for f in tickets_dir.glob("*.md"))
|
|
233
|
+
|
|
234
|
+
# Build markers
|
|
235
|
+
markers = []
|
|
236
|
+
if name == current_session:
|
|
237
|
+
markers.append("[bold cyan]* session[/bold cyan]")
|
|
238
|
+
if name == git_normalized:
|
|
239
|
+
markers.append("[green]* git[/green]")
|
|
240
|
+
marker_str = f" ({', '.join(markers)})" if markers else ""
|
|
241
|
+
|
|
242
|
+
console.print(f" {original}{branch_status} [{ticket_count} tickets]{marker_str}")
|
|
243
|
+
return
|
|
244
|
+
|
|
245
|
+
# Switch mode: validate and update
|
|
246
|
+
normalized = normalize_branch_name(branch)
|
|
247
|
+
branch_dir = base / ".kd" / "branches" / normalized
|
|
248
|
+
if not branch_dir.exists():
|
|
249
|
+
print_error(f"Branch '{branch}' not found in .kd/branches/.")
|
|
250
|
+
error_console.print("Available branches:")
|
|
251
|
+
branches_dir = base / ".kd" / "branches"
|
|
252
|
+
if branches_dir.exists():
|
|
253
|
+
for d in sorted(branches_dir.iterdir()):
|
|
254
|
+
if d.is_dir():
|
|
255
|
+
error_console.print(f" {d.name}")
|
|
256
|
+
raise typer.Exit(code=1)
|
|
257
|
+
|
|
258
|
+
set_current_run(base, normalized)
|
|
259
|
+
|
|
260
|
+
# Print summary
|
|
261
|
+
state_path = branch_dir / "state.json"
|
|
262
|
+
original = branch
|
|
263
|
+
if state_path.exists():
|
|
264
|
+
try:
|
|
265
|
+
state = read_json(state_path)
|
|
266
|
+
original = state.get("branch", branch)
|
|
267
|
+
except (FileNotFoundError, KeyError):
|
|
268
|
+
pass
|
|
269
|
+
|
|
270
|
+
tickets_dir = branch_dir / "tickets"
|
|
271
|
+
tickets = list_tickets(tickets_dir) if tickets_dir.exists() else []
|
|
272
|
+
open_count = sum(1 for t in tickets if t.status != "closed")
|
|
273
|
+
closed_count = sum(1 for t in tickets if t.status == "closed")
|
|
274
|
+
|
|
275
|
+
git_branch = get_current_git_branch()
|
|
276
|
+
git_info = f" git: {git_branch}" if git_branch else ""
|
|
277
|
+
if git_branch and normalize_branch_name(git_branch) != normalized:
|
|
278
|
+
git_info += " [yellow](mismatch)[/yellow]"
|
|
279
|
+
|
|
280
|
+
console.print(f"Switched to [bold]{original}[/bold]")
|
|
281
|
+
console.print(f" {open_count} open, {closed_count} closed tickets{git_info}")
|
|
282
|
+
|
|
283
|
+
|
|
171
284
|
@app.command(help="Mark the current session as done.")
|
|
172
285
|
def done(
|
|
173
286
|
feature: Annotated[str | None, typer.Argument(help="Branch name (defaults to current session).")] = None,
|
|
@@ -219,7 +332,7 @@ def done(
|
|
|
219
332
|
# Clean up associated worktrees (read from state.json worktrees map)
|
|
220
333
|
worktrees = state.get("worktrees", {})
|
|
221
334
|
if worktrees:
|
|
222
|
-
if not force:
|
|
335
|
+
if not force and sys.stdin.isatty():
|
|
223
336
|
names = ", ".join(worktrees.keys())
|
|
224
337
|
typer.confirm(f"Remove {len(worktrees)} worktree(s) ({names})?", abort=True)
|
|
225
338
|
for ticket_id, wt_path in worktrees.items():
|
|
@@ -324,11 +437,11 @@ def status(
|
|
|
324
437
|
if ticket.status in status_counts:
|
|
325
438
|
status_counts[ticket.status] += 1
|
|
326
439
|
|
|
327
|
-
# Count ready tickets (open
|
|
440
|
+
# Count ready tickets (open with all deps closed — startable, not already started)
|
|
328
441
|
status_by_id = {t.id: t.status for t in tickets}
|
|
329
442
|
ready_count = 0
|
|
330
443
|
for ticket in tickets:
|
|
331
|
-
if ticket.status
|
|
444
|
+
if ticket.status != "open":
|
|
332
445
|
continue
|
|
333
446
|
all_deps_closed = all(status_by_id.get(dep, "unknown") == "closed" for dep in ticket.deps)
|
|
334
447
|
if all_deps_closed:
|