spec-runner 2.2.1__tar.gz → 2.2.2__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.2.1/src/spec_runner.egg-info → spec_runner-2.2.2}/PKG-INFO +1 -1
- {spec_runner-2.2.1 → spec_runner-2.2.2}/pyproject.toml +1 -1
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/logging.py +9 -4
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/obs.py +41 -6
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/task.py +1 -1
- {spec_runner-2.2.1 → spec_runner-2.2.2/src/spec_runner.egg-info}/PKG-INFO +1 -1
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner.egg-info/SOURCES.txt +1 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_logging.py +25 -0
- spec_runner-2.2.2/tests/test_task.py +29 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/LICENSE +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/README.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/setup.cfg +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/__init__.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/audit.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/audit_log.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/cli.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/cli_info.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/cli_plan.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/config.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/events.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/execution.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/executor.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/git_ops.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/github_sync.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/hooks.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/init_cmd.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/mcp_server.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/notifications.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/plugins.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/prompt.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/py.typed +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/report.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/review.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/runner.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/SKILL.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/Makefile.template +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/design.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/executor.config.yaml +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/executor.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/phase-design.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/phase-requirements.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/phase-tasks.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.claude.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.codex.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.llama.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.ollama.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.opencode.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.pi.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/requirements.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/task.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/tasks.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/workflow.template.md +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/state.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/task_commands.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/tui.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/validate.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner/verify.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner.egg-info/dependency_links.txt +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner.egg-info/entry_points.txt +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner.egg-info/requires.txt +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/src/spec_runner.egg-info/top_level.txt +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_audit.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_audit_log.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_config.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_costs.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_e2e.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_events.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_execution.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_gh_sync.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_hooks.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_json_result_contract.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_mcp.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_notifications.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_obs.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_obs_contract.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_plan_full.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_plugins.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_prompt.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_report.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_runner.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_spec_prefix.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_state.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_task_diff.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_tui.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_validate.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_verify.py +0 -0
- {spec_runner-2.2.1 → spec_runner-2.2.2}/tests/test_watch.py +0 -0
|
@@ -28,13 +28,18 @@ def redact_sensitive(logger: object, method_name: object, event_dict: dict) -> d
|
|
|
28
28
|
|
|
29
29
|
def setup_logging(
|
|
30
30
|
level: str = "info",
|
|
31
|
-
json_output: bool = True, # ignored — obs always emits JSON
|
|
31
|
+
json_output: bool = True, # ignored — obs always emits JSON to the file sink
|
|
32
32
|
log_file: Path | None = None,
|
|
33
|
-
tui_mode: bool = False, #
|
|
33
|
+
tui_mode: bool = False, # True → no console sink (the TUI owns the screen)
|
|
34
34
|
) -> None:
|
|
35
|
-
"""Delegate to obs.init_logging; preserved signature for back-compat.
|
|
35
|
+
"""Delegate to obs.init_logging; preserved signature for back-compat.
|
|
36
|
+
|
|
37
|
+
Outside TUI mode a compact progress sink is mirrored to stderr so plain
|
|
38
|
+
``run``/``watch`` invocations aren't silent; the JSON file sink is always
|
|
39
|
+
written. In TUI mode stderr stays free so it doesn't corrupt the display.
|
|
40
|
+
"""
|
|
36
41
|
log_dir = log_file.parent if log_file else None
|
|
37
|
-
obs.init_logging("spec-runner", level=level, log_dir=log_dir)
|
|
42
|
+
obs.init_logging("spec-runner", level=level, log_dir=log_dir, console=not tui_mode)
|
|
38
43
|
|
|
39
44
|
|
|
40
45
|
def get_logger(module: str) -> structlog.BoundLogger:
|
|
@@ -12,6 +12,7 @@ import logging as _stdlib_logging
|
|
|
12
12
|
import os
|
|
13
13
|
import re
|
|
14
14
|
import secrets
|
|
15
|
+
import sys
|
|
15
16
|
import time
|
|
16
17
|
from collections.abc import Iterator
|
|
17
18
|
from contextlib import contextmanager
|
|
@@ -128,6 +129,33 @@ def _reshape_to_otel(project: str):
|
|
|
128
129
|
return processor
|
|
129
130
|
|
|
130
131
|
|
|
132
|
+
def _console_progress():
|
|
133
|
+
"""Side-effect processor: emit a compact human line to the current stderr.
|
|
134
|
+
|
|
135
|
+
Resolves ``sys.stderr`` at call time (not at bind time) so it never writes
|
|
136
|
+
to a stream that was swapped out or closed — e.g. under pytest capture, or
|
|
137
|
+
if the host reassigns stderr mid-run. Returns ``event_dict`` unchanged so
|
|
138
|
+
the JSON file sink still receives the full OTel record; the console copy is
|
|
139
|
+
trimmed of trace/transport plumbing (``pipeline_id``, span/trace ids). Must
|
|
140
|
+
run after ``_redact`` (secrets masked) and before ``_reshape_to_otel``.
|
|
141
|
+
"""
|
|
142
|
+
renderer = structlog.dev.ConsoleRenderer(colors=sys.stderr.isatty())
|
|
143
|
+
|
|
144
|
+
def processor(logger, method_name, event_dict):
|
|
145
|
+
line = {
|
|
146
|
+
k: v
|
|
147
|
+
for k, v in event_dict.items()
|
|
148
|
+
if not k.startswith("_") and k not in ("pipeline_id", "parent_span_id")
|
|
149
|
+
}
|
|
150
|
+
line["level"] = method_name
|
|
151
|
+
line.setdefault("timestamp", datetime.now().strftime("%H:%M:%S"))
|
|
152
|
+
sys.stderr.write(renderer(logger, method_name, line) + "\n")
|
|
153
|
+
sys.stderr.flush()
|
|
154
|
+
return event_dict
|
|
155
|
+
|
|
156
|
+
return processor
|
|
157
|
+
|
|
158
|
+
|
|
131
159
|
def _default_log_dir() -> Path:
|
|
132
160
|
env_dir = os.environ.get("ORCHESTRA_LOG_DIR")
|
|
133
161
|
if env_dir:
|
|
@@ -142,6 +170,7 @@ def init_logging(
|
|
|
142
170
|
level: str | None = None,
|
|
143
171
|
log_dir: Path | None = None,
|
|
144
172
|
redact_keys: list[str] | None = None,
|
|
173
|
+
console: bool = False,
|
|
145
174
|
) -> None:
|
|
146
175
|
global _initialized
|
|
147
176
|
_initialized = False
|
|
@@ -176,13 +205,19 @@ def init_logging(
|
|
|
176
205
|
level_name = (level or os.environ.get("ORCHESTRA_LOG_LEVEL") or "info").lower()
|
|
177
206
|
min_level = {"debug": 10, "info": 20, "warning": 30, "error": 40}.get(level_name, 20)
|
|
178
207
|
|
|
208
|
+
processors: list[Any] = [
|
|
209
|
+
structlog.contextvars.merge_contextvars,
|
|
210
|
+
_redact(all_redact),
|
|
211
|
+
]
|
|
212
|
+
if console:
|
|
213
|
+
processors.append(_console_progress())
|
|
214
|
+
processors += [
|
|
215
|
+
_reshape_to_otel(project),
|
|
216
|
+
structlog.processors.JSONRenderer(sort_keys=False),
|
|
217
|
+
]
|
|
218
|
+
|
|
179
219
|
structlog.configure(
|
|
180
|
-
processors=
|
|
181
|
-
structlog.contextvars.merge_contextvars,
|
|
182
|
-
_redact(all_redact),
|
|
183
|
-
_reshape_to_otel(project),
|
|
184
|
-
structlog.processors.JSONRenderer(sort_keys=False),
|
|
185
|
-
],
|
|
220
|
+
processors=processors,
|
|
186
221
|
wrapper_class=structlog.make_filtering_bound_logger(min_level),
|
|
187
222
|
logger_factory=structlog.WriteLoggerFactory(file=output_path.open("a")),
|
|
188
223
|
cache_logger_on_first_use=True,
|
|
@@ -18,7 +18,7 @@ CHECKLIST_ITEM = re.compile(r"^- \[([ x])\] (.+)$")
|
|
|
18
18
|
TRACES_TO = re.compile(r"\*\*Traces to:\*\* (.+)")
|
|
19
19
|
DEPENDS_ON = re.compile(r"\*\*Depends on:\*\* (.+)")
|
|
20
20
|
BLOCKS = re.compile(r"\*\*Blocks:\*\* (.+)")
|
|
21
|
-
ESTIMATE = re.compile(r"Est: (\d+(
|
|
21
|
+
ESTIMATE = re.compile(r"Est: (\d+(?:\.\d+)?(?:[-–]\d+(?:\.\d+)?)?[dh])")
|
|
22
22
|
|
|
23
23
|
STATUS_EMOJI = {"todo": "⬜", "in_progress": "🔄", "done": "✅", "blocked": "⏸️"}
|
|
24
24
|
|
|
@@ -21,6 +21,31 @@ class TestSetupLogging:
|
|
|
21
21
|
setup_logging(tui_mode=True, log_file=log_file)
|
|
22
22
|
|
|
23
23
|
|
|
24
|
+
class TestConsoleProgress:
|
|
25
|
+
"""Console progress sink (non-TUI mode mirrors a compact line to stderr)."""
|
|
26
|
+
|
|
27
|
+
def test_non_tui_mirrors_progress_to_stderr(self, tmp_path, capsys):
|
|
28
|
+
setup_logging(tui_mode=False, log_file=tmp_path / "run.log")
|
|
29
|
+
get_logger("console_on_test").info("ping_console", task_id="TASK-099")
|
|
30
|
+
err = capsys.readouterr().err
|
|
31
|
+
assert "ping_console" in err
|
|
32
|
+
assert "TASK-099" in err
|
|
33
|
+
|
|
34
|
+
def test_tui_mode_keeps_stderr_quiet(self, tmp_path, capsys):
|
|
35
|
+
setup_logging(tui_mode=True, log_file=tmp_path / "tui.log")
|
|
36
|
+
get_logger("console_off_test").info("quiet_console", task_id="TASK-100")
|
|
37
|
+
err = capsys.readouterr().err
|
|
38
|
+
assert "quiet_console" not in err
|
|
39
|
+
|
|
40
|
+
def test_console_line_omits_trace_plumbing(self, tmp_path, capsys):
|
|
41
|
+
setup_logging(tui_mode=False, log_file=tmp_path / "run.log")
|
|
42
|
+
get_logger("console_trace_test").info("trace_check")
|
|
43
|
+
err = capsys.readouterr().err
|
|
44
|
+
assert "trace_check" in err
|
|
45
|
+
assert "pipeline_id" not in err
|
|
46
|
+
assert "_trace_id" not in err
|
|
47
|
+
|
|
48
|
+
|
|
24
49
|
class TestGetLogger:
|
|
25
50
|
"""Tests for get_logger."""
|
|
26
51
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Regression tests for task parsing (spec/tasks.md)."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
from spec_runner.task import parse_tasks
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _single_task(tmp_path: Path, estimate_line: str) -> str:
|
|
9
|
+
"""Parse a one-task file and return the parsed estimate."""
|
|
10
|
+
f = tmp_path / "tasks.md"
|
|
11
|
+
f.write_text(f"### TASK-001: Bootstrap\n{estimate_line}\n")
|
|
12
|
+
(task,) = parse_tasks(f)
|
|
13
|
+
return task.estimate
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TestEstimateParsing:
|
|
17
|
+
def test_integer_estimate_parsed(self, tmp_path: Path) -> None:
|
|
18
|
+
assert _single_task(tmp_path, "P0 | todo | Est: 2d") == "2d"
|
|
19
|
+
|
|
20
|
+
def test_decimal_estimate_parsed(self, tmp_path: Path) -> None:
|
|
21
|
+
"""Decimal estimates (e.g. 1.5d) must parse, not read as missing."""
|
|
22
|
+
assert _single_task(tmp_path, "P0 | todo | Est: 1.5d") == "1.5d"
|
|
23
|
+
|
|
24
|
+
def test_endash_range_parsed(self, tmp_path: Path) -> None:
|
|
25
|
+
"""En-dash ranges (1–1.5d, U+2013) must parse, not read as missing."""
|
|
26
|
+
assert _single_task(tmp_path, "P0 | todo | Est: 1–1.5d") == "1–1.5d"
|
|
27
|
+
|
|
28
|
+
def test_ascii_hyphen_range_still_parsed(self, tmp_path: Path) -> None:
|
|
29
|
+
assert _single_task(tmp_path, "P0 | todo | Est: 1-2d") == "1-2d"
|
|
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
|
{spec_runner-2.2.1 → spec_runner-2.2.2}/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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|