snodo 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.
- snodo/__init__.py +1 -0
- snodo/__main__.py +6 -0
- snodo/agents/__init__.py +0 -0
- snodo/agents/adapter.py +18 -0
- snodo/cli/__init__.py +0 -0
- snodo/cli/commands/__init__.py +79 -0
- snodo/cli/commands/agent_cmd.py +101 -0
- snodo/cli/commands/config_cmd.py +138 -0
- snodo/cli/commands/dashboard_cmd.py +38 -0
- snodo/cli/commands/init_cmd.py +156 -0
- snodo/cli/commands/install_cmd.py +217 -0
- snodo/cli/commands/job_cmd.py +143 -0
- snodo/cli/commands/mode_cmd.py +107 -0
- snodo/cli/commands/plan_cmd.py +127 -0
- snodo/cli/commands/resolve_cmd.py +53 -0
- snodo/cli/commands/run_cmd.py +871 -0
- snodo/cli/commands/sandbox_cmd.py +103 -0
- snodo/cli/commands/serve_cmd.py +144 -0
- snodo/cli/commands/session_cmd.py +92 -0
- snodo/cli/config.py +287 -0
- snodo/cli/main.py +572 -0
- snodo/coders/__init__.py +67 -0
- snodo/coders/base.py +26 -0
- snodo/coders/litellm.py +164 -0
- snodo/coders/mock.py +36 -0
- snodo/compiler/__init__.py +0 -0
- snodo/compiler/models.py +262 -0
- snodo/compiler/verifier.py +307 -0
- snodo/core/__init__.py +0 -0
- snodo/core/interfaces.py +86 -0
- snodo/dashboard/__init__.py +10 -0
- snodo/dashboard/app.py +109 -0
- snodo/dashboard/panels/__init__.py +11 -0
- snodo/dashboard/panels/agents.py +69 -0
- snodo/dashboard/panels/events.py +73 -0
- snodo/dashboard/panels/jobs.py +84 -0
- snodo/dashboard/panels/plans.py +72 -0
- snodo/engine/__init__.py +0 -0
- snodo/engine/loop.py +1205 -0
- snodo/engine/policy.py +324 -0
- snodo/infrastructure/__init__.py +0 -0
- snodo/infrastructure/audit.py +231 -0
- snodo/infrastructure/memory.py +287 -0
- snodo/infrastructure/paths.py +26 -0
- snodo/infrastructure/session.py +351 -0
- snodo/infrastructure/state.py +56 -0
- snodo/infrastructure/tokens.py +250 -0
- snodo/jobs/__init__.py +298 -0
- snodo/jobs/runner.py +80 -0
- snodo/jobs/wrapper.py +74 -0
- snodo/mcp/__init__.py +0 -0
- snodo/mcp/git.py +212 -0
- snodo/mcp/installer.py +442 -0
- snodo/mcp/planner.py +665 -0
- snodo/mcp/pr.py +110 -0
- snodo/mcp/resolution.py +74 -0
- snodo/mcp/server.py +742 -0
- snodo/mcp/shell.py +323 -0
- snodo/mcp/transport.py +119 -0
- snodo/mcp/workspace.py +239 -0
- snodo/predicates/__init__.py +11 -0
- snodo/predicates/base.py +57 -0
- snodo/predicates/registry.py +53 -0
- snodo/predicates/scope.py +53 -0
- snodo/predicates/secrets.py +92 -0
- snodo/predicates/tests.py +78 -0
- snodo/protocols/__init__.py +1 -0
- snodo/protocols/templates/2+n.yml +124 -0
- snodo/protocols/templates/__init__.py +1 -0
- snodo/protocols/templates/solo.yml +54 -0
- snodo/protocols/templates/team.yml +117 -0
- snodo/providers/__init__.py +16 -0
- snodo/providers/base.py +106 -0
- snodo/providers/github.py +158 -0
- snodo/providers/local.py +54 -0
- snodo/providers/registry.py +219 -0
- snodo/sandbox/__init__.py +39 -0
- snodo/sandbox/base.py +96 -0
- snodo/sandbox/docker_sandbox.py +201 -0
- snodo/sandbox/local_sandbox.py +101 -0
- snodo/validators/__init__.py +5 -0
- snodo/validators/context.py +49 -0
- snodo/validators/llm_validator.py +202 -0
- snodo/validators/protocol_adherence.py +259 -0
- snodo/validators/quality.py +177 -0
- snodo/validators/registry.py +52 -0
- snodo-0.1.0.dist-info/METADATA +1035 -0
- snodo-0.1.0.dist-info/RECORD +92 -0
- snodo-0.1.0.dist-info/WHEEL +5 -0
- snodo-0.1.0.dist-info/entry_points.txt +3 -0
- snodo-0.1.0.dist-info/licenses/LICENSE +663 -0
- snodo-0.1.0.dist-info/top_level.txt +1 -0
snodo/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.1.0"
|
snodo/__main__.py
ADDED
snodo/agents/__init__.py
ADDED
|
File without changes
|
snodo/agents/adapter.py
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Agent adapter layer - backward compatibility re-exports.
|
|
2
|
+
|
|
3
|
+
FILE: snodo/agents/adapter.py
|
|
4
|
+
|
|
5
|
+
All adapter classes now live in snodo.coders.
|
|
6
|
+
This module re-exports them for backward compatibility.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from snodo.coders import ( # noqa: F401
|
|
10
|
+
LiteLLMAdapter,
|
|
11
|
+
MockAdapter,
|
|
12
|
+
BasicCoderAdapter,
|
|
13
|
+
MockCoderAdapter,
|
|
14
|
+
create_coder,
|
|
15
|
+
AdapterError,
|
|
16
|
+
LLMCallError,
|
|
17
|
+
ParseError,
|
|
18
|
+
)
|
snodo/cli/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Snodo CLI command modules.
|
|
2
|
+
|
|
3
|
+
Shared utilities used across command modules live here.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
import yaml
|
|
11
|
+
|
|
12
|
+
from snodo.compiler.models import Protocol
|
|
13
|
+
from snodo.compiler.verifier import verify_protocol, ProtocolWellFormednessError
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
_TEMPLATES_DIR = Path(__file__).parent.parent.parent / "protocols" / "templates"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _load_template(name: str) -> str:
|
|
20
|
+
"""Load a protocol template YAML file from disk.
|
|
21
|
+
|
|
22
|
+
Templates live as standalone YAML files in snodo/protocols/templates/.
|
|
23
|
+
This replaces the previous approach of embedding templates as Python
|
|
24
|
+
triple-quoted string constants, making protocols reviewable documents
|
|
25
|
+
and editable without code changes (Paper Section 6.4).
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
name: Template name without extension (e.g., "solo", "team", "2+n")
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
Raw YAML content as a string
|
|
32
|
+
"""
|
|
33
|
+
return (_TEMPLATES_DIR / f"{name}.yml").read_text()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# Default protocol template
|
|
37
|
+
DEFAULT_PROTOCOL = _load_template("team")
|
|
38
|
+
|
|
39
|
+
# Alias for clarity
|
|
40
|
+
TEAM_PROTOCOL = DEFAULT_PROTOCOL
|
|
41
|
+
|
|
42
|
+
SOLO_PROTOCOL = _load_template("solo")
|
|
43
|
+
|
|
44
|
+
TWO_PLUS_N_PROTOCOL = _load_template("2+n")
|
|
45
|
+
|
|
46
|
+
PROTOCOL_TEMPLATES = {"solo": SOLO_PROTOCOL, "team": TEAM_PROTOCOL, "2+n": TWO_PLUS_N_PROTOCOL}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def load_protocol(protocol_path: Path) -> Optional[Protocol]:
|
|
50
|
+
"""Load, parse, and verify protocol from YAML file.
|
|
51
|
+
|
|
52
|
+
Runs all WF1-WF5 well-formedness checks after parsing.
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
with open(protocol_path) as f:
|
|
56
|
+
data = yaml.safe_load(f)
|
|
57
|
+
|
|
58
|
+
protocol = Protocol(**data)
|
|
59
|
+
|
|
60
|
+
# WF1-WF5 verification (Section 4.4)
|
|
61
|
+
result = verify_protocol(protocol)
|
|
62
|
+
if not result.passed:
|
|
63
|
+
raise ProtocolWellFormednessError(result.errors)
|
|
64
|
+
|
|
65
|
+
return protocol
|
|
66
|
+
|
|
67
|
+
except FileNotFoundError:
|
|
68
|
+
print(f"Error: Protocol file not found: {protocol_path}", file=sys.stderr)
|
|
69
|
+
print("Run 'snodo init' to create default protocol.", file=sys.stderr)
|
|
70
|
+
return None
|
|
71
|
+
except yaml.YAMLError as e:
|
|
72
|
+
print(f"Error: Invalid YAML in protocol file: {e}", file=sys.stderr)
|
|
73
|
+
return None
|
|
74
|
+
except ProtocolWellFormednessError as e:
|
|
75
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
76
|
+
return None
|
|
77
|
+
except Exception as e:
|
|
78
|
+
print(f"Error: Failed to parse protocol: {e}", file=sys.stderr)
|
|
79
|
+
return None
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Agent command - Manage agent memory and threads.
|
|
2
|
+
|
|
3
|
+
FILE: snodo/cli/commands/agent_cmd.py (Task 5.2)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
import time
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def agent_command(args) -> int:
|
|
11
|
+
"""Manage agent memory and threads."""
|
|
12
|
+
from snodo.infrastructure.memory import AgentMemoryManager, MemoryError
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
manager = AgentMemoryManager()
|
|
16
|
+
except Exception as e:
|
|
17
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
18
|
+
return 1
|
|
19
|
+
|
|
20
|
+
action = args.agent_action
|
|
21
|
+
try:
|
|
22
|
+
if action == "list":
|
|
23
|
+
return _agent_list(manager)
|
|
24
|
+
elif action == "memory":
|
|
25
|
+
return _agent_memory(manager, args.agent_id)
|
|
26
|
+
elif action == "reset":
|
|
27
|
+
return _agent_reset(manager, args.agent_id)
|
|
28
|
+
elif action == "rotate":
|
|
29
|
+
return _agent_rotate(manager, args.agent_id)
|
|
30
|
+
else:
|
|
31
|
+
print("Unknown agent action. Use: list, memory, reset, rotate", file=sys.stderr)
|
|
32
|
+
return 1
|
|
33
|
+
except MemoryError as e:
|
|
34
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _agent_list(manager) -> int:
|
|
39
|
+
"""List all registered agents."""
|
|
40
|
+
agents = manager.list_agents()
|
|
41
|
+
if not agents:
|
|
42
|
+
print("No agents found.")
|
|
43
|
+
print("Agents are created automatically when you run tasks.")
|
|
44
|
+
return 0
|
|
45
|
+
|
|
46
|
+
print(f"{'ID':<30} {'Thread':<12} {'Tasks':<8} {'Last Used'}")
|
|
47
|
+
print("-" * 72)
|
|
48
|
+
for agent in agents:
|
|
49
|
+
thread_short = agent["thread_id"][:8] + "..."
|
|
50
|
+
task_count = agent.get("task_count", 0)
|
|
51
|
+
last_used = _format_time(agent.get("last_used_at"))
|
|
52
|
+
print(f"{agent['id']:<30} {thread_short:<12} {task_count:<8} {last_used}")
|
|
53
|
+
return 0
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _agent_memory(manager, agent_id: str) -> int:
|
|
57
|
+
"""Show memory summary for an agent."""
|
|
58
|
+
agent = manager.get_agent(agent_id)
|
|
59
|
+
if agent is None:
|
|
60
|
+
print(f"Error: Agent not found: {agent_id}", file=sys.stderr)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
summary = manager.get_memory_summary(agent_id)
|
|
64
|
+
|
|
65
|
+
print(f"Agent: {agent_id}")
|
|
66
|
+
print(f"Thread ID: {agent['thread_id']}")
|
|
67
|
+
print(f"Project: {agent.get('project', 'N/A')}")
|
|
68
|
+
print(f"Mode: {agent.get('mode', 'N/A')}")
|
|
69
|
+
print(f"Tasks completed: {agent.get('task_count', 0)}")
|
|
70
|
+
print(f"Created: {_format_time(agent.get('created_at'))}")
|
|
71
|
+
print(f"Last used: {_format_time(agent.get('last_used_at'))}")
|
|
72
|
+
print()
|
|
73
|
+
print(f"Checkpoints: {summary.get('checkpoint_count', 0)}")
|
|
74
|
+
print(f"Database: {'exists' if summary.get('db_exists') else 'not created'}")
|
|
75
|
+
return 0
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _agent_reset(manager, agent_id: str) -> int:
|
|
79
|
+
"""Reset agent memory (clear checkpoints and assign new thread)."""
|
|
80
|
+
result = manager.reset_memory(agent_id)
|
|
81
|
+
print(f"Agent {agent_id} memory cleared.")
|
|
82
|
+
print(f"New thread ID: {result['thread_id']}")
|
|
83
|
+
return 0
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _agent_rotate(manager, agent_id: str) -> int:
|
|
87
|
+
"""Rotate agent thread ID (keeps old checkpoints)."""
|
|
88
|
+
result = manager.rotate_thread(agent_id)
|
|
89
|
+
print(f"Agent {agent_id} thread rotated.")
|
|
90
|
+
print(f"New thread ID: {result['thread_id']}")
|
|
91
|
+
return 0
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _format_time(ts) -> str:
|
|
95
|
+
"""Format a timestamp for display."""
|
|
96
|
+
if not ts:
|
|
97
|
+
return "N/A"
|
|
98
|
+
try:
|
|
99
|
+
return time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(ts))
|
|
100
|
+
except (TypeError, ValueError, OSError):
|
|
101
|
+
return "N/A"
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"""Config command - Manage Snodo configuration and API keys.
|
|
2
|
+
|
|
3
|
+
FILE: snodo/cli/commands/config_cmd.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
|
|
8
|
+
from snodo.cli.config import ConfigManager, ConfigError, DEFAULT_MODEL
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def config_command(args) -> int:
|
|
12
|
+
"""Manage Snodo configuration and API keys."""
|
|
13
|
+
mgr = ConfigManager()
|
|
14
|
+
|
|
15
|
+
if args.config_action == "show":
|
|
16
|
+
return _config_show(mgr)
|
|
17
|
+
elif args.config_action == "add":
|
|
18
|
+
return _config_add(mgr, args.provider, args.key)
|
|
19
|
+
elif args.config_action == "remove":
|
|
20
|
+
return _config_remove(mgr, args.provider)
|
|
21
|
+
elif args.config_action == "test":
|
|
22
|
+
return _config_test(mgr)
|
|
23
|
+
elif args.config_action == "set":
|
|
24
|
+
return _config_set(mgr, args.key, args.value)
|
|
25
|
+
elif args.config_action == "get":
|
|
26
|
+
return _config_get(mgr, args.key)
|
|
27
|
+
else:
|
|
28
|
+
print("Unknown config action. Use: show, add, remove, test, set, get", file=sys.stderr)
|
|
29
|
+
return 1
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _config_show(mgr: ConfigManager) -> int:
|
|
33
|
+
"""Show current configuration."""
|
|
34
|
+
config = mgr.load()
|
|
35
|
+
keys = config.get("api_keys", {})
|
|
36
|
+
model = config.get("model", DEFAULT_MODEL)
|
|
37
|
+
|
|
38
|
+
print(f"Config: {mgr.config_path}")
|
|
39
|
+
print(f"Model: {model}")
|
|
40
|
+
print()
|
|
41
|
+
|
|
42
|
+
if keys:
|
|
43
|
+
print("API Keys:")
|
|
44
|
+
for provider, key in keys.items():
|
|
45
|
+
print(f" {provider}: {ConfigManager.mask_key(key)}")
|
|
46
|
+
else:
|
|
47
|
+
print("No API keys configured.")
|
|
48
|
+
print(" Add one: snodo config add <provider> <key>")
|
|
49
|
+
return 0
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _config_add(mgr: ConfigManager, provider: str, key: str) -> int:
|
|
53
|
+
"""Add an API key."""
|
|
54
|
+
try:
|
|
55
|
+
mgr.add_key(provider, key)
|
|
56
|
+
masked = ConfigManager.mask_key(key)
|
|
57
|
+
print(f"✓ Stored {provider} key: {masked}")
|
|
58
|
+
return 0
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
61
|
+
return 1
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _config_remove(mgr: ConfigManager, provider: str) -> int:
|
|
65
|
+
"""Remove an API key."""
|
|
66
|
+
if mgr.remove_key(provider):
|
|
67
|
+
print(f"✓ Removed {provider} key")
|
|
68
|
+
return 0
|
|
69
|
+
else:
|
|
70
|
+
print(f"No key found for provider: {provider}", file=sys.stderr)
|
|
71
|
+
return 1
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _config_test(mgr: ConfigManager) -> int:
|
|
75
|
+
"""Test all configured API keys."""
|
|
76
|
+
config = mgr.load()
|
|
77
|
+
keys = config.get("api_keys", {})
|
|
78
|
+
if not keys:
|
|
79
|
+
print("No API keys configured. Add one first:")
|
|
80
|
+
print(" snodo config add <provider> <key>")
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
print("Testing API keys...")
|
|
84
|
+
results = mgr.test_keys()
|
|
85
|
+
all_ok = True
|
|
86
|
+
for provider, ok in results.items():
|
|
87
|
+
status = "✓ valid" if ok else "✗ invalid"
|
|
88
|
+
print(f" {provider}: {status}")
|
|
89
|
+
if not ok:
|
|
90
|
+
all_ok = False
|
|
91
|
+
return 0 if all_ok else 1
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _config_set(mgr: ConfigManager, key: str, value: str) -> int:
|
|
95
|
+
"""Set a config value using dot notation."""
|
|
96
|
+
parts = key.split(".", 1)
|
|
97
|
+
if len(parts) == 2 and parts[0] == "engine":
|
|
98
|
+
engine_key = parts[1]
|
|
99
|
+
if engine_key in ("max_subtask_depth", "max_session_age_days", "token_ttl_seconds"):
|
|
100
|
+
try:
|
|
101
|
+
int_value = int(value)
|
|
102
|
+
except ValueError:
|
|
103
|
+
print(f"Error: {engine_key} must be an integer", file=sys.stderr)
|
|
104
|
+
return 1
|
|
105
|
+
try:
|
|
106
|
+
mgr.set_engine_value(engine_key, int_value)
|
|
107
|
+
except (ValueError, ConfigError) as e:
|
|
108
|
+
print(f"Error: {e}", file=sys.stderr)
|
|
109
|
+
return 1
|
|
110
|
+
else:
|
|
111
|
+
mgr.set_engine_value(engine_key, value)
|
|
112
|
+
print(f"Set {key} = {value}")
|
|
113
|
+
return 0
|
|
114
|
+
elif key == "model":
|
|
115
|
+
mgr.set_model(value)
|
|
116
|
+
print(f"Set model = {value}")
|
|
117
|
+
return 0
|
|
118
|
+
else:
|
|
119
|
+
print(f"Error: Unknown config key: {key}", file=sys.stderr)
|
|
120
|
+
return 1
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _config_get(mgr: ConfigManager, key: str) -> int:
|
|
124
|
+
"""Get a config value using dot notation."""
|
|
125
|
+
parts = key.split(".", 1)
|
|
126
|
+
if len(parts) == 2 and parts[0] == "engine":
|
|
127
|
+
value = mgr.get_engine_value(parts[1])
|
|
128
|
+
if value is None:
|
|
129
|
+
print(f"Not set: {key}", file=sys.stderr)
|
|
130
|
+
return 1
|
|
131
|
+
print(value)
|
|
132
|
+
return 0
|
|
133
|
+
elif key == "model":
|
|
134
|
+
print(mgr.get_model())
|
|
135
|
+
return 0
|
|
136
|
+
else:
|
|
137
|
+
print(f"Error: Unknown config key: {key}", file=sys.stderr)
|
|
138
|
+
return 1
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Dashboard command - Launch TUI dashboard.
|
|
2
|
+
|
|
3
|
+
FILE: snodo/cli/commands/dashboard_cmd.py (Task 5.3)
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def snop_entry():
|
|
11
|
+
"""Entry point for the 'snop' shortcut command."""
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
args = SimpleNamespace()
|
|
14
|
+
sys.exit(dashboard_command(args))
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dashboard_command(args) -> int:
|
|
18
|
+
"""Launch the Snodo TUI dashboard."""
|
|
19
|
+
project_root = str(Path.cwd())
|
|
20
|
+
|
|
21
|
+
# Verify .snodo/ exists
|
|
22
|
+
snodo_dir = Path(project_root) / ".snodo"
|
|
23
|
+
if not snodo_dir.is_dir():
|
|
24
|
+
print("Error: Not a snodo project (no .snodo/ directory)", file=sys.stderr)
|
|
25
|
+
print("Run 'snodo init' first.", file=sys.stderr)
|
|
26
|
+
return 1
|
|
27
|
+
|
|
28
|
+
try:
|
|
29
|
+
from snodo.dashboard.app import run_dashboard
|
|
30
|
+
run_dashboard(project_root=project_root)
|
|
31
|
+
return 0
|
|
32
|
+
except ImportError as e:
|
|
33
|
+
print(f"Error: Dashboard requires 'textual' package: {e}", file=sys.stderr)
|
|
34
|
+
print("Install with: pip install textual", file=sys.stderr)
|
|
35
|
+
return 1
|
|
36
|
+
except Exception as e:
|
|
37
|
+
print(f"Error: Dashboard failed: {e}", file=sys.stderr)
|
|
38
|
+
return 1
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"""Init command - Initialize Snodo project structure.
|
|
2
|
+
|
|
3
|
+
FILE: snodo/cli/commands/init_cmd.py
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import yaml
|
|
10
|
+
|
|
11
|
+
from snodo.cli.commands import PROTOCOL_TEMPLATES
|
|
12
|
+
from snodo.infrastructure.state import ProjectState, write_state
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _select_template(args) -> str:
|
|
16
|
+
"""Select protocol template from flag or interactive prompt.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
The selected template YAML string.
|
|
20
|
+
"""
|
|
21
|
+
template_name = getattr(args, "template", None)
|
|
22
|
+
|
|
23
|
+
if template_name:
|
|
24
|
+
return PROTOCOL_TEMPLATES[template_name]
|
|
25
|
+
|
|
26
|
+
# Interactive prompt
|
|
27
|
+
print("Choose protocol template:")
|
|
28
|
+
print(" 1. solo - Single developer (producer merges directly)")
|
|
29
|
+
print(" 2. team - Team workflow (producer + reviewer + planner)")
|
|
30
|
+
print(" 3. 2+n - Paper reference config (producer + reviewer)")
|
|
31
|
+
choice = input("Select [1/2/3]: ").strip()
|
|
32
|
+
|
|
33
|
+
if choice == "1":
|
|
34
|
+
return PROTOCOL_TEMPLATES["solo"]
|
|
35
|
+
elif choice == "2":
|
|
36
|
+
return PROTOCOL_TEMPLATES["team"]
|
|
37
|
+
elif choice == "3":
|
|
38
|
+
return PROTOCOL_TEMPLATES["2+n"]
|
|
39
|
+
else:
|
|
40
|
+
print(f"Invalid choice: {choice!r}. Using team template.", file=sys.stderr)
|
|
41
|
+
return PROTOCOL_TEMPLATES["team"]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _pick_mode(args, modes: list, default_mode: str) -> str:
|
|
45
|
+
"""Interactive mode picker. Returns selected mode_id.
|
|
46
|
+
|
|
47
|
+
Skips picker when:
|
|
48
|
+
- --mode <m> passed (validated against available modes)
|
|
49
|
+
- Not a TTY (piped / CI — keep default silently)
|
|
50
|
+
"""
|
|
51
|
+
cli_mode = getattr(args, "mode", None)
|
|
52
|
+
|
|
53
|
+
# Build mode_id -> info lookup
|
|
54
|
+
mode_info: dict = {}
|
|
55
|
+
for m in modes:
|
|
56
|
+
mid = m.get("mode_id", "")
|
|
57
|
+
name = m.get("name", mid)
|
|
58
|
+
tools = m.get("tools", [])
|
|
59
|
+
mode_info[mid] = {"name": name, "tools": tools}
|
|
60
|
+
|
|
61
|
+
if cli_mode:
|
|
62
|
+
if cli_mode not in mode_info:
|
|
63
|
+
available = ", ".join(sorted(mode_info.keys()))
|
|
64
|
+
print(
|
|
65
|
+
f"Error: Mode '{cli_mode}' not in protocol. "
|
|
66
|
+
f"Available: {available}",
|
|
67
|
+
file=sys.stderr,
|
|
68
|
+
)
|
|
69
|
+
raise SystemExit(1)
|
|
70
|
+
return cli_mode
|
|
71
|
+
|
|
72
|
+
# Non-TTY → keep default silently (CI / piped)
|
|
73
|
+
if not sys.stdin.isatty():
|
|
74
|
+
return default_mode
|
|
75
|
+
|
|
76
|
+
# Single-mode protocol → no choice needed
|
|
77
|
+
if len(mode_info) <= 1:
|
|
78
|
+
return default_mode
|
|
79
|
+
|
|
80
|
+
# Interactive picker
|
|
81
|
+
print()
|
|
82
|
+
print("Select your starting mode:")
|
|
83
|
+
mode_ids = sorted(mode_info.keys())
|
|
84
|
+
default_idx = mode_ids.index(default_mode) if default_mode in mode_ids else 0
|
|
85
|
+
|
|
86
|
+
for i, mid in enumerate(mode_ids):
|
|
87
|
+
info = mode_info[mid]
|
|
88
|
+
tools_str = ", ".join(info["tools"]) if info["tools"] else "none"
|
|
89
|
+
marker = " [default]" if mid == default_mode else ""
|
|
90
|
+
print(f" {i + 1}. {info['name']} ({mid}) tools: {tools_str}{marker}")
|
|
91
|
+
|
|
92
|
+
try:
|
|
93
|
+
choice = input(f"Select [1-{len(mode_ids)}, default={default_idx + 1}]: ").strip()
|
|
94
|
+
if not choice:
|
|
95
|
+
return default_mode
|
|
96
|
+
idx = int(choice) - 1
|
|
97
|
+
if 0 <= idx < len(mode_ids):
|
|
98
|
+
return mode_ids[idx]
|
|
99
|
+
except (ValueError, KeyboardInterrupt):
|
|
100
|
+
pass
|
|
101
|
+
|
|
102
|
+
print(f"Using default: {default_mode}")
|
|
103
|
+
return default_mode
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def init_command(args) -> int:
|
|
107
|
+
"""Initialize Snodo project structure."""
|
|
108
|
+
snodo_dir = Path(".snodo")
|
|
109
|
+
|
|
110
|
+
if snodo_dir.exists():
|
|
111
|
+
if not args.force:
|
|
112
|
+
print("Error: .snodo/ already exists. Use --force to overwrite.", file=sys.stderr)
|
|
113
|
+
return 1
|
|
114
|
+
print("Warning: Overwriting existing .snodo/ directory")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
snodo_dir.mkdir(exist_ok=True)
|
|
118
|
+
print(f"Created {snodo_dir}/")
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"Error: Failed to create .snodo/ directory: {e}", file=sys.stderr)
|
|
121
|
+
return 1
|
|
122
|
+
|
|
123
|
+
# Select template
|
|
124
|
+
template = _select_template(args)
|
|
125
|
+
|
|
126
|
+
protocol_file = snodo_dir / "protocol.yml"
|
|
127
|
+
try:
|
|
128
|
+
protocol_file.write_text(template + "\n")
|
|
129
|
+
print(f"Created {protocol_file}")
|
|
130
|
+
except Exception as e:
|
|
131
|
+
print(f"Error: Failed to create protocol.yml: {e}", file=sys.stderr)
|
|
132
|
+
return 1
|
|
133
|
+
|
|
134
|
+
# Write .snodo/state.json — set current_mode from protocol.initial_mode
|
|
135
|
+
# Ctrl-C safe: this write IS the state; no subsequent prompt can kill it.
|
|
136
|
+
try:
|
|
137
|
+
data = yaml.safe_load(template)
|
|
138
|
+
initial_mode = data.get("initial_mode", "")
|
|
139
|
+
modes = data.get("modes", [])
|
|
140
|
+
if initial_mode:
|
|
141
|
+
write_state(".", ProjectState(current_mode=initial_mode))
|
|
142
|
+
|
|
143
|
+
# Interactive mode picker (or --mode flag skips it)
|
|
144
|
+
selected_mode = _pick_mode(args, modes, initial_mode)
|
|
145
|
+
if selected_mode and selected_mode != initial_mode:
|
|
146
|
+
write_state(".", ProjectState(current_mode=selected_mode))
|
|
147
|
+
print(f"Active mode: {selected_mode or initial_mode}")
|
|
148
|
+
except Exception as e:
|
|
149
|
+
print(f"Warning: Could not write state.json: {e}", file=sys.stderr)
|
|
150
|
+
|
|
151
|
+
print("\nSnodo initialized successfully!")
|
|
152
|
+
print("\nNext steps:")
|
|
153
|
+
print(" 1. Edit .snodo/protocol.yml to customize your protocol")
|
|
154
|
+
print(" 2. Run: snodo run \"your task description\"")
|
|
155
|
+
|
|
156
|
+
return 0
|