@jmoyers/harness 0.1.0

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 (214) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +145 -0
  3. package/native/ptyd/Cargo.lock +16 -0
  4. package/native/ptyd/Cargo.toml +7 -0
  5. package/native/ptyd/src/main.rs +257 -0
  6. package/package.json +90 -0
  7. package/scripts/build-ptyd.sh +73 -0
  8. package/scripts/control-plane-daemon.ts +277 -0
  9. package/scripts/cursor-hook-relay.ts +82 -0
  10. package/scripts/harness-animate.ts +469 -0
  11. package/scripts/harness-bin.js +77 -0
  12. package/scripts/harness-core.ts +1 -0
  13. package/scripts/harness-inspector.ts +439 -0
  14. package/scripts/harness.ts +2493 -0
  15. package/src/adapters/agent-session-state.ts +390 -0
  16. package/src/cli/gateway-record.ts +173 -0
  17. package/src/codex/live-session.ts +872 -0
  18. package/src/config/config-core.ts +1359 -0
  19. package/src/config/secrets-core.ts +170 -0
  20. package/src/control-plane/agent-realtime-api.ts +2441 -0
  21. package/src/control-plane/codex-session-stream.ts +392 -0
  22. package/src/control-plane/codex-telemetry.ts +1325 -0
  23. package/src/control-plane/lifecycle-hooks.ts +706 -0
  24. package/src/control-plane/session-summary.ts +380 -0
  25. package/src/control-plane/status/agent-status-reducer.ts +21 -0
  26. package/src/control-plane/status/reducer-base.ts +170 -0
  27. package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
  28. package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
  29. package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
  30. package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
  31. package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
  32. package/src/control-plane/status/session-status-engine.ts +76 -0
  33. package/src/control-plane/stream-client.ts +396 -0
  34. package/src/control-plane/stream-command-parser.ts +1673 -0
  35. package/src/control-plane/stream-protocol.ts +1808 -0
  36. package/src/control-plane/stream-server-background.ts +486 -0
  37. package/src/control-plane/stream-server-command.ts +2557 -0
  38. package/src/control-plane/stream-server-connection.ts +234 -0
  39. package/src/control-plane/stream-server-observed-filter.ts +112 -0
  40. package/src/control-plane/stream-server-session-runtime.ts +566 -0
  41. package/src/control-plane/stream-server-state-store.ts +15 -0
  42. package/src/control-plane/stream-server.ts +3192 -0
  43. package/src/cursor/managed-hooks.ts +282 -0
  44. package/src/domain/conversations.ts +414 -0
  45. package/src/domain/directories.ts +78 -0
  46. package/src/domain/repositories.ts +123 -0
  47. package/src/domain/tasks.ts +148 -0
  48. package/src/domain/workspace.ts +156 -0
  49. package/src/events/normalized-events.ts +124 -0
  50. package/src/mux/ansi-integrity.ts +103 -0
  51. package/src/mux/control-plane-op-queue.ts +212 -0
  52. package/src/mux/conversation-rail.ts +339 -0
  53. package/src/mux/double-click.ts +78 -0
  54. package/src/mux/dual-pane-core.ts +435 -0
  55. package/src/mux/harness-core-ui.ts +817 -0
  56. package/src/mux/input-shortcuts.ts +667 -0
  57. package/src/mux/live-mux/actions-conversation.ts +344 -0
  58. package/src/mux/live-mux/actions-repository.ts +246 -0
  59. package/src/mux/live-mux/actions-task.ts +115 -0
  60. package/src/mux/live-mux/args.ts +142 -0
  61. package/src/mux/live-mux/command-menu.ts +298 -0
  62. package/src/mux/live-mux/control-plane-records.ts +546 -0
  63. package/src/mux/live-mux/conversation-state.ts +188 -0
  64. package/src/mux/live-mux/directory-resolution.ts +34 -0
  65. package/src/mux/live-mux/event-mapping.ts +96 -0
  66. package/src/mux/live-mux/gateway-profiler.ts +152 -0
  67. package/src/mux/live-mux/gateway-render-trace.ts +177 -0
  68. package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
  69. package/src/mux/live-mux/git-parsing.ts +131 -0
  70. package/src/mux/live-mux/git-snapshot.ts +263 -0
  71. package/src/mux/live-mux/git-state.ts +136 -0
  72. package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
  73. package/src/mux/live-mux/home-pane-actions.ts +58 -0
  74. package/src/mux/live-mux/home-pane-drop.ts +44 -0
  75. package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
  76. package/src/mux/live-mux/home-pane-pointer.ts +96 -0
  77. package/src/mux/live-mux/input-forwarding.ts +112 -0
  78. package/src/mux/live-mux/layout.ts +30 -0
  79. package/src/mux/live-mux/left-nav-activation.ts +103 -0
  80. package/src/mux/live-mux/left-nav.ts +85 -0
  81. package/src/mux/live-mux/left-rail-actions.ts +118 -0
  82. package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
  83. package/src/mux/live-mux/left-rail-pointer.ts +74 -0
  84. package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
  85. package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
  86. package/src/mux/live-mux/modal-input-reducers.ts +94 -0
  87. package/src/mux/live-mux/modal-overlays.ts +287 -0
  88. package/src/mux/live-mux/modal-pointer.ts +70 -0
  89. package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
  90. package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
  91. package/src/mux/live-mux/observed-stream.ts +87 -0
  92. package/src/mux/live-mux/palette-parsing.ts +128 -0
  93. package/src/mux/live-mux/pointer-routing.ts +108 -0
  94. package/src/mux/live-mux/process-usage.ts +53 -0
  95. package/src/mux/live-mux/project-pane-pointer.ts +44 -0
  96. package/src/mux/live-mux/rail-layout.ts +244 -0
  97. package/src/mux/live-mux/render-trace-analysis.ts +213 -0
  98. package/src/mux/live-mux/render-trace-state.ts +84 -0
  99. package/src/mux/live-mux/repository-folding.ts +207 -0
  100. package/src/mux/live-mux/runtime-shutdown.ts +51 -0
  101. package/src/mux/live-mux/selection.ts +411 -0
  102. package/src/mux/live-mux/startup-utils.ts +187 -0
  103. package/src/mux/live-mux/status-timeline-state.ts +82 -0
  104. package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
  105. package/src/mux/live-mux/terminal-palette.ts +79 -0
  106. package/src/mux/new-thread-prompt.ts +165 -0
  107. package/src/mux/project-tree.ts +295 -0
  108. package/src/mux/render-frame.ts +113 -0
  109. package/src/mux/runtime-wiring.ts +185 -0
  110. package/src/mux/selector-index.ts +160 -0
  111. package/src/mux/startup-sequencer.ts +238 -0
  112. package/src/mux/task-composer.ts +289 -0
  113. package/src/mux/task-focused-pane.ts +417 -0
  114. package/src/mux/task-screen-keybindings.ts +539 -0
  115. package/src/mux/terminal-input-modes.ts +35 -0
  116. package/src/mux/workspace-path.ts +55 -0
  117. package/src/mux/workspace-rail-model.ts +701 -0
  118. package/src/mux/workspace-rail.ts +247 -0
  119. package/src/perf/perf-core.ts +307 -0
  120. package/src/pty/pty_host.ts +217 -0
  121. package/src/pty/session-broker.ts +158 -0
  122. package/src/recording/terminal-recording.ts +383 -0
  123. package/src/services/control-plane.ts +567 -0
  124. package/src/services/conversation-lifecycle.ts +176 -0
  125. package/src/services/conversation-startup-hydration.ts +47 -0
  126. package/src/services/directory-hydration.ts +49 -0
  127. package/src/services/event-persistence.ts +104 -0
  128. package/src/services/mux-ui-state-persistence.ts +82 -0
  129. package/src/services/output-load-sampler.ts +231 -0
  130. package/src/services/process-usage-refresh.ts +88 -0
  131. package/src/services/recording.ts +75 -0
  132. package/src/services/render-trace-recorder.ts +177 -0
  133. package/src/services/runtime-control-actions.ts +123 -0
  134. package/src/services/runtime-control-plane-ops.ts +131 -0
  135. package/src/services/runtime-conversation-actions.ts +113 -0
  136. package/src/services/runtime-conversation-activation.ts +78 -0
  137. package/src/services/runtime-conversation-starter.ts +171 -0
  138. package/src/services/runtime-conversation-title-edit.ts +149 -0
  139. package/src/services/runtime-directory-actions.ts +164 -0
  140. package/src/services/runtime-envelope-handler.ts +198 -0
  141. package/src/services/runtime-git-state.ts +92 -0
  142. package/src/services/runtime-input-pipeline.ts +50 -0
  143. package/src/services/runtime-input-router.ts +202 -0
  144. package/src/services/runtime-layout-resize.ts +236 -0
  145. package/src/services/runtime-left-rail-render.ts +159 -0
  146. package/src/services/runtime-main-pane-input.ts +230 -0
  147. package/src/services/runtime-modal-input.ts +119 -0
  148. package/src/services/runtime-navigation-input.ts +207 -0
  149. package/src/services/runtime-process-wiring.ts +68 -0
  150. package/src/services/runtime-rail-input.ts +287 -0
  151. package/src/services/runtime-render-flush.ts +146 -0
  152. package/src/services/runtime-render-lifecycle.ts +104 -0
  153. package/src/services/runtime-render-orchestrator.ts +108 -0
  154. package/src/services/runtime-render-pipeline.ts +167 -0
  155. package/src/services/runtime-render-state.ts +72 -0
  156. package/src/services/runtime-repository-actions.ts +197 -0
  157. package/src/services/runtime-right-pane-render.ts +132 -0
  158. package/src/services/runtime-shutdown.ts +79 -0
  159. package/src/services/runtime-stream-subscriptions.ts +56 -0
  160. package/src/services/runtime-task-composer-persistence.ts +139 -0
  161. package/src/services/runtime-task-editor-actions.ts +83 -0
  162. package/src/services/runtime-task-pane-actions.ts +198 -0
  163. package/src/services/runtime-task-pane-shortcuts.ts +189 -0
  164. package/src/services/runtime-task-pane.ts +62 -0
  165. package/src/services/runtime-workspace-actions.ts +153 -0
  166. package/src/services/runtime-workspace-observed-events.ts +190 -0
  167. package/src/services/session-projection-instrumentation.ts +190 -0
  168. package/src/services/startup-background-probe.ts +91 -0
  169. package/src/services/startup-background-resume.ts +65 -0
  170. package/src/services/startup-orchestrator.ts +166 -0
  171. package/src/services/startup-output-tracker.ts +54 -0
  172. package/src/services/startup-paint-tracker.ts +115 -0
  173. package/src/services/startup-persisted-conversation-queue.ts +45 -0
  174. package/src/services/startup-settled-gate.ts +67 -0
  175. package/src/services/startup-shutdown.ts +53 -0
  176. package/src/services/startup-span-tracker.ts +77 -0
  177. package/src/services/startup-state-hydration.ts +94 -0
  178. package/src/services/startup-visibility.ts +35 -0
  179. package/src/services/status-timeline-recorder.ts +144 -0
  180. package/src/services/task-pane-selection-actions.ts +153 -0
  181. package/src/services/task-planning-hydration.ts +58 -0
  182. package/src/services/task-planning-observed-events.ts +89 -0
  183. package/src/services/workspace-observed-events.ts +113 -0
  184. package/src/store/control-plane-store-normalize.ts +760 -0
  185. package/src/store/control-plane-store-types.ts +224 -0
  186. package/src/store/control-plane-store.ts +2951 -0
  187. package/src/store/event-store.ts +253 -0
  188. package/src/store/sqlite.ts +81 -0
  189. package/src/terminal/compat-matrix.ts +345 -0
  190. package/src/terminal/differential-checkpoints.ts +132 -0
  191. package/src/terminal/parity-suite.ts +441 -0
  192. package/src/terminal/snapshot-oracle.ts +1840 -0
  193. package/src/ui/conversation-input-forwarder.ts +114 -0
  194. package/src/ui/conversation-selection-input.ts +103 -0
  195. package/src/ui/debug-footer-notice.ts +39 -0
  196. package/src/ui/global-shortcut-input.ts +126 -0
  197. package/src/ui/input-preflight.ts +68 -0
  198. package/src/ui/input-token-router.ts +312 -0
  199. package/src/ui/input.ts +238 -0
  200. package/src/ui/kit.ts +509 -0
  201. package/src/ui/left-nav-input.ts +80 -0
  202. package/src/ui/left-rail-pointer-input.ts +148 -0
  203. package/src/ui/main-pane-pointer-input.ts +150 -0
  204. package/src/ui/modals/manager.ts +192 -0
  205. package/src/ui/mux-theme.ts +529 -0
  206. package/src/ui/panes/conversation.ts +19 -0
  207. package/src/ui/panes/home-gridfire.ts +302 -0
  208. package/src/ui/panes/home.ts +109 -0
  209. package/src/ui/panes/left-rail.ts +12 -0
  210. package/src/ui/panes/project.ts +44 -0
  211. package/src/ui/pointer-routing-input.ts +158 -0
  212. package/src/ui/repository-fold-input.ts +91 -0
  213. package/src/ui/screen.ts +210 -0
  214. package/src/ui/surface.ts +224 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Joshua Moyers
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.
package/README.md ADDED
@@ -0,0 +1,145 @@
1
+ # Harness
2
+
3
+ Harness is a terminal-native control plane for agentic coding on your local machine.
4
+
5
+ Run many agent threads in parallel across `codex`, `claude`, `cursor`, `terminal`, and `critique`, while keeping each thread in project context with one fast TUI and one typed realtime API.
6
+
7
+ ## What You Can Do
8
+
9
+ - Run many agent threads in parallel across `codex`, `claude`, `cursor`, `terminal`, and `critique`.
10
+ - Keep native CLI ergonomics in one keyboard-first workspace.
11
+ - Jump between threads in milliseconds, with 400+ FPS rendering under local workloads.
12
+ - Use `critique` threads for very fast diff/review loops, with native terminal access when you need to drop to commands.
13
+ - Keep long-running threads alive in the detached gateway so reconnects do not kill work.
14
+ - Add automation last through the typed realtime API (`projects`, `threads`, `repositories`, `tasks`, subscriptions).
15
+ - Plan work as scoped tasks (`project`, `repository`, `global`) and pull only `ready` tasks.
16
+ - Gate automation globally/per-repository/per-project (enable/disable + freeze), with optional project branch pinning and project-local task focus mode.
17
+ - See project/thread lifecycle updates from other connected clients in real time (no client restart rehydration loop).
18
+ - Get gateway-canonical thread status icons/text for structured agent threads, plus fixed one-cell title glyphs for `terminal`/`critique` threads (single server projection, no client-side status interpretation).
19
+ - Capture interleaved status debugging timelines (incoming status sources + outgoing status line/notice outputs) with a toggle and CLI commands.
20
+ - Capture focused render diagnostics (unsupported control sequences + ANSI integrity failures) with a dedicated toggle and CLI commands.
21
+ - Open a command palette with `ctrl+p`/`cmd+p`, live-filter registered actions, and execute context-aware thread/project/runtime controls.
22
+ - Open the active project's GitHub repository from the command palette.
23
+ - Open or create a GitHub pull request for the currently tracked non-default project branch directly from the command palette.
24
+
25
+ ## Demo
26
+
27
+ ![Harness multi-thread recording](assets/poem-recording.gif)
28
+
29
+ ## Quick Start
30
+
31
+ ### Prerequisites
32
+
33
+ - Bun `1.3.9+`
34
+ - Rust toolchain (used for the PTY helper; `bun install` auto-installs via `rustup` if missing)
35
+ - At least one agent CLI you plan to use (`codex`, `claude`, `cursor`, or `critique`)
36
+
37
+ ### Install and Run
38
+
39
+ ```bash
40
+ bun install
41
+ bun link
42
+ harness
43
+ ```
44
+
45
+ Harness connects to the current gateway session (or starts it in the background).
46
+
47
+ Use an isolated named session when you want separate state:
48
+
49
+ ```bash
50
+ harness --session perf-a
51
+ ```
52
+
53
+ ## Critique Support
54
+
55
+ Harness includes first-class `critique` threads:
56
+
57
+ - Available in the New Thread modal.
58
+ - Runs with `--watch` by default.
59
+ - Auto-install path enabled by default via `bunx critique@latest` when `critique` is not installed.
60
+ - `mux.conversation.critique.open-or-create` is bound to `ctrl+g` by default.
61
+
62
+ `ctrl+g` behavior is project-aware:
63
+
64
+ - If a critique thread exists for the current project, it selects it.
65
+ - If not, it creates and opens one in the main pane.
66
+
67
+ `session.interrupt` is also surfaced as a mux keybinding action (`mux.conversation.interrupt`) so teams can bind a dedicated in-client thread interrupt shortcut without overloading quit semantics.
68
+
69
+ ## GitHub PR Integration
70
+
71
+ When GitHub auth is available (`GITHUB_TOKEN` or an authenticated `gh` CLI), Harness can:
72
+
73
+ - Show `Open GitHub for This Repo` for active projects with a detected GitHub remote.
74
+ - Detect the tracked branch for the active project and show `Open PR` (if an open PR exists) or `Create PR` only when the tracked branch is not the repository default branch.
75
+ - Continuously sync open PR CI/check status into the control-plane store for realtime clients.
76
+ - If auth is unavailable, PR actions fail quietly and show a lightweight hint instead of surfacing hard errors.
77
+
78
+ ## API for Automation
79
+
80
+ Harness exposes a typed realtime client for orchestrators, policy agents, and dashboards:
81
+
82
+ ```ts
83
+ import { connectHarnessAgentRealtimeClient } from './src/control-plane/agent-realtime-api.ts';
84
+
85
+ const client = await connectHarnessAgentRealtimeClient({
86
+ host: '127.0.0.1',
87
+ port: 7777,
88
+ subscription: { includeOutput: false },
89
+ });
90
+
91
+ client.on('session.status', ({ observed }) => {
92
+ console.log(observed.sessionId, observed.status);
93
+ });
94
+
95
+ await client.close();
96
+ ```
97
+
98
+ Key orchestration calls are available in the same client:
99
+
100
+ - `client.tasks.pull(...)`
101
+ - `client.projects.status(projectId)`
102
+ - `client.projects.settings.get(projectId)` / `client.projects.settings.update(projectId, update)`
103
+ - `client.automation.getPolicy(...)` / `client.automation.setPolicy(...)`
104
+ ## Configuration
105
+
106
+ Runtime behavior is config-first via `harness.config.jsonc`.
107
+
108
+ Example (critique defaults + hotkey override + OpenCode theme selection):
109
+
110
+ ```jsonc
111
+ {
112
+ "critique": {
113
+ "launch": {
114
+ "defaultArgs": ["--watch"]
115
+ },
116
+ "install": {
117
+ "autoInstall": true,
118
+ "package": "critique@latest"
119
+ }
120
+ },
121
+ "mux": {
122
+ "ui": {
123
+ "theme": {
124
+ "preset": "tokyonight",
125
+ "mode": "dark",
126
+ "customThemePath": null
127
+ }
128
+ },
129
+ "keybindings": {
130
+ "mux.conversation.critique.open-or-create": ["ctrl+g"]
131
+ }
132
+ }
133
+ }
134
+ ```
135
+
136
+ `mux.ui.theme.customThemePath` can point to any local JSON file that follows the OpenCode theme schema (`https://opencode.ai/theme.json`).
137
+
138
+ ## Documentation
139
+
140
+ - `design.md` for architecture and system design principles.
141
+ - `agents.md` for execution and quality rules.
142
+
143
+ ## License
144
+
145
+ MIT (`LICENSE`)
@@ -0,0 +1,16 @@
1
+ # This file is automatically @generated by Cargo.
2
+ # It is not intended for manual editing.
3
+ version = 4
4
+
5
+ [[package]]
6
+ name = "libc"
7
+ version = "0.2.182"
8
+ source = "registry+https://github.com/rust-lang/crates.io-index"
9
+ checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112"
10
+
11
+ [[package]]
12
+ name = "ptyd"
13
+ version = "0.1.0"
14
+ dependencies = [
15
+ "libc",
16
+ ]
@@ -0,0 +1,7 @@
1
+ [package]
2
+ name = "ptyd"
3
+ version = "0.1.0"
4
+ edition = "2021"
5
+
6
+ [dependencies]
7
+ libc = "0.2"
@@ -0,0 +1,257 @@
1
+ use libc::{c_char, c_int, pid_t};
2
+ use std::ffi::CString;
3
+ use std::io;
4
+ use std::os::fd::RawFd;
5
+ use std::process::ExitCode;
6
+ use std::{mem, ptr};
7
+
8
+ const OPCODE_DATA: u8 = 0x01;
9
+ const OPCODE_RESIZE: u8 = 0x02;
10
+ const OPCODE_CLOSE: u8 = 0x03;
11
+
12
+ fn errno_code() -> Option<i32> {
13
+ io::Error::last_os_error().raw_os_error()
14
+ }
15
+
16
+ fn write_all_fd(fd: RawFd, mut buf: &[u8]) -> Result<(), ()> {
17
+ while !buf.is_empty() {
18
+ let written = unsafe { libc::write(fd, buf.as_ptr().cast(), buf.len()) };
19
+ if written < 0 {
20
+ if errno_code() == Some(libc::EINTR) {
21
+ continue;
22
+ }
23
+ return Err(());
24
+ }
25
+ let w = written as usize;
26
+ buf = &buf[w..];
27
+ }
28
+ Ok(())
29
+ }
30
+
31
+ fn signal_child(child_pid: pid_t, sig: c_int) {
32
+ let pgid = unsafe { libc::getpgid(child_pid) };
33
+ if pgid < 0 {
34
+ return;
35
+ }
36
+
37
+ if pgid == child_pid {
38
+ let _ = unsafe { libc::killpg(pgid, sig) };
39
+ } else {
40
+ let _ = unsafe { libc::kill(child_pid, sig) };
41
+ }
42
+ }
43
+
44
+ fn parse_and_apply_frames(incoming: &mut Vec<u8>, master_fd: RawFd, child_pid: pid_t) -> Result<(), ()> {
45
+ loop {
46
+ if incoming.is_empty() {
47
+ return Ok(());
48
+ }
49
+
50
+ match incoming[0] {
51
+ OPCODE_DATA => {
52
+ if incoming.len() < 5 {
53
+ return Ok(());
54
+ }
55
+ let n = u32::from_be_bytes([incoming[1], incoming[2], incoming[3], incoming[4]]) as usize;
56
+ if incoming.len() < 5 + n {
57
+ return Ok(());
58
+ }
59
+
60
+ if n > 0 {
61
+ write_all_fd(master_fd, &incoming[5..5 + n])?;
62
+ }
63
+ incoming.drain(0..(5 + n));
64
+ }
65
+ OPCODE_RESIZE => {
66
+ if incoming.len() < 5 {
67
+ return Ok(());
68
+ }
69
+
70
+ let cols = u16::from_be_bytes([incoming[1], incoming[2]]);
71
+ let rows = u16::from_be_bytes([incoming[3], incoming[4]]);
72
+
73
+ let mut ws: libc::winsize = unsafe { mem::zeroed() };
74
+ ws.ws_col = cols;
75
+ ws.ws_row = rows;
76
+ let rc = unsafe { libc::ioctl(master_fd, libc::TIOCSWINSZ, &ws) };
77
+ if rc < 0 {
78
+ return Err(());
79
+ }
80
+
81
+ signal_child(child_pid, libc::SIGWINCH);
82
+ incoming.drain(0..5);
83
+ }
84
+ OPCODE_CLOSE => {
85
+ signal_child(child_pid, libc::SIGHUP);
86
+ incoming.drain(0..1);
87
+ }
88
+ _ => {
89
+ incoming.drain(0..1);
90
+ }
91
+ }
92
+ }
93
+ }
94
+
95
+ fn child_exit_code(status: c_int) -> i32 {
96
+ if libc::WIFEXITED(status) {
97
+ return libc::WEXITSTATUS(status);
98
+ }
99
+ if libc::WIFSIGNALED(status) {
100
+ return 128 + libc::WTERMSIG(status);
101
+ }
102
+ 1
103
+ }
104
+
105
+ fn run() -> i32 {
106
+ let args: Vec<String> = std::env::args().skip(1).collect();
107
+ if args.is_empty() {
108
+ return 2;
109
+ }
110
+
111
+ let cstrings: Vec<CString> = match args
112
+ .iter()
113
+ .map(|arg| CString::new(arg.as_str()))
114
+ .collect::<Result<Vec<_>, _>>()
115
+ {
116
+ Ok(v) => v,
117
+ Err(_) => return 2,
118
+ };
119
+ let mut argv: Vec<*const c_char> = cstrings.iter().map(|s| s.as_ptr()).collect();
120
+ argv.push(ptr::null());
121
+
122
+ let mut master_fd: c_int = 0;
123
+ let mut slave_fd: c_int = 0;
124
+ let open_rc = unsafe {
125
+ libc::openpty(
126
+ &mut master_fd,
127
+ &mut slave_fd,
128
+ ptr::null_mut(),
129
+ ptr::null_mut(),
130
+ ptr::null_mut(),
131
+ )
132
+ };
133
+ if open_rc != 0 {
134
+ return 1;
135
+ }
136
+
137
+ let pid = unsafe { libc::fork() };
138
+ if pid < 0 {
139
+ unsafe {
140
+ libc::close(master_fd);
141
+ libc::close(slave_fd);
142
+ }
143
+ return 1;
144
+ }
145
+
146
+ if pid == 0 {
147
+ if unsafe { libc::setsid() } < 0 {
148
+ unsafe { libc::_exit(1) };
149
+ }
150
+
151
+ if unsafe { libc::ioctl(slave_fd, libc::TIOCSCTTY as libc::c_ulong, 0) } < 0 {
152
+ unsafe { libc::_exit(1) };
153
+ }
154
+
155
+ if unsafe { libc::dup2(slave_fd, libc::STDIN_FILENO) } < 0 {
156
+ unsafe { libc::_exit(1) };
157
+ }
158
+ if unsafe { libc::dup2(slave_fd, libc::STDOUT_FILENO) } < 0 {
159
+ unsafe { libc::_exit(1) };
160
+ }
161
+ if unsafe { libc::dup2(slave_fd, libc::STDERR_FILENO) } < 0 {
162
+ unsafe { libc::_exit(1) };
163
+ }
164
+
165
+ unsafe {
166
+ libc::close(master_fd);
167
+ libc::close(slave_fd);
168
+ libc::execvp(argv[0], argv.as_ptr());
169
+ libc::_exit(127);
170
+ }
171
+ }
172
+
173
+ unsafe {
174
+ libc::close(slave_fd);
175
+ }
176
+
177
+ let mut incoming: Vec<u8> = Vec::with_capacity(8192);
178
+ let mut io_buf = vec![0_u8; 65_536];
179
+ let mut stdin_open = true;
180
+
181
+ loop {
182
+ let mut status: c_int = 0;
183
+ let waited = unsafe { libc::waitpid(pid, &mut status, libc::WNOHANG) };
184
+ if waited == pid {
185
+ unsafe { libc::close(master_fd) };
186
+ return child_exit_code(status);
187
+ }
188
+
189
+ let stdin_fd = if stdin_open { libc::STDIN_FILENO } else { -1 };
190
+ let mut pfds = [
191
+ libc::pollfd {
192
+ fd: stdin_fd,
193
+ events: libc::POLLIN,
194
+ revents: 0,
195
+ },
196
+ libc::pollfd {
197
+ fd: master_fd,
198
+ events: libc::POLLIN,
199
+ revents: 0,
200
+ },
201
+ ];
202
+
203
+ let poll_rc = unsafe { libc::poll(pfds.as_mut_ptr(), pfds.len() as _, 100) };
204
+ if poll_rc < 0 {
205
+ if errno_code() == Some(libc::EINTR) {
206
+ continue;
207
+ }
208
+ unsafe { libc::close(master_fd) };
209
+ return 1;
210
+ }
211
+
212
+ if stdin_open && (pfds[0].revents & libc::POLLIN) != 0 {
213
+ let n = unsafe { libc::read(libc::STDIN_FILENO, io_buf.as_mut_ptr().cast(), io_buf.len()) };
214
+ if n == 0 {
215
+ stdin_open = false;
216
+ } else if n < 0 {
217
+ if errno_code() != Some(libc::EINTR) {
218
+ stdin_open = false;
219
+ }
220
+ } else {
221
+ let n_usize = n as usize;
222
+ incoming.extend_from_slice(&io_buf[..n_usize]);
223
+ if parse_and_apply_frames(&mut incoming, master_fd, pid).is_err() {
224
+ unsafe { libc::close(master_fd) };
225
+ return 1;
226
+ }
227
+ }
228
+ }
229
+
230
+ if (pfds[1].revents & libc::POLLIN) != 0 {
231
+ let n = unsafe { libc::read(master_fd, io_buf.as_mut_ptr().cast(), io_buf.len()) };
232
+ if n == 0 {
233
+ let mut status2: c_int = 0;
234
+ let _ = unsafe { libc::waitpid(pid, &mut status2, 0) };
235
+ unsafe { libc::close(master_fd) };
236
+ return child_exit_code(status2);
237
+ }
238
+ if n < 0 {
239
+ if errno_code() == Some(libc::EINTR) {
240
+ continue;
241
+ }
242
+ unsafe { libc::close(master_fd) };
243
+ return 1;
244
+ }
245
+ let n_usize = n as usize;
246
+ if write_all_fd(libc::STDOUT_FILENO, &io_buf[..n_usize]).is_err() {
247
+ unsafe { libc::close(master_fd) };
248
+ return 1;
249
+ }
250
+ }
251
+ }
252
+ }
253
+
254
+ fn main() -> ExitCode {
255
+ let code = run();
256
+ ExitCode::from((code & 0xFF) as u8)
257
+ }
package/package.json ADDED
@@ -0,0 +1,90 @@
1
+ {
2
+ "name": "@jmoyers/harness",
3
+ "version": "0.1.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "packageManager": "bun@1.3.9",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "files": [
11
+ "src",
12
+ "scripts/build-ptyd.sh",
13
+ "scripts/control-plane-daemon.ts",
14
+ "scripts/cursor-hook-relay.ts",
15
+ "scripts/harness-animate.ts",
16
+ "scripts/harness-bin.js",
17
+ "scripts/harness-core.ts",
18
+ "scripts/harness-inspector.ts",
19
+ "scripts/harness.ts",
20
+ "native/ptyd/Cargo.lock",
21
+ "native/ptyd/Cargo.toml",
22
+ "native/ptyd/src",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "bin": {
27
+ "harness": "scripts/harness-bin.js"
28
+ },
29
+ "scripts": {
30
+ "migrate:bun": "bash ./scripts/migrate-to-bun.sh",
31
+ "harness": "bun scripts/harness.ts",
32
+ "harness:core": "bun scripts/harness-core.ts",
33
+ "build:ptyd": "bash ./scripts/build-ptyd.sh",
34
+ "postinstall": "bun run build:ptyd",
35
+ "codex:live": "bun scripts/codex-live.ts",
36
+ "codex:pty:vte:passthrough": "bun scripts/codex-pty-vte-passthrough.ts",
37
+ "codex:live:mux": "bun scripts/harness-core.ts",
38
+ "codex:live:mux:record": "bun scripts/harness-core.ts --record",
39
+ "codex:live:mux:client": "bun scripts/harness-core.ts --harness-server-host 127.0.0.1 --harness-server-port 7777",
40
+ "codex:live:mux:launch": "bun scripts/codex-live-mux-launch.ts",
41
+ "mux:fixture:daemon": "bun scripts/control-plane-daemon-fixture.ts",
42
+ "mux:fixture:launch": "bun scripts/mux-fixture-launch.ts",
43
+ "terminal:recording:gif": "bun scripts/terminal-recording-to-gif.ts",
44
+ "codex:live:tail": "bun scripts/codex-live-tail.ts",
45
+ "codex:live:snapshot": "bun scripts/codex-live-snapshot.ts",
46
+ "control-plane:daemon": "bun scripts/control-plane-daemon.ts",
47
+ "terminal:parity": "bun scripts/terminal-parity.ts",
48
+ "terminal:differential": "bun scripts/terminal-differential-checkpoints.ts",
49
+ "previm:passthrough": "bun run build:ptyd",
50
+ "vim:passthrough": "bun scripts/vim-passthrough.ts",
51
+ "prebenchmark:latency": "bun run build:ptyd",
52
+ "benchmark:latency": "bun scripts/benchmark-pty-latency.ts",
53
+ "perf:mux:startup": "bun scripts/perf-mux-startup-report.ts",
54
+ "perf:mux:selector": "bun scripts/perf-mux-selector-report.ts",
55
+ "perf:mux:hotpath": "bun scripts/perf-mux-hotpath-harness.ts",
56
+ "perf:mux:launch:startup:loop": "bun scripts/perf-mux-launch-startup-loop.ts",
57
+ "perf:codex:startup:loop": "bun scripts/perf-codex-startup-loop.ts",
58
+ "loc": "bun scripts/loc-report.ts",
59
+ "loc:verify": "bun scripts/check-max-loc.ts --max-loc 2000",
60
+ "loc:verify:enforce": "bun scripts/check-max-loc.ts --max-loc 2000 --enforce",
61
+ "deadcode": "bun scripts/check-dead-code.ts",
62
+ "coverage:check": "bun scripts/check-coverage.ts --lcov .harness/coverage-bun/lcov.info --config harness.coverage.jsonc",
63
+ "release": "bun scripts/release.ts",
64
+ "lint": "oxlint --deny-warnings --disable-unicorn-plugin -c .oxlintrc.json src test scripts packages/harness-ai/src",
65
+ "format": "oxfmt --config .oxfmtrc.json src test scripts packages/harness-ai/src",
66
+ "format:check": "oxfmt --check --config .oxfmtrc.json src test scripts packages/harness-ai/src",
67
+ "typecheck": "tsc --noEmit",
68
+ "test": "bun run build:ptyd && bun test",
69
+ "test:integration:codex-status": "bun scripts/integration-codex-status-sequence.ts",
70
+ "test:integration:codex-status:long": "bun scripts/integration-codex-status-sequence.ts --timeout-ms 180000 --prompt \"Write three short poems titled Dawn, Voltage, and Orbit. Before each poem, perform one repository inspection action and include one factual line from that action. Use at least three total tool actions and do not edit any files.\"",
71
+ "smoke:harness-ai": "bun scripts/harness-ai-smoke.ts",
72
+ "smoke:harness-ai:parity": "bun scripts/harness-ai-parity-smoke.mts",
73
+ "test:coverage": "bun run build:ptyd && bun test --coverage --coverage-reporter=lcov --coverage-dir .harness/coverage-bun && bun run coverage:check",
74
+ "verify": "bun run format:check && bun run lint && bun run typecheck && bun run deadcode && bun run test:coverage",
75
+ "verify:strict": "bun run verify && bun run loc:verify:enforce"
76
+ },
77
+ "devDependencies": {
78
+ "@ai-sdk/anthropic": "^3.0.45",
79
+ "@types/node": "^22.17.0",
80
+ "ai": "^6.0.91",
81
+ "oxfmt": "^0.33.0",
82
+ "oxlint": "^1.48.0",
83
+ "typescript": "^5.9.2",
84
+ "zod": "^4.3.6"
85
+ },
86
+ "dependencies": {
87
+ "@napi-rs/canvas": "^0.1.92",
88
+ "gifenc": "^1.0.3"
89
+ }
90
+ }
@@ -0,0 +1,73 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ RUSTUP_INSTALL_URL="https://sh.rustup.rs"
5
+
6
+ add_cargo_to_path() {
7
+ export PATH="$HOME/.cargo/bin:$PATH"
8
+ }
9
+
10
+ source_cargo_env_if_present() {
11
+ local env_path="$HOME/.cargo/env"
12
+ if [[ -f "$env_path" ]]; then
13
+ # shellcheck disable=SC1090
14
+ source "$env_path"
15
+ fi
16
+ }
17
+
18
+ have_cargo() {
19
+ command -v cargo >/dev/null 2>&1
20
+ }
21
+
22
+ print_rust_required_message() {
23
+ cat <<'EOF' >&2
24
+ error: Rust toolchain is required to build the native PTY helper.
25
+
26
+ Install Rust manually:
27
+ https://www.rust-lang.org/tools/install
28
+
29
+ Or allow automatic install (default behavior):
30
+ bun install
31
+
32
+ To disable automatic install and fail fast:
33
+ HARNESS_AUTO_INSTALL_RUST=0 bun install
34
+ EOF
35
+ }
36
+
37
+ install_rustup_noninteractive() {
38
+ if ! command -v curl >/dev/null 2>&1; then
39
+ echo "error: curl is required to auto-install Rust with rustup." >&2
40
+ print_rust_required_message
41
+ exit 1
42
+ fi
43
+ echo "installing Rust toolchain via rustup..."
44
+ curl --proto '=https' --tlsv1.2 -sSf "$RUSTUP_INSTALL_URL" | sh -s -- -y
45
+ }
46
+
47
+ ensure_rust_toolchain() {
48
+ add_cargo_to_path
49
+ source_cargo_env_if_present
50
+ if have_cargo; then
51
+ return
52
+ fi
53
+
54
+ # Default to automatic rustup install for non-interactive setup flows.
55
+ if [[ "${HARNESS_AUTO_INSTALL_RUST:-1}" == "0" ]]; then
56
+ print_rust_required_message
57
+ exit 1
58
+ fi
59
+ install_rustup_noninteractive
60
+
61
+ source_cargo_env_if_present
62
+ add_cargo_to_path
63
+ if ! have_cargo; then
64
+ echo "error: Rust installation completed but cargo was not found in PATH." >&2
65
+ print_rust_required_message
66
+ exit 1
67
+ fi
68
+ }
69
+
70
+ ensure_rust_toolchain
71
+ cargo build --manifest-path native/ptyd/Cargo.toml --release
72
+ mkdir -p bin
73
+ cp native/ptyd/target/release/ptyd bin/ptyd