clearbox 0.2.1__tar.gz → 0.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 (36) hide show
  1. {clearbox-0.2.1 → clearbox-0.2.2}/PKG-INFO +23 -4
  2. {clearbox-0.2.1 → clearbox-0.2.2}/README.md +22 -3
  3. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/__main__.py +32 -3
  4. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/mcp_tools.py +13 -5
  5. {clearbox-0.2.1 → clearbox-0.2.2}/examples/demo_plan.py +8 -3
  6. {clearbox-0.2.1 → clearbox-0.2.2}/examples/file_conflict_plan.py +3 -0
  7. {clearbox-0.2.1 → clearbox-0.2.2}/pyproject.toml +1 -1
  8. {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_agents_api.py +7 -2
  9. {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_file_conflicts.py +3 -1
  10. {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_flow.py +11 -5
  11. {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_mixed_run.py +7 -2
  12. {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_rework.py +7 -2
  13. {clearbox-0.2.1 → clearbox-0.2.2}/.gitignore +0 -0
  14. {clearbox-0.2.1 → clearbox-0.2.2}/LICENSE +0 -0
  15. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/__init__.py +0 -0
  16. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/agents.py +0 -0
  17. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/app.py +0 -0
  18. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/bootstrap.py +0 -0
  19. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/engine.py +0 -0
  20. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/__init__.py +0 -0
  21. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/base.py +0 -0
  22. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/claude_sdk.py +0 -0
  23. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/mock.py +0 -0
  24. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/openai_check.py +0 -0
  25. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/openai_compat.py +0 -0
  26. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/probe.py +0 -0
  27. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/registry.py +0 -0
  28. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/tools.py +0 -0
  29. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/mcp_bridge.py +0 -0
  30. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/static/app.js +0 -0
  31. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/static/index.html +0 -0
  32. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/static/style.css +0 -0
  33. {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/store.py +0 -0
  34. {clearbox-0.2.1 → clearbox-0.2.2}/examples/mixed_plan.py +0 -0
  35. {clearbox-0.2.1 → clearbox-0.2.2}/tests/fake_openai.py +0 -0
  36. {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_openai_harness.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: clearbox
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Observable, reviewable agent work: DAG plans over MCP, executed node by node and watched live
5
5
  Project-URL: Homepage, https://github.com/KaiHabermann/Clearbox
6
6
  Project-URL: Repository, https://github.com/KaiHabermann/Clearbox
@@ -576,7 +576,7 @@ clearbox --db $HOME\.clearbox\vc.db --workdir $HOME\.clearbox\workspace
576
576
  ```
577
577
 
578
578
  The repo contains a `.mcp.json`, so Claude Code started in this folder offers the `clearbox`
579
- server automatically. Create plans with `working_directory` set to the repo path so built-in agents
579
+ server automatically. Plans take `working_directory`, so point it at the repo path and built-in agents
580
580
  work on the code. To pick up the agents' changes, stop the service and run
581
581
  `uv tool install --force .` again.
582
582
 
@@ -595,8 +595,8 @@ and every later plan can use it. Two things worth knowing when a plan builds Cle
595
595
  | Flag | Env | Default |
596
596
  | --- | --- | --- |
597
597
  | `--host`, `--port` | `CLEARBOX_HOST`, `CLEARBOX_PORT` | `127.0.0.1`, `8765` |
598
- | `--db` | `CLEARBOX_DB` | `clearbox.db` |
599
- | `--workdir` | `CLEARBOX_WORKDIR` | `workspace` |
598
+ | `--db` | `CLEARBOX_DB` | `<data dir>/clearbox.db` |
599
+ | `--workdir` | `CLEARBOX_WORKDIR` | `<data dir>/workspace` |
600
600
  | `--max-concurrency` | `CLEARBOX_MAX_CONCURRENCY` | `3` |
601
601
  | `--permission-mode` | `CLEARBOX_PERMISSION_MODE` | `acceptEdits` |
602
602
  | `--model` | `CLEARBOX_MODEL` | SDK default |
@@ -653,6 +653,25 @@ tests/test_mixed_run.py per-node agents and max_parallel in a real run
653
653
  tests/test_openai_harness.py the agent loop against tests/fake_openai.py (no network, no DB)
654
654
  ```
655
655
 
656
+ ## Where Clearbox keeps its data
657
+
658
+ The database and the fallback workspace live in the platform's data directory:
659
+
660
+ | OS | path |
661
+ | --- | --- |
662
+ | Windows | `%LOCALAPPDATA%\clearbox\` |
663
+ | macOS | `~/Library/Application Support/clearbox/` |
664
+ | Linux | `$XDG_DATA_HOME/clearbox/`, else `~/.local/share/clearbox/` |
665
+
666
+ `--db` / `CLEARBOX_DB` and `--workdir` / `CLEARBOX_WORKDIR` override both. Before 0.2.2 these
667
+ defaulted to `clearbox.db` and `workspace/` relative to the current directory, so launching from
668
+ two folders gave you two unrelated databases; a `clearbox.db` already sitting in the directory you
669
+ start from is still used, and the banner says so.
670
+
671
+ Every top-level plan must name its own `working_directory` -- the directory its agents work in,
672
+ normally a repository. Subplans inherit it. Two plans with overlapping directories cannot run at
673
+ the same time, since file-conflict locking only orders nodes within one plan tree.
674
+
656
675
  ## Releasing
657
676
 
658
677
  The git tag is the trigger and the single source of truth. Bump the version, tag it, push:
@@ -545,7 +545,7 @@ clearbox --db $HOME\.clearbox\vc.db --workdir $HOME\.clearbox\workspace
545
545
  ```
546
546
 
547
547
  The repo contains a `.mcp.json`, so Claude Code started in this folder offers the `clearbox`
548
- server automatically. Create plans with `working_directory` set to the repo path so built-in agents
548
+ server automatically. Plans take `working_directory`, so point it at the repo path and built-in agents
549
549
  work on the code. To pick up the agents' changes, stop the service and run
550
550
  `uv tool install --force .` again.
551
551
 
@@ -564,8 +564,8 @@ and every later plan can use it. Two things worth knowing when a plan builds Cle
564
564
  | Flag | Env | Default |
565
565
  | --- | --- | --- |
566
566
  | `--host`, `--port` | `CLEARBOX_HOST`, `CLEARBOX_PORT` | `127.0.0.1`, `8765` |
567
- | `--db` | `CLEARBOX_DB` | `clearbox.db` |
568
- | `--workdir` | `CLEARBOX_WORKDIR` | `workspace` |
567
+ | `--db` | `CLEARBOX_DB` | `<data dir>/clearbox.db` |
568
+ | `--workdir` | `CLEARBOX_WORKDIR` | `<data dir>/workspace` |
569
569
  | `--max-concurrency` | `CLEARBOX_MAX_CONCURRENCY` | `3` |
570
570
  | `--permission-mode` | `CLEARBOX_PERMISSION_MODE` | `acceptEdits` |
571
571
  | `--model` | `CLEARBOX_MODEL` | SDK default |
@@ -622,6 +622,25 @@ tests/test_mixed_run.py per-node agents and max_parallel in a real run
622
622
  tests/test_openai_harness.py the agent loop against tests/fake_openai.py (no network, no DB)
623
623
  ```
624
624
 
625
+ ## Where Clearbox keeps its data
626
+
627
+ The database and the fallback workspace live in the platform's data directory:
628
+
629
+ | OS | path |
630
+ | --- | --- |
631
+ | Windows | `%LOCALAPPDATA%\clearbox\` |
632
+ | macOS | `~/Library/Application Support/clearbox/` |
633
+ | Linux | `$XDG_DATA_HOME/clearbox/`, else `~/.local/share/clearbox/` |
634
+
635
+ `--db` / `CLEARBOX_DB` and `--workdir` / `CLEARBOX_WORKDIR` override both. Before 0.2.2 these
636
+ defaulted to `clearbox.db` and `workspace/` relative to the current directory, so launching from
637
+ two folders gave you two unrelated databases; a `clearbox.db` already sitting in the directory you
638
+ start from is still used, and the banner says so.
639
+
640
+ Every top-level plan must name its own `working_directory` -- the directory its agents work in,
641
+ normally a repository. Subplans inherit it. Two plans with overlapping directories cannot run at
642
+ the same time, since file-conflict locking only orders nodes within one plan tree.
643
+
625
644
  ## Releasing
626
645
 
627
646
  The git tag is the trigger and the single source of truth. Bump the version, tag it, push:
@@ -13,14 +13,39 @@ from .app import Settings, create_app
13
13
  from .bootstrap import StartupError
14
14
 
15
15
 
16
+ LEGACY_DB = "clearbox.db" # the pre-0.2.2 default: relative to wherever you launched
17
+
18
+
19
+ def default_data_dir() -> str:
20
+ """Where an installed clearbox keeps its database and workspaces.
21
+
22
+ The old defaults were relative (``clearbox.db``, ``workspace``), so running the
23
+ command from two directories silently gave you two unrelated databases and left
24
+ stray files in whatever folder you happened to be in.
25
+ """
26
+ if sys.platform == "win32":
27
+ base = os.environ.get("LOCALAPPDATA") or os.path.expanduser(r"~\AppData\Local")
28
+ elif sys.platform == "darwin":
29
+ base = os.path.expanduser("~/Library/Application Support")
30
+ else:
31
+ base = os.environ.get("XDG_DATA_HOME") or os.path.expanduser("~/.local/share")
32
+ return os.path.join(base, "clearbox")
33
+
34
+
16
35
  def main() -> None:
17
36
  env = os.environ.get
37
+ data_dir = default_data_dir()
38
+ # A database left in the current directory by an older version still wins, so nobody's
39
+ # plans disappear on upgrade. The banner says where the new default lives.
40
+ legacy = os.path.abspath(LEGACY_DB) if os.path.isfile(LEGACY_DB) else None
18
41
  ap = argparse.ArgumentParser(prog="clearbox", description=__doc__)
19
42
  ap.add_argument("--host", default=env("CLEARBOX_HOST", "127.0.0.1"))
20
43
  ap.add_argument("--port", type=int, default=int(env("CLEARBOX_PORT", "8765")))
21
- ap.add_argument("--db", default=env("CLEARBOX_DB", "clearbox.db"), help="SQLite file")
22
- ap.add_argument("--workdir", default=env("CLEARBOX_WORKDIR", "workspace"),
23
- help="base directory; each plan's agents work in <workdir>/plan-<id>")
44
+ ap.add_argument("--db", default=env("CLEARBOX_DB", legacy or os.path.join(data_dir, "clearbox.db")),
45
+ help=f"SQLite file (default: {os.path.join(data_dir, 'clearbox.db')})")
46
+ ap.add_argument("--workdir", default=env("CLEARBOX_WORKDIR", os.path.join(data_dir, "workspace")),
47
+ help="fallback directory for plans created before a working directory was "
48
+ "required; each such plan's agents work in <workdir>/plan-<id>")
24
49
  ap.add_argument("--mock", action="store_true", default=env("CLEARBOX_AGENT") == "mock",
25
50
  help="use fake agents (no Claude sessions, no cost) to try out the UI")
26
51
  ap.add_argument("--mock-speed", type=float, default=1.0)
@@ -69,6 +94,10 @@ def main() -> None:
69
94
  print(f"Clearbox UI: http://{settings.host}:{settings.port}/")
70
95
  print(f"MCP endpoint: {settings.mcp_url} (agents: {settings.agent})")
71
96
  print(f"Agents: {app.state.agents_line}")
97
+ print(f"Database: {os.path.abspath(settings.db)}")
98
+ if legacy and os.path.abspath(settings.db) == legacy:
99
+ print(f" (a clearbox.db in this folder; the default is now "
100
+ f"{os.path.join(data_dir, 'clearbox.db')} -- move it there, or keep passing --db)")
72
101
  # Ctrl+C must always get the port back. uvicorn waits for open connections *before* running
73
102
  # the lifespan shutdown, and /api/stream is endless, so the app has to notice the stop itself:
74
103
  # it watches server.should_exit, which is set the moment the signal arrives. The timeout is
@@ -202,7 +202,9 @@ depends on it (from_node_id must finish before to_node_id starts). Execution wal
202
202
  from the roots to the leaves; independent nodes run in parallel.
203
203
 
204
204
  Building a plan:
205
- 1. create_plan(title, description) -> plan_id
205
+ 1. create_plan(title, description, working_directory) -> plan_id
206
+ working_directory is required: the absolute path of the existing directory the plan's
207
+ agents work in (usually the repository). Subplans inherit it.
206
208
  2. add_node(plan_id, title, description) -> [plan_id, node_id] (one call per piece of work)
207
209
  3. add_edge(plan_id, from_node_id, to_node_id) (dependencies)
208
210
  A node can itself be a small plan: create_plan(..., parent_plan_id, parent_node_id) attaches
@@ -367,9 +369,10 @@ def build_mcp(engine: Engine) -> MCPServer:
367
369
  Pass parent_plan_id and parent_node_id to make this plan the subplan of that node
368
370
  (the node then represents "do this whole small plan" instead of a single task).
369
371
 
370
- working_directory (top-level plans only): absolute path of an existing directory the
371
- plan's agents work in, e.g. a code repository. Defaults to <workdir>/plan-<id>.
372
- Subplans always use the working directory of their top-level plan.
372
+ working_directory: REQUIRED for a top-level plan -- the absolute path of an existing
373
+ directory the plan's agents work in, e.g. a code repository. Say where the work happens
374
+ rather than letting agents edit files somewhere you did not choose. Subplans inherit it
375
+ from their top-level plan and must not pass it.
373
376
 
374
377
  max_parallel (top-level plans only): cap how many nodes of this plan's whole tree
375
378
  (including its subplans) run at once, regardless of the server's global concurrency
@@ -378,9 +381,14 @@ def build_mcp(engine: Engine) -> MCPServer:
378
381
  """
379
382
  if parent_plan_id is not None and max_parallel is not None:
380
383
  raise ToolError("max_parallel can only be set on top-level plans")
384
+ if parent_plan_id is None and working_directory is None:
385
+ raise ToolError(
386
+ "working_directory is required for a top-level plan: pass the absolute path of "
387
+ "the existing directory its agents should work in (e.g. the repository)")
381
388
  if working_directory is not None:
382
389
  if parent_plan_id is not None:
383
- raise ToolError("working_directory can only be set on top-level plans")
390
+ raise ToolError("working_directory can only be set on top-level plans; a subplan "
391
+ "uses its top-level plan's directory")
384
392
  try:
385
393
  validate_working_directory(working_directory)
386
394
  except PlanError as e:
@@ -10,11 +10,12 @@ from __future__ import annotations
10
10
  import argparse
11
11
  import asyncio
12
12
  import json
13
+ import tempfile
13
14
 
14
15
  from mcp.client.client import Client
15
16
 
16
17
 
17
- async def main(url: str, start: bool) -> None:
18
+ async def main(url: str, start: bool, workdir: str | None = None) -> None:
18
19
  async with Client(url) as c:
19
20
  async def call(name: str, **args):
20
21
  res = await c.call_tool(name, args)
@@ -24,8 +25,11 @@ async def main(url: str, start: bool) -> None:
24
25
  return res.structured_content.get("result", res.structured_content)
25
26
  return json.loads(res.content[0].text)
26
27
 
28
+ # a top-level plan must say where its agents work; a fresh temp dir by default,
29
+ # since this demo writes a whole little app
27
30
  plan = await call("create_plan", title="Tiny CLI todo app",
28
- description="Write a small Python command-line todo app with tests and docs.")
31
+ description="Write a small Python command-line todo app with tests and docs.",
32
+ working_directory=workdir or tempfile.mkdtemp(prefix="clearbox-demo-"))
29
33
  print("plan", plan)
30
34
 
31
35
  async def node(title: str, desc: str, plan_id: int = plan) -> int:
@@ -62,5 +66,6 @@ if __name__ == "__main__":
62
66
  ap = argparse.ArgumentParser()
63
67
  ap.add_argument("--url", default="http://127.0.0.1:8765/mcp")
64
68
  ap.add_argument("--start", action="store_true")
69
+ ap.add_argument("--dir", help="working directory for the plan (default: a fresh temp dir)")
65
70
  a = ap.parse_args()
66
- asyncio.run(main(a.url, a.start))
71
+ asyncio.run(main(a.url, a.start, a.dir))
@@ -19,6 +19,7 @@ from __future__ import annotations
19
19
  import argparse
20
20
  import asyncio
21
21
  import json
22
+ import tempfile
22
23
  import time
23
24
 
24
25
  from mcp.client.client import Client
@@ -37,6 +38,7 @@ async def main(a: argparse.Namespace) -> None:
37
38
  plan = await call(
38
39
  "create_plan", title="File conflict demo",
39
40
  description="Two nodes write shared.txt and must serialize; a third writes its own file freely.",
41
+ working_directory=getattr(a, "dir", None) or tempfile.mkdtemp(prefix="clearbox-conflict-"),
40
42
  )
41
43
  print("plan", plan)
42
44
 
@@ -86,4 +88,5 @@ if __name__ == "__main__":
86
88
  ap = argparse.ArgumentParser()
87
89
  ap.add_argument("--url", default="http://127.0.0.1:8765/mcp")
88
90
  ap.add_argument("--start", action="store_true")
91
+ ap.add_argument("--dir", help="working directory for the plan (default: a fresh temp dir)")
89
92
  asyncio.run(main(ap.parse_args()))
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "clearbox"
3
- version = "0.2.1"
3
+ version = "0.2.2"
4
4
  description = "Observable, reviewable agent work: DAG plans over MCP, executed node by node and watched live"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -7,11 +7,16 @@ and -- explicitly -- that no response body ever contains `api_key` or the secret
7
7
  Run: uv run clearbox --mock (one shell)
8
8
  uv run python tests/test_agents_api.py [http://127.0.0.1:8765]
9
9
  """
10
- import asyncio, json, sys, threading, time, uuid
10
+ import asyncio, json, sys, tempfile, threading, time, uuid
11
11
  import urllib.error
12
12
  import urllib.request
13
13
  from mcp.client.client import Client
14
14
 
15
+ # Each top-level plan needs its own working directory: it is required, and two plans
16
+ # must not run in the same folder at once.
17
+ def wd():
18
+ return tempfile.mkdtemp(prefix="cb-test-")
19
+
15
20
  URL = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8765"
16
21
  SECRET = "sk-test-" + uuid.uuid4().hex
17
22
  bodies: list[str] = [] # every REST response body, scanned for leaks at the end
@@ -50,7 +55,7 @@ async def main():
50
55
  sc = r.structured_content
51
56
  return sc.get("result", sc) if sc is not None else json.loads(r.content[0].text)
52
57
 
53
- p = await call("create_plan", title="Agents API")
58
+ p = await call("create_plan", title="Agents API", working_directory=wd())
54
59
  sp_node = (await call("add_node", plan_id=p, title="parent"))[1]
55
60
  n2 = (await call("add_node", plan_id=p, title="leaf"))[1]
56
61
  sp = await call("create_plan", title="sub", parent_plan_id=p, parent_node_id=sp_node)
@@ -169,7 +169,9 @@ async def main():
169
169
 
170
170
  async def plan(title, specs, edges=(), parent=None, start=True):
171
171
  """specs: list of files lists (None = declare nothing); returns (plan_id, [node ids])."""
172
- kw = {"parent_plan_id": parent[0], "parent_node_id": parent[1]} if parent else {}
172
+ # a top-level plan needs its own working directory; two plans must not share one
173
+ kw = ({"parent_plan_id": parent[0], "parent_node_id": parent[1]} if parent
174
+ else {"working_directory": tempfile.mkdtemp(prefix="cb-test-")})
173
175
  pid = await call("create_plan", title=f"{title} {TAG}", **kw)
174
176
  if parent is None:
175
177
  plans.append(pid)
@@ -7,11 +7,16 @@ Scheduling with files is proven in test_file_conflicts.py.
7
7
  Run: uv run clearbox --mock --mock-speed 20 (one shell)
8
8
  uv run python tests/test_flow.py [http://127.0.0.1:8765]
9
9
  """
10
- import asyncio, json, sys, time
10
+ import asyncio, json, sys, tempfile, time
11
11
  import urllib.error
12
12
  import urllib.request
13
13
  from mcp.client.client import Client
14
14
 
15
+ # Each top-level plan needs its own working directory: it is required, and two plans
16
+ # must not run in the same folder at once.
17
+ def wd():
18
+ return tempfile.mkdtemp(prefix="cb-test-")
19
+
15
20
  URL = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8765"
16
21
 
17
22
  async def main():
@@ -24,7 +29,7 @@ async def main():
24
29
  return sc.get("result", sc) if sc is not None else json.loads(r.content[0].text)
25
30
 
26
31
  # --- cycle rejection
27
- p = await call("create_plan", title="External flow")
32
+ p = await call("create_plan", title="External flow", working_directory=wd())
28
33
  a = (await call("add_node", plan_id=p, title="A"))[1]
29
34
  b = (await call("add_node", plan_id=p, title="B"))[1]
30
35
  cc = (await call("add_node", plan_id=p, title="C"))[1]
@@ -76,7 +81,8 @@ async def main():
76
81
  print("external flow OK")
77
82
 
78
83
  # --- builtin flow with a wide graph, wait for completion
79
- q = await call("create_plan", title="Wide builtin graph", description="layout test")
84
+ q = await call("create_plan", title="Wide builtin graph", description="layout test",
85
+ working_directory=wd())
80
86
  ids = [(await call("add_node", plan_id=q, title=f"Step {i}", description=f"do {i}"))[1] for i in range(8)]
81
87
  for x, y in [(0,1),(0,2),(0,3),(1,4),(2,4),(3,5),(4,6),(5,6),(0,6),(6,7),(1,7)]:
82
88
  await call("add_edge", plan_id=q, from_node_id=ids[x], to_node_id=ids[y])
@@ -101,7 +107,7 @@ async def main():
101
107
  fast, strong = f"flow-fast-{tag}", f"flow-strong-{tag}"
102
108
  await call("create_agent", name=fast, harness="mock", params={"speed": 20}, max_parallel=1)
103
109
  await call("create_agent", name=strong, harness="mock")
104
- r = await call("create_plan", title="Agent resolution")
110
+ r = await call("create_plan", title="Agent resolution", working_directory=wd())
105
111
  r1 = (await call("add_node", plan_id=r, title="plain"))[1]
106
112
  r2 = (await call("add_node", plan_id=r, title="override"))[1]
107
113
  r3 = (await call("add_node", plan_id=r, title="expands"))[1]
@@ -152,7 +158,7 @@ async def main():
152
158
  except urllib.error.HTTPError as e:
153
159
  return e.code, json.loads(e.read())
154
160
 
155
- f = await call("create_plan", title="Files")
161
+ f = await call("create_plan", title="Files", working_directory=wd())
156
162
  f1 = (await call("add_node", plan_id=f, title="one", files=["Shared.txt", "docs/"]))[1]
157
163
  f2 = (await call("add_node", plan_id=f, title="two"))[1]
158
164
  f3 = (await call("add_node", plan_id=f, title="three", files=[".\\shared.txt"]))[1]
@@ -11,11 +11,16 @@ caps, not the global limit, are what holds the nodes back.
11
11
  Run: uv run clearbox --mock (one shell)
12
12
  uv run python tests/test_mixed_run.py [http://127.0.0.1:8765]
13
13
  """
14
- import asyncio, json, sys, time, uuid
14
+ import asyncio, json, sys, tempfile, time, uuid
15
15
  import urllib.error
16
16
  import urllib.request
17
17
  from mcp.client.client import Client
18
18
 
19
+ # Each top-level plan needs its own working directory: it is required, and two plans
20
+ # must not run in the same folder at once.
21
+ def wd():
22
+ return tempfile.mkdtemp(prefix="cb-test-")
23
+
19
24
  URL = sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8765"
20
25
 
21
26
 
@@ -52,7 +57,7 @@ async def build() -> tuple[int, list[int]]:
52
57
  r = await c.call_tool(name, a)
53
58
  sc = r.structured_content
54
59
  return sc.get("result", sc) if sc is not None else json.loads(r.content[0].text)
55
- pid = await call("create_plan", title=f"Mixed run {tag}")
60
+ pid = await call("create_plan", title=f"Mixed run {tag}", working_directory=wd())
56
61
  return pid, [(await call("add_node", plan_id=pid, title=f"N{i}"))[1] for i in range(8)]
57
62
 
58
63
 
@@ -13,10 +13,15 @@ edits something.
13
13
  Run: uv run clearbox --mock (one shell)
14
14
  uv run python tests/test_rework.py [http://127.0.0.1:8765]
15
15
  """
16
- import asyncio, json, sys, time, urllib.error, urllib.request
16
+ import asyncio, json, sys, tempfile, time, urllib.error, urllib.request
17
17
 
18
18
  from mcp.client.client import Client
19
19
 
20
+ # Each top-level plan needs its own working directory: it is required, and two plans
21
+ # must not run in the same folder at once.
22
+ def wd():
23
+ return tempfile.mkdtemp(prefix="cb-test-")
24
+
20
25
  URL = (sys.argv[1] if len(sys.argv) > 1 else "http://127.0.0.1:8765").rstrip("/")
21
26
  MARKER = "MARKER-rework-brief-must-reach-the-prompt"
22
27
 
@@ -51,7 +56,7 @@ async def main():
51
56
  sc = r.structured_content
52
57
  return sc.get("result", sc) if sc is not None else json.loads(r.content[0].text)
53
58
 
54
- pid = await call("create_plan", title="Rework test")
59
+ pid = await call("create_plan", title="Rework test", working_directory=wd())
55
60
  a = (await call("add_node", plan_id=pid, title="A", description="first step"))[1]
56
61
  b = (await call("add_node", plan_id=pid, title="B", description="second step"))[1]
57
62
  await call("add_edge", plan_id=pid, from_node_id=a, to_node_id=b)
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