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.
- {clearbox-0.2.1 → clearbox-0.2.2}/PKG-INFO +23 -4
- {clearbox-0.2.1 → clearbox-0.2.2}/README.md +22 -3
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/__main__.py +32 -3
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/mcp_tools.py +13 -5
- {clearbox-0.2.1 → clearbox-0.2.2}/examples/demo_plan.py +8 -3
- {clearbox-0.2.1 → clearbox-0.2.2}/examples/file_conflict_plan.py +3 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/pyproject.toml +1 -1
- {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_agents_api.py +7 -2
- {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_file_conflicts.py +3 -1
- {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_flow.py +11 -5
- {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_mixed_run.py +7 -2
- {clearbox-0.2.1 → clearbox-0.2.2}/tests/test_rework.py +7 -2
- {clearbox-0.2.1 → clearbox-0.2.2}/.gitignore +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/LICENSE +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/__init__.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/agents.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/app.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/bootstrap.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/engine.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/__init__.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/base.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/claude_sdk.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/mock.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/openai_check.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/openai_compat.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/probe.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/registry.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/harness/tools.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/mcp_bridge.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/static/app.js +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/static/index.html +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/static/style.css +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/clearbox/store.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/examples/mixed_plan.py +0 -0
- {clearbox-0.2.1 → clearbox-0.2.2}/tests/fake_openai.py +0 -0
- {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.
|
|
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.
|
|
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` |
|
|
599
|
-
| `--workdir` | `CLEARBOX_WORKDIR` |
|
|
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.
|
|
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` |
|
|
568
|
-
| `--workdir` | `CLEARBOX_WORKDIR` |
|
|
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"),
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
371
|
-
plan's agents work in, e.g. a code repository.
|
|
372
|
-
|
|
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()))
|
|
@@ -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
|
-
|
|
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
|
|
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
|