kingdom-cli 0.2.1__tar.gz → 0.3.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.2.1 → kingdom_cli-0.3.0}/PKG-INFO +20 -5
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/README.md +19 -4
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/pyproject.toml +1 -1
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/agent.py +10 -2
- kingdom_cli-0.3.0/src/kingdom/cli/__init__.py +466 -0
- kingdom_cli-0.3.0/src/kingdom/cli/config.py +138 -0
- kingdom_cli-0.3.0/src/kingdom/cli/council.py +1220 -0
- kingdom_cli-0.3.0/src/kingdom/cli/design.py +94 -0
- kingdom_cli-0.3.0/src/kingdom/cli/display.py +42 -0
- kingdom_cli-0.3.0/src/kingdom/cli/helpers.py +148 -0
- kingdom_cli-0.3.0/src/kingdom/cli/peasant.py +1370 -0
- kingdom_cli-0.3.0/src/kingdom/cli/ticket.py +1229 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/config.py +3 -10
- kingdom_cli-0.3.0/src/kingdom/council/__init__.py +48 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/council/council.py +8 -2
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/council/worker.py +2 -11
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/design.py +1 -1
- kingdom_cli-0.3.0/src/kingdom/harness.py +1144 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/parsing.py +30 -1
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/skill/SKILL.md +0 -6
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/skill/references/peasants.md +1 -5
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/state.py +47 -105
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/thread.py +25 -20
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/ticket.py +248 -77
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/app.py +15 -12
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/widgets.py +2 -2
- kingdom_cli-0.3.0/src/kingdom/worker.py +54 -0
- kingdom_cli-0.3.0/src/kingdom/worktree.py +133 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom_cli.egg-info/PKG-INFO +20 -5
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom_cli.egg-info/SOURCES.txt +19 -4
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_cli.py +83 -218
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_cli_council.py +109 -108
- kingdom_cli-0.3.0/tests/test_cli_design_breakdown.py +78 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_cli_peasant.py +580 -143
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_cli_status.py +19 -23
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_cli_ticket_current.py +10 -10
- kingdom_cli-0.3.0/tests/test_cli_ticket_deps.py +382 -0
- kingdom_cli-0.3.0/tests/test_cli_ticket_lifecycle.py +904 -0
- kingdom_cli-0.3.0/tests/test_cli_ticket_links.py +83 -0
- kingdom_cli-0.3.0/tests/test_cli_ticket_list.py +953 -0
- kingdom_cli-0.3.0/tests/test_cli_ticket_show.py +340 -0
- kingdom_cli-0.3.0/tests/test_cli_ticket_worklog.py +102 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_config.py +7 -13
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_council.py +66 -7
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_done.py +15 -38
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_harness.py +720 -178
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_init.py +35 -195
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_parsing.py +66 -1
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_session.py +0 -7
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_state.py +38 -6
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_thread.py +24 -12
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_ticket.py +380 -15
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui.py +53 -43
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_integration.py +0 -8
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_reply.py +0 -10
- kingdom_cli-0.3.0/tests/test_worker.py +106 -0
- kingdom_cli-0.3.0/tests/test_worktree.py +25 -0
- kingdom_cli-0.2.1/src/kingdom/cli.py +0 -4960
- kingdom_cli-0.2.1/src/kingdom/council/__init__.py +0 -13
- kingdom_cli-0.2.1/src/kingdom/harness.py +0 -729
- kingdom_cli-0.2.1/tests/test_cli_design_breakdown.py +0 -83
- kingdom_cli-0.2.1/tests/test_cli_ticket.py +0 -3087
- kingdom_cli-0.2.1/tests/test_cli_whoami.py +0 -46
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/LICENSE +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/setup.cfg +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/__init__.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/__main__.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/breakdown.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/council/base.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/council/bundle.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/session.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/skill/__init__.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/skill/references/__init__.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/skill/references/council.md +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/skill/references/tickets.md +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/synthesis.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/__init__.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/chat.tcss +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/clipboard.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/poll.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom/tui/terminal.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom_cli.egg-info/dependency_links.txt +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom_cli.egg-info/entry_points.txt +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom_cli.egg-info/requires.txt +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/src/kingdom_cli.egg-info/top_level.txt +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_agent.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_breakdown.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_design.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_skill.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_synthesis.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_clipboard.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_poll.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_terminal.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_thinking_persistence.py +0 -0
- {kingdom_cli-0.2.1 → kingdom_cli-0.3.0}/tests/test_tui_widgets.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: kingdom-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.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
|
|
@@ -58,10 +58,12 @@ Design with the council, break into tickets, dispatch peasants, review, and merg
|
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
kd start # initialize branch session
|
|
61
|
-
kd chat --new
|
|
61
|
+
kd council chat --new # discuss design with the council TUI
|
|
62
62
|
kd design approve # lock in the design
|
|
63
|
-
|
|
63
|
+
# create tickets from the design (via the kingdom skill or manually)
|
|
64
64
|
kd peasant start <id> # dispatch parallel workers
|
|
65
|
+
kd peasant review <id> # review completed work
|
|
66
|
+
kd peasant accept <id> # accept and close, or reject with feedback
|
|
65
67
|
kd done # archive and clean up
|
|
66
68
|
```
|
|
67
69
|
|
|
@@ -95,7 +97,6 @@ Design docs, council sessions, and peasant workers are all optional. A branch wi
|
|
|
95
97
|
## Getting Started
|
|
96
98
|
|
|
97
99
|
```bash
|
|
98
|
-
kd init # one-time: create .kd/ directory
|
|
99
100
|
kd start # start a session on the current branch
|
|
100
101
|
```
|
|
101
102
|
|
|
@@ -103,7 +104,7 @@ Configure council agent CLIs in `.kd/config.json` (check effective config with `
|
|
|
103
104
|
|
|
104
105
|
## Chat Modes
|
|
105
106
|
|
|
106
|
-
The council chat TUI (`kd chat`) supports four modes, configured via `council.chat.mode`:
|
|
107
|
+
The council chat TUI (`kd council chat`) supports four modes, configured via `council.chat.mode`:
|
|
107
108
|
|
|
108
109
|
| Mode | First turn | Auto-turns | Default |
|
|
109
110
|
|------|-----------|------------|---------|
|
|
@@ -136,6 +137,20 @@ All state lives in `.kd/` as plain Markdown and JSON files, tracked in git along
|
|
|
136
137
|
|
|
137
138
|
No database. No server. Just files on disk.
|
|
138
139
|
|
|
140
|
+
## Commands
|
|
141
|
+
|
|
142
|
+
| Group | Description |
|
|
143
|
+
|-------|-------------|
|
|
144
|
+
| `kd start` / `kd done` / `kd status` | Branch lifecycle — initialize, finish, and inspect sessions |
|
|
145
|
+
| `kd design` | Manage design documents (`show`, `approve`) |
|
|
146
|
+
| `kd council` | Multi-model council — `ask`, `chat`, `review`, `show`, `list`, `watch`, `reset`, `retry` |
|
|
147
|
+
| `kd ticket` (alias `kd tk`) | Ticket management — `create`, `list`, `show`, `start`, `close`, `deps`, and more |
|
|
148
|
+
| `kd peasant` | Worker agents — `start`, `stop`, `review`, `accept`, `reject`, `msg`, `read`, `watch` |
|
|
149
|
+
| `kd config` | View and manage configuration |
|
|
150
|
+
| `kd doctor` | Check config and agent CLIs |
|
|
151
|
+
|
|
152
|
+
Run `kd <command> --help` for full flags and options.
|
|
153
|
+
|
|
139
154
|
## Development
|
|
140
155
|
|
|
141
156
|
```bash
|
|
@@ -31,10 +31,12 @@ Design with the council, break into tickets, dispatch peasants, review, and merg
|
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
33
|
kd start # initialize branch session
|
|
34
|
-
kd chat --new
|
|
34
|
+
kd council chat --new # discuss design with the council TUI
|
|
35
35
|
kd design approve # lock in the design
|
|
36
|
-
|
|
36
|
+
# create tickets from the design (via the kingdom skill or manually)
|
|
37
37
|
kd peasant start <id> # dispatch parallel workers
|
|
38
|
+
kd peasant review <id> # review completed work
|
|
39
|
+
kd peasant accept <id> # accept and close, or reject with feedback
|
|
38
40
|
kd done # archive and clean up
|
|
39
41
|
```
|
|
40
42
|
|
|
@@ -68,7 +70,6 @@ Design docs, council sessions, and peasant workers are all optional. A branch wi
|
|
|
68
70
|
## Getting Started
|
|
69
71
|
|
|
70
72
|
```bash
|
|
71
|
-
kd init # one-time: create .kd/ directory
|
|
72
73
|
kd start # start a session on the current branch
|
|
73
74
|
```
|
|
74
75
|
|
|
@@ -76,7 +77,7 @@ Configure council agent CLIs in `.kd/config.json` (check effective config with `
|
|
|
76
77
|
|
|
77
78
|
## Chat Modes
|
|
78
79
|
|
|
79
|
-
The council chat TUI (`kd chat`) supports four modes, configured via `council.chat.mode`:
|
|
80
|
+
The council chat TUI (`kd council chat`) supports four modes, configured via `council.chat.mode`:
|
|
80
81
|
|
|
81
82
|
| Mode | First turn | Auto-turns | Default |
|
|
82
83
|
|------|-----------|------------|---------|
|
|
@@ -109,6 +110,20 @@ All state lives in `.kd/` as plain Markdown and JSON files, tracked in git along
|
|
|
109
110
|
|
|
110
111
|
No database. No server. Just files on disk.
|
|
111
112
|
|
|
113
|
+
## Commands
|
|
114
|
+
|
|
115
|
+
| Group | Description |
|
|
116
|
+
|-------|-------------|
|
|
117
|
+
| `kd start` / `kd done` / `kd status` | Branch lifecycle — initialize, finish, and inspect sessions |
|
|
118
|
+
| `kd design` | Manage design documents (`show`, `approve`) |
|
|
119
|
+
| `kd council` | Multi-model council — `ask`, `chat`, `review`, `show`, `list`, `watch`, `reset`, `retry` |
|
|
120
|
+
| `kd ticket` (alias `kd tk`) | Ticket management — `create`, `list`, `show`, `start`, `close`, `deps`, and more |
|
|
121
|
+
| `kd peasant` | Worker agents — `start`, `stop`, `review`, `accept`, `reject`, `msg`, `read`, `watch` |
|
|
122
|
+
| `kd config` | View and manage configuration |
|
|
123
|
+
| `kd doctor` | Check config and agent CLIs |
|
|
124
|
+
|
|
125
|
+
Run `kd <command> --help` for full flags and options.
|
|
126
|
+
|
|
112
127
|
## Development
|
|
113
128
|
|
|
114
129
|
```bash
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "kingdom-cli"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.3.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"
|
|
@@ -435,13 +435,19 @@ COMMAND_BUILDERS: dict[str, CommandBuilder] = {
|
|
|
435
435
|
}
|
|
436
436
|
|
|
437
437
|
|
|
438
|
-
def clean_agent_env(
|
|
438
|
+
def clean_agent_env(
|
|
439
|
+
role: str | None = None,
|
|
440
|
+
agent_name: str | None = None,
|
|
441
|
+
kd_base: str | None = None,
|
|
442
|
+
) -> dict[str, str]:
|
|
439
443
|
"""Return an env dict safe for spawning agent CLI subprocesses.
|
|
440
444
|
|
|
441
445
|
Strips ``CLAUDECODE`` so child ``claude`` processes don't refuse to start
|
|
442
446
|
with a "nested session" error when ``kd`` is invoked from inside Claude Code.
|
|
443
447
|
|
|
444
|
-
Optionally injects ``KD_ROLE
|
|
448
|
+
Optionally injects ``KD_ROLE``, ``KD_AGENT_NAME``, and ``KD_BASE``.
|
|
449
|
+
``KD_BASE`` ensures worktree agents resolve the project root against the
|
|
450
|
+
main repo, not the worktree's copy of ``.kd/``.
|
|
445
451
|
"""
|
|
446
452
|
import os
|
|
447
453
|
|
|
@@ -450,6 +456,8 @@ def clean_agent_env(role: str | None = None, agent_name: str | None = None) -> d
|
|
|
450
456
|
env["KD_ROLE"] = role
|
|
451
457
|
if agent_name:
|
|
452
458
|
env["KD_AGENT_NAME"] = agent_name
|
|
459
|
+
if kd_base:
|
|
460
|
+
env["KD_BASE"] = kd_base
|
|
453
461
|
return env
|
|
454
462
|
|
|
455
463
|
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
"""Command-line interface for Kingdom.
|
|
2
|
+
|
|
3
|
+
Usage example:
|
|
4
|
+
kd --help
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import json
|
|
10
|
+
import os
|
|
11
|
+
import subprocess
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Annotated
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.panel import Panel
|
|
18
|
+
|
|
19
|
+
from kingdom.council import Council, create_council # noqa: F401 (Council used by tests)
|
|
20
|
+
from kingdom.design import ensure_design_initialized
|
|
21
|
+
from kingdom.state import (
|
|
22
|
+
branch_root,
|
|
23
|
+
clear_current_run,
|
|
24
|
+
ensure_base_layout,
|
|
25
|
+
ensure_branch_layout,
|
|
26
|
+
find_project_root,
|
|
27
|
+
get_current_git_branch,
|
|
28
|
+
normalize_branch_name,
|
|
29
|
+
read_json,
|
|
30
|
+
resolve_current_run,
|
|
31
|
+
set_current_run,
|
|
32
|
+
state_root,
|
|
33
|
+
write_json,
|
|
34
|
+
)
|
|
35
|
+
from kingdom.ticket import Ticket, list_tickets
|
|
36
|
+
from kingdom.worktree import create_worktree, remove_worktree, worktree_path_for # noqa: F401
|
|
37
|
+
|
|
38
|
+
from .config import check_cli, check_config, config_app, get_doctor_checks
|
|
39
|
+
from .council import council_app
|
|
40
|
+
from .design import design_app, get_branch_paths, get_doc_status # noqa: F401 (re-export)
|
|
41
|
+
from .display import error_console, print_error, styled_echo
|
|
42
|
+
from .helpers import install_skill, is_git_repo, require_project_root, verbose_echo # noqa: F401
|
|
43
|
+
from .peasant import ( # noqa: F401
|
|
44
|
+
PeasantContext,
|
|
45
|
+
launch_work_background,
|
|
46
|
+
launch_work_tmux,
|
|
47
|
+
peasant_app,
|
|
48
|
+
resolve_peasant_context,
|
|
49
|
+
)
|
|
50
|
+
from .ticket import format_ticket_line, format_ticket_summary, get_tickets_dir, ticket_app # noqa: F401
|
|
51
|
+
|
|
52
|
+
NO_COLOR = "NO_COLOR" in os.environ or os.environ.get("TERM") == "dumb"
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Main app
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
app = typer.Typer(
|
|
59
|
+
name="kd",
|
|
60
|
+
help="Kingdom CLI.",
|
|
61
|
+
add_completion=False,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@app.callback()
|
|
66
|
+
def app_callback(
|
|
67
|
+
ctx: typer.Context,
|
|
68
|
+
verbose: Annotated[bool, typer.Option("--verbose", "-v", help="Print debug output.")] = False,
|
|
69
|
+
) -> None:
|
|
70
|
+
ctx.ensure_object(dict)["verbose"] = verbose
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
# Sub-app mounting
|
|
75
|
+
# ---------------------------------------------------------------------------
|
|
76
|
+
|
|
77
|
+
app.add_typer(council_app, name="council")
|
|
78
|
+
app.add_typer(design_app, name="design")
|
|
79
|
+
app.add_typer(peasant_app, name="peasant")
|
|
80
|
+
app.add_typer(config_app, name="config")
|
|
81
|
+
app.add_typer(ticket_app, name="ticket")
|
|
82
|
+
app.add_typer(ticket_app, name="tk", hidden=True) # Alias for muscle memory
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ---------------------------------------------------------------------------
|
|
86
|
+
# Top-level commands
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command(help="Initialize a branch-based session and state.")
|
|
91
|
+
def start(
|
|
92
|
+
branch: Annotated[str | None, typer.Argument(help="Branch name (defaults to current git branch).")] = None,
|
|
93
|
+
force: Annotated[
|
|
94
|
+
bool, typer.Option("--force", "-f", help="Force start even if a session is already active.")
|
|
95
|
+
] = False,
|
|
96
|
+
) -> None:
|
|
97
|
+
# If KD_BASE is explicitly set, require it to be valid — no auto-init fallback.
|
|
98
|
+
# Otherwise, fall back to cwd so auto-init can create .kd/ in a fresh repo.
|
|
99
|
+
if os.environ.get("KD_BASE"):
|
|
100
|
+
base = require_project_root()
|
|
101
|
+
else:
|
|
102
|
+
try:
|
|
103
|
+
base = find_project_root()
|
|
104
|
+
except ValueError:
|
|
105
|
+
base = Path.cwd()
|
|
106
|
+
|
|
107
|
+
# Auto-init if .kd/ doesn't exist (with git check)
|
|
108
|
+
if not state_root(base).exists():
|
|
109
|
+
if not is_git_repo(base):
|
|
110
|
+
print_error("Not a git repository. Initialize a git repo first, then run `kd start`.")
|
|
111
|
+
raise typer.Exit(code=1)
|
|
112
|
+
# Always auto-init at the git root, not wherever cwd happens to be
|
|
113
|
+
try:
|
|
114
|
+
git_root_result = subprocess.run(
|
|
115
|
+
["git", "rev-parse", "--show-toplevel"],
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
cwd=base,
|
|
119
|
+
timeout=5,
|
|
120
|
+
)
|
|
121
|
+
if git_root_result.returncode == 0 and git_root_result.stdout.strip():
|
|
122
|
+
base = Path(git_root_result.stdout.strip())
|
|
123
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
124
|
+
pass # Fall back to using base as-is
|
|
125
|
+
typer.echo("Auto-initializing .kd/ directory...")
|
|
126
|
+
ensure_base_layout(base)
|
|
127
|
+
install_skill()
|
|
128
|
+
|
|
129
|
+
# Check for existing current run
|
|
130
|
+
current_path = state_root(base) / "current"
|
|
131
|
+
if current_path.exists() and not force:
|
|
132
|
+
existing = current_path.read_text(encoding="utf-8").strip()
|
|
133
|
+
print_error(f"A session is already active: {existing}")
|
|
134
|
+
error_console.print(" Use --force to override, or run `kd done` first.")
|
|
135
|
+
raise typer.Exit(code=1)
|
|
136
|
+
|
|
137
|
+
# Determine branch name
|
|
138
|
+
if branch is None:
|
|
139
|
+
branch = get_current_git_branch()
|
|
140
|
+
if branch is None:
|
|
141
|
+
print_error("Detached HEAD state. Please provide a branch name:")
|
|
142
|
+
error_console.print(" kd start <branch-name>")
|
|
143
|
+
raise typer.Exit(code=1)
|
|
144
|
+
|
|
145
|
+
# Normalize branch name for directory
|
|
146
|
+
normalized = normalize_branch_name(branch)
|
|
147
|
+
|
|
148
|
+
# Create branch layout
|
|
149
|
+
branch_dir = ensure_branch_layout(base, branch)
|
|
150
|
+
|
|
151
|
+
# Initialize design doc with template
|
|
152
|
+
design_path = branch_dir / "design.md"
|
|
153
|
+
ensure_design_initialized(design_path, branch)
|
|
154
|
+
|
|
155
|
+
# Write .kd/current with normalized name
|
|
156
|
+
set_current_run(base, normalized)
|
|
157
|
+
|
|
158
|
+
# Update state.json with original branch name
|
|
159
|
+
state_path = branch_dir / "state.json"
|
|
160
|
+
state = read_json(state_path)
|
|
161
|
+
state["branch"] = branch
|
|
162
|
+
write_json(state_path, state)
|
|
163
|
+
|
|
164
|
+
typer.echo(f"Started session for branch {branch}")
|
|
165
|
+
typer.echo(f" Location: {branch_dir}")
|
|
166
|
+
typer.echo(f" Design: {design_path}")
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
@app.command(help="Mark the current session as done.")
|
|
170
|
+
def done(
|
|
171
|
+
feature: Annotated[str | None, typer.Argument(help="Branch name (defaults to current session).")] = None,
|
|
172
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Close even if open tickets remain.")] = False,
|
|
173
|
+
) -> None:
|
|
174
|
+
"""Mark a session as done (status transition only, no file moves)."""
|
|
175
|
+
from datetime import UTC, datetime
|
|
176
|
+
|
|
177
|
+
base = require_project_root()
|
|
178
|
+
|
|
179
|
+
# Resolve feature: use argument or fall back to current session
|
|
180
|
+
if feature is None:
|
|
181
|
+
try:
|
|
182
|
+
feature = resolve_current_run(base)
|
|
183
|
+
except RuntimeError:
|
|
184
|
+
print_error("No active session. Pass the branch name: `kd done <branch>`")
|
|
185
|
+
raise typer.Exit(code=1) from None
|
|
186
|
+
|
|
187
|
+
# Get the branch directory (normalized name)
|
|
188
|
+
normalized = normalize_branch_name(feature)
|
|
189
|
+
source_dir = branch_root(base, feature)
|
|
190
|
+
|
|
191
|
+
# Check if it exists
|
|
192
|
+
if not source_dir.exists():
|
|
193
|
+
print_error(f"Branch '{feature}' not found.")
|
|
194
|
+
raise typer.Exit(code=1)
|
|
195
|
+
|
|
196
|
+
# Check for open tickets
|
|
197
|
+
if not force:
|
|
198
|
+
tickets_dir = source_dir / "tickets"
|
|
199
|
+
open_tickets = [t for t in list_tickets(tickets_dir) if t.status != "closed"]
|
|
200
|
+
if open_tickets:
|
|
201
|
+
print_error(f"{len(open_tickets)} open ticket(s) on '{feature}':")
|
|
202
|
+
for t in open_tickets:
|
|
203
|
+
error_console.print(f" {t.id} \\[{t.status}] {t.title}")
|
|
204
|
+
error_console.print("\nClose tickets, move them to backlog with `kd tk move`, or use --force.")
|
|
205
|
+
raise typer.Exit(code=1)
|
|
206
|
+
|
|
207
|
+
# Update state.json with status and timestamp
|
|
208
|
+
state_path = source_dir / "state.json"
|
|
209
|
+
if state_path.exists():
|
|
210
|
+
state = read_json(state_path)
|
|
211
|
+
else:
|
|
212
|
+
state = {}
|
|
213
|
+
state["status"] = "done"
|
|
214
|
+
state["done_at"] = datetime.now(UTC).isoformat()
|
|
215
|
+
write_json(state_path, state)
|
|
216
|
+
|
|
217
|
+
# Clean up associated worktrees (read from state.json worktrees map)
|
|
218
|
+
worktrees = state.get("worktrees", {})
|
|
219
|
+
if worktrees:
|
|
220
|
+
if not force:
|
|
221
|
+
names = ", ".join(worktrees.keys())
|
|
222
|
+
typer.confirm(f"Remove {len(worktrees)} worktree(s) ({names})?", abort=True)
|
|
223
|
+
for ticket_id, wt_path in worktrees.items():
|
|
224
|
+
wt = Path(wt_path)
|
|
225
|
+
if wt.exists():
|
|
226
|
+
result = subprocess.run(
|
|
227
|
+
["git", "worktree", "remove", "--force", str(wt)],
|
|
228
|
+
capture_output=True,
|
|
229
|
+
text=True,
|
|
230
|
+
)
|
|
231
|
+
if result.returncode != 0:
|
|
232
|
+
typer.echo(f"Warning: Failed to remove worktree {ticket_id}: {result.stderr.strip()}")
|
|
233
|
+
state["worktrees"] = {}
|
|
234
|
+
write_json(state_path, state)
|
|
235
|
+
|
|
236
|
+
# Clear current session pointer (only if this was the current session)
|
|
237
|
+
current_path = state_root(base) / "current"
|
|
238
|
+
session_cleared = False
|
|
239
|
+
if current_path.exists():
|
|
240
|
+
current_feature = current_path.read_text(encoding="utf-8").strip()
|
|
241
|
+
if current_feature == normalized:
|
|
242
|
+
clear_current_run(base)
|
|
243
|
+
session_cleared = True
|
|
244
|
+
|
|
245
|
+
# Summary
|
|
246
|
+
tickets_dir = source_dir / "tickets"
|
|
247
|
+
all_tickets = list_tickets(tickets_dir)
|
|
248
|
+
closed_count = sum(1 for t in all_tickets if t.status == "closed")
|
|
249
|
+
|
|
250
|
+
console = Console()
|
|
251
|
+
|
|
252
|
+
lines: list[str] = []
|
|
253
|
+
if closed_count:
|
|
254
|
+
lines.append(f"[cyan]{closed_count}[/cyan] tickets closed")
|
|
255
|
+
if session_cleared:
|
|
256
|
+
lines.append("Session cleared")
|
|
257
|
+
|
|
258
|
+
push_reminder = ""
|
|
259
|
+
try:
|
|
260
|
+
rev_result = subprocess.run(
|
|
261
|
+
["git", "rev-list", "--count", "@{u}..HEAD"],
|
|
262
|
+
capture_output=True,
|
|
263
|
+
text=True,
|
|
264
|
+
)
|
|
265
|
+
if rev_result.returncode == 0:
|
|
266
|
+
ahead = int(rev_result.stdout.strip())
|
|
267
|
+
if ahead > 0:
|
|
268
|
+
push_reminder = f"[yellow]{ahead} unpushed commit(s) — remember to push[/yellow]"
|
|
269
|
+
else:
|
|
270
|
+
push_reminder = "[yellow]No upstream branch — remember to push[/yellow]"
|
|
271
|
+
except (subprocess.SubprocessError, ValueError) as exc:
|
|
272
|
+
push_reminder = f"[yellow]Could not check upstream status: {exc}[/yellow]"
|
|
273
|
+
|
|
274
|
+
if push_reminder:
|
|
275
|
+
lines.append(push_reminder)
|
|
276
|
+
|
|
277
|
+
body = "\n".join(lines) if lines else "[dim]No additional info[/dim]"
|
|
278
|
+
panel = Panel(body, title=f"[bold green]Done: {feature}[/bold green]", border_style="green")
|
|
279
|
+
console.print(panel)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
@app.command(help="Show current branch, design doc status, and breakdown status.")
|
|
283
|
+
def status(
|
|
284
|
+
output_json: Annotated[bool, typer.Option("--json", help="Output as JSON for machine consumption.")] = False,
|
|
285
|
+
) -> None:
|
|
286
|
+
base = require_project_root()
|
|
287
|
+
try:
|
|
288
|
+
feature = resolve_current_run(base)
|
|
289
|
+
except RuntimeError as exc:
|
|
290
|
+
print_error(str(exc))
|
|
291
|
+
raise typer.Exit(code=1) from None
|
|
292
|
+
|
|
293
|
+
normalized = normalize_branch_name(feature)
|
|
294
|
+
bdir = branch_root(base, feature)
|
|
295
|
+
state_path = bdir / "state.json"
|
|
296
|
+
design_path = bdir / "design.md"
|
|
297
|
+
breakdown_path = bdir / "breakdown.md"
|
|
298
|
+
|
|
299
|
+
# Read state to get original branch name
|
|
300
|
+
if state_path.exists():
|
|
301
|
+
state = read_json(state_path)
|
|
302
|
+
else:
|
|
303
|
+
state = {}
|
|
304
|
+
|
|
305
|
+
# Original branch name (stored in state.json) vs normalized directory name
|
|
306
|
+
original_branch = state.get("branch", feature)
|
|
307
|
+
|
|
308
|
+
# Get design and breakdown status
|
|
309
|
+
design_status = get_doc_status(design_path)
|
|
310
|
+
breakdown_status = get_doc_status(breakdown_path)
|
|
311
|
+
|
|
312
|
+
# Get design doc path relative to base for display
|
|
313
|
+
design_path_str = str(design_path.relative_to(base)) if design_path.exists() else None
|
|
314
|
+
|
|
315
|
+
# Get ticket counts
|
|
316
|
+
tickets_dir = get_tickets_dir(base)
|
|
317
|
+
tickets = list_tickets(tickets_dir) if tickets_dir.exists() else []
|
|
318
|
+
|
|
319
|
+
# Count by status
|
|
320
|
+
status_counts = {"open": 0, "in_progress": 0, "in_review": 0, "closed": 0}
|
|
321
|
+
for ticket in tickets:
|
|
322
|
+
if ticket.status in status_counts:
|
|
323
|
+
status_counts[ticket.status] += 1
|
|
324
|
+
|
|
325
|
+
# Count ready tickets (open/in_progress with all deps closed — excludes in_review)
|
|
326
|
+
status_by_id = {t.id: t.status for t in tickets}
|
|
327
|
+
ready_count = 0
|
|
328
|
+
for ticket in tickets:
|
|
329
|
+
if ticket.status not in ("open", "in_progress"):
|
|
330
|
+
continue
|
|
331
|
+
all_deps_closed = all(status_by_id.get(dep, "unknown") == "closed" for dep in ticket.deps)
|
|
332
|
+
if all_deps_closed:
|
|
333
|
+
ready_count += 1
|
|
334
|
+
|
|
335
|
+
# Design approved status
|
|
336
|
+
design_approved = state.get("design_approved", False)
|
|
337
|
+
|
|
338
|
+
# Build output structure
|
|
339
|
+
output = {
|
|
340
|
+
"branch": original_branch,
|
|
341
|
+
"normalized_branch": normalized,
|
|
342
|
+
"design_path": design_path_str,
|
|
343
|
+
"design_status": design_status,
|
|
344
|
+
"design_approved": design_approved,
|
|
345
|
+
"breakdown_status": breakdown_status,
|
|
346
|
+
"tickets": status_counts,
|
|
347
|
+
"ready_count": ready_count,
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
# Group tickets by assignee
|
|
351
|
+
role = os.environ.get("KD_ROLE", "")
|
|
352
|
+
agent_name = os.environ.get("KD_AGENT_NAME", "")
|
|
353
|
+
if not role:
|
|
354
|
+
role = "hand" if os.environ.get("CLAUDECODE") else "king"
|
|
355
|
+
|
|
356
|
+
assigned: dict[str, list[Ticket]] = {}
|
|
357
|
+
for ticket in tickets:
|
|
358
|
+
if ticket.assignee:
|
|
359
|
+
assigned.setdefault(ticket.assignee, []).append(ticket)
|
|
360
|
+
|
|
361
|
+
output["role"] = role
|
|
362
|
+
output["agent_name"] = agent_name
|
|
363
|
+
output["assignments"] = {k: [t.id for t in v] for k, v in assigned.items()}
|
|
364
|
+
|
|
365
|
+
if output_json:
|
|
366
|
+
typer.echo(json.dumps(output, indent=2))
|
|
367
|
+
else:
|
|
368
|
+
# Human-readable output
|
|
369
|
+
typer.echo(f"Branch: {original_branch}")
|
|
370
|
+
if design_path_str:
|
|
371
|
+
approved_str = " (approved)" if design_approved else ""
|
|
372
|
+
typer.echo(f"Design: {design_path_str}{approved_str}")
|
|
373
|
+
typer.echo()
|
|
374
|
+
total = sum(status_counts.values())
|
|
375
|
+
typer.echo(
|
|
376
|
+
f"Tickets: {status_counts['open']} open, {status_counts['in_progress']} in progress, "
|
|
377
|
+
f"{status_counts['in_review']} in review, {status_counts['closed']} closed, "
|
|
378
|
+
f"{ready_count} ready ({total} total)"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
if assigned:
|
|
382
|
+
typer.echo()
|
|
383
|
+
typer.echo("Assignments:")
|
|
384
|
+
for assignee, assignee_tickets in assigned.items():
|
|
385
|
+
for t in assignee_tickets:
|
|
386
|
+
typer.echo(f" {assignee}: {t.id} [{t.status}] {t.title}")
|
|
387
|
+
|
|
388
|
+
|
|
389
|
+
@app.command(help="Check config and agent CLIs.")
|
|
390
|
+
def doctor(
|
|
391
|
+
output_json: Annotated[bool, typer.Option("--json", help="Output as JSON.")] = False,
|
|
392
|
+
) -> None:
|
|
393
|
+
"""Validate config and verify agent CLIs are installed."""
|
|
394
|
+
from kingdom.state import state_root as _state_root
|
|
395
|
+
|
|
396
|
+
base = require_project_root()
|
|
397
|
+
has_issues = False
|
|
398
|
+
|
|
399
|
+
# 1. Config validation
|
|
400
|
+
config_path = _state_root(base) / "config.json"
|
|
401
|
+
config_ok, config_error = check_config(base)
|
|
402
|
+
|
|
403
|
+
if not config_ok:
|
|
404
|
+
has_issues = True
|
|
405
|
+
|
|
406
|
+
if output_json:
|
|
407
|
+
config_result = {"exists": config_path.exists(), "valid": config_ok, "error": config_error}
|
|
408
|
+
else:
|
|
409
|
+
typer.echo("\nConfig:")
|
|
410
|
+
if not config_path.exists():
|
|
411
|
+
styled_echo(" ○ No config.json (using defaults)", fg=typer.colors.YELLOW)
|
|
412
|
+
elif config_ok:
|
|
413
|
+
styled_echo(" ✓ config.json valid", fg=typer.colors.GREEN)
|
|
414
|
+
else:
|
|
415
|
+
styled_echo(f" ✗ config.json: {config_error}", fg=typer.colors.RED)
|
|
416
|
+
|
|
417
|
+
# 2. Agent CLI checks (skip if config is invalid — can't resolve agents)
|
|
418
|
+
cli_results: dict[str, dict[str, bool | str | None]] = {}
|
|
419
|
+
cli_issues: list[dict[str, str]] = []
|
|
420
|
+
|
|
421
|
+
if config_ok:
|
|
422
|
+
doctor_checks = get_doctor_checks(base)
|
|
423
|
+
for check in doctor_checks:
|
|
424
|
+
installed, error = check_cli(check["command"])
|
|
425
|
+
cli_results[check["name"]] = {"installed": installed, "error": error}
|
|
426
|
+
if not installed:
|
|
427
|
+
cli_issues.append({"name": check["name"], "hint": check["install_hint"]})
|
|
428
|
+
|
|
429
|
+
if output_json:
|
|
430
|
+
console = Console()
|
|
431
|
+
console.print_json(json.dumps({"config": config_result, "agents": cli_results}, indent=2))
|
|
432
|
+
else:
|
|
433
|
+
if not config_ok:
|
|
434
|
+
typer.echo("\nAgent CLIs:")
|
|
435
|
+
styled_echo(" ○ Skipped (fix config first)", fg=typer.colors.YELLOW)
|
|
436
|
+
else:
|
|
437
|
+
typer.echo("\nAgent CLIs:")
|
|
438
|
+
for check in doctor_checks:
|
|
439
|
+
name = check["name"]
|
|
440
|
+
result = cli_results[name]
|
|
441
|
+
if result["installed"]:
|
|
442
|
+
styled_echo(f" ✓ {name:12} (installed)", fg=typer.colors.GREEN)
|
|
443
|
+
else:
|
|
444
|
+
styled_echo(f" ✗ {name:12} (not found)", fg=typer.colors.RED)
|
|
445
|
+
|
|
446
|
+
if cli_issues:
|
|
447
|
+
typer.echo("\nIssues found:")
|
|
448
|
+
for issue in cli_issues:
|
|
449
|
+
typer.echo(f" {issue['name']}: {issue['hint']}")
|
|
450
|
+
typer.echo()
|
|
451
|
+
|
|
452
|
+
if has_issues or cli_issues:
|
|
453
|
+
raise typer.Exit(code=1)
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
# ---------------------------------------------------------------------------
|
|
457
|
+
# Entry point
|
|
458
|
+
# ---------------------------------------------------------------------------
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def main() -> None:
|
|
462
|
+
app()
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
if __name__ == "__main__":
|
|
466
|
+
main()
|