coxswain-tools 0.1.0b1__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 (62) hide show
  1. coxswain_tools-0.1.0b1/LICENSE +21 -0
  2. coxswain_tools-0.1.0b1/PKG-INFO +135 -0
  3. coxswain_tools-0.1.0b1/README.md +100 -0
  4. coxswain_tools-0.1.0b1/agent_tools/__init__.py +9 -0
  5. coxswain_tools-0.1.0b1/agent_tools/cartridge_screen.py +98 -0
  6. coxswain_tools-0.1.0b1/agent_tools/cleanup.py +63 -0
  7. coxswain_tools-0.1.0b1/agent_tools/cli.py +1415 -0
  8. coxswain_tools-0.1.0b1/agent_tools/doctor.py +186 -0
  9. coxswain_tools-0.1.0b1/agent_tools/editor_model.py +356 -0
  10. coxswain_tools-0.1.0b1/agent_tools/epic.py +47 -0
  11. coxswain_tools-0.1.0b1/agent_tools/events.py +117 -0
  12. coxswain_tools-0.1.0b1/agent_tools/fragments.py +96 -0
  13. coxswain_tools-0.1.0b1/agent_tools/hud.py +70 -0
  14. coxswain_tools-0.1.0b1/agent_tools/install.py +88 -0
  15. coxswain_tools-0.1.0b1/agent_tools/install_exec.py +114 -0
  16. coxswain_tools-0.1.0b1/agent_tools/land.py +97 -0
  17. coxswain_tools-0.1.0b1/agent_tools/plan.py +36 -0
  18. coxswain_tools-0.1.0b1/agent_tools/provenance.py +80 -0
  19. coxswain_tools-0.1.0b1/agent_tools/records.py +220 -0
  20. coxswain_tools-0.1.0b1/agent_tools/release.py +129 -0
  21. coxswain_tools-0.1.0b1/agent_tools/route.py +644 -0
  22. coxswain_tools-0.1.0b1/agent_tools/runs.py +106 -0
  23. coxswain_tools-0.1.0b1/agent_tools/runs_top.py +99 -0
  24. coxswain_tools-0.1.0b1/agent_tools/runs_top_screen.py +170 -0
  25. coxswain_tools-0.1.0b1/agent_tools/setup_install.py +198 -0
  26. coxswain_tools-0.1.0b1/agent_tools/setup_screen.py +91 -0
  27. coxswain_tools-0.1.0b1/agent_tools/setup_tui.py +220 -0
  28. coxswain_tools-0.1.0b1/coxswain_tools.egg-info/PKG-INFO +135 -0
  29. coxswain_tools-0.1.0b1/coxswain_tools.egg-info/SOURCES.txt +60 -0
  30. coxswain_tools-0.1.0b1/coxswain_tools.egg-info/dependency_links.txt +1 -0
  31. coxswain_tools-0.1.0b1/coxswain_tools.egg-info/entry_points.txt +3 -0
  32. coxswain_tools-0.1.0b1/coxswain_tools.egg-info/requires.txt +4 -0
  33. coxswain_tools-0.1.0b1/coxswain_tools.egg-info/top_level.txt +1 -0
  34. coxswain_tools-0.1.0b1/pyproject.toml +47 -0
  35. coxswain_tools-0.1.0b1/setup.cfg +4 -0
  36. coxswain_tools-0.1.0b1/tests/test_cartridge_screen.py +121 -0
  37. coxswain_tools-0.1.0b1/tests/test_cleanup.py +43 -0
  38. coxswain_tools-0.1.0b1/tests/test_cli_help.py +51 -0
  39. coxswain_tools-0.1.0b1/tests/test_doctor.py +237 -0
  40. coxswain_tools-0.1.0b1/tests/test_doctor_cli.py +264 -0
  41. coxswain_tools-0.1.0b1/tests/test_editor_model.py +588 -0
  42. coxswain_tools-0.1.0b1/tests/test_epic_and_cli.py +50 -0
  43. coxswain_tools-0.1.0b1/tests/test_events.py +146 -0
  44. coxswain_tools-0.1.0b1/tests/test_fragments.py +145 -0
  45. coxswain_tools-0.1.0b1/tests/test_install.py +260 -0
  46. coxswain_tools-0.1.0b1/tests/test_install_exec.py +243 -0
  47. coxswain_tools-0.1.0b1/tests/test_land.py +232 -0
  48. coxswain_tools-0.1.0b1/tests/test_launcher.py +103 -0
  49. coxswain_tools-0.1.0b1/tests/test_packaging.py +71 -0
  50. coxswain_tools-0.1.0b1/tests/test_provenance.py +118 -0
  51. coxswain_tools-0.1.0b1/tests/test_records.py +39 -0
  52. coxswain_tools-0.1.0b1/tests/test_release.py +286 -0
  53. coxswain_tools-0.1.0b1/tests/test_route.py +824 -0
  54. coxswain_tools-0.1.0b1/tests/test_route_cli.py +525 -0
  55. coxswain_tools-0.1.0b1/tests/test_runs.py +162 -0
  56. coxswain_tools-0.1.0b1/tests/test_runs_top.py +60 -0
  57. coxswain_tools-0.1.0b1/tests/test_runs_top_screen.py +101 -0
  58. coxswain_tools-0.1.0b1/tests/test_series.py +183 -0
  59. coxswain_tools-0.1.0b1/tests/test_setup_install.py +224 -0
  60. coxswain_tools-0.1.0b1/tests/test_setup_install_cli.py +186 -0
  61. coxswain_tools-0.1.0b1/tests/test_setup_screen.py +175 -0
  62. coxswain_tools-0.1.0b1/tests/test_setup_tui.py +193 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Patrick Pfenning
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.
@@ -0,0 +1,135 @@
1
+ Metadata-Version: 2.4
2
+ Name: coxswain-tools
3
+ Version: 0.1.0b1
4
+ Summary: Provides the `cox` command, the coxswain's operator tools: run records, traces, cleanup, the HUD, plans. `agent-tools` is kept as an alias for one release.
5
+ License: MIT License
6
+
7
+ Copyright (c) 2026 Patrick Pfenning
8
+
9
+ Permission is hereby granted, free of charge, to any person obtaining a copy
10
+ of this software and associated documentation files (the "Software"), to deal
11
+ in the Software without restriction, including without limitation the rights
12
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13
+ copies of the Software, and to permit persons to whom the Software is
14
+ furnished to do so, subject to the following conditions:
15
+
16
+ The above copyright notice and this permission notice shall be included in all
17
+ copies or substantial portions of the Software.
18
+
19
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
25
+ SOFTWARE.
26
+
27
+ Project-URL: Repository, https://github.com/ppfenning/coxswain-tools
28
+ Requires-Python: >=3.12
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Requires-Dist: pyyaml>=6
32
+ Provides-Extra: dev
33
+ Requires-Dist: pytest; extra == "dev"
34
+ Dynamic: license-file
35
+
36
+ # agent-tools
37
+
38
+ Deterministic tools the agent seats call **instead of spending tokens**. Anything
39
+ a seat would otherwise do by reading a file wholesale and reasoning about it —
40
+ summing a run's cost, counting what a traced node did, cleaning up after a run,
41
+ posting to the HUD, serving a plan — is a function here that reads it and answers.
42
+
43
+ The mould, every time: a pure core with no I/O, the filesystem and the network
44
+ at the edges, dry-run by default where a write is involved, and tests that never
45
+ touch a real run.
46
+
47
+ ## The fifth repository
48
+
49
+ | Repository | Owns |
50
+ |---|---|
51
+ | [`agent-cartridges`](https://github.com/ppfenning/agent-cartridges) | who a run works for |
52
+ | [`agent-graphs`](https://github.com/ppfenning/agent-graphs) | what runs, and the harness that runs it |
53
+ | [`agent-cast`](https://github.com/ppfenning/agent-cast) | who speaks |
54
+ | [`agent-voice-hud`](https://github.com/ppfenning/agent-voice-hud) | where you hear and see it |
55
+ | **`agent-tools`** | what the seats run so they do not have to think |
56
+
57
+ Tools that read agent-graphs' records and post to agent-voice-hud belong to
58
+ neither; a seat routes to them by name the way it routes to skills. Nothing here
59
+ names an employer, a tracker, or a person — CI refuses it.
60
+
61
+ ## Install
62
+
63
+ See [`docs/getting-started.md`](docs/getting-started.md) for the full setup, from cloning all three repositories to a verified first run.
64
+
65
+ Not yet on PyPI: once the first tag ships, this package will be `coxswain-tools`, installable as `pip install coxswain-tools` or `uv tool install coxswain-tools`.
66
+
67
+ ```bash
68
+ git clone https://github.com/ppfenning/coxswain-tools ~/repos/coxswain-tools
69
+ cd ~/repos/coxswain-tools && uv venv && uv pip install -e ".[dev]"
70
+ uv tool install -e . # `agent-tools` on PATH for every seat
71
+ ```
72
+
73
+ ## Commands
74
+
75
+ Bare `cox`, with no subcommand, opens the coxswain session: a real Claude Code
76
+ session with the `coxswain` plugin loaded, working directory at the profile's
77
+ `workspace_dir`. `agent-tools` still works this release as an alias for `cox`.
78
+
79
+ ```
80
+ cox runs usage RUN [--runs-dir runs] [--json] cost, turns, cache share — by role and by model
81
+ cox runs trace RUN [--role build] [-v] per node: turns, cost, tools, reads, whole-file reads, commands
82
+ cox runs clean RUN --repo PATH [--apply] the run's worktrees and scratch branches; phase branches kept; dry-run by default
83
+ cox runs land RUN --repo PATH [--task T] [--apply] [--no-merge] plan and land an approved run: pick branch, cherry-pick, PR, merge on green, clean; dry-run by default
84
+ cox runs events [--runs-dir runs] [--follow] [--json] tail a run's log, trace and usage files as a live event stream
85
+ cox runs top — live table of runs in flight (next task wires the screen)
86
+ cox epic watch PIDFILE [--log LOG] block until a detached run exits (or the cap), then the outcome lines
87
+ cox hud ops FILE|- replace the HUD's ops list (id, label, status, persona?, detail?)
88
+ cox hud say TEXT [--persona P] [--voice V] speak a line through the HUD
89
+ cox hud inbox show|arm|clear read, wait for, or clear directives
90
+ cox hud cast the seats the HUD's org ring shows
91
+ cox plan serve DIR [--check] [--no-open] lint, serve through the local bridge, open in Brave
92
+ cox route context [--json] what a session reads at start: team, queued intake, runs in flight, initiatives with ready work; exits 0 even with no profile
93
+ cox route status [--json] every run with a pidfile or a log: alive or exited, started, the log's outcome lines
94
+ cox route file --repo PATH --title TEXT [--body FILE|-] [--phase NAME] [--intake] write a one-task initiative, or with --intake an intake item; exit 2 if a target path exists or the profile is missing
95
+ cox route launch epic --initiative DIR [--repo PATH] [--fix-attempts N] [--dry-run] start the harness detached with a pidfile and log, and AGENT_GRAPHS_TRACE_DIR set to `<runs_dir>/<run-id>-trace` so every node writes a trace; exit 2 on a missing profile or harness venv, a missing initiative.md, a dirty repo, or a live run of the same initiative
96
+ cox route launch decompose --idea FILE --initiative-id ID [--dry-run] start the harness detached; exit 2 on a missing profile, harness venv, or idea file
97
+ cox route launch cos [--dry-run] start the chief of staff detached: it reads intake and runs, dispatches within the bound, and consumes what it dispatched
98
+ cox setup a small terminal UI over setup doctor, setup install and cartridge init (needs a terminal)
99
+ cox setup doctor [--profile PATH] [--json] read-only: profile, paths, harness venv, cartridge, skills, provider, workspace — a table and an exit code
100
+ cox setup install --root DIR --team T --workspace DIR [--plugins] [--hook] [--force-profile] [--dry-run] venvs, agent-tools on PATH, the profile, optionally the provider plugin and a session-start hook; dry-run prints the plan
101
+ cox install --root DIR [--manifest PATH] [--provider NAME] [--with FLAG] [--team T] [--workspace DIR] [--dry-run] the plan over coxswain's manifest.toml: clone, fetch, skip or refuse per component, then setup_install, doctor, desktop; --dry-run only prints it, otherwise it runs each step and exits 0 only if every step ran clean
102
+ cox upgrade --root DIR [--manifest PATH] [--provider NAME] [--with FLAG] [--team T] [--workspace DIR] [--to VERSION] [--dry-run] same plan as install, but refuses (exit 2, naming the directory) if any present checkout is dirty; --to overrides every component's pinned tag for this run
103
+ cox versions [--root DIR] [--manifest PATH] pinned vs. installed tag per component, and status: ok, drift, missing, extra
104
+ ```
105
+
106
+ ## Maintainers
107
+
108
+ `cox dev` holds commands a maintainer of the coxswain repositories runs; nothing
109
+ here is needed to use Coxswain. `cox release` is a one-release alias that prints
110
+ `moved: use cox dev release` and exits 2.
111
+
112
+ ```
113
+ cox dev release VERSION [--dry-run] [--manifest PATH] [--root DIR] [--checkout NAME=PATH] [--umbrella PATH] the lockstep plan: tag every component, bump the manifest (skipped on a first cut of the declared version), notes, tag_self; exit 2 on refuse (bad semver, an existing tag, a lesser version, or a checkout that is not a ppfenning/coxswain remote); without --dry-run, executes only a first-cut plan — tags and pushes every component and the umbrella in turn, refusing before tagging anything if a checkout is dirty, off its default branch, the release note is missing, or the plan still carries a bump_manifest step (bump and commit the manifest by hand first)
114
+ ```
115
+
116
+ Every command that reads a record is pure over parsed data and unit-tested
117
+ against fixtures; every command that writes is dry-run unless `--apply`.
118
+
119
+ The `route` group reads one profile, `~/.config/agent-tools/profile.yaml`:
120
+ `team`, `cartridges_dir`, `skills_roots`, `provider_profile`, `harness_dir`,
121
+ `workspace_dir`, and `assume`, the gate answer detached runs are started
122
+ with. `--profile PATH` overrides the location for one command,
123
+ `AGENT_TOOLS_PROFILE` overrides it for a shell, and the default path is read
124
+ when neither is set. Without a profile, `route context` prints one line and
125
+ exits 0; `file`, `launch` and `status` exit 2 and name the path they looked
126
+ for. `--dry-run` on either `launch` prints the argv, the pidfile and the log
127
+ path and starts nothing.
128
+
129
+ ## Why this exists
130
+
131
+ One day of live epics found every defect by reading a usage file or a trace,
132
+ by hand, in a chief-of-staff's own turns: summing costs, counting Bash calls,
133
+ noticing a node read a 1,900-line file whole. Each of those readings cost
134
+ tokens and produced the same answer every time. They are functions now, and
135
+ the steward seat runs them.
@@ -0,0 +1,100 @@
1
+ # agent-tools
2
+
3
+ Deterministic tools the agent seats call **instead of spending tokens**. Anything
4
+ a seat would otherwise do by reading a file wholesale and reasoning about it —
5
+ summing a run's cost, counting what a traced node did, cleaning up after a run,
6
+ posting to the HUD, serving a plan — is a function here that reads it and answers.
7
+
8
+ The mould, every time: a pure core with no I/O, the filesystem and the network
9
+ at the edges, dry-run by default where a write is involved, and tests that never
10
+ touch a real run.
11
+
12
+ ## The fifth repository
13
+
14
+ | Repository | Owns |
15
+ |---|---|
16
+ | [`agent-cartridges`](https://github.com/ppfenning/agent-cartridges) | who a run works for |
17
+ | [`agent-graphs`](https://github.com/ppfenning/agent-graphs) | what runs, and the harness that runs it |
18
+ | [`agent-cast`](https://github.com/ppfenning/agent-cast) | who speaks |
19
+ | [`agent-voice-hud`](https://github.com/ppfenning/agent-voice-hud) | where you hear and see it |
20
+ | **`agent-tools`** | what the seats run so they do not have to think |
21
+
22
+ Tools that read agent-graphs' records and post to agent-voice-hud belong to
23
+ neither; a seat routes to them by name the way it routes to skills. Nothing here
24
+ names an employer, a tracker, or a person — CI refuses it.
25
+
26
+ ## Install
27
+
28
+ See [`docs/getting-started.md`](docs/getting-started.md) for the full setup, from cloning all three repositories to a verified first run.
29
+
30
+ Not yet on PyPI: once the first tag ships, this package will be `coxswain-tools`, installable as `pip install coxswain-tools` or `uv tool install coxswain-tools`.
31
+
32
+ ```bash
33
+ git clone https://github.com/ppfenning/coxswain-tools ~/repos/coxswain-tools
34
+ cd ~/repos/coxswain-tools && uv venv && uv pip install -e ".[dev]"
35
+ uv tool install -e . # `agent-tools` on PATH for every seat
36
+ ```
37
+
38
+ ## Commands
39
+
40
+ Bare `cox`, with no subcommand, opens the coxswain session: a real Claude Code
41
+ session with the `coxswain` plugin loaded, working directory at the profile's
42
+ `workspace_dir`. `agent-tools` still works this release as an alias for `cox`.
43
+
44
+ ```
45
+ cox runs usage RUN [--runs-dir runs] [--json] cost, turns, cache share — by role and by model
46
+ cox runs trace RUN [--role build] [-v] per node: turns, cost, tools, reads, whole-file reads, commands
47
+ cox runs clean RUN --repo PATH [--apply] the run's worktrees and scratch branches; phase branches kept; dry-run by default
48
+ cox runs land RUN --repo PATH [--task T] [--apply] [--no-merge] plan and land an approved run: pick branch, cherry-pick, PR, merge on green, clean; dry-run by default
49
+ cox runs events [--runs-dir runs] [--follow] [--json] tail a run's log, trace and usage files as a live event stream
50
+ cox runs top — live table of runs in flight (next task wires the screen)
51
+ cox epic watch PIDFILE [--log LOG] block until a detached run exits (or the cap), then the outcome lines
52
+ cox hud ops FILE|- replace the HUD's ops list (id, label, status, persona?, detail?)
53
+ cox hud say TEXT [--persona P] [--voice V] speak a line through the HUD
54
+ cox hud inbox show|arm|clear read, wait for, or clear directives
55
+ cox hud cast the seats the HUD's org ring shows
56
+ cox plan serve DIR [--check] [--no-open] lint, serve through the local bridge, open in Brave
57
+ cox route context [--json] what a session reads at start: team, queued intake, runs in flight, initiatives with ready work; exits 0 even with no profile
58
+ cox route status [--json] every run with a pidfile or a log: alive or exited, started, the log's outcome lines
59
+ cox route file --repo PATH --title TEXT [--body FILE|-] [--phase NAME] [--intake] write a one-task initiative, or with --intake an intake item; exit 2 if a target path exists or the profile is missing
60
+ cox route launch epic --initiative DIR [--repo PATH] [--fix-attempts N] [--dry-run] start the harness detached with a pidfile and log, and AGENT_GRAPHS_TRACE_DIR set to `<runs_dir>/<run-id>-trace` so every node writes a trace; exit 2 on a missing profile or harness venv, a missing initiative.md, a dirty repo, or a live run of the same initiative
61
+ cox route launch decompose --idea FILE --initiative-id ID [--dry-run] start the harness detached; exit 2 on a missing profile, harness venv, or idea file
62
+ cox route launch cos [--dry-run] start the chief of staff detached: it reads intake and runs, dispatches within the bound, and consumes what it dispatched
63
+ cox setup a small terminal UI over setup doctor, setup install and cartridge init (needs a terminal)
64
+ cox setup doctor [--profile PATH] [--json] read-only: profile, paths, harness venv, cartridge, skills, provider, workspace — a table and an exit code
65
+ cox setup install --root DIR --team T --workspace DIR [--plugins] [--hook] [--force-profile] [--dry-run] venvs, agent-tools on PATH, the profile, optionally the provider plugin and a session-start hook; dry-run prints the plan
66
+ cox install --root DIR [--manifest PATH] [--provider NAME] [--with FLAG] [--team T] [--workspace DIR] [--dry-run] the plan over coxswain's manifest.toml: clone, fetch, skip or refuse per component, then setup_install, doctor, desktop; --dry-run only prints it, otherwise it runs each step and exits 0 only if every step ran clean
67
+ cox upgrade --root DIR [--manifest PATH] [--provider NAME] [--with FLAG] [--team T] [--workspace DIR] [--to VERSION] [--dry-run] same plan as install, but refuses (exit 2, naming the directory) if any present checkout is dirty; --to overrides every component's pinned tag for this run
68
+ cox versions [--root DIR] [--manifest PATH] pinned vs. installed tag per component, and status: ok, drift, missing, extra
69
+ ```
70
+
71
+ ## Maintainers
72
+
73
+ `cox dev` holds commands a maintainer of the coxswain repositories runs; nothing
74
+ here is needed to use Coxswain. `cox release` is a one-release alias that prints
75
+ `moved: use cox dev release` and exits 2.
76
+
77
+ ```
78
+ cox dev release VERSION [--dry-run] [--manifest PATH] [--root DIR] [--checkout NAME=PATH] [--umbrella PATH] the lockstep plan: tag every component, bump the manifest (skipped on a first cut of the declared version), notes, tag_self; exit 2 on refuse (bad semver, an existing tag, a lesser version, or a checkout that is not a ppfenning/coxswain remote); without --dry-run, executes only a first-cut plan — tags and pushes every component and the umbrella in turn, refusing before tagging anything if a checkout is dirty, off its default branch, the release note is missing, or the plan still carries a bump_manifest step (bump and commit the manifest by hand first)
79
+ ```
80
+
81
+ Every command that reads a record is pure over parsed data and unit-tested
82
+ against fixtures; every command that writes is dry-run unless `--apply`.
83
+
84
+ The `route` group reads one profile, `~/.config/agent-tools/profile.yaml`:
85
+ `team`, `cartridges_dir`, `skills_roots`, `provider_profile`, `harness_dir`,
86
+ `workspace_dir`, and `assume`, the gate answer detached runs are started
87
+ with. `--profile PATH` overrides the location for one command,
88
+ `AGENT_TOOLS_PROFILE` overrides it for a shell, and the default path is read
89
+ when neither is set. Without a profile, `route context` prints one line and
90
+ exits 0; `file`, `launch` and `status` exit 2 and name the path they looked
91
+ for. `--dry-run` on either `launch` prints the argv, the pidfile and the log
92
+ path and starts nothing.
93
+
94
+ ## Why this exists
95
+
96
+ One day of live epics found every defect by reading a usage file or a trace,
97
+ by hand, in a chief-of-staff's own turns: summing costs, counting Bash calls,
98
+ noticing a node read a 1,900-line file whole. Each of those readings cost
99
+ tokens and produced the same answer every time. They are functions now, and
100
+ the steward seat runs them.
@@ -0,0 +1,9 @@
1
+ """Deterministic tools the agent seats call instead of spending tokens.
2
+
3
+ The mould, every time: a pure core with no I/O, the filesystem and the network
4
+ at the edges, dry-run where a write is involved, and tests that never touch a
5
+ real run. Anything a seat would otherwise do by reading a file wholesale and
6
+ reasoning about it belongs here as a function that reads it and answers.
7
+ """
8
+
9
+ __version__ = "0.1.0"
@@ -0,0 +1,98 @@
1
+ """The effect runner for the cartridge editor: the only place that touches
2
+ a subprocess, writes a fragment, or rewrites the profile file.
3
+ `editor_model.py` decides which `Effect` to run; this module only runs the
4
+ one it is handed. No curses here — that edge is the next task.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ from pathlib import Path
13
+
14
+ from agent_tools.editor_model import Effect
15
+ from agent_tools.fragments import FragmentError, write_fragment
16
+ from agent_tools.route import parse_profile
17
+ from agent_tools.setup_screen import resolved_argv
18
+
19
+
20
+ def _write_fragment(effect: Effect, ctx: dict, write) -> dict:
21
+ team_dir = Path(ctx["cartridges_dir"]) / ctx["team"]
22
+ try:
23
+ write(team_dir, effect.payload["edits"])
24
+ except (FragmentError, OSError) as exc:
25
+ return {"provenance_error": str(exc)}
26
+ return {}
27
+
28
+
29
+ def _launch(argv: list[str], run):
30
+ """Runs `argv`; a binary that cannot launch is a value, never an exception
31
+ out of the loop (`setup_screen.run_action` makes the same promise)."""
32
+ try:
33
+ return run(argv, capture_output=True, text=True), None
34
+ except OSError as exc:
35
+ return None, f"{argv[0]}: {exc}"
36
+
37
+
38
+ def _run_probe(ctx: dict, run) -> dict:
39
+ result, failure = _launch(list(ctx["probe_argv"]), run)
40
+ if failure is not None:
41
+ return {"provenance_error": failure}
42
+ if result.returncode != 0:
43
+ return {"provenance_error": result.stderr or f"probe exited {result.returncode}"}
44
+ try:
45
+ return json.loads(result.stdout)
46
+ except json.JSONDecodeError as exc:
47
+ return {"provenance_error": str(exc)}
48
+
49
+
50
+ def _init_cartridge(effect: Effect, ctx: dict, run) -> dict:
51
+ name = effect.payload["name"]
52
+ venv_cartridge = f"{ctx.get('root', '')}/agent-cartridges/.venv/bin/cartridge"
53
+ argv = resolved_argv(["cartridge", "init", name, "--cartridges-dir", str(ctx["cartridges_dir"]),
54
+ "--extends", effect.payload["extends"]],
55
+ cartridge_on_path=shutil.which("cartridge") is not None,
56
+ venv_cartridge_exists=Path(venv_cartridge).exists(), venv_cartridge=venv_cartridge)
57
+ result, failure = _launch(argv, run)
58
+ if failure is not None:
59
+ return {"returncode": 127, "team": name, "output": failure}
60
+ output = "\n".join(((result.stdout or "") + (result.stderr or "")).splitlines()[-5:])
61
+ return {"returncode": result.returncode, "team": name, "output": output}
62
+
63
+
64
+ def _set_profile_team(effect: Effect, ctx: dict) -> dict:
65
+ team = effect.payload["team"]
66
+ path = Path(ctx["profile_path"])
67
+ lines = path.read_text().splitlines() if path.exists() else []
68
+ rewritten = [f"team: {team}" if line.startswith("team:") else line for line in lines]
69
+ if not any(line.startswith("team:") for line in lines):
70
+ rewritten.append(f"team: {team}")
71
+ path.write_text("\n".join(rewritten) + "\n")
72
+ return {"returncode": 0, "team": team}
73
+
74
+
75
+ def run_effect(effect: Effect, ctx: dict, *, run=subprocess.run, write=write_fragment) -> dict:
76
+ """Runs one `Effect`, chosen by the caller; every branch returns."""
77
+ if effect.kind == "write_fragment":
78
+ return _write_fragment(effect, ctx, write)
79
+ if effect.kind == "run_probe":
80
+ return _run_probe(ctx, run)
81
+ if effect.kind == "init_cartridge":
82
+ return _init_cartridge(effect, ctx, run)
83
+ if effect.kind == "set_profile_team":
84
+ return _set_profile_team(effect, ctx)
85
+ return {"provenance_error": f"unknown effect kind {effect.kind!r}"}
86
+
87
+
88
+ def _expanded(value, home: str):
89
+ if isinstance(value, str) and (value == "~" or value.startswith("~/")):
90
+ return home + value[1:]
91
+ if isinstance(value, list):
92
+ return [_expanded(item, home) for item in value]
93
+ return value
94
+
95
+
96
+ def profile_fields(text: str, home: str) -> dict:
97
+ """Profile fields, `~` expanded against `home` — never `$HOME`/`expanduser()`."""
98
+ return {key: _expanded(value, home) for key, value in parse_profile(text).items()}
@@ -0,0 +1,63 @@
1
+ """Clean up after a harness run: its worktrees and scratch branches, never its phase branches.
2
+
3
+ Pure planning, then one edge that runs git. Dry-run is the default; nothing is
4
+ removed unless asked. A phase branch (`epic/<initiative>/<phase>`) is what the
5
+ next run stacks on and is never touched; the task branches
6
+ (`agents/<run-id>/<task>`) and the scratch branches (`<phase>--<task>`) belong
7
+ to a finished run and go.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import subprocess
13
+ from collections.abc import Sequence
14
+ from pathlib import Path
15
+ from typing import Any
16
+
17
+ __all__ = ["apply_cleanup", "git_branches", "git_worktrees", "plan_cleanup"]
18
+
19
+
20
+ def _git(repo: Path, *args: str) -> str:
21
+ return subprocess.run(["git", "-C", str(repo), *args], capture_output=True, text=True).stdout
22
+
23
+
24
+ def git_worktrees(repo: Path) -> list[str]:
25
+ return [line.split(" ", 1)[1] for line in _git(repo, "worktree", "list", "--porcelain").splitlines() if line.startswith("worktree ")]
26
+
27
+
28
+ def git_branches(repo: Path) -> list[str]:
29
+ return [line.lstrip("*+ ").strip() for line in _git(repo, "branch", "--list").splitlines() if line.strip()]
30
+
31
+
32
+ def plan_cleanup(*, run_id: str, worktrees: Sequence[str], branches: Sequence[str], worktree_root: str) -> dict[str, Any]:
33
+ """Pure: what a run left behind. Phase branches are listed as kept, explicitly."""
34
+ root = str(Path(worktree_root).expanduser()) + "/" + run_id
35
+ doomed_worktrees = [w for w in worktrees if w.startswith(root + "/") or w == root]
36
+ doomed_branches = [b for b in branches if b.startswith(f"agents/{run_id}/") or ("--" in b and b.startswith("epic/"))]
37
+ kept = [b for b in branches if b.startswith("epic/") and "--" not in b]
38
+ return {"run_id": run_id, "worktrees": doomed_worktrees, "branches": doomed_branches, "kept_phase_branches": kept, "root": root}
39
+
40
+
41
+ def apply_cleanup(repo: Path | str, plan: dict[str, Any], *, dry_run: bool = True) -> list[str]:
42
+ """The edge. Returns what was (or would be) done, one line each."""
43
+ repo = Path(repo)
44
+ lines = []
45
+ for w in plan["worktrees"]:
46
+ lines.append(f"{'would remove' if dry_run else 'removed'} worktree {w}")
47
+ if not dry_run:
48
+ subprocess.run(["git", "-C", str(repo), "worktree", "remove", "--force", w], capture_output=True)
49
+ if not dry_run:
50
+ subprocess.run(["git", "-C", str(repo), "worktree", "prune"], capture_output=True)
51
+ for b in plan["branches"]:
52
+ lines.append(f"{'would delete' if dry_run else 'deleted'} branch {b}")
53
+ if not dry_run:
54
+ subprocess.run(["git", "-C", str(repo), "branch", "-D", b], capture_output=True)
55
+ root = Path(plan["root"])
56
+ if root.exists():
57
+ lines.append(f"{'would remove' if dry_run else 'removed'} directory {root}")
58
+ if not dry_run:
59
+ import shutil
60
+ shutil.rmtree(root, ignore_errors=True)
61
+ for b in plan["kept_phase_branches"]:
62
+ lines.append(f"kept phase branch {b}")
63
+ return lines