subtask-mcp 0.2.6__tar.gz → 0.2.7__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 (66) hide show
  1. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/PKG-INFO +2 -2
  2. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/README.md +1 -1
  3. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/__init__.py +1 -1
  4. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/events.py +23 -5
  5. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask_mcp.egg-info/PKG-INFO +2 -2
  6. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_security.py +16 -0
  7. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/LICENSE +0 -0
  8. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/pyproject.toml +0 -0
  9. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/setup.cfg +0 -0
  10. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/admission.py +0 -0
  11. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/capacity.py +0 -0
  12. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/cli.py +0 -0
  13. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/config.py +0 -0
  14. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/core.py +0 -0
  15. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/doctor.py +0 -0
  16. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/__init__.py +0 -0
  17. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/base.py +0 -0
  18. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/claude_api.py +0 -0
  19. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/codex.py +0 -0
  20. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/demo.py +0 -0
  21. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/dsh.py +0 -0
  22. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/kimi.py +0 -0
  23. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/opencode.py +0 -0
  24. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/engines/registry.py +0 -0
  25. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/evidence.py +0 -0
  26. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/fastserver.py +0 -0
  27. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/gc.py +0 -0
  28. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/__init__.py +0 -0
  29. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/claude_plugin.py +0 -0
  30. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/clients.py +0 -0
  31. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/commands.py +0 -0
  32. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/doctor.py +0 -0
  33. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/fileops.py +0 -0
  34. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/installer.py +0 -0
  35. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/ownership.py +0 -0
  36. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/install/tomledit.py +0 -0
  37. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/jobs.py +0 -0
  38. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/ledger.py +0 -0
  39. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/narrate.py +0 -0
  40. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/policy.py +0 -0
  41. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/procid.py +0 -0
  42. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/progress.py +0 -0
  43. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/redact.py +0 -0
  44. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/roi.py +0 -0
  45. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/router.py +0 -0
  46. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/runners.py +0 -0
  47. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/schemacheck.py +0 -0
  48. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/server.py +0 -0
  49. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/skill/SKILL.md +0 -0
  50. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/telemetry.py +0 -0
  51. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/tools.json +0 -0
  52. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask/worker.py +0 -0
  53. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask_mcp.egg-info/SOURCES.txt +0 -0
  54. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask_mcp.egg-info/dependency_links.txt +0 -0
  55. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask_mcp.egg-info/entry_points.txt +0 -0
  56. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask_mcp.egg-info/requires.txt +0 -0
  57. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/subtask_mcp.egg-info/top_level.txt +0 -0
  58. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_concurrency.py +0 -0
  59. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_engines.py +0 -0
  60. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_falsify.py +0 -0
  61. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_independent_client.py +0 -0
  62. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_install.py +0 -0
  63. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_protocol.py +0 -0
  64. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_routing_results.py +0 -0
  65. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_schema.py +0 -0
  66. {subtask_mcp-0.2.6 → subtask_mcp-0.2.7}/tests/test_subtask.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subtask-mcp
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Background sub-agents for Claude Code, Codex, Kimi and OpenCode, over MCP or CLI, with a spend cap
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/tedin7/subtask
@@ -70,7 +70,7 @@ The distribution is `subtask-mcp` (the PyPI name `subtask` is an unrelated
70
70
  project). Full guide, per client, with native commands: [docs/install.md](docs/install.md).
71
71
 
72
72
  ```sh
73
- uv tool install "git+https://github.com/tedin7/subtask@v0.2.6" # or: uv tool install /path/to/checkout
73
+ uv tool install "git+https://github.com/tedin7/subtask@v0.2.7" # or: uv tool install /path/to/checkout
74
74
  subtask install --client all # prints the plan for every detected client
75
75
  subtask install --client all --yes # applies it: MCP server + skill
76
76
  subtask doctor --client all # checks app, config entry and a real handshake
@@ -39,7 +39,7 @@ The distribution is `subtask-mcp` (the PyPI name `subtask` is an unrelated
39
39
  project). Full guide, per client, with native commands: [docs/install.md](docs/install.md).
40
40
 
41
41
  ```sh
42
- uv tool install "git+https://github.com/tedin7/subtask@v0.2.6" # or: uv tool install /path/to/checkout
42
+ uv tool install "git+https://github.com/tedin7/subtask@v0.2.7" # or: uv tool install /path/to/checkout
43
43
  subtask install --client all # prints the plan for every detected client
44
44
  subtask install --client all --yes # applies it: MCP server + skill
45
45
  subtask doctor --client all # checks app, config entry and a real handshake
@@ -1,3 +1,3 @@
1
1
  """subtask: background sub-agents for coding agents."""
2
2
 
3
- __version__ = "0.2.6"
3
+ __version__ = "0.2.7"
@@ -35,11 +35,24 @@ def emit(
35
35
  state: str | None = None,
36
36
  source: str = "lifecycle",
37
37
  attempt: int | None = None,
38
+ ) -> dict | None:
39
+ """Append one event. Never raises: progress must not be able to fail a job."""
40
+ try:
41
+ return _emit(job_id, phase, msg, state, source, attempt)
42
+ except (OSError, ValueError, TypeError) as exc:
43
+ import sys
44
+
45
+ print(f"subtask: progress event not written: {type(exc).__name__}: {exc}", file=sys.stderr)
46
+ return None
47
+
48
+
49
+ def _emit(
50
+ job_id: str, phase: str, msg: str, state: str | None, source: str, attempt: int | None
38
51
  ) -> dict | None:
39
52
  path = _path(job_id)
40
53
  if not path.parent.is_dir():
41
54
  return None
42
- with path.open("a+", encoding="utf-8") as fh:
55
+ with path.open("ab+") as fh:
43
56
  fcntl.flock(fh, fcntl.LOCK_EX)
44
57
  last = _last(fh)
45
58
  seq = int(last.get("seq", 0)) + 1
@@ -57,16 +70,21 @@ def emit(
57
70
  "source": source,
58
71
  }
59
72
  fh.seek(0, os.SEEK_END)
60
- fh.write(json.dumps(event, ensure_ascii=False) + "\n")
73
+ fh.write((json.dumps(event, ensure_ascii=False) + "\n").encode("utf-8"))
61
74
  fh.flush()
62
75
  return event
63
76
 
64
77
 
65
78
  def _last(fh) -> dict:
79
+ """The last complete event, reading only the tail (bytes: the cut may split a UTF-8 character)."""
66
80
  fh.seek(0, os.SEEK_END)
67
81
  size = fh.tell()
68
82
  fh.seek(max(0, size - 4096))
69
- lines = fh.read().splitlines()
83
+ data = fh.read()
84
+ text = data.decode("utf-8", errors="replace") if isinstance(data, bytes) else data
85
+ lines = text.splitlines()
86
+ if size > 4096 and lines:
87
+ lines = lines[1:] # the first line is cut by the seek: never trust it
70
88
  for line in reversed(lines):
71
89
  try:
72
90
  return json.loads(line)
@@ -99,7 +117,7 @@ def read_since(job_id: str, offset: int = 0, limit: int = 500) -> tuple[list[dic
99
117
  def last(job_id: str) -> dict:
100
118
  path = _path(job_id)
101
119
  try:
102
- with path.open("r", encoding="utf-8") as fh:
120
+ with path.open("rb") as fh:
103
121
  return _last(fh)
104
- except FileNotFoundError:
122
+ except (FileNotFoundError, OSError):
105
123
  return {}
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subtask-mcp
3
- Version: 0.2.6
3
+ Version: 0.2.7
4
4
  Summary: Background sub-agents for Claude Code, Codex, Kimi and OpenCode, over MCP or CLI, with a spend cap
5
5
  License: MIT
6
6
  Project-URL: Homepage, https://github.com/tedin7/subtask
@@ -70,7 +70,7 @@ The distribution is `subtask-mcp` (the PyPI name `subtask` is an unrelated
70
70
  project). Full guide, per client, with native commands: [docs/install.md](docs/install.md).
71
71
 
72
72
  ```sh
73
- uv tool install "git+https://github.com/tedin7/subtask@v0.2.6" # or: uv tool install /path/to/checkout
73
+ uv tool install "git+https://github.com/tedin7/subtask@v0.2.7" # or: uv tool install /path/to/checkout
74
74
  subtask install --client all # prints the plan for every detected client
75
75
  subtask install --client all --yes # applies it: MCP server + skill
76
76
  subtask doctor --client all # checks app, config entry and a real handshake
@@ -180,3 +180,19 @@ def test_isolated_is_refused_when_bwrap_cannot_run(home, monkeypatch):
180
180
  monkeypatch.setattr(runners, "bwrap_available", lambda: False)
181
181
  out = core.start("x", engine="kimi", cwd=str(home), policy="isolated")
182
182
  assert "bubblewrap" in out and "started" not in out
183
+
184
+
185
+ def test_progress_log_survives_multibyte_text_across_the_tail_boundary(home):
186
+ """Reading the last event seeks back 4096 bytes: that may land inside a UTF-8 character."""
187
+ import json
188
+
189
+ from subtask import events, jobs
190
+
191
+ folder = jobs.job_dir("st-utf8utf800")
192
+ folder.mkdir(parents=True)
193
+ line = json.dumps({"seq": 1, "msg": "…" * 50}, ensure_ascii=False) + "\n" # "…" is 3 bytes
194
+ for pad in range(3): # one of the three alignments puts byte -4096 inside a "…"
195
+ body = ("x" * pad + "\n" + line * 40).encode()
196
+ (folder / "progress.jsonl").write_bytes(body)
197
+ assert events.emit("st-utf8utf800", "tool", "dopo", source="adapter") is not None, pad
198
+ assert events.last("st-utf8utf800")["msg"] == "dopo"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes