backseat-harness 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.
- backseat_harness-0.1.0/.gitignore +40 -0
- backseat_harness-0.1.0/LICENSE +21 -0
- backseat_harness-0.1.0/PKG-INFO +152 -0
- backseat_harness-0.1.0/README.md +133 -0
- backseat_harness-0.1.0/docs/media/web-chat-generate-task.png +0 -0
- backseat_harness-0.1.0/docs/media/web-mainpage-example.png +0 -0
- backseat_harness-0.1.0/docs/media/web-task-example.png +0 -0
- backseat_harness-0.1.0/docs/media/web-workspace-example.png +0 -0
- backseat_harness-0.1.0/publish-to-pypi.ps1 +38 -0
- backseat_harness-0.1.0/pyproject.toml +76 -0
- backseat_harness-0.1.0/src/backseat_harness/__init__.py +35 -0
- backseat_harness-0.1.0/src/backseat_harness/capture.py +38 -0
- backseat_harness-0.1.0/src/backseat_harness/checklist.py +138 -0
- backseat_harness-0.1.0/src/backseat_harness/cli.py +1437 -0
- backseat_harness-0.1.0/src/backseat_harness/compact.py +52 -0
- backseat_harness-0.1.0/src/backseat_harness/config.py +544 -0
- backseat_harness-0.1.0/src/backseat_harness/daemon.py +792 -0
- backseat_harness-0.1.0/src/backseat_harness/db.py +648 -0
- backseat_harness-0.1.0/src/backseat_harness/exitwatch.py +66 -0
- backseat_harness-0.1.0/src/backseat_harness/history.py +413 -0
- backseat_harness-0.1.0/src/backseat_harness/jobs.py +287 -0
- backseat_harness-0.1.0/src/backseat_harness/logs.py +110 -0
- backseat_harness-0.1.0/src/backseat_harness/loop.py +950 -0
- backseat_harness-0.1.0/src/backseat_harness/manager.py +702 -0
- backseat_harness-0.1.0/src/backseat_harness/manager_persona.md +56 -0
- backseat_harness-0.1.0/src/backseat_harness/marker.py +102 -0
- backseat_harness-0.1.0/src/backseat_harness/models.py +328 -0
- backseat_harness-0.1.0/src/backseat_harness/operator.py +405 -0
- backseat_harness-0.1.0/src/backseat_harness/operator_persona.md +236 -0
- backseat_harness-0.1.0/src/backseat_harness/platform.py +444 -0
- backseat_harness-0.1.0/src/backseat_harness/progress.py +168 -0
- backseat_harness-0.1.0/src/backseat_harness/providers/__init__.py +82 -0
- backseat_harness-0.1.0/src/backseat_harness/providers/base.py +134 -0
- backseat_harness-0.1.0/src/backseat_harness/providers/claude.py +119 -0
- backseat_harness-0.1.0/src/backseat_harness/providers/kiro.py +184 -0
- backseat_harness-0.1.0/src/backseat_harness/providers/opencode.py +125 -0
- backseat_harness-0.1.0/src/backseat_harness/py.typed +0 -0
- backseat_harness-0.1.0/src/backseat_harness/scheduler.py +324 -0
- backseat_harness-0.1.0/src/backseat_harness/spawn.py +430 -0
- backseat_harness-0.1.0/src/backseat_harness/storage.py +112 -0
- backseat_harness-0.1.0/src/backseat_harness/web/__init__.py +1 -0
- backseat_harness-0.1.0/src/backseat_harness/web/caddy.py +470 -0
- backseat_harness-0.1.0/src/backseat_harness/web/caddy_setup.py +574 -0
- backseat_harness-0.1.0/src/backseat_harness/web/cli.py +149 -0
- backseat_harness-0.1.0/src/backseat_harness/web/config.py +15 -0
- backseat_harness-0.1.0/src/backseat_harness/web/main.py +33 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/__init__.py +1 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/chat.py +239 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/index.py +162 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/search.py +210 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/shell.py +292 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/task.py +138 -0
- backseat_harness-0.1.0/src/backseat_harness/web/routes/workspace.py +135 -0
- backseat_harness-0.1.0/src/backseat_harness/web/security.py +97 -0
- backseat_harness-0.1.0/src/backseat_harness/web/services/__init__.py +1 -0
- backseat_harness-0.1.0/src/backseat_harness/web/services/chat_search.py +685 -0
- backseat_harness-0.1.0/src/backseat_harness/web/services/task.py +531 -0
- backseat_harness-0.1.0/src/backseat_harness/web/services/workspace.py +370 -0
- backseat_harness-0.1.0/src/backseat_harness/web/shell/manager.py +224 -0
- backseat_harness-0.1.0/src/backseat_harness/web/shell/pty_backend.py +245 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/_chat_page.html +23 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/_generate_task_button.html +6 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/_generate_task_script.html +14 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/_markdown_box.html +58 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/base.html +558 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/chat.html +85 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/index.html +97 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/search_results.html +22 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/shell.html +222 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/task.html +250 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/transcript.html +12 -0
- backseat_harness-0.1.0/src/backseat_harness/web/templates/workspace.html +145 -0
- backseat_harness-0.1.0/tests/conftest.py +221 -0
- backseat_harness-0.1.0/tests/manual/live_claude_slots.py +316 -0
- backseat_harness-0.1.0/tests/manual/live_opencode_slots.py +376 -0
- backseat_harness-0.1.0/tests/manual/repro_step10.py +290 -0
- backseat_harness-0.1.0/tests/test_caddy.py +365 -0
- backseat_harness-0.1.0/tests/test_caddy_setup.py +403 -0
- backseat_harness-0.1.0/tests/test_capture.py +166 -0
- backseat_harness-0.1.0/tests/test_checklist.py +157 -0
- backseat_harness-0.1.0/tests/test_cli.py +1711 -0
- backseat_harness-0.1.0/tests/test_config.py +515 -0
- backseat_harness-0.1.0/tests/test_daemon.py +1207 -0
- backseat_harness-0.1.0/tests/test_e2e.py +242 -0
- backseat_harness-0.1.0/tests/test_exitwatch.py +40 -0
- backseat_harness-0.1.0/tests/test_generate_flow.py +233 -0
- backseat_harness-0.1.0/tests/test_history.py +306 -0
- backseat_harness-0.1.0/tests/test_jobs.py +329 -0
- backseat_harness-0.1.0/tests/test_logs.py +228 -0
- backseat_harness-0.1.0/tests/test_logs_live.py +130 -0
- backseat_harness-0.1.0/tests/test_loop.py +2365 -0
- backseat_harness-0.1.0/tests/test_manager.py +454 -0
- backseat_harness-0.1.0/tests/test_manager_promote.py +417 -0
- backseat_harness-0.1.0/tests/test_marker.py +64 -0
- backseat_harness-0.1.0/tests/test_operator.py +343 -0
- backseat_harness-0.1.0/tests/test_platform.py +352 -0
- backseat_harness-0.1.0/tests/test_progress.py +155 -0
- backseat_harness-0.1.0/tests/test_provider_claude.py +108 -0
- backseat_harness-0.1.0/tests/test_provider_kiro.py +332 -0
- backseat_harness-0.1.0/tests/test_provider_opencode.py +148 -0
- backseat_harness-0.1.0/tests/test_registry.py +882 -0
- backseat_harness-0.1.0/tests/test_scheduler.py +249 -0
- backseat_harness-0.1.0/tests/test_spawn.py +410 -0
- backseat_harness-0.1.0/tests/test_storage.py +230 -0
- backseat_harness-0.1.0/tests/test_task_visibility.py +253 -0
- backseat_harness-0.1.0/tests/test_web.py +4283 -0
- backseat_harness-0.1.0/tests/test_web_generate_task.py +338 -0
- backseat_harness-0.1.0/tests/test_web_generate_task_shell.py +179 -0
- backseat_harness-0.1.0/tests/test_web_new_chat.py +263 -0
- backseat_harness-0.1.0/uv.lock +1879 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
.eggs/
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.whl
|
|
9
|
+
|
|
10
|
+
# Virtual envs / tooling
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.pyddock/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.ruff_cache/
|
|
16
|
+
.mypy_cache/
|
|
17
|
+
|
|
18
|
+
# uv
|
|
19
|
+
# (uv.lock is intentionally committed)
|
|
20
|
+
|
|
21
|
+
# Local tooling config (per-developer, not project artifacts)
|
|
22
|
+
.kiro/
|
|
23
|
+
.kilo/
|
|
24
|
+
.opencode/
|
|
25
|
+
.pyddock/
|
|
26
|
+
|
|
27
|
+
# Throwaway live-run scratch workspace (disposable, not a project artifact)
|
|
28
|
+
test_workspace/
|
|
29
|
+
|
|
30
|
+
# Task workspace (operator/agent working notes; never committed)
|
|
31
|
+
.backseat/
|
|
32
|
+
|
|
33
|
+
# Personal planning notes (local only)
|
|
34
|
+
TODO.md
|
|
35
|
+
|
|
36
|
+
# OS / editor
|
|
37
|
+
.DS_Store
|
|
38
|
+
Thumbs.db
|
|
39
|
+
# Playwright MCP browser artifacts (disposable snapshots/logs)
|
|
40
|
+
.playwright-mcp/
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anthony
|
|
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,152 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: backseat-harness
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Meta-harness that drives an off-the-shelf CLI agent one small step at a time, with an adversarial operator reviewing each turn.
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: chat-mother-forker>=0.2.1
|
|
9
|
+
Requires-Dist: fastapi
|
|
10
|
+
Requires-Dist: httpx
|
|
11
|
+
Requires-Dist: jinja2
|
|
12
|
+
Requires-Dist: psutil>=5.9
|
|
13
|
+
Requires-Dist: ptyprocess; sys_platform != 'win32'
|
|
14
|
+
Requires-Dist: python-multipart
|
|
15
|
+
Requires-Dist: pywinpty>=2.0; sys_platform == 'win32'
|
|
16
|
+
Requires-Dist: uvicorn[standard]
|
|
17
|
+
Requires-Dist: websockets
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# backseat-harness
|
|
21
|
+
|
|
22
|
+
Simple, single-task agent loops. _Buckle up, you're not at the wheel anymore._
|
|
23
|
+
|
|
24
|
+
## What it does
|
|
25
|
+
|
|
26
|
+
backseat-harness automates vibecoding toward a goal. You write the goal once, as the requirements for finishing a task. The harness drives an off-the-shelf CLI coding agent (opencode, claude, or kiro-cli) one small step at a time toward it. This is the "managed" agent session. After each step, a separate operator agent reviews the whole change, code, tests, and transcript, and writes the next prompt. That is the ordinary vibecoding loop (prompt, work, review, next prompt) that coding models are already trained to follow.
|
|
27
|
+
|
|
28
|
+
The managed agent keeps the long-horizon context across the whole task, instead of passing lossy summaries between agents. The operator is a fresh, adversarial reviewer: it starts each review cold and reads the full diff itself, along with a transcript of the managed agent session. Rather than a fixed pipeline of stages, it is a continuous, unstructured adversary. Each managed turn is a plain session of the provider's own CLI, so you can intervene and `takeover` a lane and drive it yourself at any time.
|
|
29
|
+
|
|
30
|
+
## Requirements
|
|
31
|
+
|
|
32
|
+
- Python 3.11 or newer.
|
|
33
|
+
- At least one provider CLI on your PATH, installed and signed in: `claude`, `kiro-cli`, or `opencode`. Each CLI uses its own login.
|
|
34
|
+
- Managed runs are auto-approved, since a non-interactive run has nobody to answer a prompt.
|
|
35
|
+
|
|
36
|
+
## Quick start
|
|
37
|
+
|
|
38
|
+
The quick start sets up [Caddy](https://caddyserver.com) in front of the web interface (HTTPS + a login prompt).
|
|
39
|
+
|
|
40
|
+
1. Install the tool:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install backseat-harness
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
2. Install caddy and write the caddyfile (caddy is installed with `winget` if it is missing; on Windows this must run from an elevated, Administrator shell):
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
backseat-harness setup-caddy # installs caddy, opens the site port, writes ~/.backseat/caddyfile
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
<!-- TODO: the Linux branch (apt/dnf/pacman + ufw/firewalld) and the LaunchAgent daemon install are untested -->
|
|
53
|
+
|
|
54
|
+
3. Install and start the daemon (caddy is expected on `PATH`):
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
backseat-harness daemon install
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
4. Open the local web page and sign in — `https://localhost:8443` (or the hostname you configured for Caddy).
|
|
61
|
+
|
|
62
|
+
## Upgrading
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv tool upgrade backseat-harness
|
|
66
|
+
backseat-harness daemon restart # restart the daemon on the new version
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Web interface
|
|
70
|
+
|
|
71
|
+
The main workflow, in brief:
|
|
72
|
+
|
|
73
|
+
1. **Register a workspace** — add a folder (it gets a `.backseat/` marker) and open it.
|
|
74
|
+
|
|
75
|
+

|
|
76
|
+
|
|
77
|
+
2. **Start a chat** — from the workspace web UI (New chat), your IDE, or the CLI of your choice. Discuss a plan for your idea interactively with the agent.
|
|
78
|
+
|
|
79
|
+

|
|
80
|
+
|
|
81
|
+
3. **Generate a task from the chat** — the ✨ Task button distils the finished chat into task requirements (`goal.md`).
|
|
82
|
+
|
|
83
|
+

|
|
84
|
+
|
|
85
|
+
4. **The task completes autonomously** — the daemon runs it to the goal: managed agent step, operator review, next step.
|
|
86
|
+
|
|
87
|
+
5. **Amend or takeover** — after a run, add a brief to guide/redirect it, or Takeover to drive the managed session yourself. On exit the task continues autonomously.
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
For simpler requests, skip the chat: **add a task directly** with a name and goal, and let it run (fire-and-forget).
|
|
92
|
+
|
|
93
|
+
## Configuration
|
|
94
|
+
|
|
95
|
+
Two optional files under `~/.backseat/`:
|
|
96
|
+
|
|
97
|
+
**`config.toml`** — providers, models, timeouts, loop budgets, web port. Layered: `~/.backseat/config.toml`, then `.backseat/config.toml` in the workspace, then `.backseat/tasks/<slug>/config.toml`; higher layers win. See [`examples/.backseat/config.toml`](examples/.backseat/config.toml).
|
|
98
|
+
|
|
99
|
+
**`caddyfile`** — when present, the web interface is served over HTTPS with a login prompt (`auto` mode). A minimal file:
|
|
100
|
+
|
|
101
|
+
<!-- caddyfile-example:start -->
|
|
102
|
+
```
|
|
103
|
+
site.lan:8443 {
|
|
104
|
+
tls internal
|
|
105
|
+
basic_auth {
|
|
106
|
+
alice <bcrypt hash>
|
|
107
|
+
}
|
|
108
|
+
reverse_proxy 127.0.0.1:8090
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
<!-- caddyfile-example:end -->
|
|
112
|
+
|
|
113
|
+
Replace `<bcrypt hash>` with `caddy hash-password`; the `reverse_proxy` port must match `[web] port` (default 8090). `backseat-harness setup-caddy` writes this file for you. The web app itself listens on `127.0.0.1` only.
|
|
114
|
+
|
|
115
|
+
Choose the serving mode with `--mode` on `web`, or `--web` on the daemon: `auto` (caddy when the caddyfile exists), `direct` (plain http only), `caddy` (caddyfile required), `none` (CLI only).
|
|
116
|
+
|
|
117
|
+
## CLI
|
|
118
|
+
|
|
119
|
+
Everything the web does is also a subcommand:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
backseat-harness init <slug> [goal] # scaffold a task lane
|
|
123
|
+
backseat-harness run <slug> "goal" # create and drive a task
|
|
124
|
+
backseat-harness run <slug> "brief" # resume with an instruction for this run
|
|
125
|
+
backseat-harness takeover <slug> # drive the managed session yourself, then resume
|
|
126
|
+
backseat-harness generate-task <provider>:<conversation_id> # task from a chat
|
|
127
|
+
backseat-harness status <slug>
|
|
128
|
+
backseat-harness list
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The daemon runs ready lanes unattended, one at a time per workspace:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
backseat-harness register-workspace /path/to/workspace
|
|
135
|
+
backseat-harness daemon install # register and start (Task Scheduler / systemd / LaunchAgent)
|
|
136
|
+
backseat-harness daemon status|restart|stop
|
|
137
|
+
backseat-harness daemon install --uninstall
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
Daemon output goes to `~/.backseat/daemon.log`; `daemon run --web` serves the web interface from the same process.
|
|
141
|
+
|
|
142
|
+
## Development
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
uv sync
|
|
146
|
+
uv run ruff check .
|
|
147
|
+
uv run pytest
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## License
|
|
151
|
+
|
|
152
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# backseat-harness
|
|
2
|
+
|
|
3
|
+
Simple, single-task agent loops. _Buckle up, you're not at the wheel anymore._
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
backseat-harness automates vibecoding toward a goal. You write the goal once, as the requirements for finishing a task. The harness drives an off-the-shelf CLI coding agent (opencode, claude, or kiro-cli) one small step at a time toward it. This is the "managed" agent session. After each step, a separate operator agent reviews the whole change, code, tests, and transcript, and writes the next prompt. That is the ordinary vibecoding loop (prompt, work, review, next prompt) that coding models are already trained to follow.
|
|
8
|
+
|
|
9
|
+
The managed agent keeps the long-horizon context across the whole task, instead of passing lossy summaries between agents. The operator is a fresh, adversarial reviewer: it starts each review cold and reads the full diff itself, along with a transcript of the managed agent session. Rather than a fixed pipeline of stages, it is a continuous, unstructured adversary. Each managed turn is a plain session of the provider's own CLI, so you can intervene and `takeover` a lane and drive it yourself at any time.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python 3.11 or newer.
|
|
14
|
+
- At least one provider CLI on your PATH, installed and signed in: `claude`, `kiro-cli`, or `opencode`. Each CLI uses its own login.
|
|
15
|
+
- Managed runs are auto-approved, since a non-interactive run has nobody to answer a prompt.
|
|
16
|
+
|
|
17
|
+
## Quick start
|
|
18
|
+
|
|
19
|
+
The quick start sets up [Caddy](https://caddyserver.com) in front of the web interface (HTTPS + a login prompt).
|
|
20
|
+
|
|
21
|
+
1. Install the tool:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
uv tool install backseat-harness
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
2. Install caddy and write the caddyfile (caddy is installed with `winget` if it is missing; on Windows this must run from an elevated, Administrator shell):
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
backseat-harness setup-caddy # installs caddy, opens the site port, writes ~/.backseat/caddyfile
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
<!-- TODO: the Linux branch (apt/dnf/pacman + ufw/firewalld) and the LaunchAgent daemon install are untested -->
|
|
34
|
+
|
|
35
|
+
3. Install and start the daemon (caddy is expected on `PATH`):
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
backseat-harness daemon install
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
4. Open the local web page and sign in — `https://localhost:8443` (or the hostname you configured for Caddy).
|
|
42
|
+
|
|
43
|
+
## Upgrading
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
uv tool upgrade backseat-harness
|
|
47
|
+
backseat-harness daemon restart # restart the daemon on the new version
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Web interface
|
|
51
|
+
|
|
52
|
+
The main workflow, in brief:
|
|
53
|
+
|
|
54
|
+
1. **Register a workspace** — add a folder (it gets a `.backseat/` marker) and open it.
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
2. **Start a chat** — from the workspace web UI (New chat), your IDE, or the CLI of your choice. Discuss a plan for your idea interactively with the agent.
|
|
59
|
+
|
|
60
|
+

|
|
61
|
+
|
|
62
|
+
3. **Generate a task from the chat** — the ✨ Task button distils the finished chat into task requirements (`goal.md`).
|
|
63
|
+
|
|
64
|
+

|
|
65
|
+
|
|
66
|
+
4. **The task completes autonomously** — the daemon runs it to the goal: managed agent step, operator review, next step.
|
|
67
|
+
|
|
68
|
+
5. **Amend or takeover** — after a run, add a brief to guide/redirect it, or Takeover to drive the managed session yourself. On exit the task continues autonomously.
|
|
69
|
+
|
|
70
|
+

|
|
71
|
+
|
|
72
|
+
For simpler requests, skip the chat: **add a task directly** with a name and goal, and let it run (fire-and-forget).
|
|
73
|
+
|
|
74
|
+
## Configuration
|
|
75
|
+
|
|
76
|
+
Two optional files under `~/.backseat/`:
|
|
77
|
+
|
|
78
|
+
**`config.toml`** — providers, models, timeouts, loop budgets, web port. Layered: `~/.backseat/config.toml`, then `.backseat/config.toml` in the workspace, then `.backseat/tasks/<slug>/config.toml`; higher layers win. See [`examples/.backseat/config.toml`](examples/.backseat/config.toml).
|
|
79
|
+
|
|
80
|
+
**`caddyfile`** — when present, the web interface is served over HTTPS with a login prompt (`auto` mode). A minimal file:
|
|
81
|
+
|
|
82
|
+
<!-- caddyfile-example:start -->
|
|
83
|
+
```
|
|
84
|
+
site.lan:8443 {
|
|
85
|
+
tls internal
|
|
86
|
+
basic_auth {
|
|
87
|
+
alice <bcrypt hash>
|
|
88
|
+
}
|
|
89
|
+
reverse_proxy 127.0.0.1:8090
|
|
90
|
+
}
|
|
91
|
+
```
|
|
92
|
+
<!-- caddyfile-example:end -->
|
|
93
|
+
|
|
94
|
+
Replace `<bcrypt hash>` with `caddy hash-password`; the `reverse_proxy` port must match `[web] port` (default 8090). `backseat-harness setup-caddy` writes this file for you. The web app itself listens on `127.0.0.1` only.
|
|
95
|
+
|
|
96
|
+
Choose the serving mode with `--mode` on `web`, or `--web` on the daemon: `auto` (caddy when the caddyfile exists), `direct` (plain http only), `caddy` (caddyfile required), `none` (CLI only).
|
|
97
|
+
|
|
98
|
+
## CLI
|
|
99
|
+
|
|
100
|
+
Everything the web does is also a subcommand:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
backseat-harness init <slug> [goal] # scaffold a task lane
|
|
104
|
+
backseat-harness run <slug> "goal" # create and drive a task
|
|
105
|
+
backseat-harness run <slug> "brief" # resume with an instruction for this run
|
|
106
|
+
backseat-harness takeover <slug> # drive the managed session yourself, then resume
|
|
107
|
+
backseat-harness generate-task <provider>:<conversation_id> # task from a chat
|
|
108
|
+
backseat-harness status <slug>
|
|
109
|
+
backseat-harness list
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
The daemon runs ready lanes unattended, one at a time per workspace:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
backseat-harness register-workspace /path/to/workspace
|
|
116
|
+
backseat-harness daemon install # register and start (Task Scheduler / systemd / LaunchAgent)
|
|
117
|
+
backseat-harness daemon status|restart|stop
|
|
118
|
+
backseat-harness daemon install --uninstall
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Daemon output goes to `~/.backseat/daemon.log`; `daemon run --web` serves the web interface from the same process.
|
|
122
|
+
|
|
123
|
+
## Development
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
uv sync
|
|
127
|
+
uv run ruff check .
|
|
128
|
+
uv run pytest
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## License
|
|
132
|
+
|
|
133
|
+
MIT. See [LICENSE](LICENSE).
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# publish-to-pypi.ps1 - Publish to PyPI: clean, build, upload to testpypi, then pypi.
|
|
2
|
+
|
|
3
|
+
$ErrorActionPreference = "Stop"
|
|
4
|
+
|
|
5
|
+
# Determine project name from the git repo root
|
|
6
|
+
$repoRoot = git rev-parse --show-toplevel 2>$null
|
|
7
|
+
if ($LASTEXITCODE -ne 0 -or -not $repoRoot) { throw "Not inside a git repository" }
|
|
8
|
+
$ProjectName = Split-Path -Leaf $repoRoot
|
|
9
|
+
Write-Host "Project name: $ProjectName"
|
|
10
|
+
|
|
11
|
+
# Clean dist
|
|
12
|
+
Write-Host "Cleaning dist/..."
|
|
13
|
+
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
|
|
14
|
+
|
|
15
|
+
# Build
|
|
16
|
+
Write-Host "> uv build"
|
|
17
|
+
uv build
|
|
18
|
+
if ($LASTEXITCODE -ne 0) { throw "Build failed" }
|
|
19
|
+
|
|
20
|
+
# Upload to testpypi
|
|
21
|
+
Write-Host "> uvx twine upload --repository testpypi dist/*"
|
|
22
|
+
uvx twine upload --repository testpypi dist/*
|
|
23
|
+
if ($LASTEXITCODE -ne 0) { throw "testpypi upload failed" }
|
|
24
|
+
|
|
25
|
+
# Hint for testing
|
|
26
|
+
Write-Host "`nTo test from testpypi:"
|
|
27
|
+
Write-Host " uvx --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple/ $ProjectName"
|
|
28
|
+
|
|
29
|
+
# Pause for confirmation
|
|
30
|
+
$response = Read-Host "`nType 'y' to upload to pypi (anything else aborts)"
|
|
31
|
+
if ($response -ne 'y') { Write-Host "Aborted."; exit 0 }
|
|
32
|
+
|
|
33
|
+
# Upload to pypi
|
|
34
|
+
Write-Host "> uvx twine upload --repository $ProjectName dist/*"
|
|
35
|
+
uvx twine upload --repository $ProjectName dist/*
|
|
36
|
+
if ($LASTEXITCODE -ne 0) { throw "pypi upload failed" }
|
|
37
|
+
|
|
38
|
+
Write-Host "`nDone!"
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "backseat-harness"
|
|
7
|
+
description = "Meta-harness that drives an off-the-shelf CLI agent one small step at a time, with an adversarial operator reviewing each turn."
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
license = "MIT"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
dependencies = [
|
|
13
|
+
# Chat history access: search/fork/checkpoint of managed sessions.
|
|
14
|
+
# Compaction-by-fork and operator review both depend on it, so it is core.
|
|
15
|
+
# 0.2.0 first shipped the public library API (fork_chat / find_chats /
|
|
16
|
+
# ChatRef.size_bytes); 0.2.1 adds the public load_chat that turn capture uses
|
|
17
|
+
# for a full, untruncated transcript.
|
|
18
|
+
"chat-mother-forker>=0.2.1",
|
|
19
|
+
# Pid liveliness for the process registry (~/.backseat/db.sqlite): a stored
|
|
20
|
+
# pid is only trustworthy alongside psutil's create_time() to catch pid
|
|
21
|
+
# reuse, which is what the task-row claim and orphan sweep check against.
|
|
22
|
+
"psutil>=5.9",
|
|
23
|
+
# Web interface (task web-interface §3 scaffold).
|
|
24
|
+
"fastapi",
|
|
25
|
+
"uvicorn[standard]",
|
|
26
|
+
"websockets",
|
|
27
|
+
"jinja2",
|
|
28
|
+
"httpx",
|
|
29
|
+
"python-multipart",
|
|
30
|
+
"pywinpty>=2.0; sys_platform=='win32'",
|
|
31
|
+
"ptyprocess; sys_platform!='win32'",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
backseat-harness = "backseat_harness.cli:main"
|
|
36
|
+
|
|
37
|
+
# Version lives in exactly one place: src/backseat_harness/__init__.py (__version__).
|
|
38
|
+
# Hatchling reads it from there for both `uv build` and PyPI publishing.
|
|
39
|
+
[tool.hatch.version]
|
|
40
|
+
path = "src/backseat_harness/__init__.py"
|
|
41
|
+
|
|
42
|
+
[tool.hatch.build.targets.wheel]
|
|
43
|
+
packages = ["src/backseat_harness"]
|
|
44
|
+
|
|
45
|
+
# The sdist is for building the wheel, so it carries pyproject.toml, README,
|
|
46
|
+
# LICENSE and src/ -- not the repo's dev tooling (trunk), agent config, caches,
|
|
47
|
+
# or local harness state.
|
|
48
|
+
[tool.hatch.build.targets.sdist]
|
|
49
|
+
exclude = [
|
|
50
|
+
"/.backseat",
|
|
51
|
+
"/.kilo",
|
|
52
|
+
"/.kiro",
|
|
53
|
+
"/.opencode",
|
|
54
|
+
"/.playwright-mcp",
|
|
55
|
+
"/.pyddock",
|
|
56
|
+
"/.pytest_cache",
|
|
57
|
+
"/.ruff_cache",
|
|
58
|
+
"/.trunk",
|
|
59
|
+
"/test_workspace",
|
|
60
|
+
]
|
|
61
|
+
|
|
62
|
+
[dependency-groups]
|
|
63
|
+
dev = [
|
|
64
|
+
"pytest>=8.0.0",
|
|
65
|
+
"ruff>=0.6.0",
|
|
66
|
+
]
|
|
67
|
+
|
|
68
|
+
[tool.pytest.ini_options]
|
|
69
|
+
testpaths = ["tests"]
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 100
|
|
73
|
+
target-version = "py310"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.lint]
|
|
76
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""backseat-harness: drive an off-the-shelf CLI agent one small step at a time.
|
|
2
|
+
|
|
3
|
+
A meta-harness that spawns a managed CLI agent (Claude Code, opencode, kiro-cli),
|
|
4
|
+
lets it take a single turn, then hands a fresh adversarial *operator* the
|
|
5
|
+
transcript plus a durable checklist to review progress and decide the next prompt.
|
|
6
|
+
|
|
7
|
+
__version__ is the single source of truth for the package version. pyproject.toml
|
|
8
|
+
reads it from here (via tool.hatch.version), so this is the only place to edit
|
|
9
|
+
before a release.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
from backseat_harness.models import (
|
|
15
|
+
Binding,
|
|
16
|
+
Generation,
|
|
17
|
+
LedgerEntry,
|
|
18
|
+
TaskResult,
|
|
19
|
+
TaskState,
|
|
20
|
+
TurnOutcome,
|
|
21
|
+
Verdict,
|
|
22
|
+
VerdictKind,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"__version__",
|
|
27
|
+
"Binding",
|
|
28
|
+
"Generation",
|
|
29
|
+
"LedgerEntry",
|
|
30
|
+
"TaskResult",
|
|
31
|
+
"TaskState",
|
|
32
|
+
"TurnOutcome",
|
|
33
|
+
"Verdict",
|
|
34
|
+
"VerdictKind",
|
|
35
|
+
]
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Full-session transcript capture: a reviewable artifact of the managed work.
|
|
2
|
+
|
|
3
|
+
After each managed turn the harness overwrites ``.backseat/tasks/<slug>/transcript.md``
|
|
4
|
+
so the operator (and a human) can inspect what the agent actually did --
|
|
5
|
+
including full, untruncated tool-call results, which the middle-truncated
|
|
6
|
+
compaction fork deliberately drops.
|
|
7
|
+
|
|
8
|
+
The transcript comes from chat-mother-forker's ``load_chat`` (via history.py):
|
|
9
|
+
the *complete* session for this task's marker, already annotated with turn
|
|
10
|
+
headers and ``TOOL_CALL``/``TOOL_RESULT`` labels. It is a single file rewritten
|
|
11
|
+
each turn; because compaction forks each generation into a fresh session, the
|
|
12
|
+
loaded session naturally tracks the current generation's work.
|
|
13
|
+
|
|
14
|
+
The captured text is run through ``marker.redact`` before it is written, for the
|
|
15
|
+
same reason the operator fork is: if the operator reads and quotes the file, an
|
|
16
|
+
unredacted root UUID would make the operator's own stored session match the
|
|
17
|
+
task's marker search -- and the newest match wins, pointing sizing and
|
|
18
|
+
compaction at the operator instead of the managed agent. The generation ordinal
|
|
19
|
+
stays visible.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from backseat_harness import history
|
|
25
|
+
from backseat_harness import marker as marker_mod
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def capture(search: str, history_provider: str, root_uuid: str) -> str | None:
|
|
29
|
+
"""Load the managed session's full transcript, redacted and ready to write.
|
|
30
|
+
|
|
31
|
+
Returns the string to write, or ``None`` when there is nothing to capture
|
|
32
|
+
yet (no matching session, or an empty one). Callers treat ``None`` as "skip
|
|
33
|
+
writing a file this turn".
|
|
34
|
+
"""
|
|
35
|
+
transcript = history.load_full_transcript(search, history_provider)
|
|
36
|
+
if not transcript.strip():
|
|
37
|
+
return None
|
|
38
|
+
return marker_mod.redact(transcript, root_uuid)
|