spec-runner 2.2.0__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.
Files changed (88) hide show
  1. {spec_runner-2.2.0/src/spec_runner.egg-info → spec_runner-2.2.2}/PKG-INFO +1 -1
  2. {spec_runner-2.2.0 → spec_runner-2.2.2}/pyproject.toml +1 -1
  3. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/logging.py +9 -4
  4. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/obs.py +41 -6
  5. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/task.py +1 -1
  6. {spec_runner-2.2.0 → spec_runner-2.2.2/src/spec_runner.egg-info}/PKG-INFO +1 -1
  7. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner.egg-info/SOURCES.txt +1 -0
  8. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_logging.py +25 -0
  9. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_obs_contract.py +8 -1
  10. spec_runner-2.2.2/tests/test_task.py +29 -0
  11. {spec_runner-2.2.0 → spec_runner-2.2.2}/LICENSE +0 -0
  12. {spec_runner-2.2.0 → spec_runner-2.2.2}/README.md +0 -0
  13. {spec_runner-2.2.0 → spec_runner-2.2.2}/setup.cfg +0 -0
  14. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/__init__.py +0 -0
  15. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/audit.py +0 -0
  16. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/audit_log.py +0 -0
  17. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/cli.py +0 -0
  18. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/cli_info.py +0 -0
  19. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/cli_plan.py +0 -0
  20. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/config.py +0 -0
  21. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/events.py +0 -0
  22. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/execution.py +0 -0
  23. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/executor.py +0 -0
  24. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/git_ops.py +0 -0
  25. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/github_sync.py +0 -0
  26. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/hooks.py +0 -0
  27. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/init_cmd.py +0 -0
  28. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/mcp_server.py +0 -0
  29. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/notifications.py +0 -0
  30. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/plugins.py +0 -0
  31. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/prompt.py +0 -0
  32. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/py.typed +0 -0
  33. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/report.py +0 -0
  34. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/review.py +0 -0
  35. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/runner.py +0 -0
  36. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/SKILL.md +0 -0
  37. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/Makefile.template +0 -0
  38. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/design.template.md +0 -0
  39. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/executor.config.yaml +0 -0
  40. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/executor.py +0 -0
  41. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/phase-design.template.md +0 -0
  42. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/phase-requirements.template.md +0 -0
  43. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/phase-tasks.template.md +0 -0
  44. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.claude.md +0 -0
  45. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.codex.md +0 -0
  46. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.llama.md +0 -0
  47. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.md +0 -0
  48. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.ollama.md +0 -0
  49. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.opencode.md +0 -0
  50. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/prompts/review.pi.md +0 -0
  51. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/requirements.template.md +0 -0
  52. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/task.py +0 -0
  53. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/tasks.template.md +0 -0
  54. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/skills/spec-generator-skill/templates/workflow.template.md +0 -0
  55. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/state.py +0 -0
  56. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/task_commands.py +0 -0
  57. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/tui.py +0 -0
  58. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/validate.py +0 -0
  59. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner/verify.py +0 -0
  60. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner.egg-info/dependency_links.txt +0 -0
  61. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner.egg-info/entry_points.txt +0 -0
  62. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner.egg-info/requires.txt +0 -0
  63. {spec_runner-2.2.0 → spec_runner-2.2.2}/src/spec_runner.egg-info/top_level.txt +0 -0
  64. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_audit.py +0 -0
  65. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_audit_log.py +0 -0
  66. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_config.py +0 -0
  67. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_costs.py +0 -0
  68. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_e2e.py +0 -0
  69. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_events.py +0 -0
  70. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_execution.py +0 -0
  71. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_gh_sync.py +0 -0
  72. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_hooks.py +0 -0
  73. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_json_result_contract.py +0 -0
  74. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_mcp.py +0 -0
  75. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_notifications.py +0 -0
  76. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_obs.py +0 -0
  77. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_plan_full.py +0 -0
  78. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_plugins.py +0 -0
  79. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_prompt.py +0 -0
  80. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_report.py +0 -0
  81. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_runner.py +0 -0
  82. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_spec_prefix.py +0 -0
  83. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_state.py +0 -0
  84. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_task_diff.py +0 -0
  85. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_tui.py +0 -0
  86. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_validate.py +0 -0
  87. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_verify.py +0 -0
  88. {spec_runner-2.2.0 → spec_runner-2.2.2}/tests/test_watch.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spec-runner
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Task automation from markdown specs via Claude CLI
5
5
  Author: Andrei
6
6
  License-Expression: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "spec-runner"
7
- version = "2.2.0"
7
+ version = "2.2.2"
8
8
  description = "Task automation from markdown specs via Claude CLI"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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, # ignored obs writes to file; stdout stays free
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+(?:-\d+)?[dh])")
21
+ ESTIMATE = re.compile(r"Est: (\d+(?:\.\d+)?(?:[-–]\d+(?:\.\d+)?)?[dh])")
22
22
 
23
23
  STATUS_EMOJI = {"todo": "⬜", "in_progress": "🔄", "done": "✅", "blocked": "⏸️"}
24
24
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: spec-runner
3
- Version: 2.2.0
3
+ Version: 2.2.2
4
4
  Summary: Task automation from markdown specs via Claude CLI
5
5
  Author: Andrei
6
6
  License-Expression: MIT
@@ -78,6 +78,7 @@ tests/test_report.py
78
78
  tests/test_runner.py
79
79
  tests/test_spec_prefix.py
80
80
  tests/test_state.py
81
+ tests/test_task.py
81
82
  tests/test_task_diff.py
82
83
  tests/test_tui.py
83
84
  tests/test_validate.py
@@ -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
 
@@ -12,7 +12,14 @@ import pytest
12
12
  _SPEC_RUNNER_ROOT = Path(__file__).resolve().parents[1]
13
13
  _UMBRELLA = _SPEC_RUNNER_ROOT.parent # all_ai_orchestrators/
14
14
  _CONTRACT = _UMBRELLA / "Maestro" / "_cowork_output" / "observability-contract"
15
- _SCHEMA = json.loads((_CONTRACT / "log-schema.json").read_text())
15
+ _SCHEMA_PATH = _CONTRACT / "log-schema.json"
16
+ if not _SCHEMA_PATH.exists():
17
+ pytest.skip(
18
+ "observability-contract unavailable (external cowork workspace, "
19
+ "not checked out in standalone CI)",
20
+ allow_module_level=True,
21
+ )
22
+ _SCHEMA = json.loads(_SCHEMA_PATH.read_text())
16
23
 
17
24
 
18
25
  @pytest.fixture
@@ -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