subtask-mcp 0.2.9__tar.gz → 0.2.10__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 (69) hide show
  1. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/PKG-INFO +2 -2
  2. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/README.md +1 -1
  3. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/__init__.py +1 -1
  4. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/stephook.py +31 -27
  5. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask_mcp.egg-info/PKG-INFO +2 -2
  6. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_stephook.py +24 -0
  7. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/LICENSE +0 -0
  8. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/pyproject.toml +0 -0
  9. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/setup.cfg +0 -0
  10. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/admission.py +0 -0
  11. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/capacity.py +0 -0
  12. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/cli.py +0 -0
  13. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/config.py +0 -0
  14. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/core.py +0 -0
  15. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/doctor.py +0 -0
  16. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/__init__.py +0 -0
  17. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/base.py +0 -0
  18. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/claude_api.py +0 -0
  19. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/codex.py +0 -0
  20. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/demo.py +0 -0
  21. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/dsh.py +0 -0
  22. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/kimi.py +0 -0
  23. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/opencode.py +0 -0
  24. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/engines/registry.py +0 -0
  25. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/events.py +0 -0
  26. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/evidence.py +0 -0
  27. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/fastserver.py +0 -0
  28. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/gc.py +0 -0
  29. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/__init__.py +0 -0
  30. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/claude_plugin.py +0 -0
  31. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/clients.py +0 -0
  32. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/commands.py +0 -0
  33. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/doctor.py +0 -0
  34. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/fileops.py +0 -0
  35. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/installer.py +0 -0
  36. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/ownership.py +0 -0
  37. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/install/tomledit.py +0 -0
  38. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/jobs.py +0 -0
  39. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/ledger.py +0 -0
  40. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/narrate.py +0 -0
  41. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/policy.py +0 -0
  42. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/procid.py +0 -0
  43. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/progress.py +0 -0
  44. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/redact.py +0 -0
  45. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/roi.py +0 -0
  46. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/router.py +0 -0
  47. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/runners.py +0 -0
  48. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/schemacheck.py +0 -0
  49. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/server.py +0 -0
  50. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/skill/SKILL.md +0 -0
  51. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/telemetry.py +0 -0
  52. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/tools.json +0 -0
  53. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask/worker.py +0 -0
  54. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask_mcp.egg-info/SOURCES.txt +0 -0
  55. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask_mcp.egg-info/dependency_links.txt +0 -0
  56. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask_mcp.egg-info/entry_points.txt +0 -0
  57. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask_mcp.egg-info/requires.txt +0 -0
  58. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/subtask_mcp.egg-info/top_level.txt +0 -0
  59. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_concurrency.py +0 -0
  60. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_engines.py +0 -0
  61. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_falsify.py +0 -0
  62. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_independent_client.py +0 -0
  63. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_install.py +0 -0
  64. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_live.py +0 -0
  65. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_protocol.py +0 -0
  66. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_routing_results.py +0 -0
  67. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_schema.py +0 -0
  68. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/tests/test_security.py +0 -0
  69. {subtask_mcp-0.2.9 → subtask_mcp-0.2.10}/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.9
3
+ Version: 0.2.10
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.9" # or: uv tool install /path/to/checkout
73
+ uv tool install "git+https://github.com/tedin7/subtask@v0.2.10" # 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.9" # or: uv tool install /path/to/checkout
42
+ uv tool install "git+https://github.com/tedin7/subtask@v0.2.10" # 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.9"
3
+ __version__ = "0.2.10"
@@ -43,36 +43,40 @@ def _text(content: object) -> str:
43
43
  return ""
44
44
 
45
45
 
46
- def read_transcript(path: str | None, max_bytes: int = 400_000) -> tuple[str, list[str]]:
47
- """(last user request, recent agent actions) from a Claude Code session transcript."""
46
+ def read_transcript(path: str | None) -> tuple[str, list[str]]:
47
+ """(last user request, recent agent actions) from a Claude Code session transcript.
48
+
49
+ The whole file is streamed line by line: the request can sit before megabytes of tool output.
50
+ Lines that are not user prompts or tool calls are skipped without being parsed.
51
+ """
48
52
  if not path or not Path(path).is_file():
49
53
  return "", []
50
- with open(path, "rb") as fh:
51
- fh.seek(0, 2)
52
- fh.seek(max(0, fh.tell() - max_bytes))
53
- lines = fh.read().decode("utf-8", errors="replace").splitlines()
54
54
  request, actions = "", []
55
- for line in lines:
56
- try:
57
- row = json.loads(line)
58
- except json.JSONDecodeError:
59
- continue
60
- msg = row.get("message") if isinstance(row, dict) else None
61
- if not isinstance(msg, dict):
62
- continue
63
- content = msg.get("content")
64
- if row.get("type") == "user":
65
- is_tool_result = isinstance(content, list) and any(
66
- isinstance(b, dict) and b.get("type") == "tool_result" for b in content
67
- )
68
- text = _text(content).strip()
69
- if text and not is_tool_result and not text.startswith("<"):
70
- request, actions = text, [] # a new request starts a new chain of actions
71
- elif row.get("type") == "assistant" and isinstance(content, list):
72
- for b in content:
73
- if isinstance(b, dict) and b.get("type") == "tool_use":
74
- actions.append(_action(b.get("name", ""), b.get("input") or {}))
75
- return request[-2000:], [a for a in actions if a][-12:]
55
+ with open(path, "rb") as fh:
56
+ for raw in fh:
57
+ if b'"tool_use"' not in raw and b'"type":"user"' not in raw and b'"type": "user"' not in raw:
58
+ continue
59
+ try:
60
+ row = json.loads(raw)
61
+ except (json.JSONDecodeError, UnicodeDecodeError):
62
+ continue
63
+ msg = row.get("message") if isinstance(row, dict) else None
64
+ if not isinstance(msg, dict):
65
+ continue
66
+ content = msg.get("content")
67
+ if row.get("type") == "user":
68
+ is_tool_result = isinstance(content, list) and any(
69
+ isinstance(b, dict) and b.get("type") == "tool_result" for b in content
70
+ )
71
+ text = _text(content).strip()
72
+ if text and not is_tool_result and not text.startswith("<"):
73
+ request, actions = text, [] # a new request starts a new chain of actions
74
+ elif row.get("type") == "assistant" and isinstance(content, list):
75
+ for b in content:
76
+ if isinstance(b, dict) and b.get("type") == "tool_use":
77
+ actions.append(_action(b.get("name", ""), b.get("input") or {}))
78
+ actions = actions[-12:]
79
+ return request[-2000:], [a for a in actions if a]
76
80
 
77
81
 
78
82
  def _action(name: str, args: dict) -> str:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: subtask-mcp
3
- Version: 0.2.9
3
+ Version: 0.2.10
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.9" # or: uv tool install /path/to/checkout
73
+ uv tool install "git+https://github.com/tedin7/subtask@v0.2.10" # 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
@@ -87,3 +87,27 @@ def test_cli_hook_exits_zero_and_prints_json_only(home, tmp_path):
87
87
  bad = subprocess.run([sys.executable, "-m", "subtask.cli", "narrate-hook"], input="not json",
88
88
  capture_output=True, text=True, timeout=30, check=False) # fmt: skip
89
89
  assert bad.returncode == 0
90
+
91
+
92
+ def test_request_far_back_in_a_large_transcript_is_found(tmp_path):
93
+ """The request can sit before megabytes of tool output: the whole file is read, line by line."""
94
+ from subtask import stephook
95
+
96
+ rows = [{"type": "user", "message": {"role": "user", "content": "ripara i 10 errori"}}]
97
+ big = "x" * 50_000
98
+ rows += [
99
+ {"type": "user", "message": {"role": "user", "content": [{"type": "tool_result", "content": big}]}}
100
+ ] * 40
101
+ rows += [
102
+ {
103
+ "type": "assistant",
104
+ "message": {
105
+ "content": [{"type": "tool_use", "name": "Bash", "input": {"command": "git commit -m x"}}]
106
+ },
107
+ }
108
+ ]
109
+ path = tmp_path / "big.jsonl"
110
+ path.write_text("\n".join(json.dumps(r) for r in rows))
111
+ assert path.stat().st_size > 1_000_000
112
+ request, actions = stephook.read_transcript(str(path))
113
+ assert request == "ripara i 10 errori" and actions == ["Bash git commit -m x"]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes