holoctl 0.5.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.
Files changed (44) hide show
  1. holoctl/__init__.py +5 -0
  2. holoctl/__main__.py +56 -0
  3. holoctl/cli/__init__.py +0 -0
  4. holoctl/cli/_console.py +4 -0
  5. holoctl/cli/agent.py +101 -0
  6. holoctl/cli/board.py +153 -0
  7. holoctl/cli/compile_.py +39 -0
  8. holoctl/cli/doctor.py +126 -0
  9. holoctl/cli/init_.py +108 -0
  10. holoctl/cli/overview.py +165 -0
  11. holoctl/cli/repo.py +116 -0
  12. holoctl/cli/serve.py +20 -0
  13. holoctl/cli/sync_.py +81 -0
  14. holoctl/lib/__init__.py +0 -0
  15. holoctl/lib/board.py +341 -0
  16. holoctl/lib/compiler/__init__.py +26 -0
  17. holoctl/lib/compiler/claude.py +76 -0
  18. holoctl/lib/compiler/copilot.py +24 -0
  19. holoctl/lib/compiler/cursor.py +38 -0
  20. holoctl/lib/compiler/devin.py +51 -0
  21. holoctl/lib/compiler/generic.py +51 -0
  22. holoctl/lib/compiler/template.py +21 -0
  23. holoctl/lib/compiler/windsurf.py +22 -0
  24. holoctl/lib/config.py +102 -0
  25. holoctl/lib/discover.py +94 -0
  26. holoctl/lib/filetree.py +107 -0
  27. holoctl/lib/git.py +41 -0
  28. holoctl/lib/markdown.py +72 -0
  29. holoctl/lib/templates.py +690 -0
  30. holoctl/server/__init__.py +0 -0
  31. holoctl/server/app.py +830 -0
  32. holoctl/server/static/holoctl-ui.js +216 -0
  33. holoctl/server/static/holoctl.css +1334 -0
  34. holoctl/templates/commands/holoctl-claude.md +88 -0
  35. holoctl/templates/commands/holoctl-copilot.prompt.md +32 -0
  36. holoctl/templates/commands/holoctl-cursor.md +36 -0
  37. holoctl/templates/commands/holoctl-devin.md +32 -0
  38. holoctl/templates/commands/holoctl-windsurf.md +31 -0
  39. holoctl-0.5.0.dist-info/METADATA +297 -0
  40. holoctl-0.5.0.dist-info/RECORD +44 -0
  41. holoctl-0.5.0.dist-info/WHEEL +5 -0
  42. holoctl-0.5.0.dist-info/entry_points.txt +3 -0
  43. holoctl-0.5.0.dist-info/licenses/LICENSE +21 -0
  44. holoctl-0.5.0.dist-info/top_level.txt +1 -0
holoctl/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ try:
2
+ from importlib.metadata import version
3
+ __version__ = version("holoctl")
4
+ except Exception:
5
+ __version__ = "0.3.0"
holoctl/__main__.py ADDED
@@ -0,0 +1,56 @@
1
+ from __future__ import annotations
2
+ import sys
3
+
4
+ if sys.platform == "win32":
5
+ sys.stdout.reconfigure(encoding="utf-8", errors="replace")
6
+ sys.stderr.reconfigure(encoding="utf-8", errors="replace")
7
+
8
+ import typer
9
+
10
+ from .cli.init_ import app as _init_app, init_cmd
11
+ from .cli.board import app as _board_app
12
+ from .cli.compile_ import app as _compile_app, compile_cmd
13
+ from .cli.sync_ import app as _sync_app, sync_cmd
14
+ from .cli.doctor import app as _doctor_app, doctor_cmd
15
+ from .cli.agent import app as _agent_app
16
+ from .cli.repo import app as _repo_app
17
+ from .cli.serve import app as _serve_app, serve_cmd
18
+ from .cli.overview import app as _overview_app, overview_cmd
19
+ from . import __version__
20
+
21
+ app = typer.Typer(
22
+ name="holoctl",
23
+ help="Universal project operating system for AI coding assistants",
24
+ no_args_is_help=True,
25
+ )
26
+
27
+ # Sub-group commands
28
+ app.add_typer(_board_app, name="board", help="Manage the project board")
29
+ app.add_typer(_agent_app, name="agent", help="Manage agent definitions")
30
+ app.add_typer(_repo_app, name="repo", help="Manage repos within a project")
31
+
32
+ # Direct commands
33
+ app.command("init")(init_cmd)
34
+ app.command("compile")(compile_cmd)
35
+ app.command("sync")(sync_cmd)
36
+ app.command("doctor")(doctor_cmd)
37
+ app.command("serve")(serve_cmd)
38
+ app.command("overview")(overview_cmd)
39
+
40
+
41
+ def _version_callback(value: bool):
42
+ if value:
43
+ print(__version__)
44
+ raise typer.Exit()
45
+
46
+
47
+ @app.callback(invoke_without_command=True)
48
+ def main(
49
+ version: bool = typer.Option(None, "--version", "-v", callback=_version_callback, is_eager=True),
50
+ ctx: typer.Context = typer.Context,
51
+ ):
52
+ pass
53
+
54
+
55
+ if __name__ == "__main__":
56
+ app()
File without changes
@@ -0,0 +1,4 @@
1
+ from rich.console import Console
2
+
3
+ # Force modern Windows terminal rendering — avoids cp1252 encoding errors on legacy console.
4
+ console = Console(legacy_windows=False)
holoctl/cli/agent.py ADDED
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ from typing import Optional
4
+
5
+ import typer
6
+ from ._console import console
7
+
8
+ from ..lib.config import find_project_root
9
+ from ..lib.markdown import parse_frontmatter
10
+
11
+ app = typer.Typer(help="Manage agent definitions")
12
+
13
+
14
+ def _require_root() -> Path:
15
+ root = find_project_root()
16
+ if not root:
17
+ console.print("[red]No .holoctl/ found. Run `holoctl init` first.[/red]")
18
+ raise typer.Exit(1)
19
+ return root
20
+
21
+
22
+ @app.command("list")
23
+ def agent_list():
24
+ """List configured agents."""
25
+ root = _require_root()
26
+ agents_dir = root / ".holoctl" / "agents"
27
+ if not agents_dir.exists():
28
+ console.print("[dim]No agents configured.[/dim]")
29
+ return
30
+
31
+ for f in sorted(agents_dir.glob("*.md")):
32
+ data, _ = parse_frontmatter(f.read_text(encoding="utf-8"))
33
+ model = data.get("model", "standard")
34
+ trigger = data.get("trigger", "ticket")
35
+ model_color = "magenta" if model == "reasoning" else "dim" if model == "fast" else "cyan"
36
+ name = data.get("name", f.stem)
37
+ console.print(
38
+ f" [bold]{name:<16}[/bold] [{model_color}]{model:<10}[/{model_color}] "
39
+ f"[dim]{trigger:<16}[/dim] {data.get('description', '')}"
40
+ )
41
+
42
+
43
+ @app.command("add")
44
+ def agent_add(
45
+ name: str = typer.Argument(..., help="Agent name"),
46
+ from_template: Optional[str] = typer.Option(None, "--from", help="Base on an existing agent"),
47
+ ):
48
+ """Create a new agent definition."""
49
+ root = _require_root()
50
+ agents_dir = root / ".holoctl" / "agents"
51
+ target_path = agents_dir / f"{name}.md"
52
+
53
+ if target_path.exists():
54
+ console.print(f"[yellow]Agent {name} already exists.[/yellow]")
55
+ raise typer.Exit(1)
56
+
57
+ if from_template:
58
+ source = agents_dir / f"{from_template}.md"
59
+ if not source.exists():
60
+ console.print(f"[red]Template agent {from_template} not found.[/red]")
61
+ raise typer.Exit(1)
62
+ import re
63
+ content = source.read_text(encoding="utf-8")
64
+ content = re.sub(r"^(name:\s*).*$", rf"\g<1>{name}", content, flags=re.MULTILINE)
65
+ target_path.write_text(content, encoding="utf-8")
66
+ else:
67
+ target_path.write_text(
68
+ f"""---
69
+ name: {name}
70
+ description: "(describe what this agent does)"
71
+ model: standard
72
+ tools: [read, search, edit, write, shell]
73
+ trigger: ticket
74
+ ---
75
+
76
+ # Identity
77
+
78
+ You are the **{name}** agent. (Define identity and purpose)
79
+
80
+ # Guard Rail
81
+
82
+ (Define when this agent should refuse to work)
83
+
84
+ # Scope
85
+
86
+ (Define what this agent does and does NOT do)
87
+
88
+ # Work Order
89
+
90
+ 1. (Step-by-step work process)
91
+
92
+ # Report Format
93
+
94
+ - **Done**: bullets with file:line references.
95
+ - **Definition of Done**: each Goal item marked `[x]` or `[ ]`.
96
+ - **Suggested next step**: 1 line.
97
+ """,
98
+ encoding="utf-8",
99
+ )
100
+
101
+ console.print(f"[green]Created agent: .holoctl/agents/{name}.md[/green]")
holoctl/cli/board.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+ import json
3
+ from pathlib import Path
4
+ from typing import Optional
5
+
6
+ import typer
7
+ from ._console import console
8
+
9
+ from ..lib.config import find_project_root, load_config
10
+ from ..lib.board import Board
11
+
12
+ app = typer.Typer(help="Manage the project board (tickets, statuses, sprints)")
13
+
14
+
15
+ def _get_board() -> tuple[Board, dict, Path]:
16
+ root = find_project_root()
17
+ if not root:
18
+ console.print("[red]No .holoctl/ found. Run `holoctl init` first.[/red]")
19
+ raise typer.Exit(1)
20
+ config = load_config(root)
21
+ return Board(root, config), config, root
22
+
23
+
24
+ @app.command("stat")
25
+ def stat_cmd():
26
+ """Show ticket counts by status."""
27
+ board, _, _ = _get_board()
28
+ print(json.dumps(board.stat(), indent=2))
29
+
30
+
31
+ @app.command("get")
32
+ def get_cmd(ticket_id: str = typer.Argument(..., help="Ticket ID")):
33
+ """Get a single ticket by ID."""
34
+ board, _, _ = _get_board()
35
+ ticket = board.get(ticket_id)
36
+ if not ticket:
37
+ console.print(f"[red]Ticket {ticket_id} not found[/red]")
38
+ raise typer.Exit(1)
39
+ print(json.dumps(ticket, indent=2))
40
+
41
+
42
+ @app.command("ls")
43
+ def ls_cmd(
44
+ priority: Optional[str] = typer.Argument(None, help="Filter by priority (p0, p1, p2, p3)"),
45
+ sprint: Optional[str] = typer.Option(None, "--sprint"),
46
+ status: Optional[str] = typer.Option(None, "--status"),
47
+ agent: Optional[str] = typer.Option(None, "--agent"),
48
+ tag: Optional[str] = typer.Option(None, "--tag"),
49
+ project: Optional[str] = typer.Option(None, "--project", help="Filter by project (subdir name discovered in workspace)"),
50
+ ):
51
+ """List tickets with optional filters."""
52
+ board, _, _ = _get_board()
53
+ filters: dict = {}
54
+ if sprint:
55
+ filters["sprint"] = sprint
56
+ if status:
57
+ filters["status"] = status
58
+ if agent:
59
+ filters["agent"] = agent
60
+ if tag:
61
+ filters["tag"] = tag
62
+ if project:
63
+ filters["project"] = project
64
+ if priority and priority.startswith("p") and len(priority) == 2:
65
+ filters["priority"] = priority
66
+
67
+ tickets = board.ls(filters)
68
+ if not tickets:
69
+ console.print("[dim]No tickets match the filters.[/dim]")
70
+ return
71
+
72
+ for t in tickets:
73
+ dep = f" [dim][dep: {', '.join(t['depends'])}][/dim]" if t.get("depends") else ""
74
+ agents = ", ".join(t["agent"]) if t.get("agent") else "—"
75
+ agents_str = f"[green]{agents}[/green]"
76
+ console.print(
77
+ f"[bold]{t['id']}[/bold] {_priority_color(t['priority'])} "
78
+ f"{_status_color(t['status'])} {(t.get('sprint') or '—'):<12} "
79
+ f"{agents_str:<20} {t['title'][:50]}{dep}"
80
+ )
81
+
82
+
83
+ @app.command("move")
84
+ def move_cmd(
85
+ ticket_id: str = typer.Argument(...),
86
+ status: str = typer.Argument(...),
87
+ ):
88
+ """Move a ticket to a new status."""
89
+ board, _, _ = _get_board()
90
+ try:
91
+ result = board.move(ticket_id, status)
92
+ console.print(f"{result['id']}: {result['from']} → [bold]{result['to']}[/bold]")
93
+ except (ValueError, KeyError) as e:
94
+ console.print(f"[red]{e}[/red]")
95
+ raise typer.Exit(1)
96
+
97
+
98
+ @app.command("set")
99
+ def set_cmd(
100
+ ticket_id: str = typer.Argument(...),
101
+ field: str = typer.Argument(...),
102
+ value: list[str] = typer.Argument(...),
103
+ ):
104
+ """Set a field on a ticket."""
105
+ board, _, _ = _get_board()
106
+ try:
107
+ result = board.set(ticket_id, field, " ".join(value))
108
+ console.print(f"{result['id']}.{result['field']} = {json.dumps(result['value'])}")
109
+ except (KeyError, ValueError) as e:
110
+ msg = str(e).strip("'")
111
+ console.print(f"[red]{msg}[/red]")
112
+ raise typer.Exit(1)
113
+
114
+
115
+ @app.command("add")
116
+ def add_cmd(ticket_json: str = typer.Argument(..., help="JSON ticket data")):
117
+ """Create a new ticket from JSON."""
118
+ board, _, _ = _get_board()
119
+ try:
120
+ patch = json.loads(ticket_json)
121
+ ticket = board.add(patch)
122
+ console.print(f"[green]Created {ticket['id']}: {ticket['title']}[/green]")
123
+ print(json.dumps(ticket, indent=2))
124
+ except (json.JSONDecodeError, Exception) as e:
125
+ console.print(f"[red]{e}[/red]")
126
+ raise typer.Exit(1)
127
+
128
+
129
+ @app.command("next-id")
130
+ def next_id_cmd():
131
+ """Show the next available ticket ID."""
132
+ board, _, _ = _get_board()
133
+ print(board.next_id())
134
+
135
+
136
+ @app.command("rebuild-index")
137
+ def rebuild_index_cmd():
138
+ """Rebuild index.json from ticket .md files."""
139
+ board, _, _ = _get_board()
140
+ result = board.rebuild_index()
141
+ console.print(f"[green]Rebuilt index: {result['ticketCount']} tickets, nextId: {result['nextId']}[/green]")
142
+
143
+
144
+ def _priority_color(p: str) -> str:
145
+ colors = {"p0": "red", "p1": "yellow", "p2": "blue", "p3": "dim"}
146
+ color = colors.get(p, "white")
147
+ return f"[{color}]{p:<2}[/{color}]"
148
+
149
+
150
+ def _status_color(s: str) -> str:
151
+ colors = {"backlog": "dim", "doing": "cyan", "review": "yellow", "done": "green", "cancelled": "strike"}
152
+ color = colors.get(s, "white")
153
+ return f"[{color}]{s:<10}[/{color}]"
@@ -0,0 +1,39 @@
1
+ from __future__ import annotations
2
+ from typing import Optional
3
+
4
+ import typer
5
+ from ._console import console
6
+
7
+ from ..lib.config import find_project_root, load_config
8
+ from ..lib.compiler import compile_project
9
+
10
+ app = typer.Typer()
11
+
12
+
13
+ @app.command("compile")
14
+ def compile_cmd(
15
+ target: Optional[str] = typer.Option(None, "--target", help="Target (claude, cursor, windsurf, copilot, generic)"),
16
+ dry_run: bool = typer.Option(False, "--dry-run", help="Preview without writing files"),
17
+ ):
18
+ """Compile .holoctl/ to tool-specific files."""
19
+ root = find_project_root()
20
+ if not root:
21
+ console.print("[red]No .holoctl/ found. Run `holoctl init` first.[/red]")
22
+ raise typer.Exit(1)
23
+
24
+ config = load_config(root)
25
+ targets = [target] if target else config.get("targets", ["claude"])
26
+
27
+ for t in targets:
28
+ try:
29
+ result = compile_project(root, config, t, dry_run=dry_run)
30
+ if dry_run:
31
+ console.print(f"[dim][dry-run] {t}:[/dim]")
32
+ for f in result["files"]:
33
+ console.print(f" [dim]would write[/dim] {f}")
34
+ else:
35
+ console.print(f"[green]✓ {t}[/green] [dim]({len(result['files'])} files)[/dim]")
36
+ for f in result["files"]:
37
+ console.print(f" [dim]→[/dim] {f}")
38
+ except ValueError as e:
39
+ console.print(f"[red]✗ {t}: {e}[/red]")
holoctl/cli/doctor.py ADDED
@@ -0,0 +1,126 @@
1
+ from __future__ import annotations
2
+ import json
3
+
4
+ import typer
5
+ from ._console import console
6
+
7
+ from ..lib.config import find_project_root, load_config
8
+
9
+ app = typer.Typer()
10
+
11
+
12
+ _TARGET_OUTPUTS = {
13
+ "claude": ["CLAUDE.md", ".claude/commands"],
14
+ "cursor": [".cursor/commands", ".cursor/rules/holoctl.md"],
15
+ "windsurf": [".windsurfrules"],
16
+ "copilot": [".github/copilot-instructions.md"],
17
+ "devin": ["AGENTS.md", ".devin/skills"],
18
+ }
19
+
20
+
21
+ @app.command("doctor")
22
+ def doctor_cmd():
23
+ """Check project health."""
24
+ root = find_project_root()
25
+ if not root:
26
+ console.print("[red]No .holoctl/ found. Run `holoctl init` first.[/red]")
27
+ raise typer.Exit(1)
28
+
29
+ console.print("\n [bold]holoctl doctor[/bold]\n")
30
+ issues = 0
31
+ config = None
32
+
33
+ try:
34
+ config = load_config(root)
35
+ _check("Config", ".holoctl/config.json is valid", True)
36
+ except Exception as e:
37
+ _check("Config", f".holoctl/config.json: {e}", False)
38
+ issues += 1
39
+
40
+ # Index <-> .md sync
41
+ index_path = root / ".holoctl" / "board" / "index.json"
42
+ tickets_dir = root / ".holoctl" / "board" / "tickets"
43
+ if index_path.exists():
44
+ try:
45
+ data = json.loads(index_path.read_text(encoding="utf-8"))
46
+ indexed = {t["id"] for t in data.get("tickets", [])}
47
+ on_disk = set()
48
+ if tickets_dir.exists():
49
+ from ..lib.markdown import parse_frontmatter
50
+ for f in tickets_dir.glob("*.md"):
51
+ if f.name.startswith("_"):
52
+ continue
53
+ fm, _ = parse_frontmatter(f.read_text(encoding="utf-8"))
54
+ if fm.get("id"):
55
+ on_disk.add(fm["id"])
56
+ drift = indexed.symmetric_difference(on_disk)
57
+ if drift:
58
+ _check("Board", f"index.json out of sync ({len(drift)} drift) — run `holoctl board rebuild-index`", False)
59
+ issues += 1
60
+ else:
61
+ _check("Board", f"index.json valid ({len(indexed)} tickets, in sync)", True)
62
+ except Exception as e:
63
+ _check("Board", f"index.json parse error: {e}", False)
64
+ issues += 1
65
+ else:
66
+ _check("Board", "index.json exists", False)
67
+ issues += 1
68
+
69
+ # Agents
70
+ agents_dir = root / ".holoctl" / "agents"
71
+ if agents_dir.exists():
72
+ agent_count = len(list(agents_dir.glob("*.md")))
73
+ ok = agent_count > 0
74
+ _check("Agents", f"{agent_count} agent(s) defined", ok)
75
+ if not ok:
76
+ issues += 1
77
+ else:
78
+ _check("Agents", "agents/ directory exists", False)
79
+ issues += 1
80
+
81
+ # Commands
82
+ commands_dir = root / ".holoctl" / "commands"
83
+ if commands_dir.exists():
84
+ cmd_count = len(list(commands_dir.glob("*.md")))
85
+ ok = cmd_count > 0
86
+ _check("Commands", f"{cmd_count} command(s) defined", ok)
87
+ if not ok:
88
+ issues += 1
89
+ else:
90
+ _check("Commands", "commands/ directory exists", False)
91
+ issues += 1
92
+
93
+ # Instructions
94
+ ok = (root / ".holoctl" / "instructions.md").exists()
95
+ _check("Instructions", "instructions.md exists", ok)
96
+ if not ok:
97
+ issues += 1
98
+
99
+ # Context
100
+ ok = (root / ".holoctl" / "context").exists()
101
+ _check("Context", "context/ directory exists", ok)
102
+ if not ok:
103
+ issues += 1
104
+
105
+ # Compile targets
106
+ if config:
107
+ targets = config.get("targets", [])
108
+ for tgt in targets:
109
+ outputs = _TARGET_OUTPUTS.get(tgt, [])
110
+ missing = [o for o in outputs if not (root / o).exists()]
111
+ if missing:
112
+ _check("Compile", f"target '{tgt}' missing: {', '.join(missing)} — run `holoctl compile`", False)
113
+ issues += 1
114
+ else:
115
+ _check("Compile", f"target '{tgt}' compiled", True)
116
+
117
+ console.print("")
118
+ if issues == 0:
119
+ console.print("[green] All checks passed. Project is healthy.[/green]\n")
120
+ else:
121
+ console.print(f"[yellow] {issues} issue(s) found.[/yellow]\n")
122
+
123
+
124
+ def _check(category: str, message: str, ok: bool) -> None:
125
+ icon = "[green]✓[/green]" if ok else "[red]✗[/red]"
126
+ console.print(f" {icon} [dim]{category:<14}[/dim] {message}")
holoctl/cli/init_.py ADDED
@@ -0,0 +1,108 @@
1
+ from __future__ import annotations
2
+ import json
3
+ import re
4
+ from pathlib import Path
5
+ from typing import Optional
6
+
7
+ import typer
8
+ from ._console import console
9
+
10
+ from ..lib.config import get_defaults, save_config
11
+ from ..lib.templates import get_templates
12
+
13
+ app = typer.Typer()
14
+
15
+
16
+ @app.command("init")
17
+ def init_cmd(
18
+ name: Optional[str] = typer.Option(None, "--name", help="Project name"),
19
+ prefix: Optional[str] = typer.Option(None, "--prefix", help="Ticket ID prefix (e.g. MP)"),
20
+ targets: Optional[str] = typer.Option(None, "--targets", help="Compile targets (claude,cursor,windsurf,copilot)"),
21
+ skip_compile: bool = typer.Option(False, "--skip-compile", help="Skip auto-compile after init"),
22
+ ):
23
+ """Initialize .holoctl/ in the current directory."""
24
+ cwd = Path.cwd()
25
+ holoctl_dir = cwd / ".holoctl"
26
+
27
+ if (holoctl_dir / "config.json").exists():
28
+ console.print("[yellow].holoctl/ already exists in this directory.[/yellow]")
29
+ raise typer.Exit(1)
30
+
31
+ project_name = name or cwd.name
32
+ project_prefix = prefix or _derive_prefix(project_name)
33
+ target_list = [t.strip() for t in targets.split(",")] if targets else ["claude"]
34
+
35
+ config = get_defaults()
36
+ config["project"]["name"] = project_name
37
+ config["project"]["prefix"] = project_prefix
38
+ config["targets"] = target_list
39
+
40
+ console.print(f"\n [bold]holoctl init[/bold]\n")
41
+ console.print(f" Project: [green]{project_name}[/green]")
42
+ console.print(f" Prefix: [green]{project_prefix}[/green] (tickets: {project_prefix}-001, {project_prefix}-002, ...)")
43
+ console.print(f" Targets: [green]{', '.join(target_list)}[/green]")
44
+ console.print("")
45
+
46
+ dirs = [
47
+ ".holoctl",
48
+ ".holoctl/board",
49
+ ".holoctl/board/tickets",
50
+ ".holoctl/agents",
51
+ ".holoctl/commands",
52
+ ".holoctl/context",
53
+ ".holoctl/context/decisions",
54
+ ".holoctl/context/documents",
55
+ ]
56
+ for d in dirs:
57
+ (cwd / d).mkdir(parents=True, exist_ok=True)
58
+
59
+ save_config(cwd, config)
60
+
61
+ templates = get_templates(config)
62
+ for rel_path, content in templates.items():
63
+ full_path = cwd / rel_path
64
+ full_path.parent.mkdir(parents=True, exist_ok=True)
65
+ full_path.write_text(content, encoding="utf-8")
66
+
67
+ index_data = {
68
+ "meta": {"version": 1, "updated": _today(), "nextId": 1, "counts": {}},
69
+ "tickets": [],
70
+ }
71
+ (cwd / ".holoctl" / "board" / "index.json").write_text(
72
+ json.dumps(index_data, indent="\t") + "\n", encoding="utf-8"
73
+ )
74
+ (cwd / ".holoctl" / "activity.jsonl").write_text("", encoding="utf-8")
75
+
76
+ console.print(f" [green]✓ .holoctl/ initialized successfully.[/green]\n")
77
+
78
+ if not skip_compile:
79
+ from ..lib.compiler import compile_project
80
+ for tgt in target_list:
81
+ try:
82
+ result = compile_project(cwd, config, tgt, dry_run=False)
83
+ count = len(result.get("files", []))
84
+ console.print(f" [green]✓ compiled[/green] [bold]{tgt}[/bold] [dim]({count} files)[/dim]")
85
+ except Exception as e:
86
+ console.print(f" [red]✗ compile {tgt}:[/red] {e}")
87
+
88
+ console.print("")
89
+ console.print(" Next steps:")
90
+ console.print(f" [dim]$[/dim] holoctl board add '{{\"title\":\"My first ticket\",\"agent\":\"developer\"}}'")
91
+ console.print(f" [dim]$[/dim] holoctl serve")
92
+ console.print("")
93
+
94
+
95
+ def _today() -> str:
96
+ from datetime import date
97
+ return date.today().isoformat()
98
+
99
+
100
+ def _derive_prefix(name: str) -> str:
101
+ cleaned = re.sub(r"[^a-zA-Z0-9]", "", name)
102
+ if len(cleaned) <= 4:
103
+ return cleaned.upper()
104
+ words = re.split(r"[\s_-]+", name)
105
+ words = [w for w in words if w]
106
+ if len(words) >= 2:
107
+ return "".join(w[0] for w in words).upper()[:4]
108
+ return cleaned[:3].upper()