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.
Files changed (110) hide show
  1. backseat_harness-0.1.0/.gitignore +40 -0
  2. backseat_harness-0.1.0/LICENSE +21 -0
  3. backseat_harness-0.1.0/PKG-INFO +152 -0
  4. backseat_harness-0.1.0/README.md +133 -0
  5. backseat_harness-0.1.0/docs/media/web-chat-generate-task.png +0 -0
  6. backseat_harness-0.1.0/docs/media/web-mainpage-example.png +0 -0
  7. backseat_harness-0.1.0/docs/media/web-task-example.png +0 -0
  8. backseat_harness-0.1.0/docs/media/web-workspace-example.png +0 -0
  9. backseat_harness-0.1.0/publish-to-pypi.ps1 +38 -0
  10. backseat_harness-0.1.0/pyproject.toml +76 -0
  11. backseat_harness-0.1.0/src/backseat_harness/__init__.py +35 -0
  12. backseat_harness-0.1.0/src/backseat_harness/capture.py +38 -0
  13. backseat_harness-0.1.0/src/backseat_harness/checklist.py +138 -0
  14. backseat_harness-0.1.0/src/backseat_harness/cli.py +1437 -0
  15. backseat_harness-0.1.0/src/backseat_harness/compact.py +52 -0
  16. backseat_harness-0.1.0/src/backseat_harness/config.py +544 -0
  17. backseat_harness-0.1.0/src/backseat_harness/daemon.py +792 -0
  18. backseat_harness-0.1.0/src/backseat_harness/db.py +648 -0
  19. backseat_harness-0.1.0/src/backseat_harness/exitwatch.py +66 -0
  20. backseat_harness-0.1.0/src/backseat_harness/history.py +413 -0
  21. backseat_harness-0.1.0/src/backseat_harness/jobs.py +287 -0
  22. backseat_harness-0.1.0/src/backseat_harness/logs.py +110 -0
  23. backseat_harness-0.1.0/src/backseat_harness/loop.py +950 -0
  24. backseat_harness-0.1.0/src/backseat_harness/manager.py +702 -0
  25. backseat_harness-0.1.0/src/backseat_harness/manager_persona.md +56 -0
  26. backseat_harness-0.1.0/src/backseat_harness/marker.py +102 -0
  27. backseat_harness-0.1.0/src/backseat_harness/models.py +328 -0
  28. backseat_harness-0.1.0/src/backseat_harness/operator.py +405 -0
  29. backseat_harness-0.1.0/src/backseat_harness/operator_persona.md +236 -0
  30. backseat_harness-0.1.0/src/backseat_harness/platform.py +444 -0
  31. backseat_harness-0.1.0/src/backseat_harness/progress.py +168 -0
  32. backseat_harness-0.1.0/src/backseat_harness/providers/__init__.py +82 -0
  33. backseat_harness-0.1.0/src/backseat_harness/providers/base.py +134 -0
  34. backseat_harness-0.1.0/src/backseat_harness/providers/claude.py +119 -0
  35. backseat_harness-0.1.0/src/backseat_harness/providers/kiro.py +184 -0
  36. backseat_harness-0.1.0/src/backseat_harness/providers/opencode.py +125 -0
  37. backseat_harness-0.1.0/src/backseat_harness/py.typed +0 -0
  38. backseat_harness-0.1.0/src/backseat_harness/scheduler.py +324 -0
  39. backseat_harness-0.1.0/src/backseat_harness/spawn.py +430 -0
  40. backseat_harness-0.1.0/src/backseat_harness/storage.py +112 -0
  41. backseat_harness-0.1.0/src/backseat_harness/web/__init__.py +1 -0
  42. backseat_harness-0.1.0/src/backseat_harness/web/caddy.py +470 -0
  43. backseat_harness-0.1.0/src/backseat_harness/web/caddy_setup.py +574 -0
  44. backseat_harness-0.1.0/src/backseat_harness/web/cli.py +149 -0
  45. backseat_harness-0.1.0/src/backseat_harness/web/config.py +15 -0
  46. backseat_harness-0.1.0/src/backseat_harness/web/main.py +33 -0
  47. backseat_harness-0.1.0/src/backseat_harness/web/routes/__init__.py +1 -0
  48. backseat_harness-0.1.0/src/backseat_harness/web/routes/chat.py +239 -0
  49. backseat_harness-0.1.0/src/backseat_harness/web/routes/index.py +162 -0
  50. backseat_harness-0.1.0/src/backseat_harness/web/routes/search.py +210 -0
  51. backseat_harness-0.1.0/src/backseat_harness/web/routes/shell.py +292 -0
  52. backseat_harness-0.1.0/src/backseat_harness/web/routes/task.py +138 -0
  53. backseat_harness-0.1.0/src/backseat_harness/web/routes/workspace.py +135 -0
  54. backseat_harness-0.1.0/src/backseat_harness/web/security.py +97 -0
  55. backseat_harness-0.1.0/src/backseat_harness/web/services/__init__.py +1 -0
  56. backseat_harness-0.1.0/src/backseat_harness/web/services/chat_search.py +685 -0
  57. backseat_harness-0.1.0/src/backseat_harness/web/services/task.py +531 -0
  58. backseat_harness-0.1.0/src/backseat_harness/web/services/workspace.py +370 -0
  59. backseat_harness-0.1.0/src/backseat_harness/web/shell/manager.py +224 -0
  60. backseat_harness-0.1.0/src/backseat_harness/web/shell/pty_backend.py +245 -0
  61. backseat_harness-0.1.0/src/backseat_harness/web/templates/_chat_page.html +23 -0
  62. backseat_harness-0.1.0/src/backseat_harness/web/templates/_generate_task_button.html +6 -0
  63. backseat_harness-0.1.0/src/backseat_harness/web/templates/_generate_task_script.html +14 -0
  64. backseat_harness-0.1.0/src/backseat_harness/web/templates/_markdown_box.html +58 -0
  65. backseat_harness-0.1.0/src/backseat_harness/web/templates/base.html +558 -0
  66. backseat_harness-0.1.0/src/backseat_harness/web/templates/chat.html +85 -0
  67. backseat_harness-0.1.0/src/backseat_harness/web/templates/index.html +97 -0
  68. backseat_harness-0.1.0/src/backseat_harness/web/templates/search_results.html +22 -0
  69. backseat_harness-0.1.0/src/backseat_harness/web/templates/shell.html +222 -0
  70. backseat_harness-0.1.0/src/backseat_harness/web/templates/task.html +250 -0
  71. backseat_harness-0.1.0/src/backseat_harness/web/templates/transcript.html +12 -0
  72. backseat_harness-0.1.0/src/backseat_harness/web/templates/workspace.html +145 -0
  73. backseat_harness-0.1.0/tests/conftest.py +221 -0
  74. backseat_harness-0.1.0/tests/manual/live_claude_slots.py +316 -0
  75. backseat_harness-0.1.0/tests/manual/live_opencode_slots.py +376 -0
  76. backseat_harness-0.1.0/tests/manual/repro_step10.py +290 -0
  77. backseat_harness-0.1.0/tests/test_caddy.py +365 -0
  78. backseat_harness-0.1.0/tests/test_caddy_setup.py +403 -0
  79. backseat_harness-0.1.0/tests/test_capture.py +166 -0
  80. backseat_harness-0.1.0/tests/test_checklist.py +157 -0
  81. backseat_harness-0.1.0/tests/test_cli.py +1711 -0
  82. backseat_harness-0.1.0/tests/test_config.py +515 -0
  83. backseat_harness-0.1.0/tests/test_daemon.py +1207 -0
  84. backseat_harness-0.1.0/tests/test_e2e.py +242 -0
  85. backseat_harness-0.1.0/tests/test_exitwatch.py +40 -0
  86. backseat_harness-0.1.0/tests/test_generate_flow.py +233 -0
  87. backseat_harness-0.1.0/tests/test_history.py +306 -0
  88. backseat_harness-0.1.0/tests/test_jobs.py +329 -0
  89. backseat_harness-0.1.0/tests/test_logs.py +228 -0
  90. backseat_harness-0.1.0/tests/test_logs_live.py +130 -0
  91. backseat_harness-0.1.0/tests/test_loop.py +2365 -0
  92. backseat_harness-0.1.0/tests/test_manager.py +454 -0
  93. backseat_harness-0.1.0/tests/test_manager_promote.py +417 -0
  94. backseat_harness-0.1.0/tests/test_marker.py +64 -0
  95. backseat_harness-0.1.0/tests/test_operator.py +343 -0
  96. backseat_harness-0.1.0/tests/test_platform.py +352 -0
  97. backseat_harness-0.1.0/tests/test_progress.py +155 -0
  98. backseat_harness-0.1.0/tests/test_provider_claude.py +108 -0
  99. backseat_harness-0.1.0/tests/test_provider_kiro.py +332 -0
  100. backseat_harness-0.1.0/tests/test_provider_opencode.py +148 -0
  101. backseat_harness-0.1.0/tests/test_registry.py +882 -0
  102. backseat_harness-0.1.0/tests/test_scheduler.py +249 -0
  103. backseat_harness-0.1.0/tests/test_spawn.py +410 -0
  104. backseat_harness-0.1.0/tests/test_storage.py +230 -0
  105. backseat_harness-0.1.0/tests/test_task_visibility.py +253 -0
  106. backseat_harness-0.1.0/tests/test_web.py +4283 -0
  107. backseat_harness-0.1.0/tests/test_web_generate_task.py +338 -0
  108. backseat_harness-0.1.0/tests/test_web_generate_task_shell.py +179 -0
  109. backseat_harness-0.1.0/tests/test_web_new_chat.py +263 -0
  110. 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
+ ![backseat-harness web interface: sign-in and workspaces page](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-mainpage-example.png)
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
+ ![Workspace page](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-workspace-example.png)
80
+
81
+ 3. **Generate a task from the chat** — the ✨ Task button distils the finished chat into task requirements (`goal.md`).
82
+
83
+ ![Generate a task from the chat](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-chat-generate-task.png)
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
+ ![Task page: checklist, Run and Takeover](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-task-example.png)
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
+ ![backseat-harness web interface: sign-in and workspaces page](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-mainpage-example.png)
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
+ ![Workspace page](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-workspace-example.png)
61
+
62
+ 3. **Generate a task from the chat** — the ✨ Task button distils the finished chat into task requirements (`goal.md`).
63
+
64
+ ![Generate a task from the chat](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-chat-generate-task.png)
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
+ ![Task page: checklist, Run and Takeover](https://raw.githubusercontent.com/portablestew/backseat-harness/main/docs/media/web-task-example.png)
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).
@@ -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)