troven 0.1.0__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.
troven/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """Troven - Git-native CLI for tracking change intents and speculative development."""
2
+
3
+ __version__ = "0.1.0"
troven/__main__.py ADDED
@@ -0,0 +1,7 @@
1
+ """Entry point for python -m troven."""
2
+
3
+ import sys
4
+ from troven.cli import main
5
+
6
+ if __name__ == "__main__":
7
+ sys.exit(main())
troven/cli.py ADDED
@@ -0,0 +1,144 @@
1
+ """CLI argument parsing and command dispatch for Troven."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+
8
+ from troven import __version__
9
+ from troven.commands.accept import execute_accept
10
+ from troven.commands.create import execute_create
11
+ from troven.commands.list_cmd import execute_list
12
+ from troven.commands.open_cmd import execute_open
13
+ from troven.commands.reject import execute_reject
14
+ from troven.commands.show import execute_show
15
+ from troven.errors import TrovenError
16
+
17
+
18
+ def build_parser() -> argparse.ArgumentParser:
19
+ parser = argparse.ArgumentParser(
20
+ prog="troven",
21
+ description="Git-native CLI for tracking change intents and speculative development.",
22
+ )
23
+ parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
24
+
25
+ subparsers = parser.add_subparsers(dest="subcommand", title="subcommands", required=True)
26
+
27
+ # create
28
+ create_p = subparsers.add_parser(
29
+ "create",
30
+ help="Create a new troven worktree and initialize its change intent record.",
31
+ )
32
+ create_p.add_argument("name", help="Dash-separated name for the troven (e.g. 'fast-lexer-simd')")
33
+ create_p.add_argument("--from", dest="from_ref", help="Base branch, commit, or parent troven ID")
34
+ create_p.add_argument("--repo", dest="repo_url", help="Git repository URL to clone first")
35
+ create_p.add_argument("-f", "--force", action="store_true", help="Skip confirmation prompts")
36
+ create_p.add_argument("--no-exec", action="store_true", help="Initialize worktree without launching agent")
37
+ create_p.add_argument("command", nargs="*", help="Custom command to run in worktree (after --)")
38
+
39
+ # open
40
+ open_p = subparsers.add_parser(
41
+ "open",
42
+ help="Open an existing active or rejected troven in a worktree.",
43
+ )
44
+ open_p.add_argument("id", nargs="?", help="Troven ID (defaults to current branch/worktree)")
45
+ open_p.add_argument("--no-exec", action="store_true", help="Mount worktree without launching agent")
46
+ open_p.add_argument("command", nargs="*", help="Custom command to run in worktree (after --)")
47
+
48
+ # accept
49
+ accept_p = subparsers.add_parser(
50
+ "accept",
51
+ help="Incorporate an active troven's changes into the target branch.",
52
+ )
53
+ accept_p.add_argument("id", nargs="?", help="Troven ID (defaults to current worktree/branch)")
54
+ accept_p.add_argument("--into", dest="into_branch", help="Target branch (defaults to base_branch)")
55
+ accept_p.add_argument("-f", "--force", action="store_true", help="Bypass foreign repo confirmation")
56
+
57
+ # reject
58
+ reject_p = subparsers.add_parser(
59
+ "reject",
60
+ help="Close an active troven without merging; preserve rejection knowledge in archive ref.",
61
+ )
62
+ reject_p.add_argument("id", nargs="?", help="Troven ID (defaults to current worktree/branch)")
63
+
64
+ # list
65
+ list_p = subparsers.add_parser(
66
+ "list",
67
+ help="List all trovens in a hierarchical tree view.",
68
+ )
69
+ list_p.add_argument("--status", choices=["active", "accepted", "rejected"], help="Filter by status")
70
+
71
+ # show
72
+ show_p = subparsers.add_parser(
73
+ "show",
74
+ help="Print the full markdown record for a troven.",
75
+ )
76
+ show_p.add_argument("id", nargs="?", help="Troven ID (defaults to current worktree/branch)")
77
+
78
+ # install-skill
79
+ install_skill_p = subparsers.add_parser(
80
+ "install-skill",
81
+ help="Install the troven agent skill to ~/.agents/skills/troven and ~/.gemini/config/skills/troven.",
82
+ )
83
+
84
+ return parser
85
+
86
+
87
+ def main(argv: list[str] | None = None) -> int:
88
+ parser = build_parser()
89
+ args = parser.parse_args(argv)
90
+
91
+ try:
92
+ if args.subcommand == "create":
93
+ cmd = args.command
94
+ if cmd and cmd[0] == "--":
95
+ cmd = cmd[1:]
96
+ execute_create(
97
+ name=args.name,
98
+ from_ref=args.from_ref,
99
+ repo_url=args.repo_url,
100
+ force=args.force,
101
+ command=cmd,
102
+ no_exec=args.no_exec,
103
+ )
104
+ elif args.subcommand == "open":
105
+ cmd = args.command
106
+ if cmd and cmd[0] == "--":
107
+ cmd = cmd[1:]
108
+ execute_open(
109
+ troven_id=args.id,
110
+ command=cmd,
111
+ no_exec=args.no_exec,
112
+ )
113
+ elif args.subcommand == "accept":
114
+ execute_accept(
115
+ troven_id=args.id,
116
+ into_branch=args.into_branch,
117
+ force=args.force,
118
+ )
119
+ elif args.subcommand == "reject":
120
+ execute_reject(
121
+ troven_id=args.id,
122
+ )
123
+ elif args.subcommand == "list":
124
+ execute_list(
125
+ status_filter=args.status,
126
+ )
127
+ elif args.subcommand == "show":
128
+ execute_show(
129
+ troven_id=args.id,
130
+ )
131
+ elif args.subcommand == "install-skill":
132
+ from troven.commands.install_skill import execute_install_skill
133
+ execute_install_skill()
134
+ return 0
135
+ except TrovenError as err:
136
+ print(f"\033[31mError:\033[0m {err}", file=sys.stderr)
137
+ return 1
138
+ except KeyboardInterrupt:
139
+ print("\nAborted.", file=sys.stderr)
140
+ return 130
141
+
142
+
143
+ if __name__ == "__main__":
144
+ sys.exit(main())
@@ -0,0 +1 @@
1
+ """Troven command implementations."""
@@ -0,0 +1,134 @@
1
+ """Implementation of 'troven accept' subcommand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import datetime, timezone
6
+ from pathlib import Path
7
+ import sys
8
+
9
+ from troven import git
10
+ from troven.commands.open_cmd import detect_current_troven_id
11
+ from troven.errors import (
12
+ InvalidStateError,
13
+ MergeConflictError,
14
+ RebaseConflictError,
15
+ TrovenError,
16
+ TrovenNotFoundError,
17
+ )
18
+ from troven.state import (
19
+ get_troven_file,
20
+ load_troven,
21
+ save_troven,
22
+ validate_summary_non_empty,
23
+ )
24
+
25
+
26
+ def execute_accept(
27
+ troven_id: str | None = None,
28
+ into_branch: str | None = None,
29
+ force: bool = False,
30
+ ) -> None:
31
+ """Incorporate active troven changes into the target branch."""
32
+ repo_root = git.get_main_repo_root()
33
+
34
+ if not troven_id:
35
+ detected = detect_current_troven_id(repo_root)
36
+ if not detected:
37
+ raise TrovenError(
38
+ "Troven ID required when not run from inside a troven worktree.",
39
+ remediation="Specify a troven ID: 'troven accept <id>' or run inside the worktree.",
40
+ )
41
+ troven_id = detected
42
+
43
+ clean_id = troven_id.removeprefix("troven/")
44
+ branch_name = f"troven/{clean_id}"
45
+
46
+ if not git.branch_exists(branch_name, cwd=repo_root):
47
+ raise TrovenNotFoundError(
48
+ f"Active branch '{branch_name}' not found.",
49
+ remediation=f"Check 'troven list' to view available trovens.",
50
+ )
51
+
52
+ # Locate worktree
53
+ worktree_dir = git.find_worktree_for_branch(branch_name, cwd=repo_root)
54
+
55
+ # Load and validate troven record from worktree if present, else base repo
56
+ if worktree_dir and worktree_dir.exists() and get_troven_file(worktree_dir, clean_id).exists():
57
+ md_source_dir = worktree_dir
58
+ else:
59
+ md_source_dir = repo_root
60
+ record = load_troven(md_source_dir, clean_id)
61
+ md_path = get_troven_file(md_source_dir, clean_id)
62
+
63
+ if record.status != "active":
64
+ raise InvalidStateError(
65
+ f"Troven '{clean_id}' is in state '{record.status}', not 'active'.",
66
+ remediation="Only active trovens can be accepted.",
67
+ )
68
+
69
+ # Gate-check summary
70
+ validate_summary_non_empty(record, md_path)
71
+
72
+ target_branch = into_branch or record.base_branch or "master"
73
+
74
+ # Foreign repo warning check
75
+ if not (repo_root / ".troven").exists() and not force and sys.stdin.isatty():
76
+ ans = input(
77
+ "This repository does not appear to use troven in its target branch.\n"
78
+ f"Accepting will add .troven/ to '{target_branch}'. Confirm? [y/N]: "
79
+ ).strip()
80
+ if ans.lower() not in ("y", "yes"):
81
+ print("Aborted.")
82
+ sys.exit(0)
83
+
84
+ # Ensure any pending .troven record changes inside worktree are committed to the branch
85
+ if worktree_dir and worktree_dir.exists():
86
+ wt_md = get_troven_file(worktree_dir, clean_id)
87
+ if wt_md.exists():
88
+ git.commit_files([wt_md], f"troven: record summary for {clean_id}", cwd=worktree_dir)
89
+
90
+ # Switch base repo to target branch
91
+ git.checkout_branch(target_branch, cwd=repo_root)
92
+
93
+ # Attempt fast-forward merge
94
+ print(f"Fast-forward merging '{branch_name}' into '{target_branch}'...")
95
+ if not git.merge_ff_only(branch_name, cwd=repo_root):
96
+ print(f"Branches have diverged. Attempting rebase inside worktree...")
97
+ if not worktree_dir or not worktree_dir.exists():
98
+ raise MergeConflictError(
99
+ f"Fast-forward merge failed and worktree for '{branch_name}' is not found locally.",
100
+ remediation=f"Open the troven with 'troven open {clean_id}', rebase onto '{target_branch}', and try again.",
101
+ )
102
+
103
+ if not git.rebase(target_branch, cwd=worktree_dir):
104
+ git.rebase_abort(cwd=worktree_dir)
105
+ raise RebaseConflictError(
106
+ f"Rebase of '{branch_name}' onto '{target_branch}' encountered conflicts.",
107
+ remediation=f"Resolve conflicts manually inside '{worktree_dir}' and run 'troven accept {clean_id}'.",
108
+ )
109
+
110
+ # After rebase, retry fast-forward merge
111
+ if not git.merge_ff_only(branch_name, cwd=repo_root):
112
+ raise MergeConflictError(
113
+ f"Fast-forward merge still failed after rebase.",
114
+ remediation=f"Inspect branch '{branch_name}' in '{worktree_dir}'.",
115
+ )
116
+
117
+ # Update troven record status to accepted
118
+ record.status = "accepted"
119
+ record.resolved_at = datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds")
120
+ saved_md = save_troven(repo_root, record)
121
+
122
+ # Squash update of .troven record to target branch
123
+ git.commit_files([saved_md], f"troven: accept {clean_id}", cwd=repo_root)
124
+
125
+ # Cleanup worktree and branch
126
+ if worktree_dir and worktree_dir.exists():
127
+ print(f"Removing worktree at {worktree_dir}...")
128
+ git.remove_worktree(worktree_dir, force=True, cwd=repo_root)
129
+
130
+ print(f"Deleting branch '{branch_name}'...")
131
+ git.delete_branch(branch_name, force=True, cwd=repo_root)
132
+ git.prune_worktrees(cwd=repo_root)
133
+
134
+ print(f"\n\033[32m[Success]\033[0m Troven '{clean_id}' accepted into '{target_branch}'.")
@@ -0,0 +1,193 @@
1
+ """Implementation of 'troven create' subcommand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from datetime import date
6
+ import os
7
+ from pathlib import Path
8
+ import re
9
+ import sys
10
+
11
+ from troven import git
12
+ from troven.errors import (
13
+ InvalidNameError,
14
+ TrovenError,
15
+ TrovenExistsError,
16
+ )
17
+ from troven.state import (
18
+ TrovenRecord,
19
+ get_troven_file,
20
+ load_troven,
21
+ save_troven,
22
+ )
23
+
24
+
25
+ def validate_slug(name: str) -> None:
26
+ """Ensure name is lowercase alphanumeric with dashes."""
27
+ if not name or not re.match(r"^[a-z0-9]+(?:-[a-z0-9]+)*$", name):
28
+ raise InvalidNameError(
29
+ f"Invalid troven name: '{name}'.",
30
+ remediation="Use lowercase letters, numbers, and dashes (e.g. 'fast-lexer-simd').",
31
+ )
32
+
33
+
34
+ def execute_create(
35
+ name: str,
36
+ from_ref: str | None = None,
37
+ repo_url: str | None = None,
38
+ force: bool = False,
39
+ command: list[str] | None = None,
40
+ no_exec: bool = False,
41
+ ) -> Path:
42
+ """Create a new troven worktree and initialize change record."""
43
+ validate_slug(name)
44
+
45
+ today = date.today().isoformat()
46
+ troven_id = f"{today}-{name}"
47
+
48
+ # Handle --repo URL clone
49
+ if repo_url:
50
+ target_dir = Path.cwd() / name
51
+ if target_dir.exists():
52
+ raise TrovenError(f"Target directory {target_dir} already exists.")
53
+ print(f"Cloning {repo_url} into {target_dir}...")
54
+ git.clone_repo(repo_url, target_dir)
55
+ repo_root = target_dir
56
+ else:
57
+ repo_root = git.get_main_repo_root()
58
+
59
+ branch_name = f"troven/{troven_id}"
60
+ md_path = get_troven_file(repo_root, troven_id)
61
+
62
+ # Check for collisions
63
+ if md_path.exists():
64
+ raise TrovenExistsError(
65
+ f"Troven record '{troven_id}' already exists at {md_path}.",
66
+ remediation="Choose a different name or inspect the existing troven with 'troven show'.",
67
+ )
68
+ if git.branch_exists(branch_name, cwd=repo_root):
69
+ raise TrovenExistsError(
70
+ f"Git branch '{branch_name}' already exists.",
71
+ remediation="Delete or rename the conflicting branch, or choose a different troven name.",
72
+ )
73
+
74
+ parent_id: str | None = None
75
+ forked_from_id: str | None = None
76
+ resolved_start_point = "HEAD"
77
+ base_branch = git.get_current_branch(cwd=repo_root)
78
+ if base_branch == "HEAD":
79
+ base_branch = "master"
80
+
81
+ # Resolve --from argument
82
+ if from_ref:
83
+ # Check if from_ref is a troven ID
84
+ clean_from = from_ref.removeprefix("troven/")
85
+ archive_ref = f"refs/archive/troven/{clean_from}"
86
+ active_branch = f"troven/{clean_from}"
87
+
88
+ if git.ref_exists(archive_ref, cwd=repo_root):
89
+ if not force and sys.stdin.isatty():
90
+ ans = input(f"Troven '{clean_from}' was rejected. Create a new troven based on it? [y/N]: ").strip()
91
+ if ans.lower() not in ("y", "yes"):
92
+ print("Aborted.")
93
+ sys.exit(0)
94
+ forked_from_id = clean_from
95
+ resolved_start_point = archive_ref
96
+ elif git.branch_exists(active_branch, cwd=repo_root):
97
+ if not force and sys.stdin.isatty():
98
+ ans = input(f"Troven '{clean_from}' is an active troven. Create a sub-troven from it? [y/N]: ").strip()
99
+ if ans.lower() not in ("y", "yes"):
100
+ print("Aborted.")
101
+ sys.exit(0)
102
+ parent_id = clean_from
103
+ resolved_start_point = active_branch
104
+ base_branch = active_branch
105
+ elif git.ref_exists(from_ref, cwd=repo_root) or git.branch_exists(from_ref, cwd=repo_root):
106
+ resolved_start_point = from_ref
107
+ base_branch = from_ref
108
+ else:
109
+ raise TrovenError(
110
+ f"Cannot resolve base ref: '{from_ref}'.",
111
+ remediation="Ensure the branch, commit, or troven ID exists.",
112
+ )
113
+
114
+ base_commit = git.get_head_sha(resolved_start_point, cwd=repo_root)
115
+
116
+ # Compute worktree directory
117
+ repo_name = repo_root.name
118
+ worktree_parent = repo_root.parent / f"{repo_name}-trovens"
119
+ worktree_dir = (worktree_parent / troven_id).resolve()
120
+
121
+ if worktree_dir.exists():
122
+ raise TrovenExistsError(
123
+ f"Worktree directory {worktree_dir} already exists.",
124
+ remediation="Remove the stale directory or pick a different name.",
125
+ )
126
+
127
+ # Create .troven/<id>.md record in base repo
128
+ record = TrovenRecord(
129
+ id=troven_id,
130
+ status="active",
131
+ version=1,
132
+ parent=parent_id,
133
+ forked_from=forked_from_id,
134
+ base_branch=base_branch,
135
+ base_commit=base_commit,
136
+ )
137
+ save_troven(repo_root, record)
138
+
139
+ # Commit the record to base branch if possible before branching
140
+ try:
141
+ git.commit_files([md_path], f"troven: start {troven_id}", cwd=repo_root)
142
+ if resolved_start_point == base_branch or resolved_start_point == "HEAD":
143
+ resolved_start_point = git.get_head_sha(base_branch, cwd=repo_root)
144
+ except Exception:
145
+ pass
146
+
147
+ # Create worktree
148
+ print(f"Creating worktree at {worktree_dir} on branch '{branch_name}'...")
149
+ git.add_worktree(
150
+ worktree_dir=worktree_dir,
151
+ branch_name=branch_name,
152
+ start_point=resolved_start_point,
153
+ cwd=repo_root,
154
+ )
155
+
156
+ # Ensure worktree has the .troven file in place
157
+ save_troven(worktree_dir, record)
158
+
159
+ # Remote sync setup
160
+ git.configure_archive_remote(cwd=repo_root)
161
+
162
+ print(f"Troven '{troven_id}' initialized.")
163
+
164
+ if no_exec:
165
+ return worktree_dir
166
+
167
+ # Change into worktree and run command / agent
168
+ os.chdir(worktree_dir)
169
+
170
+ if command:
171
+ print(f"Executing: {' '.join(command)}")
172
+ os.execvp(command[0], command)
173
+
174
+ agent_cmd = os.environ.get("TROVEN_AGENT", os.environ.get("AGENT", "agy"))
175
+ boot_prompt = (
176
+ f"You are working in an isolated troven worktree for '{troven_id}' on branch '{branch_name}'.\n"
177
+ f"Troven outcome record: .troven/{troven_id}.md\n"
178
+ f"Please review README.md for project setup, building, and validation/test commands.\n"
179
+ f"When ready, write your summary to ## Summary in .troven/{troven_id}.md before calling 'troven accept' or 'troven reject'."
180
+ )
181
+
182
+ # If agy, pass prompt via -i
183
+ if agent_cmd == "agy":
184
+ os.execvp("agy", ["agy", "-i", boot_prompt])
185
+ else:
186
+ # Default fallback to executing agent or shell
187
+ try:
188
+ os.execvp(agent_cmd, [agent_cmd])
189
+ except FileNotFoundError:
190
+ print(f"Agent '{agent_cmd}' not found in PATH. Dropping into bash shell in worktree.")
191
+ os.execvp("bash", ["bash"])
192
+
193
+ return worktree_dir
@@ -0,0 +1,51 @@
1
+ """Implementation of 'troven install-skill' subcommand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.resources
6
+ import os
7
+ from pathlib import Path
8
+ import shutil
9
+
10
+
11
+ def get_skill_content() -> str:
12
+ """Read bundled SKILL.md content."""
13
+ try:
14
+ res = importlib.resources.files("troven").joinpath("skill/SKILL.md")
15
+ return res.read_text(encoding="utf-8")
16
+ except Exception:
17
+ # Fallback to local file if run from source tree
18
+ local_path = Path(__file__).resolve().parent.parent.parent.parent / "skills" / "troven" / "SKILL.md"
19
+ if local_path.exists():
20
+ return local_path.read_text(encoding="utf-8")
21
+ raise
22
+
23
+
24
+ def execute_install_skill(user_only: bool = False) -> None:
25
+ """Install the troven agent skill into agent skill discovery directories."""
26
+ content = get_skill_content()
27
+
28
+ home = Path.home()
29
+ agents_dir = home / ".agents" / "skills" / "troven"
30
+ gemini_dir = home / ".gemini" / "config" / "skills" / "troven"
31
+
32
+ # Install into ~/.agents/skills/troven/SKILL.md
33
+ agents_dir.mkdir(parents=True, exist_ok=True)
34
+ (agents_dir / "SKILL.md").write_text(content, encoding="utf-8")
35
+ print(f"\033[32m[Installed]\033[0m Skill written to {agents_dir / 'SKILL.md'}")
36
+
37
+ # Symlink or write to ~/.gemini/config/skills/troven
38
+ gemini_skills = home / ".gemini" / "config" / "skills"
39
+ if gemini_skills.exists():
40
+ if gemini_dir.is_symlink() or gemini_dir.exists():
41
+ if gemini_dir.is_symlink():
42
+ gemini_dir.unlink()
43
+ elif gemini_dir.is_dir():
44
+ shutil.rmtree(gemini_dir)
45
+ try:
46
+ gemini_dir.symlink_to(agents_dir, target_is_directory=True)
47
+ print(f"\033[32m[Linked]\033[0m Symlinked {gemini_dir} -> {agents_dir}")
48
+ except Exception:
49
+ gemini_dir.mkdir(parents=True, exist_ok=True)
50
+ (gemini_dir / "SKILL.md").write_text(content, encoding="utf-8")
51
+ print(f"\033[32m[Installed]\033[0m Skill written to {gemini_dir / 'SKILL.md'}")
@@ -0,0 +1,79 @@
1
+ """Implementation of 'troven list' subcommand."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from troven import git
8
+ from troven.state import TrovenRecord, list_all_trovens
9
+
10
+
11
+ def format_status(status: str) -> str:
12
+ if status == "accepted":
13
+ return "\033[32maccepted\033[0m"
14
+ elif status == "rejected":
15
+ return "\033[33mrejected\033[0m"
16
+ elif status == "active":
17
+ return "\033[36mactive \033[0m"
18
+ return status.ljust(8)
19
+
20
+
21
+ def execute_list(
22
+ status_filter: str | None = None,
23
+ ) -> None:
24
+ """List all trovens in a hierarchical tree view."""
25
+ repo_root = git.get_main_repo_root()
26
+ all_records = list_all_trovens(repo_root)
27
+
28
+ if not all_records:
29
+ print("No trovens found in this repository.")
30
+ return
31
+
32
+ # Filter if requested
33
+ records_by_id = {r.id: r for r in all_records}
34
+
35
+ # Build parent -> children map
36
+ children_map: dict[str | None, list[TrovenRecord]] = {}
37
+ for r in all_records:
38
+ parent_id = r.parent if r.parent in records_by_id else None
39
+ children_map.setdefault(parent_id, []).append(r)
40
+
41
+ # Check local worktrees
42
+ worktrees = git.list_worktrees(cwd=repo_root)
43
+ active_branches = {wt.branch for wt in worktrees if wt.branch}
44
+ active_paths = {wt.path for wt in worktrees}
45
+
46
+ def has_local_worktree(rec: TrovenRecord) -> bool:
47
+ branch = f"troven/{rec.id}"
48
+ if branch in active_branches:
49
+ return True
50
+ expected_path = repo_root.parent / f"{repo_root.name}-trovens" / rec.id
51
+ return expected_path.exists()
52
+
53
+ def print_node(rec: TrovenRecord, prefix: str = "", is_last: bool = True) -> None:
54
+ if status_filter and rec.status != status_filter:
55
+ # Still process children if filter doesn't match
56
+ pass
57
+ else:
58
+ connector = "└─ " if is_last else "├─ "
59
+ date_prefix = rec.created_at[:10] if rec.created_at else " "
60
+ status_str = format_status(rec.status)
61
+
62
+ wt_note = ""
63
+ if rec.status == "active" and not has_local_worktree(rec):
64
+ wt_note = " \033[90m[no local worktree]\033[0m"
65
+
66
+ ref_note = ""
67
+ if rec.status == "rejected" and rec.target_ref:
68
+ ref_note = f" \033[90m→ {rec.target_ref}\033[0m"
69
+
70
+ print(f"{prefix}{connector}{date_prefix} [{status_str}] {rec.id}{wt_note}{ref_note}")
71
+
72
+ children = children_map.get(rec.id, [])
73
+ new_prefix = prefix + (" " if is_last else "│ ")
74
+ for i, child in enumerate(children):
75
+ print_node(child, prefix=new_prefix, is_last=(i == len(children) - 1))
76
+
77
+ root_nodes = children_map.get(None, [])
78
+ for i, root in enumerate(root_nodes):
79
+ print_node(root, prefix="", is_last=(i == len(root_nodes) - 1))