5dive 0.1.0__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.
5dive-0.1.0/.gitignore ADDED
@@ -0,0 +1,56 @@
1
+ # Internal punch lists (kept local, never committed)
2
+ *_TASKS.md
3
+
4
+ # OS cruft
5
+ .DS_Store
6
+ Thumbs.db
7
+
8
+ # Editor / IDE
9
+ .vscode/
10
+ .idea/
11
+ *.swp
12
+ *.swo
13
+ *~
14
+
15
+ # Node / Bun (any local UI or tooling working trees)
16
+ node_modules/
17
+ dist/
18
+ build/
19
+ .cache/
20
+ bun.lockb
21
+
22
+ # Logs
23
+ *.log
24
+ npm-debug.log*
25
+
26
+ # Env / secrets
27
+ .env
28
+ .env.*
29
+ !.env.example
30
+
31
+ # Recovered or stashed working trees (e.g. the removed ui/ Vite SPA
32
+ # from commit 8932961 — strategic removal; if anyone restores it locally,
33
+ # don't let it accidentally get re-added to the OSS repo)
34
+ /ui/
35
+
36
+ # transient mutants written by tests/meta/harness-verdict-probe.sh during a
37
+ # `selfcheck --full` / probe sweep. They are deleted on a clean exit; a killed run
38
+ # leaves them behind, and `git add -A` in the same tree would commit them (it did).
39
+ tests/.probe-*
40
+
41
+ # DIVE-2097: transient mutated build.sh written by
42
+ # tests/build_bundle_self_order_unit.sh. Can't live under tests/.probe-* above —
43
+ # build.sh does `cd "$(dirname "$0")"`, so the mutant must run from a path that
44
+ # still sees src/, i.e. the repo root — but it is the same "deleted on a clean
45
+ # exit, survives a kill, git add -A would commit it" shape as the probe mutants.
46
+ /.dive2097-mutant-build.sh
47
+
48
+ # DIVE-2091: the built bundle is generated at TAG time by release-cut.yml,
49
+ # never committed on main — a 47k-line generated file on main invalidates every
50
+ # open PR on every merge. install.sh fetches it from the tag's tree.
51
+ /5dive
52
+ /5dive.sha256
53
+
54
+ # DIVE-3911: python package build output
55
+ python/dist/
56
+ python/src/*.egg-info/
5dive-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 5dive
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
5dive-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,95 @@
1
+ Metadata-Version: 2.5
2
+ Name: 5dive
3
+ Version: 0.1.0
4
+ Summary: Python client for the 5dive CLI — read the task queue, agents and org chart of a 5dive box.
5
+ Project-URL: Homepage, https://5dive.ai
6
+ Project-URL: Source, https://github.com/5dive-ai/5dive
7
+ Project-URL: Issues, https://github.com/5dive-ai/5dive/issues
8
+ Author: 5dive
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: 5dive,agents,automation,cli,multi-agent
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Topic :: Software Development :: Libraries
17
+ Requires-Python: >=3.9
18
+ Description-Content-Type: text/markdown
19
+
20
+ # 5dive
21
+
22
+ Python client for the [5dive](https://5dive.ai) CLI — read the task queue, agent
23
+ seats and org chart of a 5dive box from Python.
24
+
25
+ 5dive runs a team of AI coding agents on your own machine. State lives on that
26
+ box and the CLI is the interface with a stable contract over it, so this package
27
+ is a thin, dependency-free wrapper over `5dive <verb> --json` rather than an HTTP
28
+ client.
29
+
30
+ ## Install
31
+
32
+ ```bash
33
+ pip install 5dive
34
+ ```
35
+
36
+ The distribution is named `5dive`; the import name is `fivedive`, because a
37
+ Python identifier cannot begin with a digit.
38
+
39
+ ## Use
40
+
41
+ ```python
42
+ from fivedive import FiveDive
43
+
44
+ fd = FiveDive()
45
+
46
+ for t in fd.tasks("--status=todo"):
47
+ print(t["ident"], t["assignee"], t["title"])
48
+
49
+ print(fd.task("DIVE-3903")["status"])
50
+
51
+ for a in fd.agents():
52
+ print(a["name"], a["active"])
53
+ ```
54
+
55
+ Anything the CLI can answer in JSON is reachable, whether or not this package
56
+ has a named helper for it:
57
+
58
+ ```python
59
+ fd.raw("org", "tree")
60
+ fd.raw("task", "ls", "--assignee=main")
61
+ ```
62
+
63
+ ## Errors are raised, not returned
64
+
65
+ Every `--json` verb answers in one envelope:
66
+
67
+ ```json
68
+ {"ok": true, "data": {...}}
69
+ {"ok": false, "error": {"code": 4, "class": "not_found", "message": "no such task: NOPE-1"}}
70
+ ```
71
+
72
+ An unchecked `ok` is the bug this package exists to prevent, so the false branch
73
+ raises instead of handing back a dict you have to remember to test:
74
+
75
+ ```python
76
+ from fivedive import FiveDive, FiveDiveError, CliNotFound
77
+
78
+ try:
79
+ fd.task("NOPE-1")
80
+ except FiveDiveError as e:
81
+ print(e.err_class, e.code, e) # not_found 4 no such task: NOPE-1
82
+ ```
83
+
84
+ Branch on `err_class`, not on message text — the class is the stable contract.
85
+ `CliNotFound` is raised when the `5dive` binary is not on `PATH`.
86
+
87
+ ## Requirements
88
+
89
+ Python 3.9+, no third-party dependencies, and a 5dive box with the CLI
90
+ installed. Get the CLI at [5dive.ai](https://5dive.ai) or
91
+ [github.com/5dive-ai/5dive](https://github.com/5dive-ai/5dive).
92
+
93
+ ## Licence
94
+
95
+ MIT.
5dive-0.1.0/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # 5dive
2
+
3
+ Python client for the [5dive](https://5dive.ai) CLI — read the task queue, agent
4
+ seats and org chart of a 5dive box from Python.
5
+
6
+ 5dive runs a team of AI coding agents on your own machine. State lives on that
7
+ box and the CLI is the interface with a stable contract over it, so this package
8
+ is a thin, dependency-free wrapper over `5dive <verb> --json` rather than an HTTP
9
+ client.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install 5dive
15
+ ```
16
+
17
+ The distribution is named `5dive`; the import name is `fivedive`, because a
18
+ Python identifier cannot begin with a digit.
19
+
20
+ ## Use
21
+
22
+ ```python
23
+ from fivedive import FiveDive
24
+
25
+ fd = FiveDive()
26
+
27
+ for t in fd.tasks("--status=todo"):
28
+ print(t["ident"], t["assignee"], t["title"])
29
+
30
+ print(fd.task("DIVE-3903")["status"])
31
+
32
+ for a in fd.agents():
33
+ print(a["name"], a["active"])
34
+ ```
35
+
36
+ Anything the CLI can answer in JSON is reachable, whether or not this package
37
+ has a named helper for it:
38
+
39
+ ```python
40
+ fd.raw("org", "tree")
41
+ fd.raw("task", "ls", "--assignee=main")
42
+ ```
43
+
44
+ ## Errors are raised, not returned
45
+
46
+ Every `--json` verb answers in one envelope:
47
+
48
+ ```json
49
+ {"ok": true, "data": {...}}
50
+ {"ok": false, "error": {"code": 4, "class": "not_found", "message": "no such task: NOPE-1"}}
51
+ ```
52
+
53
+ An unchecked `ok` is the bug this package exists to prevent, so the false branch
54
+ raises instead of handing back a dict you have to remember to test:
55
+
56
+ ```python
57
+ from fivedive import FiveDive, FiveDiveError, CliNotFound
58
+
59
+ try:
60
+ fd.task("NOPE-1")
61
+ except FiveDiveError as e:
62
+ print(e.err_class, e.code, e) # not_found 4 no such task: NOPE-1
63
+ ```
64
+
65
+ Branch on `err_class`, not on message text — the class is the stable contract.
66
+ `CliNotFound` is raised when the `5dive` binary is not on `PATH`.
67
+
68
+ ## Requirements
69
+
70
+ Python 3.9+, no third-party dependencies, and a 5dive box with the CLI
71
+ installed. Get the CLI at [5dive.ai](https://5dive.ai) or
72
+ [github.com/5dive-ai/5dive](https://github.com/5dive-ai/5dive).
73
+
74
+ ## Licence
75
+
76
+ MIT.
@@ -0,0 +1,29 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "5dive"
7
+ version = "0.1.0"
8
+ description = "Python client for the 5dive CLI — read the task queue, agents and org chart of a 5dive box."
9
+ readme = "README.md"
10
+ requires-python = ">=3.9"
11
+ license = { text = "MIT" }
12
+ authors = [{ name = "5dive" }]
13
+ keywords = ["5dive", "agents", "multi-agent", "cli", "automation"]
14
+ classifiers = [
15
+ "Development Status :: 3 - Alpha",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Topic :: Software Development :: Libraries",
20
+ ]
21
+ dependencies = []
22
+
23
+ [project.urls]
24
+ Homepage = "https://5dive.ai"
25
+ Source = "https://github.com/5dive-ai/5dive"
26
+ Issues = "https://github.com/5dive-ai/5dive/issues"
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/fivedive"]
@@ -0,0 +1,14 @@
1
+ """Python client for the 5dive CLI.
2
+
3
+ The distribution is named ``5dive``; the import name is ``fivedive`` because a
4
+ Python identifier cannot begin with a digit.
5
+
6
+ from fivedive import FiveDive
7
+ for t in FiveDive().tasks():
8
+ print(t["ident"], t["status"])
9
+ """
10
+
11
+ from .client import FiveDive, FiveDiveError, CliNotFound
12
+
13
+ __all__ = ["FiveDive", "FiveDiveError", "CliNotFound"]
14
+ __version__ = "0.1.0"
@@ -0,0 +1,120 @@
1
+ """A thin, dependency-free wrapper over the local ``5dive`` CLI's JSON mode.
2
+
3
+ WHY THIS IS A WRAPPER AND NOT AN HTTP CLIENT: 5dive's state lives on the box the
4
+ agents run on, and the CLI is the only interface with a stable contract over it.
5
+ Every ``--json`` verb answers in one envelope::
6
+
7
+ {"ok": true, "data": {...}}
8
+ {"ok": false, "error": {"code": 4, "class": "not_found", "message": "..."}}
9
+
10
+ so the whole job of this module is to run the binary, parse that envelope, and
11
+ raise on the false branch instead of handing back a dict the caller has to
12
+ remember to check. An unchecked ``ok`` is the bug this exists to prevent.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import json
18
+ import shutil
19
+ import subprocess
20
+ from typing import Any, Dict, List, Optional, Sequence
21
+
22
+
23
+ class FiveDiveError(RuntimeError):
24
+ """The CLI answered with ``ok: false``.
25
+
26
+ Carries the machine-readable fields so callers can branch on ``err_class``
27
+ rather than matching on message text, which is not a stable contract.
28
+ """
29
+
30
+ def __init__(self, message: str, code: Optional[int] = None, err_class: Optional[str] = None):
31
+ super().__init__(message)
32
+ self.code = code
33
+ self.err_class = err_class
34
+
35
+
36
+ class CliNotFound(FiveDiveError):
37
+ """The ``5dive`` binary is not on PATH."""
38
+
39
+
40
+ class FiveDive:
41
+ """Run 5dive CLI verbs and return parsed JSON.
42
+
43
+ :param binary: path to the CLI, if it is not simply ``5dive`` on PATH.
44
+ :param timeout: seconds before a call is abandoned. ``None`` waits forever,
45
+ which is rarely what you want from a library.
46
+ """
47
+
48
+ def __init__(self, binary: str = "5dive", timeout: Optional[float] = 30.0):
49
+ self.binary = binary
50
+ self.timeout = timeout
51
+
52
+ # -- the one place a subprocess is run -------------------------------
53
+ def raw(self, *args: str) -> Any:
54
+ """Run ``5dive <args> --json`` and return the ``data`` payload.
55
+
56
+ ``--json`` is appended only when the caller has not already passed it,
57
+ so ``raw("task", "ls", "--json")`` and ``raw("task", "ls")`` agree.
58
+ """
59
+ argv: List[str] = [self.binary, *args]
60
+ if "--json" not in argv:
61
+ argv.append("--json")
62
+
63
+ if shutil.which(self.binary) is None and "/" not in self.binary:
64
+ raise CliNotFound(f"{self.binary!r} is not on PATH — is this a 5dive box?")
65
+
66
+ try:
67
+ proc = subprocess.run(argv, capture_output=True, text=True, timeout=self.timeout)
68
+ except FileNotFoundError as exc: # binary named by path, but absent
69
+ raise CliNotFound(f"cannot execute {self.binary!r}: {exc}") from exc
70
+ except subprocess.TimeoutExpired as exc:
71
+ raise FiveDiveError(f"{' '.join(argv)} timed out after {self.timeout}s") from exc
72
+
73
+ # A non-zero exit still carries a JSON envelope on the error path, so
74
+ # parse BEFORE judging the status: the envelope's message is better than
75
+ # "exited 4", and falling back to stderr only when there is no envelope
76
+ # keeps a genuine crash legible instead of masking it as a parse error.
77
+ payload = None
78
+ if proc.stdout.strip():
79
+ try:
80
+ payload = json.loads(proc.stdout)
81
+ except json.JSONDecodeError:
82
+ payload = None
83
+
84
+ if payload is None:
85
+ detail = (proc.stderr or proc.stdout or "").strip() or f"exited {proc.returncode}"
86
+ raise FiveDiveError(f"{' '.join(argv)}: no JSON envelope — {detail}")
87
+
88
+ if not payload.get("ok", False):
89
+ err = payload.get("error") or {}
90
+ raise FiveDiveError(
91
+ err.get("message", "unknown error"),
92
+ code=err.get("code"),
93
+ err_class=err.get("class"),
94
+ )
95
+ return payload.get("data")
96
+
97
+ # -- convenience readers ---------------------------------------------
98
+ def tasks(self, *flags: str) -> List[Dict[str, Any]]:
99
+ """The task queue. Extra flags pass straight through, e.g.
100
+ ``tasks("--status=todo", "--assignee=main")``."""
101
+ data = self.raw("task", "ls", *flags) or {}
102
+ return data.get("tasks", [])
103
+
104
+ def task(self, ident: str) -> Dict[str, Any]:
105
+ """One task by ident, e.g. ``task("DIVE-3903")``."""
106
+ return self.raw("task", "show", ident) or {}
107
+
108
+ def agents(self, *flags: str) -> List[Dict[str, Any]]:
109
+ """The agent seats on this box."""
110
+ data = self.raw("agent", "list", *flags)
111
+ if isinstance(data, list):
112
+ return data
113
+ return (data or {}).get("agents", [])
114
+
115
+ def version(self) -> str:
116
+ """The CLI's version string (does not use the JSON envelope)."""
117
+ proc = subprocess.run(
118
+ [self.binary, "--version"], capture_output=True, text=True, timeout=self.timeout
119
+ )
120
+ return proc.stdout.strip()
@@ -0,0 +1,78 @@
1
+ """Unit arms for the envelope contract. No 5dive binary is required: every arm
2
+ drives the real FiveDive.raw() and fakes only subprocess.run, so the parsing and
3
+ the raising under test are the shipped ones.
4
+ """
5
+ import json
6
+ import subprocess
7
+ import sys
8
+ import unittest
9
+ from pathlib import Path
10
+ from unittest import mock
11
+
12
+ sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
13
+
14
+ from fivedive import FiveDive, FiveDiveError, CliNotFound # noqa: E402
15
+
16
+
17
+ def completed(stdout="", stderr="", rc=0):
18
+ return subprocess.CompletedProcess(args=[], returncode=rc, stdout=stdout, stderr=stderr)
19
+
20
+
21
+ class EnvelopeTests(unittest.TestCase):
22
+ def setUp(self):
23
+ self.fd = FiveDive()
24
+ patcher = mock.patch("shutil.which", return_value="/usr/local/bin/5dive")
25
+ patcher.start()
26
+ self.addCleanup(patcher.stop)
27
+
28
+ def test_ok_true_returns_data(self):
29
+ env = json.dumps({"ok": True, "data": {"tasks": [{"ident": "DIVE-1"}]}})
30
+ with mock.patch("subprocess.run", return_value=completed(env)):
31
+ self.assertEqual(self.fd.tasks(), [{"ident": "DIVE-1"}])
32
+
33
+ def test_ok_false_raises_with_machine_fields(self):
34
+ env = json.dumps(
35
+ {"ok": False, "error": {"code": 4, "class": "not_found", "message": "no such task: NOPE-1"}}
36
+ )
37
+ with mock.patch("subprocess.run", return_value=completed(env, rc=4)):
38
+ with self.assertRaises(FiveDiveError) as ctx:
39
+ self.fd.task("NOPE-1")
40
+ self.assertEqual(ctx.exception.code, 4)
41
+ self.assertEqual(ctx.exception.err_class, "not_found")
42
+ self.assertIn("NOPE-1", str(ctx.exception))
43
+
44
+ def test_error_envelope_is_read_even_though_exit_is_nonzero(self):
45
+ """The regression this guards: judging returncode first would report
46
+ 'exited 4' and throw away the message the CLI actually sent."""
47
+ env = json.dumps({"ok": False, "error": {"code": 4, "class": "not_found", "message": "gone"}})
48
+ with mock.patch("subprocess.run", return_value=completed(env, stderr="error: gone", rc=4)):
49
+ with self.assertRaises(FiveDiveError) as ctx:
50
+ self.fd.raw("task", "show", "X")
51
+ self.assertEqual(str(ctx.exception), "gone")
52
+
53
+ def test_no_envelope_falls_back_to_stderr(self):
54
+ with mock.patch("subprocess.run", return_value=completed("", "segfault", rc=139)):
55
+ with self.assertRaises(FiveDiveError) as ctx:
56
+ self.fd.raw("task", "ls")
57
+ self.assertIn("segfault", str(ctx.exception))
58
+
59
+ def test_json_flag_not_duplicated(self):
60
+ env = json.dumps({"ok": True, "data": {}})
61
+ with mock.patch("subprocess.run", return_value=completed(env)) as run:
62
+ self.fd.raw("task", "ls", "--json")
63
+ self.assertEqual(run.call_args[0][0].count("--json"), 1)
64
+
65
+ def test_missing_binary_raises_cli_not_found(self):
66
+ with mock.patch("shutil.which", return_value=None):
67
+ with self.assertRaises(CliNotFound):
68
+ FiveDive().raw("task", "ls")
69
+
70
+ def test_timeout_is_reported_not_swallowed(self):
71
+ with mock.patch("subprocess.run", side_effect=subprocess.TimeoutExpired(cmd="5dive", timeout=1)):
72
+ with self.assertRaises(FiveDiveError) as ctx:
73
+ self.fd.raw("task", "ls")
74
+ self.assertIn("timed out", str(ctx.exception))
75
+
76
+
77
+ if __name__ == "__main__":
78
+ unittest.main()