spec-runner 2.1.0__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {spec_runner-2.1.0/src/spec_runner.egg-info → spec_runner-2.2.0}/PKG-INFO +3 -1
- {spec_runner-2.1.0 → spec_runner-2.2.0}/README.md +2 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/pyproject.toml +1 -1
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/audit.py +4 -12
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/cli.py +2 -1
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/config.py +4 -1
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/execution.py +5 -16
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/github_sync.py +1 -1
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/obs.py +3 -2
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/report.py +3 -11
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/runner.py +27 -2
- spec_runner-2.2.0/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.opencode.md +38 -0
- spec_runner-2.2.0/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.pi.md +38 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/state.py +6 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/tui.py +4 -3
- {spec_runner-2.1.0 → spec_runner-2.2.0/src/spec_runner.egg-info}/PKG-INFO +3 -1
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner.egg-info/SOURCES.txt +2 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_audit.py +1 -3
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_audit_log.py +2 -6
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_runner.py +36 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/LICENSE +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/setup.cfg +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/__init__.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/audit_log.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/cli_info.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/cli_plan.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/events.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/executor.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/git_ops.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/hooks.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/init_cmd.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/logging.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/mcp_server.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/notifications.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/plugins.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/prompt.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/py.typed +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/review.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/SKILL.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/Makefile.template +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/design.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/executor.config.yaml +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/executor.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/phase-design.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/phase-requirements.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/phase-tasks.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.claude.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.codex.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.llama.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.ollama.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/requirements.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/task.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/tasks.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/templates/workflow.template.md +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/task.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/task_commands.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/validate.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/verify.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner.egg-info/dependency_links.txt +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner.egg-info/entry_points.txt +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner.egg-info/requires.txt +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner.egg-info/top_level.txt +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_config.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_costs.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_e2e.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_events.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_execution.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_gh_sync.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_hooks.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_json_result_contract.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_logging.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_mcp.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_notifications.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_obs.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_obs_contract.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_plan_full.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_plugins.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_prompt.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_report.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_spec_prefix.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_state.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_task_diff.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_tui.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_validate.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_verify.py +0 -0
- {spec_runner-2.1.0 → spec_runner-2.2.0}/tests/test_watch.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spec-runner
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Task automation from markdown specs via Claude CLI
|
|
5
5
|
Author: Andrei
|
|
6
6
|
License-Expression: MIT
|
|
@@ -369,6 +369,8 @@ paths:
|
|
|
369
369
|
|-----|--------------|------------------|
|
|
370
370
|
| Claude | Yes | `{cmd} -p {prompt} --model {model}` |
|
|
371
371
|
| Codex | Yes | `{cmd} -p {prompt} --model {model}` |
|
|
372
|
+
| OpenCode ([sst/opencode](https://opencode.ai)) | Yes | `{cmd} run --model {model} {prompt}` |
|
|
373
|
+
| Pi Agent ([pi.dev](https://pi.dev)) | Yes (basename match) | `{cmd} -p --model {model} {prompt}` |
|
|
372
374
|
| Ollama | Yes | `{cmd} run {model} {prompt}` |
|
|
373
375
|
| llama-cli | Yes | `{cmd} -m {model} -p {prompt} --no-display-prompt` |
|
|
374
376
|
| Custom | Use template | `{cmd} --prompt {prompt}` |
|
|
@@ -334,6 +334,8 @@ paths:
|
|
|
334
334
|
|-----|--------------|------------------|
|
|
335
335
|
| Claude | Yes | `{cmd} -p {prompt} --model {model}` |
|
|
336
336
|
| Codex | Yes | `{cmd} -p {prompt} --model {model}` |
|
|
337
|
+
| OpenCode ([sst/opencode](https://opencode.ai)) | Yes | `{cmd} run --model {model} {prompt}` |
|
|
338
|
+
| Pi Agent ([pi.dev](https://pi.dev)) | Yes (basename match) | `{cmd} -p --model {model} {prompt}` |
|
|
337
339
|
| Ollama | Yes | `{cmd} run {model} {prompt}` |
|
|
338
340
|
| llama-cli | Yes | `{cmd} -m {model} -p {prompt} --no-display-prompt` |
|
|
339
341
|
| Custom | Use template | `{cmd} --prompt {prompt}` |
|
|
@@ -116,9 +116,7 @@ def audit_all(config: ExecutorConfig, *, strict: bool = False) -> AuditReport:
|
|
|
116
116
|
"""
|
|
117
117
|
report = AuditReport(strict=strict)
|
|
118
118
|
|
|
119
|
-
tasks: list[Task] = (
|
|
120
|
-
parse_tasks(config.tasks_file) if config.tasks_file.exists() else []
|
|
121
|
-
)
|
|
119
|
+
tasks: list[Task] = parse_tasks(config.tasks_file) if config.tasks_file.exists() else []
|
|
122
120
|
|
|
123
121
|
req_ids: set[str] = set()
|
|
124
122
|
if config.requirements_file.exists():
|
|
@@ -168,8 +166,7 @@ def audit_all(config: ExecutorConfig, *, strict: bool = False) -> AuditReport:
|
|
|
168
166
|
category=CAT_DANGLING_DESIGN_REF,
|
|
169
167
|
subject=ref,
|
|
170
168
|
message=(
|
|
171
|
-
f"{task.id} references {ref} but it is not "
|
|
172
|
-
"defined in design.md"
|
|
169
|
+
f"{task.id} references {ref} but it is not defined in design.md"
|
|
173
170
|
),
|
|
174
171
|
location=task.id,
|
|
175
172
|
)
|
|
@@ -177,9 +174,7 @@ def audit_all(config: ExecutorConfig, *, strict: bool = False) -> AuditReport:
|
|
|
177
174
|
|
|
178
175
|
# 4. Uncovered requirements — REQ defined but no task references it
|
|
179
176
|
if req_ids:
|
|
180
|
-
covered_reqs = {
|
|
181
|
-
ref for task in tasks for ref in task.traces_to if ref.startswith("REQ-")
|
|
182
|
-
}
|
|
177
|
+
covered_reqs = {ref for task in tasks for ref in task.traces_to if ref.startswith("REQ-")}
|
|
183
178
|
for req in sorted(req_ids - covered_reqs):
|
|
184
179
|
report.findings.append(
|
|
185
180
|
AuditFinding(
|
|
@@ -194,10 +189,7 @@ def audit_all(config: ExecutorConfig, *, strict: bool = False) -> AuditReport:
|
|
|
194
189
|
# 5. Uncovered designs — DESIGN defined but no task references it
|
|
195
190
|
if design_ids:
|
|
196
191
|
covered_designs = {
|
|
197
|
-
ref
|
|
198
|
-
for task in tasks
|
|
199
|
-
for ref in task.traces_to
|
|
200
|
-
if ref.startswith("DESIGN-")
|
|
192
|
+
ref for task in tasks for ref in task.traces_to if ref.startswith("DESIGN-")
|
|
201
193
|
}
|
|
202
194
|
for design in sorted(design_ids - covered_designs):
|
|
203
195
|
report.findings.append(
|
|
@@ -5,6 +5,7 @@ import json
|
|
|
5
5
|
import signal
|
|
6
6
|
import sys
|
|
7
7
|
import time
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
from datetime import datetime
|
|
9
10
|
from pathlib import Path
|
|
10
11
|
from uuid import uuid4
|
|
@@ -622,7 +623,7 @@ def _dispatch_task_command(args: argparse.Namespace) -> None:
|
|
|
622
623
|
tasks_file = Path(f"spec/{prefix}tasks.md") if prefix else TASKS_FILE
|
|
623
624
|
tasks = parse_tasks(tasks_file)
|
|
624
625
|
|
|
625
|
-
write_commands = {
|
|
626
|
+
write_commands: dict[str, Callable[..., object]] = {
|
|
626
627
|
"start": cmd_start,
|
|
627
628
|
"done": cmd_done,
|
|
628
629
|
"block": cmd_block,
|
|
@@ -138,10 +138,13 @@ class ExecutorConfig:
|
|
|
138
138
|
# Examples:
|
|
139
139
|
# claude: "{cmd} -p {prompt}" or "{cmd} -p {prompt} --model {model}"
|
|
140
140
|
# codex: "{cmd} -p {prompt}"
|
|
141
|
+
# opencode: "{cmd} run --model {model} {prompt}"
|
|
142
|
+
# pi: "{cmd} -p --model {model} {prompt}"
|
|
141
143
|
# ollama: "{cmd} run {model} {prompt}"
|
|
142
144
|
# llama-cli: "{cmd} -m {model} -p {prompt} --no-display-prompt"
|
|
143
145
|
# llama-server: "curl -s http://localhost:8080/completion -d '{{\"prompt\": {prompt}}}'"
|
|
144
|
-
# If empty, auto-detects based on command name
|
|
146
|
+
# If empty, auto-detects based on command name (claude, codex, opencode, pi,
|
|
147
|
+
# ollama, llama-cli, llama-server)
|
|
145
148
|
command_template: str = ""
|
|
146
149
|
|
|
147
150
|
# Hooks
|
|
@@ -375,7 +375,7 @@ def compute_retry_delay(error_code: ErrorCode | str, attempt: int, base_delay: i
|
|
|
375
375
|
if strategy == "fatal":
|
|
376
376
|
return 0.0
|
|
377
377
|
if strategy == "backoff_exponential":
|
|
378
|
-
return min(30.0 * (2**attempt), 300.0)
|
|
378
|
+
return float(min(30.0 * (2**attempt), 300.0))
|
|
379
379
|
return float(base_delay * (attempt + 1))
|
|
380
380
|
|
|
381
381
|
|
|
@@ -396,24 +396,13 @@ def _check_task_budget(
|
|
|
396
396
|
"""
|
|
397
397
|
spent = state.task_cost(task_id)
|
|
398
398
|
if config.task_budget_usd is not None and spent >= config.task_budget_usd:
|
|
399
|
-
return (
|
|
400
|
-
f"Task budget exceeded "
|
|
401
|
-
f"(${spent:.2f} >= ${config.task_budget_usd:.2f})"
|
|
402
|
-
)
|
|
399
|
+
return f"Task budget exceeded (${spent:.2f} >= ${config.task_budget_usd:.2f})"
|
|
403
400
|
|
|
404
|
-
if
|
|
405
|
-
config.max_retry_cost_usd is not None
|
|
406
|
-
and attempt_index > 0
|
|
407
|
-
):
|
|
401
|
+
if config.max_retry_cost_usd is not None and attempt_index > 0:
|
|
408
402
|
ts = state.get_task_state(task_id)
|
|
409
|
-
retry_spent = (
|
|
410
|
-
sum(a.cost_usd or 0.0 for a in ts.attempts[1:]) if ts else 0.0
|
|
411
|
-
)
|
|
403
|
+
retry_spent = sum(a.cost_usd or 0.0 for a in ts.attempts[1:]) if ts else 0.0
|
|
412
404
|
if retry_spent >= config.max_retry_cost_usd:
|
|
413
|
-
return (
|
|
414
|
-
f"Retry budget exceeded "
|
|
415
|
-
f"(${retry_spent:.2f} >= ${config.max_retry_cost_usd:.2f})"
|
|
416
|
-
)
|
|
405
|
+
return f"Retry budget exceeded (${retry_spent:.2f} >= ${config.max_retry_cost_usd:.2f})"
|
|
417
406
|
return None
|
|
418
407
|
|
|
419
408
|
|
|
@@ -117,7 +117,7 @@ def _status_from_issue(issue: dict) -> str:
|
|
|
117
117
|
if issue["state"] == "CLOSED":
|
|
118
118
|
return "done"
|
|
119
119
|
for label in issue.get("labels", []):
|
|
120
|
-
name = label["name"] if isinstance(label, dict) else label
|
|
120
|
+
name = str(label["name"]) if isinstance(label, dict) else str(label)
|
|
121
121
|
if name.startswith("status:"):
|
|
122
122
|
status = name.split(":", 1)[1]
|
|
123
123
|
if status in STATUS_EMOJI:
|
|
@@ -17,7 +17,7 @@ from collections.abc import Iterator
|
|
|
17
17
|
from contextlib import contextmanager
|
|
18
18
|
from datetime import UTC, datetime
|
|
19
19
|
from pathlib import Path
|
|
20
|
-
from typing import Any
|
|
20
|
+
from typing import Any, cast
|
|
21
21
|
|
|
22
22
|
import structlog
|
|
23
23
|
import ulid
|
|
@@ -190,7 +190,8 @@ def init_logging(
|
|
|
190
190
|
|
|
191
191
|
|
|
192
192
|
def get_logger(module: str | None = None) -> structlog.BoundLogger:
|
|
193
|
-
|
|
193
|
+
logger = structlog.get_logger(module=module) if module else structlog.get_logger()
|
|
194
|
+
return cast("structlog.BoundLogger", logger)
|
|
194
195
|
|
|
195
196
|
|
|
196
197
|
class Span:
|
|
@@ -50,11 +50,7 @@ class TraceabilityReport:
|
|
|
50
50
|
@property
|
|
51
51
|
def has_gaps(self) -> bool:
|
|
52
52
|
"""True when CI should flag this report as incomplete."""
|
|
53
|
-
return bool(
|
|
54
|
-
self.orphan_tasks
|
|
55
|
-
or self.uncovered_requirements
|
|
56
|
-
or self.unreferenced_designs
|
|
57
|
-
)
|
|
53
|
+
return bool(self.orphan_tasks or self.uncovered_requirements or self.unreferenced_designs)
|
|
58
54
|
|
|
59
55
|
|
|
60
56
|
def _extract_section_ids(text: str, prefix: str) -> list[str]:
|
|
@@ -135,16 +131,12 @@ def build_report(
|
|
|
135
131
|
# Gap warnings (LABS-42): identifiers defined in spec files that no
|
|
136
132
|
# task references. Useful for CI integration — CI can fail if the
|
|
137
133
|
# report has gaps, surfacing drift between specs and implementation.
|
|
138
|
-
report.uncovered_requirements = sorted(
|
|
139
|
-
req for req in all_reqs if req not in req_to_tasks
|
|
140
|
-
)
|
|
134
|
+
report.uncovered_requirements = sorted(req for req in all_reqs if req not in req_to_tasks)
|
|
141
135
|
if design_to_req:
|
|
142
136
|
referenced_designs = {
|
|
143
137
|
ref for task in tasks for ref in task.traces_to if ref.startswith("DESIGN-")
|
|
144
138
|
}
|
|
145
|
-
report.unreferenced_designs = sorted(
|
|
146
|
-
set(design_to_req) - referenced_designs
|
|
147
|
-
)
|
|
139
|
+
report.unreferenced_designs = sorted(set(design_to_req) - referenced_designs)
|
|
148
140
|
|
|
149
141
|
# Build rows
|
|
150
142
|
with ExecutorState(config) as state:
|
|
@@ -149,7 +149,8 @@ def build_cli_command(
|
|
|
149
149
|
"""Build CLI command from template or auto-detect based on command name.
|
|
150
150
|
|
|
151
151
|
Args:
|
|
152
|
-
cmd: CLI command name (e.g., "claude", "codex", "
|
|
152
|
+
cmd: CLI command name (e.g., "claude", "codex", "opencode", "pi",
|
|
153
|
+
"ollama", "llama-cli")
|
|
153
154
|
prompt: The prompt text
|
|
154
155
|
model: Model name (optional)
|
|
155
156
|
template: Command template with placeholders (optional)
|
|
@@ -182,6 +183,9 @@ def build_cli_command(
|
|
|
182
183
|
|
|
183
184
|
# Auto-detect based on command name
|
|
184
185
|
cmd_lower = cmd.lower()
|
|
186
|
+
# "pi" is too short for substring matching — match on basename only to
|
|
187
|
+
# avoid false positives like "/usr/local/bin/anti-pi" or "opencode-pi-cli".
|
|
188
|
+
cmd_basename = Path(cmd).name.lower()
|
|
185
189
|
|
|
186
190
|
if "llama-cli" in cmd_lower or "llama.cpp" in cmd_lower:
|
|
187
191
|
# llama.cpp CLI
|
|
@@ -199,6 +203,16 @@ def build_cli_command(
|
|
|
199
203
|
# Ollama CLI
|
|
200
204
|
return [cmd, "run", model or "llama3", prompt]
|
|
201
205
|
|
|
206
|
+
elif "opencode" in cmd_lower:
|
|
207
|
+
# sst/opencode: `opencode run [--model provider/id] <prompt>`
|
|
208
|
+
# Prompt is positional, model accepts "provider/model" form
|
|
209
|
+
# (e.g. "anthropic/claude-3-5-sonnet").
|
|
210
|
+
result = [cmd, "run"]
|
|
211
|
+
if model:
|
|
212
|
+
result.extend(["--model", model])
|
|
213
|
+
result.append(prompt)
|
|
214
|
+
return result
|
|
215
|
+
|
|
202
216
|
elif "codex" in cmd_lower:
|
|
203
217
|
# Codex CLI
|
|
204
218
|
result = [cmd, "-p", prompt]
|
|
@@ -206,6 +220,17 @@ def build_cli_command(
|
|
|
206
220
|
result.extend(["--model", model])
|
|
207
221
|
return result
|
|
208
222
|
|
|
223
|
+
elif cmd_basename == "pi" or cmd_basename.startswith("pi."):
|
|
224
|
+
# earendil-works/pi: `pi -p [--model X] <prompt>` (non-interactive mode)
|
|
225
|
+
# Model accepts "provider/id" or bare model name; defaults driven by
|
|
226
|
+
# `~/.config/pi/config.yaml`. Match on basename to avoid short-name
|
|
227
|
+
# collisions (see cmd_basename comment above).
|
|
228
|
+
result = [cmd, "-p"]
|
|
229
|
+
if model:
|
|
230
|
+
result.extend(["--model", model])
|
|
231
|
+
result.append(prompt)
|
|
232
|
+
return result
|
|
233
|
+
|
|
209
234
|
else:
|
|
210
235
|
# Claude CLI (default)
|
|
211
236
|
result = [cmd, "-p", prompt]
|
|
@@ -297,4 +322,4 @@ async def run_claude_async(
|
|
|
297
322
|
proc.kill()
|
|
298
323
|
await proc.wait()
|
|
299
324
|
raise
|
|
300
|
-
return stdout_bytes.decode(), stderr_bytes.decode(), proc.returncode
|
|
325
|
+
return stdout_bytes.decode(), stderr_bytes.decode(), proc.returncode or 0
|
spec_runner-2.2.0/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.opencode.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Code Review
|
|
2
|
+
|
|
3
|
+
Task: ${TASK_ID} — ${TASK_NAME}
|
|
4
|
+
|
|
5
|
+
Files changed:
|
|
6
|
+
${CHANGED_FILES}
|
|
7
|
+
|
|
8
|
+
Diff:
|
|
9
|
+
${GIT_DIFF}
|
|
10
|
+
|
|
11
|
+
## Instructions
|
|
12
|
+
|
|
13
|
+
Review the code for:
|
|
14
|
+
1. Bugs and errors
|
|
15
|
+
2. Security issues
|
|
16
|
+
3. Missing error handling
|
|
17
|
+
4. Test coverage
|
|
18
|
+
|
|
19
|
+
## Required Response Format
|
|
20
|
+
|
|
21
|
+
You MUST end your response with exactly one of these status codes on a new line:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
REVIEW_PASSED
|
|
25
|
+
```
|
|
26
|
+
Use this if the code looks good and has no issues.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
REVIEW_FIXED
|
|
30
|
+
```
|
|
31
|
+
Use this if you found and fixed issues.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
REVIEW_FAILED
|
|
35
|
+
```
|
|
36
|
+
Use this if there are issues that need manual attention.
|
|
37
|
+
|
|
38
|
+
Do not add any text after the status code.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# Code Review
|
|
2
|
+
|
|
3
|
+
Task: ${TASK_ID} — ${TASK_NAME}
|
|
4
|
+
|
|
5
|
+
Files changed:
|
|
6
|
+
${CHANGED_FILES}
|
|
7
|
+
|
|
8
|
+
Diff:
|
|
9
|
+
${GIT_DIFF}
|
|
10
|
+
|
|
11
|
+
## Instructions
|
|
12
|
+
|
|
13
|
+
Review the code for:
|
|
14
|
+
1. Bugs and errors
|
|
15
|
+
2. Security issues
|
|
16
|
+
3. Missing error handling
|
|
17
|
+
4. Test coverage
|
|
18
|
+
|
|
19
|
+
## Required Response Format
|
|
20
|
+
|
|
21
|
+
You MUST end your response with exactly one of these status codes on a new line:
|
|
22
|
+
|
|
23
|
+
```
|
|
24
|
+
REVIEW_PASSED
|
|
25
|
+
```
|
|
26
|
+
Use this if the code looks good and has no issues.
|
|
27
|
+
|
|
28
|
+
```
|
|
29
|
+
REVIEW_FIXED
|
|
30
|
+
```
|
|
31
|
+
Use this if you found and fixed issues.
|
|
32
|
+
|
|
33
|
+
```
|
|
34
|
+
REVIEW_FAILED
|
|
35
|
+
```
|
|
36
|
+
Use this if there are issues that need manual attention.
|
|
37
|
+
|
|
38
|
+
Do not add any text after the status code.
|
|
@@ -213,6 +213,7 @@ class ExecutorState:
|
|
|
213
213
|
# Init DB first so tables exist
|
|
214
214
|
self._init_db()
|
|
215
215
|
|
|
216
|
+
assert self._conn is not None
|
|
216
217
|
with self._conn:
|
|
217
218
|
# Migrate tasks and attempts
|
|
218
219
|
for task_id, task_data in data.get("tasks", {}).items():
|
|
@@ -262,6 +263,7 @@ class ExecutorState:
|
|
|
262
263
|
|
|
263
264
|
def _load(self) -> None:
|
|
264
265
|
"""Load state from SQLite into in-memory dicts."""
|
|
266
|
+
assert self._conn is not None
|
|
265
267
|
# Load tasks
|
|
266
268
|
cursor = self._conn.execute("SELECT task_id, status, started_at, completed_at FROM tasks")
|
|
267
269
|
for row in cursor.fetchall():
|
|
@@ -323,6 +325,7 @@ class ExecutorState:
|
|
|
323
325
|
|
|
324
326
|
def _save_meta(self) -> None:
|
|
325
327
|
"""Persist meta counters to SQLite."""
|
|
328
|
+
assert self._conn is not None
|
|
326
329
|
for key, value in [
|
|
327
330
|
("consecutive_failures", str(self.consecutive_failures)),
|
|
328
331
|
("total_completed", str(self.total_completed)),
|
|
@@ -340,6 +343,7 @@ class ExecutorState:
|
|
|
340
343
|
Called by external code (e.g. executor.py) when direct
|
|
341
344
|
mutations are made to in-memory state outside record_attempt/mark_running.
|
|
342
345
|
"""
|
|
346
|
+
assert self._conn is not None
|
|
343
347
|
with self._conn:
|
|
344
348
|
# Upsert all tasks
|
|
345
349
|
for task_id, ts in self.tasks.items():
|
|
@@ -415,6 +419,7 @@ class ExecutorState:
|
|
|
415
419
|
review_findings=review_findings,
|
|
416
420
|
)
|
|
417
421
|
state.attempts.append(attempt)
|
|
422
|
+
assert self._conn is not None
|
|
418
423
|
|
|
419
424
|
if success:
|
|
420
425
|
state.status = "success"
|
|
@@ -519,6 +524,7 @@ class ExecutorState:
|
|
|
519
524
|
state = self.get_task_state(task_id)
|
|
520
525
|
state.status = "running"
|
|
521
526
|
state.started_at = datetime.now().isoformat()
|
|
527
|
+
assert self._conn is not None
|
|
522
528
|
|
|
523
529
|
try:
|
|
524
530
|
with self._conn:
|
|
@@ -10,6 +10,7 @@ import contextlib
|
|
|
10
10
|
import sqlite3
|
|
11
11
|
from datetime import datetime
|
|
12
12
|
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
13
14
|
|
|
14
15
|
from textual.app import App, ComposeResult
|
|
15
16
|
from textual.binding import Binding
|
|
@@ -159,7 +160,7 @@ class StatsBar(Static):
|
|
|
159
160
|
class KanbanColumn(Vertical):
|
|
160
161
|
"""A single column in the Kanban board."""
|
|
161
162
|
|
|
162
|
-
def __init__(self, title: str, **kwargs:
|
|
163
|
+
def __init__(self, title: str, **kwargs: Any) -> None:
|
|
163
164
|
super().__init__(**kwargs)
|
|
164
165
|
self.border_title = title
|
|
165
166
|
|
|
@@ -167,7 +168,7 @@ class KanbanColumn(Vertical):
|
|
|
167
168
|
class LogPanel(Static):
|
|
168
169
|
"""Panel showing execution progress log, tailing a progress file."""
|
|
169
170
|
|
|
170
|
-
def __init__(self, **kwargs:
|
|
171
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
171
172
|
super().__init__(**kwargs)
|
|
172
173
|
self._file_pos: int = 0
|
|
173
174
|
self._lines: list[str] = []
|
|
@@ -553,6 +554,6 @@ class SpecRunnerApp(App[None]):
|
|
|
553
554
|
else:
|
|
554
555
|
log_panel.add_line(f"[bold cyan]While paused: {summary}[/bold cyan]")
|
|
555
556
|
|
|
556
|
-
def action_quit(self) -> None:
|
|
557
|
+
async def action_quit(self) -> None:
|
|
557
558
|
"""Quit the TUI."""
|
|
558
559
|
self.exit()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: spec-runner
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Task automation from markdown specs via Claude CLI
|
|
5
5
|
Author: Andrei
|
|
6
6
|
License-Expression: MIT
|
|
@@ -369,6 +369,8 @@ paths:
|
|
|
369
369
|
|-----|--------------|------------------|
|
|
370
370
|
| Claude | Yes | `{cmd} -p {prompt} --model {model}` |
|
|
371
371
|
| Codex | Yes | `{cmd} -p {prompt} --model {model}` |
|
|
372
|
+
| OpenCode ([sst/opencode](https://opencode.ai)) | Yes | `{cmd} run --model {model} {prompt}` |
|
|
373
|
+
| Pi Agent ([pi.dev](https://pi.dev)) | Yes (basename match) | `{cmd} -p --model {model} {prompt}` |
|
|
372
374
|
| Ollama | Yes | `{cmd} run {model} {prompt}` |
|
|
373
375
|
| llama-cli | Yes | `{cmd} -m {model} -p {prompt} --no-display-prompt` |
|
|
374
376
|
| Custom | Use template | `{cmd} --prompt {prompt}` |
|
|
@@ -54,6 +54,8 @@ src/spec_runner/skills/spec-generator-skill/templates/prompts/review.codex.md
|
|
|
54
54
|
src/spec_runner/skills/spec-generator-skill/templates/prompts/review.llama.md
|
|
55
55
|
src/spec_runner/skills/spec-generator-skill/templates/prompts/review.md
|
|
56
56
|
src/spec_runner/skills/spec-generator-skill/templates/prompts/review.ollama.md
|
|
57
|
+
src/spec_runner/skills/spec-generator-skill/templates/prompts/review.opencode.md
|
|
58
|
+
src/spec_runner/skills/spec-generator-skill/templates/prompts/review.pi.md
|
|
57
59
|
tests/test_audit.py
|
|
58
60
|
tests/test_audit_log.py
|
|
59
61
|
tests/test_config.py
|
|
@@ -177,9 +177,7 @@ class TestUncoveredSpec:
|
|
|
177
177
|
config = _write_specs(tmp_path, tasks, REQS_CLEAN, DESIGN_CLEAN)
|
|
178
178
|
report = audit_all(config)
|
|
179
179
|
|
|
180
|
-
uncovered_designs = [
|
|
181
|
-
f for f in report.findings if f.category == CAT_UNCOVERED_DESIGN
|
|
182
|
-
]
|
|
180
|
+
uncovered_designs = [f for f in report.findings if f.category == CAT_UNCOVERED_DESIGN]
|
|
183
181
|
assert {f.subject for f in uncovered_designs} == {"DESIGN-002"}
|
|
184
182
|
|
|
185
183
|
|
|
@@ -155,9 +155,7 @@ class TestBuildAuditLogger:
|
|
|
155
155
|
assert logger.operator == "maestro"
|
|
156
156
|
|
|
157
157
|
def test_spec_prefix_flows_through(self, tmp_path: Path) -> None:
|
|
158
|
-
config = _make_config(
|
|
159
|
-
tmp_path, audit_log_path="audit.jsonl", spec_prefix="phase5-"
|
|
160
|
-
)
|
|
158
|
+
config = _make_config(tmp_path, audit_log_path="audit.jsonl", spec_prefix="phase5-")
|
|
161
159
|
logger = build_audit_logger(config)
|
|
162
160
|
logger.record(EVENT_RUN_STARTED)
|
|
163
161
|
entry = _read_audit(logger.path)[0]
|
|
@@ -236,9 +234,7 @@ class TestExecutorStateAuditIntegration:
|
|
|
236
234
|
assert failed["details"]["last_error"] == "boom"
|
|
237
235
|
assert failed["details"]["error_code"] == "TASK_FAILED"
|
|
238
236
|
|
|
239
|
-
def test_degraded_mode_emits_state_degraded_event(
|
|
240
|
-
self, tmp_path: Path, monkeypatch
|
|
241
|
-
) -> None:
|
|
237
|
+
def test_degraded_mode_emits_state_degraded_event(self, tmp_path: Path, monkeypatch) -> None:
|
|
242
238
|
import sqlite3
|
|
243
239
|
from unittest.mock import MagicMock
|
|
244
240
|
|
|
@@ -50,6 +50,42 @@ class TestBuildCliCommand:
|
|
|
50
50
|
assert "--model" in result
|
|
51
51
|
assert "gpt-4" in result
|
|
52
52
|
|
|
53
|
+
def test_opencode_auto_detect(self):
|
|
54
|
+
result = build_cli_command("opencode", "hello")
|
|
55
|
+
assert result == ["opencode", "run", "hello"]
|
|
56
|
+
|
|
57
|
+
def test_opencode_with_model(self):
|
|
58
|
+
result = build_cli_command("opencode", "hello", model="anthropic/claude-sonnet-4-6")
|
|
59
|
+
assert result == [
|
|
60
|
+
"opencode",
|
|
61
|
+
"run",
|
|
62
|
+
"--model",
|
|
63
|
+
"anthropic/claude-sonnet-4-6",
|
|
64
|
+
"hello",
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
def test_pi_auto_detect(self):
|
|
68
|
+
result = build_cli_command("pi", "hello")
|
|
69
|
+
assert result == ["pi", "-p", "hello"]
|
|
70
|
+
|
|
71
|
+
def test_pi_with_model(self):
|
|
72
|
+
result = build_cli_command("pi", "hello", model="openai/gpt-4o")
|
|
73
|
+
assert result == ["pi", "-p", "--model", "openai/gpt-4o", "hello"]
|
|
74
|
+
|
|
75
|
+
def test_pi_path_basename_match(self):
|
|
76
|
+
# Absolute path with pi as the basename should still auto-detect.
|
|
77
|
+
result = build_cli_command("/usr/local/bin/pi", "hello")
|
|
78
|
+
assert result == ["/usr/local/bin/pi", "-p", "hello"]
|
|
79
|
+
|
|
80
|
+
def test_pi_no_false_positive_substring(self):
|
|
81
|
+
# "pipe-cli" or anything containing "pi" should NOT be treated as Pi —
|
|
82
|
+
# it must fall through to the Claude default.
|
|
83
|
+
result = build_cli_command("pipe-cli", "hello")
|
|
84
|
+
assert result[0] == "pipe-cli"
|
|
85
|
+
# Claude default uses -p too, but key signal: prompt is the third
|
|
86
|
+
# arg ("-p hello"), not the fourth ("-p" + appended prompt).
|
|
87
|
+
assert result == ["pipe-cli", "-p", "hello"]
|
|
88
|
+
|
|
53
89
|
def test_ollama_auto_detect(self):
|
|
54
90
|
result = build_cli_command("ollama", "hello", model="llama3")
|
|
55
91
|
assert result == ["ollama", "run", "llama3", "hello"]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{spec_runner-2.1.0 → spec_runner-2.2.0}/src/spec_runner/skills/spec-generator-skill/SKILL.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|