@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.
- package/LICENSE +21 -0
- package/README.md +145 -0
- package/native/ptyd/Cargo.lock +16 -0
- package/native/ptyd/Cargo.toml +7 -0
- package/native/ptyd/src/main.rs +257 -0
- package/package.json +90 -0
- package/scripts/build-ptyd.sh +73 -0
- package/scripts/control-plane-daemon.ts +277 -0
- package/scripts/cursor-hook-relay.ts +82 -0
- package/scripts/harness-animate.ts +469 -0
- package/scripts/harness-bin.js +77 -0
- package/scripts/harness-core.ts +1 -0
- package/scripts/harness-inspector.ts +439 -0
- package/scripts/harness.ts +2493 -0
- package/src/adapters/agent-session-state.ts +390 -0
- package/src/cli/gateway-record.ts +173 -0
- package/src/codex/live-session.ts +872 -0
- package/src/config/config-core.ts +1359 -0
- package/src/config/secrets-core.ts +170 -0
- package/src/control-plane/agent-realtime-api.ts +2441 -0
- package/src/control-plane/codex-session-stream.ts +392 -0
- package/src/control-plane/codex-telemetry.ts +1325 -0
- package/src/control-plane/lifecycle-hooks.ts +706 -0
- package/src/control-plane/session-summary.ts +380 -0
- package/src/control-plane/status/agent-status-reducer.ts +21 -0
- package/src/control-plane/status/reducer-base.ts +170 -0
- package/src/control-plane/status/reducers/claude-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/codex-status-reducer.ts +48 -0
- package/src/control-plane/status/reducers/critique-status-reducer.ts +15 -0
- package/src/control-plane/status/reducers/cursor-status-reducer.ts +37 -0
- package/src/control-plane/status/reducers/terminal-status-reducer.ts +15 -0
- package/src/control-plane/status/session-status-engine.ts +76 -0
- package/src/control-plane/stream-client.ts +396 -0
- package/src/control-plane/stream-command-parser.ts +1673 -0
- package/src/control-plane/stream-protocol.ts +1808 -0
- package/src/control-plane/stream-server-background.ts +486 -0
- package/src/control-plane/stream-server-command.ts +2557 -0
- package/src/control-plane/stream-server-connection.ts +234 -0
- package/src/control-plane/stream-server-observed-filter.ts +112 -0
- package/src/control-plane/stream-server-session-runtime.ts +566 -0
- package/src/control-plane/stream-server-state-store.ts +15 -0
- package/src/control-plane/stream-server.ts +3192 -0
- package/src/cursor/managed-hooks.ts +282 -0
- package/src/domain/conversations.ts +414 -0
- package/src/domain/directories.ts +78 -0
- package/src/domain/repositories.ts +123 -0
- package/src/domain/tasks.ts +148 -0
- package/src/domain/workspace.ts +156 -0
- package/src/events/normalized-events.ts +124 -0
- package/src/mux/ansi-integrity.ts +103 -0
- package/src/mux/control-plane-op-queue.ts +212 -0
- package/src/mux/conversation-rail.ts +339 -0
- package/src/mux/double-click.ts +78 -0
- package/src/mux/dual-pane-core.ts +435 -0
- package/src/mux/harness-core-ui.ts +817 -0
- package/src/mux/input-shortcuts.ts +667 -0
- package/src/mux/live-mux/actions-conversation.ts +344 -0
- package/src/mux/live-mux/actions-repository.ts +246 -0
- package/src/mux/live-mux/actions-task.ts +115 -0
- package/src/mux/live-mux/args.ts +142 -0
- package/src/mux/live-mux/command-menu.ts +298 -0
- package/src/mux/live-mux/control-plane-records.ts +546 -0
- package/src/mux/live-mux/conversation-state.ts +188 -0
- package/src/mux/live-mux/directory-resolution.ts +34 -0
- package/src/mux/live-mux/event-mapping.ts +96 -0
- package/src/mux/live-mux/gateway-profiler.ts +152 -0
- package/src/mux/live-mux/gateway-render-trace.ts +177 -0
- package/src/mux/live-mux/gateway-status-timeline.ts +166 -0
- package/src/mux/live-mux/git-parsing.ts +131 -0
- package/src/mux/live-mux/git-snapshot.ts +263 -0
- package/src/mux/live-mux/git-state.ts +136 -0
- package/src/mux/live-mux/global-shortcut-handlers.ts +143 -0
- package/src/mux/live-mux/home-pane-actions.ts +58 -0
- package/src/mux/live-mux/home-pane-drop.ts +44 -0
- package/src/mux/live-mux/home-pane-entity-click.ts +96 -0
- package/src/mux/live-mux/home-pane-pointer.ts +96 -0
- package/src/mux/live-mux/input-forwarding.ts +112 -0
- package/src/mux/live-mux/layout.ts +30 -0
- package/src/mux/live-mux/left-nav-activation.ts +103 -0
- package/src/mux/live-mux/left-nav.ts +85 -0
- package/src/mux/live-mux/left-rail-actions.ts +118 -0
- package/src/mux/live-mux/left-rail-conversation-click.ts +82 -0
- package/src/mux/live-mux/left-rail-pointer.ts +74 -0
- package/src/mux/live-mux/modal-command-menu-handler.ts +101 -0
- package/src/mux/live-mux/modal-conversation-handlers.ts +217 -0
- package/src/mux/live-mux/modal-input-reducers.ts +94 -0
- package/src/mux/live-mux/modal-overlays.ts +287 -0
- package/src/mux/live-mux/modal-pointer.ts +70 -0
- package/src/mux/live-mux/modal-prompt-handlers.ts +187 -0
- package/src/mux/live-mux/modal-task-editor-handler.ts +156 -0
- package/src/mux/live-mux/observed-stream.ts +87 -0
- package/src/mux/live-mux/palette-parsing.ts +128 -0
- package/src/mux/live-mux/pointer-routing.ts +108 -0
- package/src/mux/live-mux/process-usage.ts +53 -0
- package/src/mux/live-mux/project-pane-pointer.ts +44 -0
- package/src/mux/live-mux/rail-layout.ts +244 -0
- package/src/mux/live-mux/render-trace-analysis.ts +213 -0
- package/src/mux/live-mux/render-trace-state.ts +84 -0
- package/src/mux/live-mux/repository-folding.ts +207 -0
- package/src/mux/live-mux/runtime-shutdown.ts +51 -0
- package/src/mux/live-mux/selection.ts +411 -0
- package/src/mux/live-mux/startup-utils.ts +187 -0
- package/src/mux/live-mux/status-timeline-state.ts +82 -0
- package/src/mux/live-mux/task-pane-shortcuts.ts +206 -0
- package/src/mux/live-mux/terminal-palette.ts +79 -0
- package/src/mux/new-thread-prompt.ts +165 -0
- package/src/mux/project-tree.ts +295 -0
- package/src/mux/render-frame.ts +113 -0
- package/src/mux/runtime-wiring.ts +185 -0
- package/src/mux/selector-index.ts +160 -0
- package/src/mux/startup-sequencer.ts +238 -0
- package/src/mux/task-composer.ts +289 -0
- package/src/mux/task-focused-pane.ts +417 -0
- package/src/mux/task-screen-keybindings.ts +539 -0
- package/src/mux/terminal-input-modes.ts +35 -0
- package/src/mux/workspace-path.ts +55 -0
- package/src/mux/workspace-rail-model.ts +701 -0
- package/src/mux/workspace-rail.ts +247 -0
- package/src/perf/perf-core.ts +307 -0
- package/src/pty/pty_host.ts +217 -0
- package/src/pty/session-broker.ts +158 -0
- package/src/recording/terminal-recording.ts +383 -0
- package/src/services/control-plane.ts +567 -0
- package/src/services/conversation-lifecycle.ts +176 -0
- package/src/services/conversation-startup-hydration.ts +47 -0
- package/src/services/directory-hydration.ts +49 -0
- package/src/services/event-persistence.ts +104 -0
- package/src/services/mux-ui-state-persistence.ts +82 -0
- package/src/services/output-load-sampler.ts +231 -0
- package/src/services/process-usage-refresh.ts +88 -0
- package/src/services/recording.ts +75 -0
- package/src/services/render-trace-recorder.ts +177 -0
- package/src/services/runtime-control-actions.ts +123 -0
- package/src/services/runtime-control-plane-ops.ts +131 -0
- package/src/services/runtime-conversation-actions.ts +113 -0
- package/src/services/runtime-conversation-activation.ts +78 -0
- package/src/services/runtime-conversation-starter.ts +171 -0
- package/src/services/runtime-conversation-title-edit.ts +149 -0
- package/src/services/runtime-directory-actions.ts +164 -0
- package/src/services/runtime-envelope-handler.ts +198 -0
- package/src/services/runtime-git-state.ts +92 -0
- package/src/services/runtime-input-pipeline.ts +50 -0
- package/src/services/runtime-input-router.ts +202 -0
- package/src/services/runtime-layout-resize.ts +236 -0
- package/src/services/runtime-left-rail-render.ts +159 -0
- package/src/services/runtime-main-pane-input.ts +230 -0
- package/src/services/runtime-modal-input.ts +119 -0
- package/src/services/runtime-navigation-input.ts +207 -0
- package/src/services/runtime-process-wiring.ts +68 -0
- package/src/services/runtime-rail-input.ts +287 -0
- package/src/services/runtime-render-flush.ts +146 -0
- package/src/services/runtime-render-lifecycle.ts +104 -0
- package/src/services/runtime-render-orchestrator.ts +108 -0
- package/src/services/runtime-render-pipeline.ts +167 -0
- package/src/services/runtime-render-state.ts +72 -0
- package/src/services/runtime-repository-actions.ts +197 -0
- package/src/services/runtime-right-pane-render.ts +132 -0
- package/src/services/runtime-shutdown.ts +79 -0
- package/src/services/runtime-stream-subscriptions.ts +56 -0
- package/src/services/runtime-task-composer-persistence.ts +139 -0
- package/src/services/runtime-task-editor-actions.ts +83 -0
- package/src/services/runtime-task-pane-actions.ts +198 -0
- package/src/services/runtime-task-pane-shortcuts.ts +189 -0
- package/src/services/runtime-task-pane.ts +62 -0
- package/src/services/runtime-workspace-actions.ts +153 -0
- package/src/services/runtime-workspace-observed-events.ts +190 -0
- package/src/services/session-projection-instrumentation.ts +190 -0
- package/src/services/startup-background-probe.ts +91 -0
- package/src/services/startup-background-resume.ts +65 -0
- package/src/services/startup-orchestrator.ts +166 -0
- package/src/services/startup-output-tracker.ts +54 -0
- package/src/services/startup-paint-tracker.ts +115 -0
- package/src/services/startup-persisted-conversation-queue.ts +45 -0
- package/src/services/startup-settled-gate.ts +67 -0
- package/src/services/startup-shutdown.ts +53 -0
- package/src/services/startup-span-tracker.ts +77 -0
- package/src/services/startup-state-hydration.ts +94 -0
- package/src/services/startup-visibility.ts +35 -0
- package/src/services/status-timeline-recorder.ts +144 -0
- package/src/services/task-pane-selection-actions.ts +153 -0
- package/src/services/task-planning-hydration.ts +58 -0
- package/src/services/task-planning-observed-events.ts +89 -0
- package/src/services/workspace-observed-events.ts +113 -0
- package/src/store/control-plane-store-normalize.ts +760 -0
- package/src/store/control-plane-store-types.ts +224 -0
- package/src/store/control-plane-store.ts +2951 -0
- package/src/store/event-store.ts +253 -0
- package/src/store/sqlite.ts +81 -0
- package/src/terminal/compat-matrix.ts +345 -0
- package/src/terminal/differential-checkpoints.ts +132 -0
- package/src/terminal/parity-suite.ts +441 -0
- package/src/terminal/snapshot-oracle.ts +1840 -0
- package/src/ui/conversation-input-forwarder.ts +114 -0
- package/src/ui/conversation-selection-input.ts +103 -0
- package/src/ui/debug-footer-notice.ts +39 -0
- package/src/ui/global-shortcut-input.ts +126 -0
- package/src/ui/input-preflight.ts +68 -0
- package/src/ui/input-token-router.ts +312 -0
- package/src/ui/input.ts +238 -0
- package/src/ui/kit.ts +509 -0
- package/src/ui/left-nav-input.ts +80 -0
- package/src/ui/left-rail-pointer-input.ts +148 -0
- package/src/ui/main-pane-pointer-input.ts +150 -0
- package/src/ui/modals/manager.ts +192 -0
- package/src/ui/mux-theme.ts +529 -0
- package/src/ui/panes/conversation.ts +19 -0
- package/src/ui/panes/home-gridfire.ts +302 -0
- package/src/ui/panes/home.ts +109 -0
- package/src/ui/panes/left-rail.ts +12 -0
- package/src/ui/panes/project.ts +44 -0
- package/src/ui/pointer-routing-input.ts +158 -0
- package/src/ui/repository-fold-input.ts +91 -0
- package/src/ui/screen.ts +210 -0
- 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
|
+

|
|
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,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
|