karajan-cli 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.
- karajan/__init__.py +4 -0
- karajan/agents/architect.md +23 -0
- karajan/agents/qa.md +13 -0
- karajan/agents/researcher.md +13 -0
- karajan/agents/reviewer.md +13 -0
- karajan/cli.py +102 -0
- karajan/commands/__init__.py +2 -0
- karajan/commands/config.py +89 -0
- karajan/commands/init.py +247 -0
- karajan/commands/logs.py +30 -0
- karajan/commands/models.py +49 -0
- karajan/commands/profiles.py +23 -0
- karajan/commands/status.py +50 -0
- karajan/commands/task.py +57 -0
- karajan/config.py +241 -0
- karajan/graph.py +148 -0
- karajan/nodes/__init__.py +2 -0
- karajan/nodes/architect.py +25 -0
- karajan/nodes/dev.py +66 -0
- karajan/nodes/hitl.py +31 -0
- karajan/nodes/prd.py +26 -0
- karajan/nodes/qa.py +20 -0
- karajan/nodes/research.py +26 -0
- karajan/nodes/reviewer.py +20 -0
- karajan/profiles/solo.toml +27 -0
- karajan/profiles/team.toml +28 -0
- karajan/scripts/__init__.py +2 -0
- karajan/scripts/github.py +8 -0
- karajan/scripts/logger.py +150 -0
- karajan/scripts/tracker.py +145 -0
- karajan_cli-0.1.0.dist-info/METADATA +229 -0
- karajan_cli-0.1.0.dist-info/RECORD +35 -0
- karajan_cli-0.1.0.dist-info/WHEEL +5 -0
- karajan_cli-0.1.0.dist-info/entry_points.txt +2 -0
- karajan_cli-0.1.0.dist-info/top_level.txt +1 -0
karajan/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: architect
|
|
3
|
+
description: Technical planning agent. Designs solutions, writes ADRs, and produces implementation specs. Use before any coding task.
|
|
4
|
+
model: google/gemini-2.0-flash
|
|
5
|
+
temperature: 0.3
|
|
6
|
+
mode: primary
|
|
7
|
+
tools:
|
|
8
|
+
- read
|
|
9
|
+
- write
|
|
10
|
+
permissions:
|
|
11
|
+
allow:
|
|
12
|
+
- read: "**"
|
|
13
|
+
- write: ".agent/DECISIONS.md"
|
|
14
|
+
- write: ".workflow-state/**"
|
|
15
|
+
deny:
|
|
16
|
+
- write: "src/**"
|
|
17
|
+
- write: "*.ts"
|
|
18
|
+
- write: "*.py"
|
|
19
|
+
- bash: "*"
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
You are a Software Architect with deep experience in TypeScript, Node.js, event-driven systems, and complex integrations.
|
|
23
|
+
|
karajan/agents/qa.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: researcher
|
|
3
|
+
description: Technical research agent. Gathers context, finds patterns, identifies risks before design begins. Always runs before architect.
|
|
4
|
+
model: google/gemini-2.0-flash
|
|
5
|
+
temperature: 0.2
|
|
6
|
+
mode: primary
|
|
7
|
+
tools:
|
|
8
|
+
- read
|
|
9
|
+
- web_search
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
You are a Technical Research Specialist.
|
|
13
|
+
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: reviewer
|
|
3
|
+
description: Code review agent. Reviews diffs for correctness, conventions, and quality.
|
|
4
|
+
model: google/gemini-2.0-flash
|
|
5
|
+
temperature: 0.1
|
|
6
|
+
mode: primary
|
|
7
|
+
tools:
|
|
8
|
+
- read
|
|
9
|
+
- bash
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
You are a Tech Lead doing code review.
|
|
13
|
+
|
karajan/cli.py
ADDED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Karajan CLI entry point."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
from typing import Sequence
|
|
7
|
+
|
|
8
|
+
from . import __version__
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
prog="karajan",
|
|
14
|
+
description="An AI agent harness for software engineers. Built on opencode + LangGraph.",
|
|
15
|
+
)
|
|
16
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
17
|
+
|
|
18
|
+
init_parser = subparsers.add_parser("init", help="Initialize in current project")
|
|
19
|
+
init_parser.add_argument("--profile", "-p", choices=["solo", "team"], default=None)
|
|
20
|
+
init_parser.add_argument("--force", "-f", action="store_true")
|
|
21
|
+
|
|
22
|
+
task_parser = subparsers.add_parser("task", help="Run full workflow")
|
|
23
|
+
task_parser.add_argument("ticket")
|
|
24
|
+
task_parser.add_argument("--model", "-m", default=None)
|
|
25
|
+
task_parser.add_argument("--dev-model", default=None)
|
|
26
|
+
task_parser.add_argument("--qa-model", default=None)
|
|
27
|
+
task_parser.add_argument("--from-stage", default=None)
|
|
28
|
+
task_parser.add_argument("--dry-run", action="store_true")
|
|
29
|
+
|
|
30
|
+
status_parser = subparsers.add_parser("status", help="Show active workflows")
|
|
31
|
+
status_parser.add_argument("ticket", nargs="?", default=None)
|
|
32
|
+
|
|
33
|
+
models_parser = subparsers.add_parser("models", help="List available models")
|
|
34
|
+
models_parser.add_argument("--provider", default=None)
|
|
35
|
+
|
|
36
|
+
config_parser = subparsers.add_parser("config", help="Show current project config")
|
|
37
|
+
config_parser.add_argument("--show", action="store_true")
|
|
38
|
+
config_parser.add_argument("--set", dest="set_key", default=None)
|
|
39
|
+
|
|
40
|
+
subparsers.add_parser("version", help="Show version")
|
|
41
|
+
subparsers.add_parser("profiles", help="List profiles and defaults")
|
|
42
|
+
|
|
43
|
+
logs_parser = subparsers.add_parser("logs", help="Show run logs and cost summary")
|
|
44
|
+
logs_parser.add_argument("ticket", nargs="?", default=None)
|
|
45
|
+
logs_parser.add_argument("--tail", "-n", type=int, default=20)
|
|
46
|
+
logs_parser.add_argument("--summary", "-s", action="store_true")
|
|
47
|
+
|
|
48
|
+
return parser
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def app(argv: Sequence[str] | None = None) -> None:
|
|
52
|
+
parser = _build_parser()
|
|
53
|
+
args = parser.parse_args(argv)
|
|
54
|
+
|
|
55
|
+
if args.command is None:
|
|
56
|
+
parser.print_help()
|
|
57
|
+
raise SystemExit(0)
|
|
58
|
+
|
|
59
|
+
if args.command == "init":
|
|
60
|
+
from .commands.init import run_init
|
|
61
|
+
|
|
62
|
+
run_init(profile=args.profile, force=args.force)
|
|
63
|
+
elif args.command == "task":
|
|
64
|
+
from .commands.task import run_task
|
|
65
|
+
|
|
66
|
+
run_task(
|
|
67
|
+
ticket=args.ticket,
|
|
68
|
+
model=args.model,
|
|
69
|
+
dev_model=args.dev_model,
|
|
70
|
+
qa_model=args.qa_model,
|
|
71
|
+
from_stage=args.from_stage,
|
|
72
|
+
dry_run=args.dry_run,
|
|
73
|
+
)
|
|
74
|
+
elif args.command == "status":
|
|
75
|
+
from .commands.status import run_status
|
|
76
|
+
|
|
77
|
+
run_status(ticket=args.ticket)
|
|
78
|
+
elif args.command == "models":
|
|
79
|
+
from .commands.models import run_models
|
|
80
|
+
|
|
81
|
+
run_models(provider=args.provider)
|
|
82
|
+
elif args.command == "config":
|
|
83
|
+
from .commands.config import run_config
|
|
84
|
+
|
|
85
|
+
run_config(show=args.show, set_key=args.set_key)
|
|
86
|
+
elif args.command == "version":
|
|
87
|
+
print(f"karajan v{__version__}")
|
|
88
|
+
elif args.command == "profiles":
|
|
89
|
+
from .commands.profiles import run_profiles
|
|
90
|
+
|
|
91
|
+
run_profiles()
|
|
92
|
+
elif args.command == "logs":
|
|
93
|
+
from .commands.logs import run_logs
|
|
94
|
+
|
|
95
|
+
run_logs(ticket=args.ticket, tail=args.tail, summary=args.summary)
|
|
96
|
+
else:
|
|
97
|
+
parser.error(f"Unknown command: {args.command}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
if __name__ == "__main__":
|
|
101
|
+
app()
|
|
102
|
+
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Show or update Karajan config."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import tomllib
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from ..config import dump_config_toml, load_config
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def run_config(show: bool = False, set_key: str = None):
|
|
12
|
+
config_path = Path(".karajan/config.toml")
|
|
13
|
+
|
|
14
|
+
if set_key:
|
|
15
|
+
_set_config_value(config_path, set_key)
|
|
16
|
+
return
|
|
17
|
+
|
|
18
|
+
if show or not config_path.exists():
|
|
19
|
+
config = load_config()
|
|
20
|
+
print(dump_config_toml(config.model_dump()))
|
|
21
|
+
return
|
|
22
|
+
|
|
23
|
+
print(config_path.read_text(encoding="utf-8"))
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _set_config_value(config_path: Path, assignment: str):
|
|
27
|
+
if not config_path.exists():
|
|
28
|
+
print("No .karajan/config.toml found. Run karajan init first.")
|
|
29
|
+
raise SystemExit(1)
|
|
30
|
+
if "=" not in assignment:
|
|
31
|
+
print(f"Invalid config assignment: {assignment}")
|
|
32
|
+
raise SystemExit(1)
|
|
33
|
+
key, raw_value = (part.strip() for part in assignment.split("=", 1))
|
|
34
|
+
parts = [part for part in key.split(".") if part]
|
|
35
|
+
if not key or len(parts) != len(key.split(".")) or len(parts) > 2:
|
|
36
|
+
print(f"Invalid config path: {key}")
|
|
37
|
+
raise SystemExit(1)
|
|
38
|
+
with config_path.open("rb") as handle:
|
|
39
|
+
config = tomllib.load(handle)
|
|
40
|
+
target = _coerce_config_value(raw_value)
|
|
41
|
+
if len(parts) == 1:
|
|
42
|
+
if isinstance(config.get(parts[0]), dict):
|
|
43
|
+
print(f"Invalid config path: {key}")
|
|
44
|
+
raise SystemExit(1)
|
|
45
|
+
config[parts[0]] = target
|
|
46
|
+
else:
|
|
47
|
+
section, field = parts
|
|
48
|
+
section_data = config.get(section)
|
|
49
|
+
if section_data is None:
|
|
50
|
+
config[section] = {field: target}
|
|
51
|
+
elif isinstance(section_data, dict):
|
|
52
|
+
section_data[field] = target
|
|
53
|
+
else:
|
|
54
|
+
print(f"Invalid config path: {key}")
|
|
55
|
+
raise SystemExit(1)
|
|
56
|
+
_write_config(config_path, config)
|
|
57
|
+
print(f"✅ Set {key} = {_format_config_value(target)}")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _coerce_config_value(raw_value: str):
|
|
61
|
+
value = raw_value.strip()
|
|
62
|
+
lowered = value.lower()
|
|
63
|
+
if lowered == "true":
|
|
64
|
+
return True
|
|
65
|
+
if lowered == "false":
|
|
66
|
+
return False
|
|
67
|
+
try:
|
|
68
|
+
return int(value)
|
|
69
|
+
except ValueError:
|
|
70
|
+
try:
|
|
71
|
+
return float(value)
|
|
72
|
+
except ValueError:
|
|
73
|
+
return value
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _format_config_value(value) -> str:
|
|
77
|
+
if isinstance(value, bool):
|
|
78
|
+
return "true" if value else "false"
|
|
79
|
+
return str(value)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _write_config(config_path: Path, config: dict):
|
|
83
|
+
try:
|
|
84
|
+
import tomli_w
|
|
85
|
+
except ModuleNotFoundError:
|
|
86
|
+
config_path.write_text(dump_config_toml(config), encoding="utf-8")
|
|
87
|
+
return
|
|
88
|
+
with config_path.open("wb") as handle:
|
|
89
|
+
tomli_w.dump(config, handle)
|
karajan/commands/init.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
"""Initialize Karajan in a project."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
CONTEXT_MD = """# {project_name} - Context
|
|
10
|
+
|
|
11
|
+
## What is this
|
|
12
|
+
One sentence. What problem does it solve and for whom.
|
|
13
|
+
|
|
14
|
+
## Current state
|
|
15
|
+
Phase: exploration / mvp / production / maintenance
|
|
16
|
+
|
|
17
|
+
## Immediate goal
|
|
18
|
+
What you want to achieve in the next work sessions.
|
|
19
|
+
|
|
20
|
+
## Important context
|
|
21
|
+
Anything an agent needs to know that isn't obvious from the code.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
STACK_MD = """# Stack - {project_name}
|
|
25
|
+
|
|
26
|
+
## Runtime
|
|
27
|
+
- Node X / Python X
|
|
28
|
+
|
|
29
|
+
## Framework
|
|
30
|
+
-
|
|
31
|
+
|
|
32
|
+
## Database
|
|
33
|
+
-
|
|
34
|
+
|
|
35
|
+
## Infrastructure
|
|
36
|
+
-
|
|
37
|
+
|
|
38
|
+
## Key dependencies
|
|
39
|
+
-
|
|
40
|
+
|
|
41
|
+
## Pending decisions
|
|
42
|
+
- [ ]
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
DECISIONS_MD = """# Decisions - {project_name}
|
|
46
|
+
|
|
47
|
+
## YYYY-MM - Decision title
|
|
48
|
+
**Decided:** what was chosen
|
|
49
|
+
**Why:** reasoning
|
|
50
|
+
**Discarded:** what was rejected and why
|
|
51
|
+
**Revisit when:** condition that would make you reconsider
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
CONSTRAINTS_MD = """# Constraints - {project_name}
|
|
55
|
+
|
|
56
|
+
## Never touch without discussing
|
|
57
|
+
-
|
|
58
|
+
|
|
59
|
+
## Never add without approval
|
|
60
|
+
- New dependencies
|
|
61
|
+
- Breaking API changes
|
|
62
|
+
|
|
63
|
+
## Agent autonomy limits
|
|
64
|
+
- Agent CAN: create files, add tests, refactor within a module
|
|
65
|
+
- Agent NEEDS APPROVAL: schema changes, new dependencies, config changes
|
|
66
|
+
- Agent NEVER: push to main directly
|
|
67
|
+
"""
|
|
68
|
+
|
|
69
|
+
TASKS_MD = """# Tasks - {project_name}
|
|
70
|
+
|
|
71
|
+
## In progress
|
|
72
|
+
- [ ]
|
|
73
|
+
|
|
74
|
+
## Next
|
|
75
|
+
- [ ]
|
|
76
|
+
|
|
77
|
+
## Done this week
|
|
78
|
+
- [x]
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
AGENTS_MD = """# Agent Instructions - {project_name}
|
|
82
|
+
|
|
83
|
+
## Before any task
|
|
84
|
+
1. Read CONTEXT.md
|
|
85
|
+
2. Read STACK.md
|
|
86
|
+
3. Read CONSTRAINTS.md
|
|
87
|
+
4. Read TASKS.md
|
|
88
|
+
|
|
89
|
+
## How to work here
|
|
90
|
+
- TypeScript strict - no `any`
|
|
91
|
+
- Atomic commits per logical change
|
|
92
|
+
- Tests required for non-trivial logic
|
|
93
|
+
- If spec is ambiguous: choose conservative option + leave TODO comment
|
|
94
|
+
|
|
95
|
+
## What agent CAN do without asking
|
|
96
|
+
- Create files and components
|
|
97
|
+
- Add tests
|
|
98
|
+
- Refactor within same module
|
|
99
|
+
- Update docs
|
|
100
|
+
|
|
101
|
+
## What NEEDS approval
|
|
102
|
+
- New dependencies
|
|
103
|
+
- Schema/migration changes
|
|
104
|
+
- Changes to public API
|
|
105
|
+
- Anything touching production config
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
KARAJAN_CONFIG = """# Karajan config - {project_name}
|
|
109
|
+
# Docs: https://github.com/TU_USER/karajan
|
|
110
|
+
|
|
111
|
+
profile = "{profile}"
|
|
112
|
+
|
|
113
|
+
[models]
|
|
114
|
+
# Override profile defaults here if needed
|
|
115
|
+
|
|
116
|
+
[tracker]
|
|
117
|
+
type = "{tracker_type}"
|
|
118
|
+
|
|
119
|
+
[dev_cli]
|
|
120
|
+
tool = "{dev_cli}"
|
|
121
|
+
|
|
122
|
+
[observability]
|
|
123
|
+
enabled = false
|
|
124
|
+
provider = "local"
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def run_init(profile: str = None, force: bool = False):
|
|
129
|
+
project_name = Path.cwd().name
|
|
130
|
+
print(f"\nKarajan init - {project_name}\n")
|
|
131
|
+
|
|
132
|
+
if not profile:
|
|
133
|
+
profile = input("Profile [solo/team] (solo): ").strip() or "solo"
|
|
134
|
+
if profile not in {"solo", "team"}:
|
|
135
|
+
raise ValueError("Profile must be 'solo' or 'team'.")
|
|
136
|
+
|
|
137
|
+
tracker_type = "github" if profile == "solo" else "jira"
|
|
138
|
+
dev_cli = "opencode" if profile == "solo" else "claude"
|
|
139
|
+
|
|
140
|
+
karajan_dir = Path(".karajan")
|
|
141
|
+
if karajan_dir.exists() and not force:
|
|
142
|
+
overwrite = input(".karajan/ already exists. Overwrite? [y/N]: ").strip().lower()
|
|
143
|
+
if overwrite not in {"y", "yes"}:
|
|
144
|
+
print("Aborted.")
|
|
145
|
+
return
|
|
146
|
+
|
|
147
|
+
karajan_dir.mkdir(exist_ok=True)
|
|
148
|
+
agent_dir = Path(".agent")
|
|
149
|
+
agent_dir.mkdir(exist_ok=True)
|
|
150
|
+
|
|
151
|
+
files = {
|
|
152
|
+
agent_dir / "CONTEXT.md": CONTEXT_MD.format(project_name=project_name),
|
|
153
|
+
agent_dir / "STACK.md": STACK_MD.format(project_name=project_name),
|
|
154
|
+
agent_dir / "DECISIONS.md": DECISIONS_MD.format(project_name=project_name),
|
|
155
|
+
agent_dir / "CONSTRAINTS.md": CONSTRAINTS_MD.format(project_name=project_name),
|
|
156
|
+
agent_dir / "TASKS.md": TASKS_MD.format(project_name=project_name),
|
|
157
|
+
agent_dir / "AGENTS.md": AGENTS_MD.format(project_name=project_name),
|
|
158
|
+
karajan_dir / "config.toml": KARAJAN_CONFIG.format(
|
|
159
|
+
project_name=project_name,
|
|
160
|
+
profile=profile,
|
|
161
|
+
tracker_type=tracker_type,
|
|
162
|
+
dev_cli=dev_cli,
|
|
163
|
+
),
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for path, content in files.items():
|
|
167
|
+
if not path.exists() or force:
|
|
168
|
+
path.write_text(content, encoding="utf-8")
|
|
169
|
+
print(f" + {path}")
|
|
170
|
+
else:
|
|
171
|
+
print(f" skip {path} (exists)")
|
|
172
|
+
|
|
173
|
+
env_example = Path(".env.karajan.example")
|
|
174
|
+
if not env_example.exists() or force:
|
|
175
|
+
_write_env_example(env_example, profile)
|
|
176
|
+
print(f" + {env_example}")
|
|
177
|
+
|
|
178
|
+
_copy_agent_definitions(profile, force=force)
|
|
179
|
+
|
|
180
|
+
if profile == "solo":
|
|
181
|
+
_setup_github()
|
|
182
|
+
|
|
183
|
+
print(f"\nKarajan initialized ({profile} profile)\n")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def _write_env_example(path: Path, profile: str):
|
|
187
|
+
solo_vars = """
|
|
188
|
+
KARAJAN_MODEL_REASONING=google/gemini-2.0-flash
|
|
189
|
+
KARAJAN_MODEL_DEV=ollama/qwen2.5-coder:14b
|
|
190
|
+
KARAJAN_MODEL_QA=ollama/qwen2.5:7b
|
|
191
|
+
|
|
192
|
+
GEMINI_API_KEY=
|
|
193
|
+
GITHUB_TOKEN=
|
|
194
|
+
GITHUB_REPO=TU_USER/repo-name
|
|
195
|
+
GITHUB_BASE_BRANCH=main
|
|
196
|
+
"""
|
|
197
|
+
team_vars = """
|
|
198
|
+
KARAJAN_MODEL_REASONING=google/gemini-2.0-flash
|
|
199
|
+
KARAJAN_MODEL_DEV=anthropic/claude-sonnet-4-20250514
|
|
200
|
+
KARAJAN_MODEL_QA=anthropic/claude-haiku-4-5
|
|
201
|
+
|
|
202
|
+
GEMINI_API_KEY=
|
|
203
|
+
ANTHROPIC_API_KEY=
|
|
204
|
+
JIRA_BASE_URL=https://empresa.atlassian.net
|
|
205
|
+
JIRA_EMAIL=tu@empresa.com
|
|
206
|
+
JIRA_API_TOKEN=
|
|
207
|
+
GITHUB_TOKEN=
|
|
208
|
+
GITHUB_REPO=empresa/repo-name
|
|
209
|
+
GITHUB_BASE_BRANCH=main
|
|
210
|
+
LANGFUSE_HOST=https://cloud.langfuse.com
|
|
211
|
+
LANGFUSE_PUBLIC_KEY=
|
|
212
|
+
LANGFUSE_SECRET_KEY=
|
|
213
|
+
"""
|
|
214
|
+
path.write_text(solo_vars if profile == "solo" else team_vars, encoding="utf-8")
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _setup_github():
|
|
218
|
+
try:
|
|
219
|
+
result = subprocess.run(["gh", "--version"], capture_output=True, text=True)
|
|
220
|
+
if result.returncode != 0:
|
|
221
|
+
return
|
|
222
|
+
except FileNotFoundError:
|
|
223
|
+
return
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _copy_agent_definitions(profile: str, force: bool = False):
|
|
227
|
+
agents_src = Path(__file__).resolve().parent.parent / "agents"
|
|
228
|
+
agents_dst = Path(".agents")
|
|
229
|
+
agents_dst.mkdir(parents=True, exist_ok=True)
|
|
230
|
+
|
|
231
|
+
for agent_file in agents_src.glob("*.md"):
|
|
232
|
+
dst = agents_dst / agent_file.name
|
|
233
|
+
if dst.exists() and not force:
|
|
234
|
+
print(f" skip {dst} (exists)")
|
|
235
|
+
continue
|
|
236
|
+
|
|
237
|
+
content = agent_file.read_text(encoding="utf-8")
|
|
238
|
+
if profile == "team":
|
|
239
|
+
content = content.replace(
|
|
240
|
+
"model: google/gemini-2.0-flash",
|
|
241
|
+
"model: anthropic/claude-sonnet-4-20250514",
|
|
242
|
+
).replace(
|
|
243
|
+
"model: ollama/qwen2.5:7b",
|
|
244
|
+
"model: anthropic/claude-haiku-4-5",
|
|
245
|
+
)
|
|
246
|
+
dst.write_text(content, encoding="utf-8")
|
|
247
|
+
print(f" + {dst}")
|
karajan/commands/logs.py
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""Show run logs."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from ..scripts.logger import KarajanLogger
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def run_logs(ticket: str = None, tail: int = 20, summary: bool = False):
|
|
11
|
+
log_file = Path(".workflow-state/runs.log")
|
|
12
|
+
if not log_file.exists():
|
|
13
|
+
print("No runs logged yet")
|
|
14
|
+
return
|
|
15
|
+
|
|
16
|
+
if summary and ticket:
|
|
17
|
+
KarajanLogger().print_summary(ticket)
|
|
18
|
+
return
|
|
19
|
+
|
|
20
|
+
lines = log_file.read_text(encoding="utf-8").splitlines()
|
|
21
|
+
if ticket:
|
|
22
|
+
lines = [line for line in lines if ticket in line]
|
|
23
|
+
|
|
24
|
+
if not lines:
|
|
25
|
+
print("No runs logged yet")
|
|
26
|
+
return
|
|
27
|
+
|
|
28
|
+
for line in lines[-tail:]:
|
|
29
|
+
print(line)
|
|
30
|
+
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""List available models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
RECOMMENDED = {
|
|
10
|
+
"solo": [
|
|
11
|
+
("google/gemini-2.0-flash", "Research, Architect, PRD, Reviewer", "Free tier"),
|
|
12
|
+
("ollama/qwen2.5-coder:14b", "Dev agent", "Local, free"),
|
|
13
|
+
("ollama/qwen2.5:7b", "QA agent", "Local, free, fast"),
|
|
14
|
+
],
|
|
15
|
+
"team": [
|
|
16
|
+
("google/gemini-2.0-flash", "Research, Architect, PRD, Reviewer", "Free tier"),
|
|
17
|
+
("anthropic/claude-sonnet-4-20250514", "Dev agent (Claude Code)", "Paid"),
|
|
18
|
+
("anthropic/claude-haiku-4-5", "QA agent", "Paid, cheaper"),
|
|
19
|
+
],
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def run_models(provider: Optional[str] = None):
|
|
24
|
+
from ..config import load_config
|
|
25
|
+
|
|
26
|
+
config = load_config()
|
|
27
|
+
print(f"Recommended models for profile: {config.profile}\n")
|
|
28
|
+
for model, role, cost in RECOMMENDED.get(config.profile, []):
|
|
29
|
+
print(f"- {model} | {role} | {cost}")
|
|
30
|
+
|
|
31
|
+
print("\nCurrent config:")
|
|
32
|
+
print(f" Reasoning -> {config.models.reasoning}")
|
|
33
|
+
print(f" Dev -> {config.models.dev}")
|
|
34
|
+
print(f" QA -> {config.models.qa}")
|
|
35
|
+
|
|
36
|
+
print("\nAll available models (from opencode):")
|
|
37
|
+
try:
|
|
38
|
+
command = ["opencode", "models"]
|
|
39
|
+
if provider:
|
|
40
|
+
command.extend(["--provider", provider])
|
|
41
|
+
result = subprocess.run(command, capture_output=True, text=True, timeout=10)
|
|
42
|
+
if result.returncode == 0:
|
|
43
|
+
print(result.stdout)
|
|
44
|
+
else:
|
|
45
|
+
print("Run `opencode auth login` to configure providers.")
|
|
46
|
+
except (FileNotFoundError, subprocess.TimeoutExpired):
|
|
47
|
+
print("opencode not installed or not responding.")
|
|
48
|
+
print("Install: npm install -g opencode")
|
|
49
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""List profiles."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ..config import _load_profile, list_profiles
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run_profiles():
|
|
9
|
+
print("Available profiles:\n")
|
|
10
|
+
for profile_name in list_profiles():
|
|
11
|
+
data = _load_profile(profile_name)
|
|
12
|
+
meta = data.get("meta", {})
|
|
13
|
+
models = data.get("models", {})
|
|
14
|
+
observability = data.get("observability", {})
|
|
15
|
+
print(f"{profile_name} - {meta.get('description', '')}")
|
|
16
|
+
print(f" Tracker: {meta.get('tracker', '—')}")
|
|
17
|
+
print(f" Dev CLI: {meta.get('dev_cli', '—')}")
|
|
18
|
+
print(f" Reasoning: {models.get('reasoning', '—')}")
|
|
19
|
+
print(f" Dev model: {models.get('dev', '—')}")
|
|
20
|
+
print(f" QA model: {models.get('qa', '—')}")
|
|
21
|
+
print(f" Observability: {observability.get('enabled', False)}")
|
|
22
|
+
print()
|
|
23
|
+
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Show active workflows."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
STAGE_EMOJI = {
|
|
11
|
+
"research": "🔍",
|
|
12
|
+
"plan": "🏗️",
|
|
13
|
+
"hitl_plan": "👤",
|
|
14
|
+
"prd": "📝",
|
|
15
|
+
"hitl_prd": "👤",
|
|
16
|
+
"dev": "💻",
|
|
17
|
+
"review": "🔎",
|
|
18
|
+
"qa": "🧪",
|
|
19
|
+
"hitl_pr": "👤",
|
|
20
|
+
"done": "✅",
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def run_status(ticket: Optional[str] = None):
|
|
25
|
+
state_dir = Path(".workflow-state")
|
|
26
|
+
if not state_dir.exists():
|
|
27
|
+
print("No active workflows")
|
|
28
|
+
return
|
|
29
|
+
|
|
30
|
+
state_files = list(state_dir.glob("*.json"))
|
|
31
|
+
if ticket:
|
|
32
|
+
state_files = [path for path in state_files if ticket.lower() in path.stem.lower()]
|
|
33
|
+
|
|
34
|
+
if not state_files:
|
|
35
|
+
print("No active workflows")
|
|
36
|
+
return
|
|
37
|
+
|
|
38
|
+
print("Active Workflows")
|
|
39
|
+
for state_file in sorted(state_files):
|
|
40
|
+
try:
|
|
41
|
+
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
42
|
+
except Exception:
|
|
43
|
+
print(f"{state_file.stem}: unknown")
|
|
44
|
+
continue
|
|
45
|
+
ticket_id = state.get("ticket_id", state_file.stem)
|
|
46
|
+
stage = state.get("current_stage", "unknown")
|
|
47
|
+
summary = state.get("ticket", {}).get("summary", "")
|
|
48
|
+
emoji = STAGE_EMOJI.get(stage, "⚙️")
|
|
49
|
+
print(f"{ticket_id} | {emoji} {stage} | {summary[:50]}")
|
|
50
|
+
|